Chris Collins

Lab · Playground

Boids

Craig Reynolds' flocking model — three local rules per bird, and a flock emerges; the mouse is a predator.

Code blocks on this page are editable — change them and press Run.

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.

Flocking with separation, alignment and cohesion
// 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.3 and 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.02 gives sweeping starling-like arcs; 0.3 jittery 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.