Ground Snapping

The simulation itself is 2D - it reasons entirely on the world X/Y plane and stores no Z. To place agents on uneven terrain, the crowd actor tracks each agent's Z height itself (UE-side) and can re-trace the ground under every agent each tick. This is the ground snapping system.

Agents snapped and tilted to a slope
Agents snapped and tilted to a slope

How Z is tracked

  • When you SpawnAgent(Position), Position.Z becomes that agent's initial tracked Z.
  • With Snap To Ground off, agents keep that spawn Z forever (fine for flat levels).
  • With Snap To Ground on, every tick the actor traces the ground under each agent's X/Y and glues its tracked Z to the hit - so positions and transforms follow the terrain.

The tracked Z is keyed by the agent's stable id, so it survives the index reshuffles that removals cause.

Enabling it

Turn on Ground → Snap To Ground and configure the trace:

Property What it does
Snap To Ground (bSnapToGround) Master switch. Off = agents keep their spawn Z.
Ground Trace Channel Collision channel the downward trace runs against (default Visibility).
Ground Trace Up/Down Limit The trace runs from Z + UpLimit down to Z - DownLimit, bounding how much height an agent can pick up per tick (steps, slopes). On a miss the agent keeps its current Z.
Ground Offset Added to the hit to get the final Z - e.g. the half-height of the mesh the transforms drive.
Align Transforms To Ground (bAlignTransformsToGround) Tilt the rendered transform so the mesh up-axis follows the traced ground normal (slopes). Facing stays on the travel direction.

The self-collision trap

Performance: the per-tick trace cap

Ground snapping costs one line trace per agent per tick, which dominates the frame at high agent counts. Max Ground Traces Per Tick caps it:

  • 0 (default) - trace every agent every tick (accurate, most expensive).
  • N > 0 - trace only the next N agents each tick in round-robin order; the rest keep their last Z until their turn.

For thousands of slow-moving agents on gentle terrain, a cap of a few hundred is usually indistinguishable from tracing all of them, at a fraction of the cost.

Reading the ground normal

TArray<FVector> Normals = Crowd->GetAgentGroundNormals({});
// World up until a trace has hit; the traced surface normal afterwards.

This is the same normal bAlignTransformsToGround uses, exposed in case you want to drive your own decal/marker alignment.