5.2.1 Boids — Flocking from Three Rules
Implement Craig Reynolds's 1987 boids algorithm — separation, alignment, cohesion — and watch coordinated flocks emerge from agents that only see their nearest neighbours.
Overview
A starling murmuration looks like one organism, but each bird is following three rules and looking at maybe seven neighbours. Craig Reynolds published that observation in 1987 as the boids algorithm — short for bird-oid objects — and it became the canonical demonstration of emergent behaviour, the way that complex global patterns arise from agents that share no global view [1]. In this lesson you will implement separation, alignment, and cohesion, render 30 boids into a 400×400 GIF, and feel the moment when the system tips from random drift into coordinated motion. The same three-rule recipe ran the bat swarm in Batman Returns (1992) and the wildebeest stampede in The Lion King (1994); it has not aged a day.
Learning objectives
- Articulate what “emergent behaviour” means — and why local rules are sufficient to produce global coordination.
- Implement the three boids rules: separation (avoid crowding), alignment (match heading), and cohesion (stay together).
- Use a perception radius to give each boid a local neighbourhood — and recognise that radius as the algorithm’s only knob with units.
- Tune rule weights to dial the flock from “diffuse cloud” to “tight ball” to “parallel stream”.
Quick start — see the flock in action
The simplest possible flock — cohesion only, no neighbours, just everyone steering toward the centre of mass.
import numpy as np
from PIL import Image, ImageDraw
import imageio.v2 as imageio
positions = np.random.rand(30, 2) * 400
velocities = (np.random.rand(30, 2) - 0.5) * 4
frames = []
for _ in range(150):
img = Image.new('RGB', (400, 400), (20, 20, 30))
draw = ImageDraw.Draw(img)
for x, y in positions:
draw.ellipse([x - 3, y - 3, x + 3, y + 3], fill=(0, 200, 200))
frames.append(np.array(img))
centre = positions.mean(axis=0)
positions += (centre - positions) * 0.01 + velocities * 0.5
positions %= 400 # wrap at edges
imageio.mimsave('simple_flock.gif', frames, fps=20)
Core concepts
Concept 1 — Emergence from local rules
Each boid sees only the neighbours inside its perception radius — typically 50 pixels in a 400×400 canvas. It applies three steering rules to those neighbours, sums the resulting forces, and updates its velocity. That is the entire algorithm. No boid knows where the flock is going, what shape it is, or how big it is. The flock is not represented anywhere in the program — it is what you see from the outside [2].
This is self-organisation in the technical sense: the global pattern is not designed, planned, or coordinated. It is a side-effect of local interactions [3]. The same dynamic shows up in slime moulds, ant colonies, traffic jams, and applause synchronising in a concert hall.
Concept 2 — The three rules
Separation — steer away from neighbours that are too close. Without it the flock collapses to a point.
if distance < personal_space:
away = my_position - neighbour_position
separation_force += away / distance # 1/d weighting Alignment — match the average velocity of nearby neighbours. Without it the flock has no direction.
average_velocity = neighbour_velocities.mean(axis=0)
alignment_force = average_velocity - my_velocity Cohesion — steer toward the centre of mass of nearby neighbours. Without it the flock falls apart.
centre_of_mass = neighbour_positions.mean(axis=0)
cohesion_force = centre_of_mass - my_position Concept 3 — Rule weights as flock personality
The same three rules with different weights produce flocks with completely different temperaments. There is no “correct” set of weights — the choice is aesthetic.
| Parameter | Typical | Effect |
|---|---|---|
perception_radius | 50 px | How far each boid can “see” neighbours |
separation_weight | 1.5 | Strength of collision avoidance |
alignment_weight | 1.0 | Strength of velocity matching |
cohesion_weight | 1.0 | Strength of flock cohesion |
max_speed | 4 px/f | Hard ceiling on velocity magnitude |
Push separation high and the flock spreads into a nervous cloud. Push cohesion high and it balls up and bounces around its own centre. Push alignment high and it streams in parallel lanes like a school of fish.
Exercises
Three exercises in Execute → Modify → Create order: run the canonical simulation, retune its weights, then add a fourth rule of your own.
Run the canonical flock
Download boids.py and run it. It produces boids_simulation.gif: 50 boids over 200 frames, all three rules at default weights.
Reflection questions
- Why do scattered boids gradually form groups? Which rule is doing the pulling?
- What happens when two groups meet? Do they merge, pass through each other, or repel?
- Why do boids on the edge of a flock behave differently from boids in the interior?
Answers
Why scattered boids form groups — cohesion. Each boid steers toward the average position of whatever neighbours fall inside its perception radius, so any two boids within range of each other start drifting closer. Once they are close, alignment kicks in and locks their heading. Cohesion bootstraps groups; alignment freezes them.
Two groups meeting — depends on their relative velocity. If they are travelling in similar directions, alignment merges them within a few frames. If they are travelling in opposite directions, separation pushes them apart before cohesion can stitch them together; they pass through each other or briefly tangle. The outcome is unpredictable in detail but always one of those three possibilities.
Edge versus interior — an interior boid is pulled by cohesion in every direction at once, so the forces cancel and the boid drifts with its local average. An edge boid has neighbours only on one side, so cohesion always pulls it inward. The flock has a soft boundary not because anyone codes one but because the geometry of “neighbours on one side” enforces it.
Retune the rule weights
Three preset experiments. Edit the weights at the top of boids.py, re-render the GIF, and watch the flock change personality.
Goals
- Separation-dominant:
SEPARATION=3.0, ALIGNMENT=0.5, COHESION=0.5. - Alignment-dominant:
SEPARATION=0.5, ALIGNMENT=3.0, COHESION=0.5. - Cohesion-dominant:
SEPARATION=0.5, ALIGNMENT=0.5, COHESION=3.0.
Goal 1 — separation-dominant
The flock spreads into a diffuse, nervous cloud. Boids maintain large distances from each other; you can almost see the personal-space bubbles. Coordination breaks — each boid behaves more like a solo agent with mild herd-attraction. Great if you want the look of a scattered fleet.
Goal 2 — alignment-dominant
Parallel streams. Once any small group locks onto a heading, alignment overwhelms the other rules and the heading propagates outward. The flock elongates in the direction of motion and often splits into two or three lanes. Closest to a starling murmuration in feel.
Goal 3 — cohesion-dominant
Tight ball that oscillates. With cohesion six times stronger than separation, boids overshoot the centre of mass, swing past it, and overshoot the other way. The result is a vibrating cluster that bounces around inside its own perimeter. Useful for “swarming insects” but it loses the flowing motion.
Add a fourth rule — obstacle avoidance
Use boids_starter.py, which contains the three rules already wired up and an unimplemented obstacle_avoidance(positions) function. Add a circular obstacle at the centre of the canvas and make boids steer around it.
Requirements
- Boids within
1.5 * OBSTACLE_RADIUSof the obstacle centre should steer away. - The avoidance force should be stronger when the boid is closer.
- The force should point directly away from the obstacle centre.
Hint 1 — distance to obstacle
obstacle_centre = np.array([OBSTACLE_X, OBSTACLE_Y])
direction = positions[i] - obstacle_centre
distance = np.sqrt((direction ** 2).sum())direction is the vector from the obstacle to the boid. Its length is the distance.
Hint 2 — falloff with distance
if 0 < distance < OBSTACLE_RADIUS * 1.5:
unit = direction / distance
strength = (OBSTACLE_RADIUS * 1.5 - distance) / distance
steering[i] = unit * strengthstrength is large when distance is small and falls to zero at the perception edge. The pattern mirrors separation — both rules are “push away, harder when closer”.
Complete solution
def obstacle_avoidance(positions):
steering = np.zeros_like(positions)
obstacle_centre = np.array([OBSTACLE_X, OBSTACLE_Y])
for i in range(len(positions)):
direction = positions[i] - obstacle_centre
distance = np.sqrt((direction ** 2).sum())
if 0 < distance < OBSTACLE_RADIUS * 1.5:
unit = direction / distance
strength = (OBSTACLE_RADIUS * 1.5 - distance) / distance
steering[i] = unit * strength
return steeringAdd OBSTACLE_WEIGHT * obstacle_avoidance(positions) to the velocity update with OBSTACLE_WEIGHT ≈ 2.5 so it dominates other rules when active.
Make it your own
- Add a predator that moves toward the nearest boid. All boids within a large “fear radius” steer directly away. The flock will fragment and re-form behind the predator.
- Replace the wrap-at-edges rule with bounded edges that apply a soft repulsive force at the canvas border — more biological than the toroidal wrap.
- Colour each boid by the magnitude of its current velocity. The flock now visualises its own energy field.
Downloads
boids.py — full reference implementation boids_starter.py — Exercise 3 scaffold with TODOsSummary
Common pitfalls to avoid
- Forgetting to clip the maximum force magnitude. Two boids inside each other’s personal space can teleport on the next frame.
- Iterating
for i in range(n): for j in range(n)for the neighbour search. Vectorise withcdistor a KD-tree forn > 200. - Dividing by
distancewithout adistance > 0guard. A boid eventually overlaps with itself or a duplicate and the simulation explodes. - Tuning weights without re-randomising the seed each run. You will mistake one lucky initial condition for the algorithm’s behaviour.
- Edge-wrapping in pixel space without also wrapping in the neighbour search. Boids on opposite sides of the canvas should see each other if the world is toroidal.
References
- [1] Reynolds, C. W. (1987). Flocks, herds and schools: A distributed behavioral model. ACM SIGGRAPH Computer Graphics, 21(4), 25–34. doi:10.1145/37402.37406
- [2] Reynolds, C. W. (1999). Steering behaviors for autonomous characters. In Proceedings of Game Developers Conference 1999 (pp. 763–782). red3d.com/cwr/steer
- [3] Camazine, S., Deneubourg, J. L., Franks, N. R., Sneyd, J., Theraulaz, G., & Bonabeau, E. (2001). Self-Organization in Biological Systems. Princeton University Press.
- [4] Vicsek, T., & Zafeiris, A. (2012). Collective motion. Physics Reports, 517(3–4), 71–140. doi:10.1016/j.physrep.2012.03.004
- [5] Brambilla, M., Ferrante, E., Birattari, M., & Dorigo, M. (2013). Swarm robotics: A review from the swarm engineering perspective. Swarm Intelligence, 7(1), 1–41. doi:10.1007/s11721-012-0075-2
- [6] Shiffman, D. (2012). The Nature of Code, Chapter 6: Autonomous Agents. natureofcode.com
- [7] NumPy Developers. (2024). Broadcasting. NumPy Documentation. numpy.org