Steering Behaviours
Grid movement is discrete: an entity occupies a tile, moves to an adjacent tile. That works for turn-based games and many real-time ones. But the moment you want an enemy that smoothly curves toward a target, a projectile that arcs, or a group of guards that spread out without running into each other, you need movement that operates in continuous space.
Steering behaviours — introduced by Craig Reynolds in 1999 — describe motion as the difference between where an entity is heading and where it wants to head. That difference, the steering force, is applied to the entity’s velocity each frame. The result is motion that looks purposeful and organic rather than grid-snapped.
In a tile-based game, steering operates in pixel space. The tile grid still governs collision and game logic; steering governs how entities move between those checks.
What You’ll Master:
- The velocity + steering force model
- Seek: move toward a target
- Flee: move away from a threat
- Arrive: seek with deceleration as you approach
- Wander: autonomous, believable idle movement
- Combining behaviours with weighted sums
Prerequisites:
- No specific prior tutorial — but understanding the standard character object and pixel-space movement helps
Move the mouse across the canvas. The line extending from each entity is its velocity vector — you can see the steering force gradually bending it toward or away from your cursor. The purple wanderer ignores the mouse entirely.
The Model
Each entity has a position and a velocity. Steering behaviours produce a desired velocity — where the entity wants to go and how fast. The difference between desired and current velocity is the steering force, applied as acceleration each frame:
const entity = {
x: 150, y: 120,
vx: 0, vy: 0,
maxSpeed: 2.5,
maxForce: 0.15, // how quickly steering can change direction
};
// Apply a steering force
function applyForce(entity, fx, fy) {
// Clamp force to maxForce
const mag = Math.sqrt(fx * fx + fy * fy);
if (mag > entity.maxForce) { fx = fx / mag * entity.maxForce; fy = fy / mag * entity.maxForce; }
entity.vx += fx;
entity.vy += fy;
// Clamp speed to maxSpeed
const speed = Math.sqrt(entity.vx * entity.vx + entity.vy * entity.vy);
if (speed > entity.maxSpeed) {
entity.vx = entity.vx / speed * entity.maxSpeed;
entity.vy = entity.vy / speed * entity.maxSpeed;
}
}
// Each frame: apply force, then move
applyForce(entity, steeringForce.x, steeringForce.y);
entity.x += entity.vx;
entity.y += entity.vy;
maxForce controls agility — a low value produces sluggish, ship-like turning; a high value produces snappy, responsive movement.
Seek
The simplest behaviour: compute the desired velocity pointing from the entity to the target at maximum speed, then steer toward it.
function seek(entity, targetX, targetY) {
const dx = targetX - entity.x;
const dy = targetY - entity.y;
const dist = Math.sqrt(dx * dx + dy * dy);
if (dist === 0) return { x: 0, y: 0 };
// Desired velocity: toward target at max speed
const desiredVX = (dx / dist) * entity.maxSpeed;
const desiredVY = (dy / dist) * entity.maxSpeed;
// Steering force: difference between desired and current velocity
return {
x: desiredVX - entity.vx,
y: desiredVY - entity.vy,
};
}
Seek never slows down. The entity will overshoot and oscillate around the target if you don’t handle arrival separately.
Flee
The inverse of seek — desired velocity points away from the threat:
function flee(entity, threatX, threatY) {
const dx = entity.x - threatX; // reversed
const dy = entity.y - threatY;
const dist = Math.sqrt(dx * dx + dy * dy);
if (dist === 0) return { x: 0, y: 0 };
return {
x: (dx / dist) * entity.maxSpeed - entity.vx,
y: (dy / dist) * entity.maxSpeed - entity.vy,
};
}
Flee can be combined with an activation radius — the entity only flees when the threat is within a certain distance:
function fleeIfClose(entity, threatX, threatY, radius) {
const dx = entity.x - threatX;
const dy = entity.y - threatY;
const dist = Math.sqrt(dx * dx + dy * dy);
if (dist > radius) return { x: 0, y: 0 }; // outside radius — no force
return flee(entity, threatX, threatY);
}
Arrive
Seek, but the desired speed scales down as the entity approaches the target. This prevents overshoot:
function arrive(entity, targetX, targetY, slowingRadius = 60) {
const dx = targetX - entity.x;
const dy = targetY - entity.y;
const dist = Math.sqrt(dx * dx + dy * dy);
if (dist < 2) return { x: -entity.vx, y: -entity.vy }; // brake to a stop
// Inside the slowing radius, scale speed proportionally to distance
const speed = dist < slowingRadius
? entity.maxSpeed * (dist / slowingRadius)
: entity.maxSpeed;
const desiredVX = (dx / dist) * speed;
const desiredVY = (dy / dist) * speed;
return {
x: desiredVX - entity.vx,
y: desiredVY - entity.vy,
};
}
arrive is the correct choice for guards moving to a patrol point or for any entity that should stop cleanly at a destination.
Wander
Wander produces smooth, believable autonomous movement without a target. The idea: project a circle ahead of the entity, then each frame move the target point slightly around the circle’s edge. The circle drifts in a continuous, organic path rather than snapping between random waypoints:
const WANDER_RADIUS = 40; // radius of the projected circle
const WANDER_DISTANCE = 60; // how far ahead the circle sits
const WANDER_JITTER = 0.3; // how fast the angle changes
function wander(entity) {
// Drift the wander angle slightly each frame
entity.wanderAngle += (Math.random() - 0.5) * WANDER_JITTER;
// Project the circle ahead in the current direction of travel
const speed = Math.sqrt(entity.vx ** 2 + entity.vy ** 2);
const headingX = speed > 0.01 ? entity.vx / speed : Math.cos(entity.wanderAngle);
const headingY = speed > 0.01 ? entity.vy / speed : Math.sin(entity.wanderAngle);
const circleCentreX = entity.x + headingX * WANDER_DISTANCE;
const circleCentreY = entity.y + headingY * WANDER_DISTANCE;
// Target is a point on the edge of the circle
const wanderTargetX = circleCentreX + Math.cos(entity.wanderAngle) * WANDER_RADIUS;
const wanderTargetY = circleCentreY + Math.sin(entity.wanderAngle) * WANDER_RADIUS;
return seek(entity, wanderTargetX, wanderTargetY);
}
Wander requires one persistent field on the entity — wanderAngle — which accumulates the drift over time.
Combining Behaviours
The real power of steering behaviours is that they combine with weighted addition. Each behaviour returns a force vector; add them together and the entity responds to all of them simultaneously:
function computeSteering(entity, player, guards) {
// Avoid players — high weight
const fleeForce = fleeIfClose(entity, player.x, player.y, 90);
// Avoid other guards — medium weight (separation)
const separationForce = { x: 0, y: 0 };
for (const other of guards) {
if (other === entity) continue;
const sf = fleeIfClose(entity, other.x, other.y, 45);
separationForce.x += sf.x;
separationForce.y += sf.y;
}
// Wander — low weight (background behaviour)
const wanderForce = wander(entity);
return {
x: fleeForce.x * 2.0 + separationForce.x * 1.0 + wanderForce.x * 0.5,
y: fleeForce.y * 2.0 + separationForce.y * 1.0 + wanderForce.y * 0.5,
};
}
Weights control priority. A high-weight flee will dominate when the player is close; wander fills in when there’s nothing to react to.
Steering on a Tile Grid
Steering and tile grids aren’t mutually exclusive. The common pattern:
- Use the tile grid for collision and game logic (what tile am I on, what can I pick up, what blocks me)
- Use steering for movement between the collision checks
function tickEntity(entity, player) {
// Compute steering force
const force = arrive(entity, player.x, player.y);
applyForce(entity, force.x, force.y);
// Tentative new position
const newX = entity.x + entity.vx;
const newY = entity.y + entity.vy;
// Tile collision check (from the collision tutorial)
if (!wouldCollide(newX, newY, entity.width, entity.height, map)) {
entity.x = newX;
entity.y = newY;
} else {
// Stop the velocity component that caused the collision
entity.vx = 0;
entity.vy = 0;
}
}
The tile grid defines the passable space; steering decides how to traverse it. For enemies that follow complex paths, combine steering with A*: A* produces a waypoint list, steering drives smooth movement along it using arrive at each waypoint.
Next up: Bringing it Together — the full stealth game demo, combining pathfinding, FSM, and influence maps.