Nobody is in charge
A flock of starlings turning as one looks choreographed. It is not. There is no leader, no plan and no bird with a picture of the flock in its head — each animal is responding only to the handful of neighbours it can see. In 1986 Craig Reynolds built a model of this for computer graphics, presented it at SIGGRAPH the following year, and gave the simulated creatures a name: boids.
The result is the standard textbook example of emergence — behaviour that exists at the level of the group but is nowhere written into the individual. There is no rule that says "form a flock". There are three rules about neighbours, and the flock is what happens.
The three rules
Each boid looks at the neighbours within a perception radius and computes three steering vectors:
SEPARATION steer AWAY from neighbours that are too close.
Sum of (self.pos − other.pos), usually weighted by
1/distance so that the nearest neighbour dominates.
Prevents collisions. Short range.
ALIGNMENT steer toward the AVERAGE HEADING of the neighbours.
Average their velocity vectors, then steer to match.
This is what makes the flock turn together. Mid range.
COHESION steer toward the AVERAGE POSITION of the neighbours
(their centre of mass). This is what keeps the flock
from dissolving. Long range.
Sum them with weights, and that is the whole model. The subtlety is in the order of magnitude of the weights, not in the rules: separation must be strong enough at short range to beat cohesion, or the flock collapses into a single point; cohesion must beat separation at long range, or the flock evaporates. The classic hierarchy is separation > alignment > cohesion in strength, with the radii ordered the other way — separation acts over a small radius, cohesion over a large one.
Steering, not teleporting
The rules produce desired velocities, not positions. Reynolds' steering formulation converts a desired velocity into a force, and this indirection is what makes the motion look alive rather than mechanical:
steer = desired − velocity // the correction needed steer = clamp(steer, maxForce) // a bird has finite muscles acceleration += steer * weight // accumulate all three rules velocity += acceleration; velocity = clamp(velocity, maxSpeed); // a bird has a top speed position += velocity; acceleration = 0; // reset every frame
The two clamps carry all the character. maxForce limits how sharply a boid can turn — it is the turning circle, and a low value produces the wide, sweeping arcs of a large bird while a high value gives the twitchy darting of a fish. maxSpeed keeps everything in the same regime. Without the force clamp, a boid snaps instantly onto its desired velocity and the flock looks like a swarm of magnets; with it, corrections take time and the group develops the sweeping, lagging turns that read as animal.
Two refinements are almost always worth adding. A field of view: real animals cannot see behind them, so ignore neighbours outside a forward-facing arc (a dot product against the heading is enough). And a velocity-matching lag is unnecessary — but constraining the neighbour count is not. Reynolds' original used a radius; later work on real starlings suggested that birds track a roughly fixed number of nearest neighbours rather than everything inside a fixed distance, which makes the flock's cohesion scale-free. Capping the neighbour list at the k nearest (k around 6–7) is cheap and produces noticeably more robust flocks at varying densities.
The neighbour search, which is the whole cost
Every boid must find its neighbours. Done naively, that is a scan over all other boids: O(n²) per frame. It is perfectly fine up to a few hundred boids and it is where every implementation starts, but it is also the wall — a thousand boids means a million distance checks per frame, and the frame rate falls off a cliff.
The fix is the same uniform spatial hash grid that particle fluids use. Choose a cell size equal to the largest perception radius; then a boid's neighbours can only lie in its own cell and the adjacent ones — 9 cells in 2D, 27 in 3D. Rebuild the grid each frame (a counting sort is O(n)) and the query becomes proportional to the local density instead of the total population.
cellSize = maxPerceptionRadius;
key(p) = hash(floor(p.x / cellSize), floor(p.y / cellSize),
floor(p.z / cellSize));
// each frame
buckets.clear();
for (const b of boids) buckets[key(b.pos)].push(b);
for (const b of boids)
for (const cell of the 27 cells around key(b.pos))
for (const other of buckets[cell]) { ...three rules... }
Compare the squared distance against the squared radius, never the distance — a square root per pair, over a million pairs, is pure waste. And accumulate the three rules in a single pass over the neighbour list rather than three separate passes: the sums for separation, alignment and cohesion can all be built from the same iteration.
Beyond the three rules
The model composes. Every addition is just another steering vector added to the accumulator, which is precisely why boids has survived forty years as the foundation of crowd and swarm systems:
obstacle avoidance cast a probe ahead of the boid; if it will hit
something, steer along the surface's normal. Weight
it far above the three social rules — a boid should
break formation rather than fly into a wall.
goal seeking a steady pull toward a target point or path. This
is how you get migration, or a flock that follows
the cursor.
predator / flee a strong, short-lived repulsion from a marked
agent. Add it and the flock splits and re-forms —
the "fountain effect" seen in real fish schools.
wander a small, slowly-rotating random steering force.
Stops an isolated boid from flying dead straight
forever, and stops a settled flock from freezing.
The same skeleton, with different weights, becomes a shoal of fish, a herd of cattle, a crowd of pedestrians, a cloud of insects, or the swarming enemies in a game. It is one of the shortest paths in all of programming from a page of code to something that looks unmistakably alive — and none of it requires any individual boid to know that a flock exists.
Frequently asked questions
What are the three boids rules?
Separation (steer away from neighbours that are too close), alignment (steer toward the average heading of nearby neighbours) and cohesion (steer toward their average position). Each boid sees only the neighbours within its perception radius; the flock is an emergent consequence, not a rule.
Why does my flock collapse into a single point, or fly apart?
The weights are out of balance. Cohesion pulls boids together and separation pushes them apart: if cohesion dominates at short range the flock implodes, and if separation dominates at long range it evaporates. Separation should be the strongest force but act over the smallest radius; cohesion should be the weakest but act over the largest.
How do you run thousands of boids at 60 fps?
Replace the naive O(n²) neighbour scan with a uniform spatial hash grid whose cell size equals the largest perception radius. Each boid then only inspects the 9 (2D) or 27 (3D) cells around it. Also compare squared distances instead of taking square roots, and accumulate all three rules in a single pass over the neighbour list.
Try it live
Everything above runs in your browser — open 3D Boids — Flocking and change the parameters while it is running. Nothing is installed, nothing is uploaded, the whole model lives in one tab.
▶ Open 3D Boids — Flocking simulation