Flocking and Boids

Watch a flock of starlings pour across an evening sky — a murmuration — folding, splitting, rejoining, a thousand birds moving as one shimmering sheet with no conductor and no plan. For a long time people assumed such coordination needed a leader, or telepathy, or a global choreography beamed into every bird. In 1987 Craig Reynolds showed it needs none of those. He built simulated birds he called boids (“bird-oid objects”), gave each one just three simple rules about its nearby neighbours, and let them fly. Realistic flocking emerged — lifelike, unrepeatable, and entirely un-scripted.

This is one of the founding demonstrations of emergence: complex, organic group behaviour arising from many agents each following the same tiny local recipe. The same three rules, re-weighted, give you schooling fish, herding sheep, and swarming insects. Boids have flown in films (the bats and penguins of Batman Returns), games, and countless crowd shots since. Here we pin down the three rules precisely, work the vector arithmetic for a single boid, and see why the interaction must stay local.

Three rules, no leader

Each boid is a point with a position \mathbf{p} and a velocity \mathbf{v}. On every frame it looks only at its neighbours — the other boids within a radius r (optionally also within a field of view) — and computes three steering forces. A steering force is a desired nudge to the velocity: an acceleration.

That is the whole engine. There is no leader, no global path, no central plan — every boid runs the identical local computation, and the flock is what all those little decisions add up to. Separation pushes boids apart; cohesion pulls them together; alignment lines them up. Held in tension, they produce a group that is neither a rigid lattice nor a scattering cloud, but a living, breathing flock.

From three forces to one motion

Let a boid have neighbours j \in N (those within radius r). The three rules are three vectors:

