When a UCapsuleComponent or USphereComponent has bDynamicObstacle set to true, the engine generates a cylinder-shaped navigation area modifier via UShapeComponent::GetNavigationData, which calls FCompositeNavModifier::CreateAreaModifiers(UPrimitiveComponent*, …). Three separate bugs in this code path cause the generated nav modifier to be significantly larger than the actual physics collision body.
Bug 1: SphylElem.Length Misused as CapsuleHalfHeight
File: Engine/Source/Runtime/Engine/Private/AI/Navigation/NavigationModifier.cpp
Function: FCompositeNavModifier::CreateAreaModifiers (SphylElems loop)
FKSphylElem.Length stores the total length of the cylindrical portion of the capsule shape, equal to 2 * (CapsuleHalfHeight - CapsuleRadius). The code incorrectly treats this value as if it were CapsuleHalfHeight. As a result:
- The vertical offset applied to find the cylinder bottom is too large by (CapsuleRadius) units.
- The total height passed to FAreaNavModifier is SphylElem.Length * 2.0f, which equals 4 * (CapsuleHalfHeight - CapsuleRadius), instead of the correct 2 * CapsuleHalfHeight.
Example with CapsuleRadius = 80, CapsuleHalfHeight = 200:
SphylElem.Length = 2 * (200 - 80) = 240
Generated modifier: bottom offset = -240, height = 480
Correct values: bottom offset = -200, height = 400
The modifier is 20% taller than the capsule and positioned 40 units too low.
The second CreateAreaModifiers overload (FCollisionShape, FTransform) handles the equivalent capsule case correctly using CapsuleHalfHeight directly, which confirms this is unintentional.
Bug 2: Radius Scaled by max(X, Y) Instead of min(X, Y)
File: Engine/Source/Runtime/Engine/Private/AI/Navigation/NavigationModifier.cpp
Function: FAreaNavModifier::FAreaNavModifier(float Radius, float Height, FTransform)
The cylinder FAreaNavModifier constructor scales the input radius by FMath::Max(Scale3D.X, Scale3D.Y), producing the circumscribed circle of the non-uniformly scaled shape. However:
- UCapsuleComponent::GetScaledCapsuleRadius() scales by min(X, Y) (inscribed circle)
- USphereComponent::GetScaledSphereRadius() scales by GetMinimumAxisScale() which is min(X, Y, Z) (largest inscribed sphere)
Both component APIs explicitly document using the minimum axis scale so that the physics collision remains conservative and fits inside the shape. The nav modifier using the maximum axis scale produces an obstacle radius that exceeds the actual physics collision radius for any component with non-uniform XY scaling.
UBoxComponent::GetScaledBoxExtent() multiplies each axis independently and its path through SetBox transforms all 8 corners by the full component transform, so the box shape is not affected by this bug.
Bug 3: Cylinder Bounding Box Centered at Bottom Instead of Midpoint
File: Engine/Source/Runtime/Engine/Private/AI/Navigation/NavigationModifier.cpp
Function: FAreaNavModifier::FAreaNavModifier(float Radius, float Height, FTransform)
The FAreaNavModifier cylinder convention (matching dtMarkCylinderArea in Recast) stores Points[0] as the cylinder bottom and Height as the full height going upward. However the bounding box is built as: FBox::BuildAABB(Points[0], FVector(RadiusScaled, RadiusScaled, HeightScaled)).
FBox::BuildAABB takes a center and half-extents. Using the cylinder bottom as the center and the full height as the half-extent causes the bounding box to extend a full HeightScaled distance below the cylinder bottom, doubling the total Z extent.
For the HitCapsule example (CapsuleRadius = 80, CapsuleHalfHeight = 200) after applying Bug 1’s incorrect offset, the bounding box spans 960 cm in Z while the actual capsule is only 400 cm tall, a factor of 2.4x.
Even after fixing Bug 1, the bounding box with the correct 400 cm height would still extend 400 cm below the cylinder bottom unnecessarily, producing a total Z span of 800 cm instead of the correct 400 cm. This causes far more navmesh tiles to be marked as dirty than necessary whenever the actor moves, resulting in excessive navmesh rebuild cost.
The correct bounding box should use the cylinder midpoint as center and HeightScaled/2 as the Z half-extent.