Autonomous Movement

Steering Behaviors

25 min read Advanced Interactive Simulator

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.

Key Insight: Instead of setting position = target, you compute steeringForce = desired_velocity - current_velocity and add it to your agent every frame. Inertia emerges naturally.

The Five Core Behaviors

Seek
Move toward a target at full speed
Flee
Move away from a target
Arrive
Seek but slow down near target
Wander
Random organic exploration
Flock
Separation + Alignment + Cohesion

Live Simulator

Click the canvas to move the target. Switch behaviors to see how the agent reacts.

Mode: Seek

The Math: A Vector Agent

Every steering agent has three vectors: position, velocity, and acceleration. Each frame, we apply Newton's laws:

vehicle.js
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.

seek-flee.js
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.

arrive.js
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.

wander.js
// 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.

  1. Separation: Steer away from agents that are too close (avoid crowding)
  2. Alignment: Try to match the average heading of neighbors
  3. Cohesion: Steer toward the average position of neighbors (stick together)
flock.js
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
}
Combining Behaviors: Real games layer multiple steering forces together. A solider that flocks with teammates, seeks cover, AND avoids obstacles is just applying multiple weighted forces in a single applyForce() call each frame.