\mathbf{s} = \sum_{j \in N} \frac{\mathbf{p} - \mathbf{p}_j}{\lVert \mathbf{p} - \mathbf{p}_j \rVert} \quad(\text{separation}), \qquad \mathbf{a} = \frac{1}{|N|}\sum_{j \in N} \mathbf{v}_j \quad(\text{alignment}), \mathbf{c} = \left(\frac{1}{|N|}\sum_{j \in N} \mathbf{p}_j\right) - \mathbf{p} \quad(\text{cohesion, toward the neighbours' centre}).

Each rule earns a weight — w_s, w_a, w_c — and the weighted sum is the boid's steering acceleration. We then integrate: acceleration changes velocity, velocity changes position (clamping the speed so boids don't run away):

\mathbf{a}_{\text{steer}} = w_s\,\mathbf{s} + w_a\,\mathbf{a} + w_c\,\mathbf{c}, \qquad \mathbf{v}' = \operatorname{clamp}\big(\mathbf{v} + \mathbf{a}_{\text{steer}}\,\Delta t\big), \qquad \mathbf{p}' = \mathbf{p} + \mathbf{v}'\,\Delta t.

The weights are the personality dials. Crank cohesion and drop separation and you get a tight, dense school of fish; raise separation and loosen cohesion and you get a diffuse, gassy swarm; push alignment hard and the flock stiffens into a marching formation. Tuning those three numbers is how an animator directs the crowd's character without ever scripting a single trajectory.

In pseudocode the per-boid update is short — a superset of JavaScript, so this reads as untyped TypeScript:

function steer(me, boids, r, ws, wa, wc) { let sep = [0, 0], ali = [0, 0], coh = [0, 0], n = 0; for (const b of boids) { if (b === me) continue; const d = dist(me.p, b.p); if (d > r) continue; // LOCAL only: ignore far boids sep = add(sep, scale(sub(me.p, b.p), 1 / d)); // away, unit-ish ali = add(ali, b.v); // sum neighbour velocities coh = add(coh, b.p); // sum neighbour positions n++; } if (n === 0) return [0, 0]; // no neighbours → no steering ali = scale(ali, 1 / n); // average heading coh = sub(scale(coh, 1 / n), me.p); // toward centre return add(add(scale(sep, ws), scale(ali, wa)), scale(coh, wc)); }

Seeing the three forces on one boid

Below is our boid B at the origin with two neighbours inside its neighbour radius r (the dashed circle). The three coloured arrows are the steering vectors it computes this frame: separation pushing it away from the crowd, cohesion pulling it toward the neighbours' centre, and alignment matching their average heading. The boid's actual acceleration is the weighted sum of these three.

Notice both neighbours sit inside the circle — only those count. A boid far outside the radius exerts no force at all, which is exactly what makes flocking cheap and realistic: real birds react to the handful of neighbours they can actually see, not to the whole flock.

Worked example: one boid, two neighbours

Put boid B at the origin, \mathbf{p} = (0,0), with velocity \mathbf{v} = (1, 0). It has two neighbours inside its radius:

\mathbf{p}_1 = (2, 0),\; \mathbf{v}_1 = (0, 1); \qquad \mathbf{p}_2 = (0, 2),\; \mathbf{v}_2 = (0, -1).

Separation — sum the unit vectors pointing from each neighbour back to B. From neighbour 1: \mathbf{p} - \mathbf{p}_1 = (-2, 0), unit (-1, 0). From neighbour 2: \mathbf{p} - \mathbf{p}_2 = (0, -2), unit (0, -1).

\mathbf{s} = (-1, 0) + (0, -1) = (-1, -1).

So separation steers B down-and-left, away from the two neighbours that sit up-and-right of it. Good — that avoids the crowd.

Alignment — average the neighbours' velocities:

\mathbf{a} = \tfrac{1}{2}\big[(0, 1) + (0, -1)\big] = (0, 0).

The two neighbours head in opposite vertical directions, so their average heading cancels — alignment contributes nothing this frame.

Cohesion — steer toward the neighbours' centre of mass, then subtract our own position:

\bar{\mathbf{p}} = \tfrac{1}{2}\big[(2, 0) + (0, 2)\big] = (1, 1), \qquad \mathbf{c} = \bar{\mathbf{p}} - \mathbf{p} = (1, 1).

Cohesion points up-and-right, straight at the neighbours' centre — exactly opposite to separation, as it should be: one keeps the flock together, the other keeps it from collapsing. Now combine with example weights w_s = 1.5,\; w_a = 1,\; w_c = 1:

\mathbf{a}_{\text{steer}} = 1.5\,(-1, -1) + 1\,(0, 0) + 1\,(1, 1) = (-0.5, -0.5).

Because we weighted separation a little heavier than cohesion, the net steering is a gentle push away from the crowd — B keeps its comfortable spacing while still belonging to the flock. Dial those weights differently and the very same neighbour arrangement produces a different move.

This is the heart of emergence — and it genuinely surprised people. No boid stores the flock's shape, counts the population, or plans where the group is going. Each one only ever answers a purely local question: “Given the few boids I can see right now, which way should I nudge?” The graceful global pattern — the murmuration folding and splitting — exists nowhere in any individual; it lives only in the interaction of many identical local decisions. It is the same idea as an ant colony finding food or a market finding a price: sophisticated collective behaviour with no central controller. Reynolds' boids became a textbook example precisely because the code is so short and the result so lifelike that it makes emergence tangible. Related steering ideas — obstacle avoidance, goal-seeking (a boid also nudged toward a target), fleeing a predator, path following — bolt straight on as extra weighted forces, which is the subject of steering behaviours and, scaled up with pathfinding and animation, of crowd simulation.

Two classic ways to wreck a flock. First, unbalanced weights. The three forces only produce lifelike motion when they're held in tension — push any one too hard and the flock degenerates:

Good flocking lives in a narrow band where all three roughly balance. Second, keep it local. It is tempting to let every boid react to all the others — but that is both unrealistic (a real bird can't see the whole flock, only its neighbours) and expensive: all-pairs interaction is O(n^2), which melts for large flocks. Restrict each boid to neighbours within radius r and use a spatial hash (a grid) to find them, and the cost drops to about O(n \cdot k) for k neighbours each — the flock both looks right and scales.