Lab · Playground
Boids
Craig Reynolds' flocking model — three local rules per bird, and a flock emerges; the mouse is a predator.
In 1986 Craig Reynolds showed that a convincing flock needs no leader and no plan. Each "boid" looks only at the neighbours within a small radius and steers by three rules:
-
Separation — move away from neighbours that are too close.
-
Alignment — turn toward the average heading of the neighbours.
-
Cohesion — move toward the average position of the neighbours.
Add the three steering forces up (each with its own weight), cap the speed and the turning force, and flocks, streams and swirls appear on their own. This version adds a fourth rule — flee — from the mouse pointer, so you can split the flock and watch it re-form. Each boid is coloured by its heading, which makes the aligned groups easy to see.
// Things to change: the three weights, the radius, and the speed limits.
const N = 140;
const RADIUS = 46; // how far a boid can see
const SEPARATION = 1.6;
const ALIGNMENT = 1.0;
const COHESION = 0.8;
const FLEE = 3.0; // from the mouse
const MAX_SPEED = 3.2;
const MAX_FORCE = 0.06;
let boids = [];
function setup() {
createCanvas(min(windowWidth, 640), 320);
colorMode(HSB, 360, 100, 100, 100);
for (let i = 0; i < N; i++) {
boids.push({ pos: createVector(random(width), random(height)), vel: p5.Vector.random2D().mult(random(1, MAX_SPEED)) });
}
}
// Turn a desired direction into a capped steering force.
function steer(b, desired) {
if (desired.magSq() === 0) return desired;
desired.setMag(MAX_SPEED).sub(b.vel);
return desired.limit(MAX_FORCE);
}
function flock(b) {
const sep = createVector(), ali = createVector(), coh = createVector();
let n = 0;
for (const o of boids) {
if (o === b) continue;
const d = p5.Vector.dist(b.pos, o.pos);
if (d > RADIUS) continue;
n++;
ali.add(o.vel);
coh.add(o.pos);
if (d < RADIUS * 0.5) sep.add(p5.Vector.sub(b.pos, o.pos).div(max(d * d, 1))); // closer pushes harder
}
const acc = createVector();
if (n) {
acc.add(steer(b, sep).mult(SEPARATION));
acc.add(steer(b, ali).mult(ALIGNMENT));
acc.add(steer(b, coh.div(n).sub(b.pos)).mult(COHESION));
}
if (mouseX > 0 && mouseX < width && mouseY > 0 && mouseY < height) {
const away = p5.Vector.sub(b.pos, createVector(mouseX, mouseY));
if (away.mag() < RADIUS * 2) acc.add(steer(b, away).mult(FLEE));
}
return acc;
}
function draw() {
background(220, 35, 12, 35); // translucent: short motion blur
const forces = boids.map(flock); // everyone decides first, then everyone moves
boids.forEach((b, i) => {
b.vel.add(forces[i]).limit(MAX_SPEED);
b.pos.add(b.vel);
b.pos.x = (b.pos.x + width) % width; // wrap around the edges
b.pos.y = (b.pos.y + height) % height;
const a = b.vel.heading();
push();
translate(b.pos.x, b.pos.y);
rotate(a);
noStroke();
fill((degrees(a) + 360) % 360, 65, 100);
triangle(7, 0, -5, 3.5, -5, -3.5);
pop();
});
}
Things to change
-
COHESION = 0— the flock breaks into loose parallel streams that never clump. -
ALIGNMENT = 0— boids huddle in swarming knots like gnats, with no common heading. -
SEPARATION = 4— a spaced-out crystal-like formation;0.3and they pile on top of each other. -
RADIUS— a short sight radius (20) makes many small flocks; a long one (120) one big flock. -
MAX_FORCE— how sharply a boid can turn.0.02gives sweeping starling-like arcs;0.3jittery fish. -
N— every boid checks every other one, so the cost grows with N²; past ~400 a phone will struggle. A grid of buckets (spatial hashing) is the usual fix.