Boids Flocking Simulation
Implement Craig Reynolds' Boids algorithm — autonomous agents that exhibit flocking behavior through three simple rules: separation, alignment, and cohesion.
Paste the complete example into main.js in the editor, then click Restart.
The Three Rules
| Rule | What It Does | Visual Effect |
|---|---|---|
| Separation | Steer away from nearby boids | Prevents crowding |
| Alignment | Match velocity of nearby boids | Creates coordinated movement |
| Cohesion | Move toward center of nearby boids | Keeps the flock together |
Complete Implementation
// 1. State Setup
if (!state.init) {
state.boids = [];
state.boidCount = 60;
state.perceptionRadius = 60;
state.maxSpeed = 120;
state.maxForce = 40;
for (let i = 0; i < state.boidCount; i++) {
state.boids.push({
x: Math.random() * w,
y: Math.random() * h,
vx: (Math.random() - 0.5) * state.maxSpeed,
vy: (Math.random() - 0.5) * state.maxSpeed,
});
}
state.init = true;
}
// 2. Physics Step — Boids rules
const perception = state.perceptionRadius;
const maxSpeed = state.maxSpeed;
const maxForce = state.maxForce;
for (const boid of state.boids) {
let sepX = 0, sepY = 0, sepCount = 0;
let aliX = 0, aliY = 0, aliCount = 0;
let cohX = 0, cohY = 0, cohCount = 0;
for (const other of state.boids) {
if (other === boid) continue;
const dx = other.x - boid.x;
const dy = other.y - boid.y;
const dist = Math.sqrt(dx * dx + dy * dy);
if (dist < perception) {
// Separation: steer away from close neighbors
if (dist < perception * 0.5 && dist > 0) {
sepX -= dx / dist;
sepY -= dy / dist;
sepCount++;
}
// Alignment: match average velocity
aliX += other.vx;
aliY += other.vy;
aliCount++;
// Cohesion: steer toward average position
cohX += other.x;
cohY += other.y;
cohCount++;
}
}
// Compute steering forces
let fx = 0, fy = 0;
if (sepCount > 0) {
fx += (sepX / sepCount) * 1.5;
fy += (sepY / sepCount) * 1.5;
}
if (aliCount > 0) {
const avgVx = aliX / aliCount;
const avgVy = aliY / aliCount;
fx += (avgVx - boid.vx) * 0.05;
fy += (avgVy - boid.vy) * 0.05;
}
if (cohCount > 0) {
const avgX = cohX / cohCount;
const avgY = cohY / cohCount;
fx += (avgX - boid.x) * 0.005;
fy += (avgY - boid.y) * 0.005;
}
// Apply steering force (clamped)
const fMag = Math.sqrt(fx * fx + fy * fy);
if (fMag > maxForce) {
fx = (fx / fMag) * maxForce;
fy = (fy / fMag) * maxForce;
}
boid.vx += fx * dt;
boid.vy += fy * dt;
// Clamp speed
const speed = Math.sqrt(boid.vx * boid.vx + boid.vy * boid.vy);
if (speed > maxSpeed) {
boid.vx = (boid.vx / speed) * maxSpeed;
boid.vy = (boid.vy / speed) * maxSpeed;
}
// Update position
boid.x += boid.vx * dt;
boid.y += boid.vy * dt;
// Wrap around edges
if (boid.x < 0) boid.x = w;
if (boid.x > w) boid.x = 0;
if (boid.y < 0) boid.y = h;
if (boid.y > h) boid.y = 0;
}
// 3. Render Return
return {
particles: state.boids.map((b) => ({
x: b.x,
y: b.y,
r: 4,
color: '#8B5CF6',
})),
vectors: state.boids.filter((_, i) => i % 4 === 0).map((b) => ({
x: b.vx * 0.15,
y: b.vy * 0.15,
ox: b.x,
oy: b.y,
color: '#22C55E',
})),
text: {
'Boids': state.boids.length,
'Perception': state.perceptionRadius + ' px',
},
};📐 MATH CHECK Each rule produces a steering vector. The final force is the weighted sum:
- Separation:
steer = -sum(r̂_ij) / N(normalized away from neighbors)- Alignment:
steer = v_avg - v_self(match group velocity)- Cohesion:
steer = p_avg - p_self(move toward group center)Weights (1.5, 0.05, 0.005) are tuned for visual appeal. Increase separation weight to spread the flock; increase cohesion weight to cluster tightly.
Parameter Tuning
| Parameter | Effect | Default | Try |
|---|---|---|---|
boidCount | Number of agents | 60 | 30, 100, 200 |
perceptionRadius | How far each boid "sees" | 60 | 40, 100, 150 |
maxSpeed | Maximum velocity | 120 | 60, 200 |
maxForce | Maximum steering acceleration | 40 | 20, 80 |
| Separation weight | Avoid crowding | 1.5 | 0.5, 3.0 |
| Alignment weight | Match direction | 0.05 | 0.01, 0.1 |
| Cohesion weight | Move toward center | 0.005 | 0.001, 0.02 |
💡 PRO TIP Separation should always have the highest weight. If boids overlap or cluster too tightly, increase it. If the flock flies apart, increase cohesion. If the flock rotates instead of moving forward, increase alignment.
Edge Wrapping vs. Bounding
The example uses toroidal wrapping (boids wrap from one edge to the opposite). For bounding behavior instead:
// Replace the wrap logic with:
const margin = 50;
let turnX = 0, turnY = 0;
if (boid.x < margin) turnX = 1;
if (boid.x > w - margin) turnX = -1;
if (boid.y < margin) turnY = 1;
if (boid.y > h - margin) turnY = -1;
boid.vx += turnX * maxForce * dt;
boid.vy += turnY * maxForce * dt;This makes boids turn away from edges, creating a contained flock.
Extending This Example
- Add predator avoidance: spawn a predator on click, boids flee from it
- Implement food seeking: boids cluster toward a food source
- Add different species with different behaviors (larger boids dominate)
- Plot the center of mass trajectory over time
- Add obstacle avoidance with circles the boids must steer around
Same Result with ai.flock()
The whole simulation above collapses to a few lines with the ai API:
import ai from 'ai';
if (!state.flock) {
state.flock = Array.from({ length: 60 }, () =>
ai.agent({
x: Math.random() * w, y: Math.random() * h,
maxSpeed: 120, maxForce: 45,
brain: (me, sense) =>
ai.flock(me, ai.neighbors(state.flock, me, 60),
{ perception: 60, sep: 1.5, ali: 1, coh: 1, maxForce: 45 }),
})
);
}
for (const b of state.flock) {
ai.think(b, ai.sense(b, w, h, time), dt);
ai.wrap(b, w, h);
}
return {
particles: state.flock.map((b) => ({ x: b.x, y: b.y, r: 4, color: '#8B5CF6' })),
text: { Boids: state.flock.length },
};The brain decides the steering force; ai.flock computes separation + alignment + cohesion with the same weights you tuned above. Add cursor interaction for free — see the AI cookbook.