Summary
Steering behaviours, formalised by Craig Reynolds (1999), describe how autonomous agents can move through a world in a lifelike, purposeful way. Rather than snapping an object to a position, steering applies a force that gradually changes the agent’s velocity — resulting in curved, momentum-respecting paths. The core idea is reinforced in both Millington and Buckland, who treat steering as a practical layer between high-level choice and low-level motion. (Millington, Artificial Intelligence for Games, see source-artificial-intelligence-for-games; Buckland, Programming Game AI by Example, see source-programming-game-ai-by-example)
The core idea is:
steering force = desired velocity − current velocity
This single formula makes all steering behaviours work. The agent calculates where it wants to go (desired velocity) and subtracts where it is actually going (current velocity) to produce the corrective force to apply this frame.
(Shiffman, The Nature of Code, Ch. 5, see source-nature-of-code)
JavaScript to Unity/C# bridge
Shiffman’s examples are presented in p5.js, where vectors, update logic, and drawing are all shown together in a small sketch. In Unity, the same steering idea usually becomes:
- a component holding
position,velocity, andacceleration - one method per steering behaviour such as
Seek()orArrive() - an
Update()loop that applies force, integrates motion, and then resets acceleration
So the translation students should focus on is conceptual, not cosmetic: p5.Vector becomes Vector2 or Vector3, and draw() becomes the Unity update loop.
Autonomous agent definition
An autonomous agent has three key properties:
- Limited perception — it senses only its immediate surroundings, not the whole world.
- Action from information — it computes a force based on what it perceives.
- Decentralised control — no external authority directs it; complex group behaviour emerges from local interactions.
This is the foundation for flocking, crowds, enemy patrol AI, and ecosystem simulations.
The Vehicle class
All steering behaviours share a common agent structure:
// Unity C# — Vehicle base
public class Vehicle : MonoBehaviour
{
Vector2 position;
Vector2 velocity;
Vector2 acceleration;
float maxSpeed = 4f;
float maxForce = 0.1f;
void ApplyForce(Vector2 force) => acceleration += force;
void Update()
{
velocity += acceleration;
velocity = Vector2.ClampMagnitude(velocity, maxSpeed);
position += velocity;
acceleration = Vector2.zero; // reset each frame
}
}maxForce limits how sharply the agent can steer. maxSpeed limits how fast it travels. Both together produce natural-feeling motion.
Core behaviours
Seek
Move toward a target at maximum speed.
Vector2 Seek(Vector2 target)
{
Vector2 desired = (target - position).normalized * maxSpeed;
return desired - velocity; // steer toward desired
}The agent will overshoot and oscillate around the target unless combined with arrive.
Arrive
Approach a target while decelerating. Within a defined radius, desired speed is proportional to distance, so the agent slows to zero exactly at the target.
Vector2 Arrive(Vector2 target, float slowRadius = 100f)
{
Vector2 toTarget = target - position;
float distance = toTarget.magnitude;
float speed = distance < slowRadius
? maxSpeed * (distance / slowRadius)
: maxSpeed;
Vector2 desired = toTarget.normalized * speed;
return desired - velocity;
}Use arrive for any agent that should stop — player-controlled cursors, projectiles homing in, NPCs walking to a waypoint.
Wander
Produce apparently purposeful, organic movement without a fixed target. A random point is chosen on a circle projected ahead of the agent each frame; the direction updates smoothly rather than jumping, creating temporal coherence.
float wanderAngle = 0f;
Vector2 Wander()
{
float wanderRadius = 25f;
float wanderDistance = 80f;
float wanderChange = 0.3f;
wanderAngle += Random.Range(-wanderChange, wanderChange);
Vector2 circleCenter = velocity.normalized * wanderDistance;
Vector2 displacement = new Vector2(
Mathf.Cos(wanderAngle) * wanderRadius,
Mathf.Sin(wanderAngle) * wanderRadius
);
Vector2 desired = (circleCenter + displacement).normalized * maxSpeed;
return desired - velocity;
}The key insight is that random input goes into the angle on the circle, not directly into the velocity — this creates smooth rather than jittery wandering.
Flow field following
A 2D grid of vectors describes environmental forces — wind, current, scent gradient. The agent queries the grid cell it occupies and steers toward that vector as its desired direction.
Vector2 FollowFlow(Vector2[,] field, int cols, int rows, float resolution)
{
int col = Mathf.Clamp((int)(position.x / resolution), 0, cols - 1);
int row = Mathf.Clamp((int)(position.y / resolution), 0, rows - 1);
Vector2 desired = field[col, row].normalized * maxSpeed;
return desired - velocity;
}Flow fields can be generated from Perlin noise for organic-looking migration, or from pathfinding results for crowd movement.
Path following
The agent follows a polyline path. Each frame it projects its predicted future position onto the nearest path segment, finds the point slightly ahead of that projection (the target), and seeks it. The projection uses a dot product (scalar projection) rather than trigonometry.
Dot product application: if one vector is normalised, dot(a, normalised(b)) gives the component of a along b — the closest point on the segment to the agent, without needing atan2.
Group behaviours: flocking
Flocking (Craig Reynolds, 1986 — “Boids”) produces convincing group motion from three local rules applied independently to each agent:
Separation
Steer away from nearby neighbours. Calculate the average of all vectors pointing away from neighbours within a threshold distance, weighted inversely by distance (closer = stronger repulsion).
Alignment
Steer toward the average velocity of nearby neighbours. The agent matches the heading of its local group.
Cohesion
Steer toward the average position of nearby neighbours. Equivalent to using seek on the group’s centroid.
void Flock(List<Vehicle> neighbours)
{
Vector2 sep = Separate(neighbours) * 1.5f; // weights
Vector2 ali = Align(neighbours) * 1.0f;
Vector2 coh = Cohere(neighbours) * 1.0f;
ApplyForce(sep);
ApplyForce(ali);
ApplyForce(coh);
}The emergent result: no bird is in charge. From these three cheap local rules, the flock avoids obstacles, reforms after splitting, and moves with organic variation. Adjusting the three weights changes flock personality.
Combining behaviours
Steering forces are vectors — they can be added with weights to produce blended behaviour:
| Combination | Effect |
|---|---|
| Seek + Separation | Mob that pursues player while not stacking |
| Wander + Cohesion | Roaming creature that stays near its group |
| Arrive + Alignment | Formation movement to a goal |
| Flow field + Separation | Crowd following a path without bunching |
The key implementation pattern: behaviour methods return a force rather than applying it directly, so the agent’s central update can weight and combine them before applying.
In practice (Unity)
Unity’s built-in NavMesh system handles pathfinding in complex 3D environments and should be preferred for navigation. Custom steering behaviours add value when:
- Agents need organic, non-snappy movement (arrive, wander)
- The world is open without obstacles (open-world patrol, space games)
- Large crowds or flocks must move cheaply without individual pathfinding costs
- Agents respond to dynamic force fields (wind zones, repulsion areas)
For NavMesh + steering combined: use NavMesh for destination pathfinding, then apply steering forces on the final approach for smooth deceleration and crowd separation.
For a simple 2D teaching version using sprite squares or dots, see:
- Vehicle2D.cs — reusable steering body with
Seek()andArrive() - SeekArriveDemo.cs — drives the vehicle toward a target or the mouse
- TargetFollower.cs — optional visual target marker
A minimal Unity translation of the steering loop looks like this:
private void Update()
{
Vector2 steering = Arrive(target.position);
ApplyForce(steering);
velocity += acceleration * Time.deltaTime;
velocity = Vector2.ClampMagnitude(velocity, maxSpeed);
transform.position += (Vector3)(velocity * Time.deltaTime);
acceleration = Vector2.zero;
}This version intentionally uses direct transform movement rather than Rigidbody2D so students can see the steering formula more clearly before introducing engine physics. See overview-unity-nature-of-code-examples for how this fits into the wider Unity route.
Performance note: Flocking requires every agent to check every other agent — O(n²). For large flocks, partition space with a spatial hash or Unity’s Physics.OverlapCircleNonAlloc to limit neighbour checks to a radius.
Scene setup for the Unity example
Use the seek-and-arrive scripts as a small 2D steering lab:
- Create a new 2D scene.
- Create a small square or triangle sprite named
Vehicle. - Add
Vehicle2Dto theVehicleobject. - Add
SeekArriveDemoto the same object. - Create another small sprite named
Target. - Add
TargetFollowerto theTargetobject if you want the target to follow the mouse. - Assign
Targetto thetargetfield onSeekArriveDemo, or leave it empty and use the mouse target mode. - Start with
useArriveenabled, then compare it with seek. - Press Play and watch whether the vehicle slows down near the target.
Keep the first test simple: one vehicle, one target, no obstacles. Add more agents only after the motion of one agent makes sense.
Code walkthrough
Vehicle2D owns the movement state. It stores velocity and acceleration, while the Transform stores the visible position. ApplyForce() adds a steering force to the current acceleration, then Update() integrates acceleration into velocity and velocity into position.
Seek() calculates a desired velocity from the vehicle to the target, then subtracts the current velocity. That subtraction is the core Reynolds steering formula. The result is clamped by maxForce, which prevents the vehicle from turning instantly.
Arrive() uses the same idea but changes target speed based on distance. Inside slowRadius, the target speed is interpolated down towards zero. That is why arrive can stop cleanly while seek tends to overshoot.
SeekArriveDemo chooses the target position, calls either vehicle.Arrive() or vehicle.Seek(), then passes the steering force into ApplyForce(). This keeps the behaviour choice separate from the movement body.
TargetFollower is only a helper. It converts mouse position from screen space to world space and moves a visible target marker there.
What to change first
| Change | Expected effect |
|---|---|
maxSpeed | changes the vehicle’s top speed |
maxForce | changes how sharply the vehicle can turn |
slowRadius | changes when arrive starts slowing down |
useArrive | switches between overshooting seek and smoother arrive |
orientToVelocity | rotates the sprite towards its movement direction |
Debugging checklist
- If the vehicle does not move, check that
SeekArriveDemoandVehicle2Dare on the same GameObject. - If the vehicle moves away from the target, check the target position and camera conversion.
- If movement looks jerky, lower
maxForceor check whether the target is jumping around. - If arrive never slows down, increase
slowRadius. - If the sprite points sideways, rotate the sprite art or adjust the visual child object.
Practice
Create a seek-and-arrive comparison:
- Run the scene with
useArriveenabled. - Record how the vehicle behaves near the target.
- Disable
useArriveand run again. - Increase
slowRadiusand run arrive again. - Write three sentences comparing overshoot, turn sharpness and stopping behaviour.
Self-test
- What does
desired velocity - current velocityproduce? - Why does
maxForcematter? - Why does seek often overshoot the target?
- What does
slowRadiuscontrol in arrive? - Why is
TargetFollowerseparate fromVehicle2D?
Answers
- It produces the steering force, which is the correction needed to move from the current velocity towards the desired velocity.
maxForcelimits how strongly the vehicle can steer in one update, so motion changes gradually.- Seek always aims for maximum speed towards the target and does not slow down as it gets close.
slowRadiuscontrols the distance at which the vehicle begins reducing its desired speed.TargetFolloweronly chooses a target position.Vehicle2Downs movement, which keeps responsibilities separate.
Trade-offs
| Steering behaviours | NavMesh / pathfinding | |
|---|---|---|
| Best for | Open environments, large crowds, organic motion | Complex environments, corridors, obstacles |
| Cost | O(n²) for flocking unless spatially partitioned | Bake time + navigation mesh memory |
| Motion quality | Smooth, momentum-respecting | Can feel snappy without additional steering |
| Obstacle avoidance | Requires explicit avoidance behaviour | Built-in |
| Dynamic obstacles | Reactive per-frame | Requires NavMesh rebake or local avoidance layer |
Examples
- Boids (Reynolds 1986) — the original flocking simulation
- Half-Life 2 — combine soldiers use separation + formation steering
- No Man’s Sky — flora fauna use flow-field following for migration
- Unity’s Crowd system — NavMesh + local avoidance is a commercial implementation of cohesion + separation
Flow field pathfinding for crowds
When many agents share a common goal, computing individual paths is wasteful. Flow field pathfinding (used in Supreme Commander 2, Fieldrunners 2) precomputes a direction vector for every cell in the world pointing toward the nearest goal:
- Build a cost field (terrain traversal costs per cell).
- Run a wavefront integration (similar to Dijkstra in reverse) from the goal outward, producing an integration field (accumulated cost-to-goal per cell).
- Convert the integration field into a flow field (normalised direction vectors).
- All agents share the flow field; each queries their current cell and steers in the indicated direction.
The key advantage: O(grid size) to build the field once; then O(1) per additional agent regardless of the agent count. Compare this to individual A*: O(path length × log n) per agent. For 1,000+ agents with a common goal, flow fields are dramatically more efficient.
Steering with a flow field is handled exactly like the FollowFlow behaviour already described above, often combined with separation and alignment for crowd appearance.
(Emerson, Game AI Pro 360, Ch. 7; Pentheny, Ch. 8; see source-game-ai-pro-360-movement-pathfinding)
Collision avoidance: beyond simple separation
Preplanned locomotion avoidance
Traditional steering avoids collisions reactively. For animation-driven locomotion systems (where characters follow pre-planned paths precisely), a more structured approach is possible (Anguelov, Ch. 6, Hitman: Absolution):
- Detect: Slide the agent’s collision sphere along its planned path; if it intersects another agent’s sphere within a look-ahead window, a collision is predicted.
- Resolve trivially: Try alternative speeds — slow down or stop until the path becomes collision-free.
- Resolve nontrivially: If speed change is insufficient (e.g., head-on collision, stationary blocker), compute an avoidance point offset orthogonally from the collision position and reroute the path through it using cubic Bézier curves.
This approach detects collisions around corners (the full path is checked, not just current velocity), avoids the oscillation artefacts common in reactive RVO systems, and handles 30 agents for ~0.5 ms on PS3.
RVO and ORCA
Reciprocal Velocity Obstacles (RVO) and Optimal Reciprocal Collision Avoidance (ORCA) are velocity-space approaches for multi-agent collision avoidance used in Unity’s NavMesh crowd system. Each agent computes a set of forbidden velocities that would lead to future collisions, then chooses the velocity closest to its goal velocity that avoids all forbidden regions. ORCA uses linear half-plane constraints to make this a linear programming problem solvable in milliseconds per agent.
Unity’s NavMeshAgent with avoidancePriority uses an ORCA variant. See context-steering for a fundamentally different approach that guarantees collision avoidance through context maps rather than velocity-space computation.
Formation movement
Formations can be navigated using steering circles (Bjore, Ch. 5, Game AI Pro 360): given the formation’s current position/facing and a target position/facing plus a turn radius, two steering circles are computed (one at start, one at goal). The path follows an arc on the start circle, a straight tangent segment, and an arc on the goal circle — exactly analogous to a Dubins path. This guarantees the formation arrives at the correct position and orientation without making turns tighter than the formation can execute.
Two styles of following rows within the formation:
- Column: each unit follows the unit ahead of it at a fixed distance — fluid but rows are not preserved.
- Band: rows are preserved; left-most or right-most unit of each row leads, and other units in the row maintain lateral alignment relative to it.
Limitations of traditional steering
(Fray, Game AI Pro 360, Ch. 14)
Traditional steering is a statistical method — most of the time it produces acceptable directions. Problems surface when:
- Individual agents are closely observed (a racing car, a companion NPC) and inconsistent movements are immersion-breaking.
- An avoid behaviour and a chase behaviour produce vectors that cancel, stalling the agent.
- Collision avoidance requires tight guarantees rather than best-effort results.
For these cases, context-steering (context maps) provides a principled alternative that guarantees collision avoidance while keeping individual behaviours small and stateless.
Related
- source-nature-of-code — primary source for Reynolds steering (Ch. 5)
- source-game-ai-pro-360-movement-pathfinding — source for flow fields, collision avoidance, formations, context steering
- context-steering — context maps as a principled alternative to vector-based steering
- pathfinding-algorithms — A*, JPS+, Theta* for global path planning
- game-ai-agent-design — the 3-layer model; steering is the middle layer
- npc-performance-at-scale — flow fields and hierarchical pathfinding for large crowds
- genetic-algorithms — evolving steering weights using GA
- procedural-generation — flow fields generated from Perlin noise
- unity-rigidbody2d — Unity’s physics model; AddForce is the Unity equivalent of ApplyForce
- unity-gamemanager-pattern — central manager for flocking populations
- cpp-classes-and-oop — virtual function pattern for polymorphic agent behaviours
- overview-unity-nature-of-code-examples — Unity/C# learning route for Nature of Code topics