Lesson 6
Steering
A decision says where to go and a path says which way; steering moves the agent along it, a little every frame, without hitting anything. It isn't one algorithm but four choices: how the agent moves, where on the path it aims, how it notices walls, and how those combine. Compared on one course, including how many directions it really needs.
The earlier lessons chose what to do: attack, take cover, fetch the potion. A pathfinder turns that into where: a route of waypoints. Steering is the last step: turning “I want to be over there” into a velocity, smoothly, every frame, without walking into anything on the way.
“Steering” sounds like one algorithm, but it’s a stack of four separate choices:
- The motion model: what the agent can physically do. Slide in any direction, turn on the spot, or drive like a car.
- Path following: where on the path to aim right now.
- Sensing: how the agent notices walls, and things the path doesn’t know about.
- Combining: how “go this way” and “not that way” become one velocity.
Other agents are a fifth problem with better tools, because their positions and velocities are already known: that’s the next lesson.
Reynolds’ vocabulary
The classic formulation is Craig Reynolds’ Steering Behaviors for Autonomous Characters (1999). Each character is a vehicle: a point mass with a velocity, a maximum speed and a maximum steering force. A behaviour looks at the world and returns a steering force, the difference between the velocity it wants and the one it has. Forces are combined, clamped to the maximum force, and integrated:
Vec2 seek(const Vehicle& v, Vec2 target) {
Vec2 desired = normalize(target - v.pos) * v.max_speed; // full speed, straight at it
return desired - v.vel; // the change we'd like
}
void integrate(Vehicle& v, Vec2 steering, float dt) {
v.vel = truncate(v.vel + truncate(steering, v.max_force) * dt, v.max_speed);
v.pos += v.vel * dt;
}
Loading…
The arrows are the vehicle’s velocity (blue), its desired velocity (green, dashed) and the steering force (yellow).
- Seek aims at the target at full speed, so it overshoots and orbits. Arrive brakes inside a slowing radius and stops on it: use it at the end of a path. Flee is seek reversed.
- Pursue and evade aim at where the other vehicle will be. Untick Predict and the pursuer chases the target’s tail.
- Wander seeks a point that drifts a little at a time round a circle ahead, so the path meanders smoothly.
- Avoid obstacles casts one feeler ahead and pushes sideways from whatever it hits.
- Flock is Reynolds’ boids (1987): separation, alignment and cohesion with neighbours. Set any to zero and see what’s lost.
These are the vocabulary. The rest of this lesson is about putting them together for an agent that has to get somewhere.
The course
Six agents follow a planner’s path across three maps. The path was string-pulled, and keeps an agent’s radius from every corner (a path that touches corners drags agents into walls whatever the steering does; the pathing lab builds clearance into the path). Each part of the stack can be swapped:
Loading…
Click an agent to see what it senses. The tables below are measured on this widget, with every setting at its default unless named, and summed over the three maps; stuck counts agents still short of the goal after 45 seconds. Wobble is how much agents turn per second on average: a smooth agent turns only at corners.
Motion models
- Point mass. Reynolds’ model: the agent accelerates in any direction, up to a maximum force. It slides round corners. Right for spaceships and fish, floaty for people.
- Turns on the spot. The agent has a heading and a maximum turn rate, moves only forwards, and slows down to turn: no speed at all when facing away from where it wants to go. Most characters work like this, especially with root-motion animation.
- Car. The agent can’t turn faster than its speed divided by its minimum turning radius, so it can’t turn standing still at all. It has to notice a wall while it can still turn away from it, and a real vehicle in a dead end needs to reverse (the Reeds–Shepp planners in the pathing lab).
| Moves like | Context steering: time, contact | Three whiskers: time, contact |
|---|---|---|
| Point mass | 28.2 s, 0.0% | 29.9 s, 2.7% |
| Turns on the spot | 28.1 s, 0.0% | 33.2 s, 5.6% |
| Car | 29.1 s, 1.1% | 28.6 s, 1.2% |
The motion model changes what the sensing has to cope with. Whiskers suit the car, whose wide turns rarely bring a wall close to the side, and suit the agent that turns on the spot least: it swings its heading round sharply, and three rays fixed to the heading swing with it, so walls beside it go unseen. Context steering looks all the way round and does well with all three.
Following a path
- Next waypoint: seek the next corner, and switch to the one after when close enough. Switch too early and agents cut the corner; too late and they overshoot it.
- Carrot on the path (pure pursuit): find the closest point on the path, then aim a fixed distance further along it. The aim point slides smoothly round corners, so the motion does too. Reynolds’ own path following is similar: predict where you’ll be in a moment, and steer back only if that’s too far from the path.
- Straight at the goal: no path at all.
| Following (with context steering) | Time | Stuck |
|---|---|---|
| Next waypoint | 28.5 s | 0 |
| Carrot on the path | 28.2 s | 0 |
| Straight at the goal | 97.4 s | 12 |
Without a path, every agent on the dead-end map walks into the cup and stays there, and context steering can’t save it: it chooses well among nearby directions, but escaping a dead end means going away from the goal for a while, which only a planner knows to do. Local steering and pathfinding do different jobs.
Production systems go one step further. Detour (Recast’s runtime) keeps a path corridor, the list of navmesh polygons the path runs through, and every frame steers towards the furthest corner it can see along it. That’s string pulling done on the fly, so the path stays valid as the agent is pushed off it.
Noticing walls
The path was planned around the walls it knew about. Steering still has to notice what it didn’t: props left out of the navmesh, things that move, and the agent drifting off the path because something pushed it.
- Nothing. Trust the path. Fine on the corridor map; on the pillars map, the planner’s straight line runs through a pillar it didn’t know about, and agents pile into it.
- One feeler. Cast a ray along the velocity, as long as the agent travels in a moment. On a hit, push off the surface, and sideways (a wall hit head-on has a normal pointing straight back, which only brakes). It sees nothing that isn’t dead ahead, so it clips corners.
- Three whiskers. A centre ray and two shorter ones angled out. Catches corners for three rays per cast.
- Wall query (no rays). Ask the geometry for wall segments within a radius, and push away from each. No rays at all: with a navmesh, these are its boundary edges (Detour’s crowd collects the same segments, and treats them as obstacles when it chooses a velocity). Pushing away from them is smooth and cheap, but it’s a potential field, and potential fields have local minima: a pillar dead ahead pushes straight back exactly as hard as the path pulls forward, and the agent stops.
- Context steering (described by Andrew Fray). A ring of directions round the agent. Each ray writes danger into its direction, and into the neighbouring directions the agent’s body would sweep through. The path writes interest: how well each direction matches where it wants to go. Then mask: throw away every direction more dangerous than the safest one, and take the most interesting of what’s left, interpolated with its neighbours so the heading is smooth.
| Walls found by | Time | Stuck | Contact | Wobble | Rays per agent per second |
|---|---|---|---|---|---|
| Nothing | 65.6 s | 3 | 51.9% | 19°/s | 0 |
| One feeler | 66.5 s | 1 | 29.2% | 136°/s | 60 |
| Three whiskers | 29.9 s | 0 | 2.7% | 57°/s | 180 |
| Wall query | 68.5 s | 3 | 0.0% | 41°/s | 0 |
| Context steering, 16 rays | 28.2 s | 0 | 0.0% | 46°/s | 960 |
Turn on Moving crates and the crates cross the path. Context steering still never gets stuck (37.3 s, 0.8% contact), and whiskers hold up (32.4 s, 5.2%).
Context steering wins because it chooses a direction instead of adding up pushes. Pushes can cancel: a wall on the left and a wall on the right, or a pillar straight ahead and the path straight through it, sum to nothing, and the agent stalls or wobbles. A ring with one direction picked from it can’t be cancelled into a standstill. It’s also easy to debug: draw the ring, as the widget does, and you can see why the agent went the way it did.
How many directions
Context steering casts the most rays in the table, so how small can the ring be?
| Rays in the ring | Prefer last direction | No preference |
|---|---|---|
| 8 | 30.3 s, wobble 67°/s | 66.5 s, 2 stuck, wobble 441°/s |
| 12 | 28.8 s, wobble 53°/s | 65.7 s, 1 stuck, wobble 299°/s |
| 16 | 28.2 s, wobble 46°/s | 28.9 s, wobble 56°/s |
| 24 | 28.3 s, wobble 46°/s | 28.5 s, wobble 53°/s |
| 32 | 28.2 s, wobble 43°/s | 28.8 s, wobble 49°/s |
With 16 or more, little changes. With fewer, two directions often score almost the same, and without a preference the agent flips between them every tick and can get stuck dithering in front of a pillar. Preferring the last direction (a bonus to directions close to the one chosen last tick: 0.3 here) is hysteresis, the same fix as for decisions, and it makes a ring of 8 rays nearly as good as one of 32.
Rays only see the world as it was when they were cast, which is fine for walls and poor for things that move. There are better tools for moving things than rays, though: anything the game simulates has a known position and velocity, and local avoidance uses them directly.
Combining
Context steering is itself a way of combining wants. The other common ways:
- A weighted sum of forces, as Reynolds’ behaviours and the feelers here do. Simple, and it works, until forces cancel: “follow the path” and “avoid the pillar” in exactly opposite directions sum to nothing.
- Priorities. Ask behaviours in order of importance (avoid collisions, then separation, then path following), and fill the force budget from the top; lower priorities only get what’s left. No cancelling, but a high priority can starve everything else.
- Context maps, as above: every behaviour writes interest or danger into the same ring, and one direction is picked. Several behaviours combine without cancelling, because the result is a choice, not a sum.
In an ECS
Split it into systems by how often each part needs to run:
- A sensing system casts each agent’s ring and stores the hits in a
SenseRingcomponent. Rays can be batched or run as a job. - A path following system reads
Position,PathCorridorand writes a desired velocity. - A steering system builds each context ring from the remembered hits and the desired velocity, and writes
Velocity. - A movement system applies the motion model.
Every agent writes only its own components, so all of these parallelise. Keep the rings small and fixed-size (16 floats each for interest and danger) inside the component, not in separate allocations.
Takeaways
- Steering is four choices: motion model, path following, sensing and combining. Changing one changes what the others have to cope with.
- Follow the path with a carrot, and let the planner handle dead ends; no local method can.
- Choose a direction rather than summing pushes. Context steering doesn’t cancel into a standstill, and its rings are easy to debug.
- Prefer the last direction. It stops a small ring dithering; 12–16 directions is plenty.
- Use rays for what nothing else knows about. Walls can come from the navmesh, other agents from the spatial index.