Pixels2GenAI
Path i Foundations
M 05 · 5.2.1 · hands-on

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.

Duration20–25 min
Levelintermediate
Load3 core concepts
Prereqs5.1.1 (particle systems), basic NumPy broadcasting

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

  1. Articulate what “emergent behaviour” means — and why local rules are sufficient to produce global coordination.
  2. Implement the three boids rules: separation (avoid crowding), alignment (match heading), and cohesion (stay together).
  3. Use a perception radius to give each boid a local neighbourhood — and recognise that radius as the algorithm’s only knob with units.
  4. 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.

python · quick_start.py
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)
Animated flock of cyan dots drifting and coalescing on a dark blue background
Fig. 1 The full simulation — 50 boids, all three rules, 150 frames. Watch the moment around frame 40 when isolated agents start moving in synchronised streams.

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].

Three panels labelled Separation, Alignment, Cohesion, each showing arrows indicating the steering direction
Fig. 2 The three rules, side by side. Each acts only on neighbours inside the perception radius.

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.

A single boid with arrows pointing away from three crowding neighbours
Fig. 3 Separation: stronger when closer. The force points from the neighbour to me, scaled by inverse distance.
python · separation_demo.py
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.

A boid with an arrow steering toward the mean velocity vector of its neighbours
Fig. 4 Alignment: steer to match the average heading of nearby neighbours.
python · alignment_demo.py
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.

A boid with an arrow pointing toward the geometric centre of its neighbours
Fig. 5 Cohesion: steer toward the average position of nearby neighbours.
python · cohesion_demo.py
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.

ParameterTypicalEffect
perception_radius50 pxHow far each boid can “see” neighbours
separation_weight1.5Strength of collision avoidance
alignment_weight1.0Strength of velocity matching
cohesion_weight1.0Strength of flock cohesion
max_speed4 px/fHard 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.

EXECUTE I.

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.

boids.py — complete reference implementation

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?
MODIFY II.

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

  1. Separation-dominant: SEPARATION=3.0, ALIGNMENT=0.5, COHESION=0.5.
  2. Alignment-dominant: SEPARATION=0.5, ALIGNMENT=3.0, COHESION=0.5.
  3. Cohesion-dominant: SEPARATION=0.5, ALIGNMENT=0.5, COHESION=3.0.
CREATE III.

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.

boids_starter.py — Exercise 3 scaffold

Requirements

  • Boids within 1.5 * OBSTACLE_RADIUS of 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.
Boids flocking and steering around a dark red circular obstacle in the centre of the canvas
Fig. 6 Obstacle avoidance in action — boids flow around the centre obstacle while maintaining their three-rule flocking behaviour.

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 TODOs

Summary

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 with cdist or a KD-tree for n > 200.
  • Dividing by distance without a distance > 0 guard. 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. [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. [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. [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. [4] Vicsek, T., & Zafeiris, A. (2012). Collective motion. Physics Reports, 517(3–4), 71–140. doi:10.1016/j.physrep.2012.03.004
  5. [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. [6] Shiffman, D. (2012). The Nature of Code, Chapter 6: Autonomous Agents. natureofcode.com
  7. [7] NumPy Developers. (2024). Broadcasting. NumPy Documentation. numpy.org