The global pattern has a local cause.
From a distance, the flock reads as one organism. Move closer and that unity dissolves into agents with incomplete information.
Emergent motion, explained from the inside
Each agent can see only a few neighbors. Three tiny steering decisions turn that limited view into motion that feels coordinated, alive, and larger than any one boid.
In 1986, Craig Reynolds showed that convincing group motion does not require choreography. Give every bird-like agent the same small loop and let the pattern emerge.
A boid has no map of the flock, no destination shared with everyone, and no idea what the overall shape should be. It only reacts to what is nearby, one frame at a time.
Find the neighbors inside a limited radius and field of view.
Turn crowding, headings, and positions into three steering vectors.
Add the vectors, limit the turn, move forward, and repeat.
Too close? Turn away.
When another boid enters the red safety radius, the tracked boid proposes a turn in the opposite direction. Several nearby boids become one combined push, so the response is strongest where the crowd is densest.
separate(boid, neighbors)Codefunction separate(boid, neighbors) {
return neighbors
.filter((other) => distance(boid, other) < MIN_DISTANCE)
.reduce((steer, other) => ({
x: steer.x + boid.x - other.x,
y: steer.y + boid.y - other.y,
}), { x: 0, y: 0 });
}Where are the neighbors heading?
The small green arrows are nearby headings. Their average becomes the larger green proposal on the tracked boid. It does not copy any single neighbor; it continuously compromises with the group it can currently see.
align(boid, neighbors)Codefunction align(boid, neighbors) {
if (neighbors.length === 0) return { x: 0, y: 0 };
const average = mean(neighbors.map((other) => other.velocity));
return {
x: average.x - boid.velocity.x,
y: average.y - boid.velocity.y,
};
}Where is the local middle?
Average the positions of visible neighbors and a temporary center appears. The blue cross marks that point; the dashed arrow is the boid's proposal to move toward it. As neighbors change, the center moves too.
cohere(boid, neighbors)Codefunction cohere(boid, neighbors) {
if (neighbors.length === 0) return { x: 0, y: 0 };
const center = mean(neighbors.map((other) => other.position));
return {
x: center.x - boid.position.x,
y: center.y - boid.position.y,
};
}From a distance, the flock reads as one organism. Move closer and that unity dissolves into agents with incomplete information.
There are lots of ways to tune a flock. Change the weight of each proposal step and the same simple agents become a loose cloud, a directional school, or a nervous cluster.
Move through the field to disturb it. The flock will follow you.