Physics, autonomous agents, and emergence
Learning objectives
- learner can build object-oriented agents with velocity/acceleration physics and collision detection
- learner can define local rules (boids, Braitenberg, springs, repulsion) that produce emergent global behaviour
- learner can reason about how initial conditions and noise shape the emergent form of a rule-based system
Capstone — one whole task that evidences the objectives
Implement a live agent-based ecosystem — flocking boids or a spring-mass/repulsion network — where OOP agents with physics and collisions self-organise, and demonstrate how changing initial conditions and noise reshapes the emergent form.
Prerequisite modules
This module is where your visuals stop being drawings and start being populations. In a live set, an agent ecosystem is the visual layer that stays alive between your edits: a murmuration drifting over a techno drop, a spring network twitching in time with a bassline. The audience sees an organism, not a loop — and the whole task here is to build one on a Processing/p5 canvas that you can perturb live without it collapsing into either uniformity or chaos.
The arc starts scaffolded: define one agent class (“a class is a template; an instance is one concrete object”), give it the velocity-plus-acceleration update, and get a single body bouncing. Then multiply it into an array and add circle-circle collision — these two frame-loop mechanics are the part-task drills, because during a performance you will retype them from muscle memory. With mechanics in hand, the module shifts to behaviour: Braitenberg’s sensor-to-motor wiring is the minimal template for “agents that seem to want things,” and Reynolds’ three boids rules or the spring/repulsion force pair show how purely local rules self-organise into flocks and readable layouts. The final, unsupported stretch is compositional control: reseeding initial positions and injecting bounded jitter, so you can steer the macro form without touching the rules — exactly what the capstone demands you demonstrate live.
Required atoms are the load-bearing set: OOP structure, physics integration, collision, the emergence concept, each named local-rule family, and the two perturbation principles — the capstone fails without any one of them. Supporting atoms widen the palette: inheritance for specialised agent variants, the multiply-by-100 instinct, and cousins like circle packing, DLA growth, and human-driven agents that suggest where this ecosystem thinking goes next.
Walkthrough
You’ll build a population that looks alive — a flock that self-organises from local rules — starting from one bouncing dot. Paste each sketch into the p5 web editor and press Run (▶). Each step is a complete, standalone sketch. Vectors (createVector, p5.Vector) do the physics.
1 — one agent, one class. A class is a template; an instance is one concrete agent with its own state. Give it a position and velocity and a show(), and bounce it off the walls — the object owns its own behaviour ([[oop-classes-instances]], [[processing-oop-class]]).
class Mover {
constructor(x, y) { this.pos = createVector(x, y); this.vel = createVector(2.4, 1.6); }
update() {
this.pos.add(this.vel);
if (this.pos.x < 0 || this.pos.x > width) this.vel.x *= -1;
if (this.pos.y < 0 || this.pos.y > height) this.vel.y *= -1;
}
show() { noStroke(); fill(255, 180, 0); circle(this.pos.x, this.pos.y, 18); }
}
let m;
function setup() { createCanvas(400, 400); m = new Mover(80, 120); }
function draw() { background(20); m.update(); m.show(); }
2 — physics, and a population. Real motion integrates acceleration into velocity into position. Add a downward acc (gravity), a floor bounce that loses energy, and push 30 movers into an array — the “multiply one into many” instinct ([[processing-velocity-acceleration]]).
class Mover {
constructor(x, y) {
this.pos = createVector(x, y);
this.vel = createVector(random(-2, 2), 0);
this.acc = createVector(0, 0.06); // gravity
}
update() {
this.vel.add(this.acc);
this.pos.add(this.vel);
if (this.pos.y > height) { this.pos.y = height; this.vel.y *= -0.8; } // lossy floor
if (this.pos.x < 0 || this.pos.x > width) this.vel.x *= -1;
}
show() { noStroke(); fill(120, 200, 255); circle(this.pos.x, this.pos.y, 14); }
}
let movers = [];
function setup() {
createCanvas(400, 400);
for (let i = 0; i < 30; i++) movers.push(new Mover(random(width), random(height / 2)));
}
function draw() { background(20); for (let m of movers) { m.update(); m.show(); } }
3 — collisions couple them. Agents that ignore each other aren’t an ecosystem. Test every pair with a circle–circle distance check and reverse both on contact — the crudest collision, but enough to make the population interact ([[collision-detection-circles]]).
class Ball {
constructor(x, y) { this.pos = createVector(x, y); this.vel = createVector(random(-1, 1), random(-1, 1)).setMag(2.2); this.r = 16; }
update() {
this.pos.add(this.vel);
if (this.pos.x < this.r || this.pos.x > width - this.r) this.vel.x *= -1;
if (this.pos.y < this.r || this.pos.y > height - this.r) this.vel.y *= -1;
}
show() { noStroke(); fill(255, 120, 150); circle(this.pos.x, this.pos.y, this.r * 2); }
}
let balls = [];
function setup() { createCanvas(400, 400); for (let i = 0; i < 8; i++) balls.push(new Ball(random(60, 340), random(60, 340))); }
function draw() {
background(20);
for (let i = 0; i < balls.length; i++) {
for (let j = i + 1; j < balls.length; j++) {
let a = balls[i], b = balls[j];
if (dist(a.pos.x, a.pos.y, b.pos.x, b.pos.y) < a.r + b.r) { a.vel.mult(-1); b.vel.mult(-1); }
}
balls[i].update(); balls[i].show();
}
}
4 — emergence from local rules. Emergence is global order that no single agent contains — it arises from everyone following the same local rule. Two simple forces prove it: a gentle pull toward centre, and repulsion from close neighbours. The population settles into a ring nobody designed — the Braitenberg lesson that “wanting” is just wiring ([[emergence-definition]], [[agent-repulsion-layout]], [[braitenberg-vehicles-emergent-behaviour]]).
class Agent {
constructor() { this.pos = createVector(random(width), random(height)); this.vel = createVector(); }
step(all) {
let f = createVector(200, 200).sub(this.pos).setMag(0.05); // rule 1: pull to centre
for (let o of all) { // rule 2: repel neighbours
let d = this.pos.copy().sub(o.pos), m = d.mag();
if (m > 0 && m < 40) f.add(d.setMag(0.6 / m));
}
this.vel.add(f).limit(2.2); this.pos.add(this.vel);
}
show() { noStroke(); fill(150, 255, 200); circle(this.pos.x, this.pos.y, 10); }
}
let agents = [];
function setup() { createCanvas(400, 400); for (let i = 0; i < 60; i++) agents.push(new Agent()); }
function draw() { background(20); for (let a of agents) a.step(agents); for (let a of agents) a.show(); }
5 — boids: the three flocking rules. Reynolds’ classic. Each boid looks only at neighbours within a radius and blends three steers — separation (avoid crowding), alignment (match their heading), cohesion (move toward their centre). Local-only, yet the whole flock murmurates ([[boids-flocking-algorithm]], [[rule-based-generative-system-design]]).
class Boid {
constructor() { this.pos = createVector(random(width), random(height)); this.vel = createVector(random(-1, 1), random(-1, 1)).setMag(2); this.acc = createVector(); }
flock(boids) {
let sep = createVector(), ali = createVector(), coh = createVector(), n = 0;
for (let o of boids) {
let d = dist(this.pos.x, this.pos.y, o.pos.x, o.pos.y);
if (o !== this && d < 50) {
sep.add(this.pos.copy().sub(o.pos).div(max(d, 0.1))); // separation
ali.add(o.vel); // alignment
coh.add(o.pos); // cohesion
n++;
}
}
if (n > 0) {
ali.div(n); coh.div(n).sub(this.pos);
this.acc.add(sep.setMag(0.06)).add(ali.setMag(0.04)).add(coh.setMag(0.03));
}
}
update() {
this.vel.add(this.acc).limit(3); this.pos.add(this.vel); this.acc.mult(0);
this.pos.x = (this.pos.x + width) % width; this.pos.y = (this.pos.y + height) % height; // wrap
}
show() { noStroke(); fill(180, 220, 255); circle(this.pos.x, this.pos.y, 7); }
}
let flock = [];
function setup() { createCanvas(400, 400); for (let i = 0; i < 80; i++) flock.push(new Boid()); }
function draw() { background(15); for (let b of flock) b.flock(flock); for (let b of flock) { b.update(); b.show(); } }
6 — a live ecosystem (the capstone). Two moves turn the flock into something you can perform: a per-boid Perlin-noise wander so it never freezes into one dead clump, and initial conditions — here all boids start clustered at centre — that reshape the whole emergent form. Change either and the macro shape changes without touching the rules. Trails tie the motion together ([[noise-prevents-system-homogenization]], [[initial-conditions-shape-emergent-form]]):
class Boid {
constructor(x, y) { this.pos = createVector(x, y); this.vel = createVector(random(-1, 1), random(-1, 1)).setMag(2); this.acc = createVector(); this.seed = random(1000); }
flock(boids) {
let sep = createVector(), ali = createVector(), coh = createVector(), n = 0;
for (let o of boids) {
let d = dist(this.pos.x, this.pos.y, o.pos.x, o.pos.y);
if (o !== this && d < 55) { sep.add(this.pos.copy().sub(o.pos).div(max(d, 0.1))); ali.add(o.vel); coh.add(o.pos); n++; }
}
if (n > 0) { ali.div(n); coh.div(n).sub(this.pos);
this.acc.add(sep.setMag(0.07)).add(ali.setMag(0.045)).add(coh.setMag(0.03)); }
let ang = noise(this.seed, frameCount * 0.005) * TWO_PI * 2; // noise wander keeps it alive
this.acc.add(createVector(cos(ang), sin(ang)).mult(0.05));
}
update() {
this.vel.add(this.acc).limit(3); this.pos.add(this.vel); this.acc.mult(0);
this.pos.x = (this.pos.x + width) % width; this.pos.y = (this.pos.y + height) % height;
}
show() { noStroke(); fill(180, 230, 255, 200); circle(this.pos.x, this.pos.y, 6); }
}
let flock = [];
function setup() {
createCanvas(400, 400);
// INITIAL CONDITIONS: a tight central cluster blooms outward; spread them instead and it never coalesces
for (let i = 0; i < 90; i++) flock.push(new Boid(200 + random(-60, 60), 200 + random(-60, 60)));
}
function draw() {
noStroke(); fill(12, 40); rect(0, 0, width, height); // trails
for (let b of flock) b.flock(flock);
for (let b of flock) { b.update(); b.show(); }
}
What good looks like. A living population reads as one organism with internal structure — you should see coherent motion (aligned streams, a settling ring) that is never fully uniform and never pure noise. The two failure poles are named in the atoms: too much cohesion and it collapses to a dead clump (add separation or noise); too little and it’s TV static (raise alignment/cohesion). The tell of a good flock is that you can point at emergent features — lanes, swirls, splits — that you never coded. (Skill map: live-visualist Domain B3 — motion with life and rhythm; and A4 — the generative-systems paradigm.)
Now make it yours. Reseed the flock spread across the whole canvas and watch the form change. Tune the three boid weights live — a fader per rule. Swap flocking for a spring-mass network: connect neighbours with spring forces (F = -k·x) for a twitching lattice ([[spring-mass-node-network]]). Colour each boid by its speed. Add a predator boid the others flee.
Runnable examples
Generated from the context/ instrument corpus by concept (redistributable idioms only). Do not edit — regenerate with gen-module-examples.mjs.
particle-system
let pts = font.textToPoints('P5', 0, 200, 200, {sampleFactor: 0.2})
p5live-0029 · CC0-1.0
physics-sim
let dir = p5.Vector.random2D().mult(random(2, 5))
p5live-0013 · CC0-1.0
Atoms in this module
Required — these gate the capstone
Supporting — enrichment, not gating
Part of curricula
- Live Visualist — zero to performing live-coded & generative visuals — Perform the set — live-coded, generative, audio-reactive visuals for an audience recommended
Unlocks — modules that require this one