Steering Behaviors
Beyond FSM: Natural Movement
You've built a bot that chases a player using FSM. But watch it move — it snaps instantly to top speed, turns on a dime, and feels robotic. That's because you're controlling its position, not its velocity.
Steering Behaviors, introduced by Craig Reynolds in 1987, work by calculating a desired velocity and applying a steering force to gradually nudge the agent toward it. The result is organic, believable movement — like a bird turning in a flock, not a chess piece jumping.
position = target, you compute
steeringForce = desired_velocity - current_velocity and add it to your agent every frame.
Inertia emerges naturally.
The Five Core Behaviors
Live Simulator
Click the canvas to move the target. Switch behaviors to see how the agent reacts.
The Math: A Vector Agent
Every steering agent has three vectors: position, velocity, and
acceleration. Each frame, we apply Newton's laws:
class Vehicle {
constructor(x, y) {
this.pos = { x, y };
this.vel = { x: 0, y: 0 };
this.acc = { x: 0, y: 0 };
this.maxSpeed = 4;
this.maxForce = 0.15; // How quickly it can turn
}
applyForce(force) {
this.acc.x += force.x;
this.acc.y += force.y;
}
update() {
// Euler integration
this.vel.x += this.acc.x;
this.vel.y += this.acc.y;
// Clamp speed
const speed = Math.hypot(this.vel.x, this.vel.y);
if (speed > this.maxSpeed) {
this.vel.x = (this.vel.x / speed) * this.maxSpeed;
this.vel.y = (this.vel.y / speed) * this.maxSpeed;
}
this.pos.x += this.vel.x;
this.pos.y += this.vel.y;
// Reset acceleration each frame
this.acc.x = 0;
this.acc.y = 0;
}
}
Seek & Flee
Seek computes a "desired velocity" pointing directly at the target at max speed, then steers toward it. Flee is simply the same force reversed.
function seek(vehicle, target) {
// 1. Desired velocity: point toward target at max speed
const dx = target.x - vehicle.pos.x;
const dy = target.y - vehicle.pos.y;
const dist = Math.hypot(dx, dy);
const desired = {
x: (dx / dist) * vehicle.maxSpeed,
y: (dy / dist) * vehicle.maxSpeed
};
// 2. Steering = desired - current velocity
let steer = {
x: desired.x - vehicle.vel.x,
y: desired.y - vehicle.vel.y
};
// 3. Limit force magnitude
steer = limit(steer, vehicle.maxForce);
return steer;
}
function flee(vehicle, threat) {
// Identical to seek, but negate the result
const steer = seek(vehicle, threat);
return { x: -steer.x, y: -steer.y };
}
Arrive: Smooth Deceleration
Arrive is Seek with a "slowing radius". When the agent enters the radius, its desired speed scales down linearly to 0. This prevents the agent from orbiting the target at full speed.
function arrive(vehicle, target, slowingRadius = 80) {
const dx = target.x - vehicle.pos.x;
const dy = target.y - vehicle.pos.y;
const dist = Math.hypot(dx, dy);
// Inside the slowing radius: scale speed down
let desiredSpeed = vehicle.maxSpeed;
if (dist < slowingRadius) {
desiredSpeed = vehicle.maxSpeed * (dist / slowingRadius);
}
const desired = {
x: (dx / dist) * desiredSpeed,
y: (dy / dist) * desiredSpeed
};
const steer = {
x: desired.x - vehicle.vel.x,
y: desired.y - vehicle.vel.y
};
return limit(steer, vehicle.maxForce);
}
Wander: Organic Exploration
Wander avoids jittery random walks by projecting a circle ahead of the agent and randomly nudging an angle on that circle each frame. The result is smoothly curving, organic movement.
// Each agent needs a persistent wander angle
vehicle.wanderAngle = Math.random() * Math.PI * 2;
function wander(vehicle) {
const WANDER_DIST = 60; // How far ahead the wander circle is
const WANDER_RADIUS = 30; // Radius of the wander circle
const WANDER_JITTER = 0.3; // How fast the angle changes
// 1. Slightly nudge the angle
vehicle.wanderAngle += (Math.random() - 0.5) * WANDER_JITTER;
// 2. Project circle center ahead of vehicle
const speed = Math.hypot(vehicle.vel.x, vehicle.vel.y) || 1;
const circleCenter = {
x: vehicle.pos.x + (vehicle.vel.x / speed) * WANDER_DIST,
y: vehicle.pos.y + (vehicle.vel.y / speed) * WANDER_DIST
};
// 3. Target is a point on the circle
const wanderTarget = {
x: circleCenter.x + Math.cos(vehicle.wanderAngle) * WANDER_RADIUS,
y: circleCenter.y + Math.sin(vehicle.wanderAngle) * WANDER_RADIUS
};
return seek(vehicle, wanderTarget);
}
Flocking: Emergent Group Behavior
Craig Reynolds' famous "boids" algorithm creates realistic flocking using just three rules combined. No central controller — each agent only looks at its neighbors.
- Separation: Steer away from agents that are too close (avoid crowding)
- Alignment: Try to match the average heading of neighbors
- Cohesion: Steer toward the average position of neighbors (stick together)
function flock(vehicle, allBoids) {
const NEIGHBOR_RADIUS = 80;
const SEPARATION_RADIUS = 30;
let sep = { x: 0, y: 0 }; // Separation force
let ali = { x: 0, y: 0 }; // Alignment force
let coh = { x: 0, y: 0 }; // Cohesion force
let neighborCount = 0;
let separateCount = 0;
for (const other of allBoids) {
if (other === vehicle) continue;
const dx = vehicle.pos.x - other.pos.x;
const dy = vehicle.pos.y - other.pos.y;
const dist = Math.hypot(dx, dy);
// Separation: push away from very close agents
if (dist < SEPARATION_RADIUS && dist > 0) {
sep.x += dx / dist; // Normalized + weighted by distance
sep.y += dy / dist;
separateCount++;
}
// Alignment + Cohesion: look at wider neighborhood
if (dist < NEIGHBOR_RADIUS) {
ali.x += other.vel.x;
ali.y += other.vel.y;
coh.x += other.pos.x;
coh.y += other.pos.y;
neighborCount++;
}
}
if (separateCount > 0) {
sep.x /= separateCount;
sep.y /= separateCount;
}
if (neighborCount > 0) {
ali.x /= neighborCount; // Average velocity → seek that direction
ali.y /= neighborCount;
coh.x /= neighborCount; // Average position → seek that point
coh.y /= neighborCount;
}
// Combine forces with weights
vehicle.applyForce(scale(sep, 1.5)); // Separation has highest priority
vehicle.applyForce(scale(ali, 1.0));
vehicle.applyForce(seek(vehicle, coh)); // Cohesion uses seek behavior
}
applyForce() call each frame.