Concept
Growing up in a fishing community in Ghana, I watched fishermen read the water. Not just the tides, but the fish themselves. A school of fish moves like a single organism, splitting around rocks, scattering from shadows, reforming behind the boat. No one fish is giving orders. The pattern emerges from each individual following a few simple rules about its neighbors.
That memory became the concept for this sketch. The Shoal is a system of 60 fish navigating an ocean current, staying together as a group, and reacting to a predator that tracks your mouse cursor. The behaviors are inspired directly by Craig Reynolds’ steering model: each fish knows nothing about the whole. It only senses what is close to it. Yet the group produces complex, lifelike motion.
The color palette is drawn from the Ghanaian flag (red, gold, and green), and each color encodes a live behavioral state. Green means a fish is flocking normally. Red means it is fleeing the predator. Gold means it has drifted away from the group and is wandering on its own.
A Highlight of Code I’m Proud Of
The piece of code I kept returning to is the flock() method, the behavior composer that decides frame by frame what each fish should be doing. What I love about it is how it uses the same underlying steering primitives in completely different combinations depending on context.
flock(others, pred, flow) {
let d = dist(this.pos.x, this.pos.y, pred.pos.x, pred.pos.y);
let fleeing = d < FLEE_RADIUS;
if (fleeing) {
// Run directly away from the predator at boosted speed
let flee = this._flee(pred.pos);
flee.mult(2.5);
this.applyForce(flee);
this.maxSpeed = FISH_MAX_SPEED * 1.5;
this.bodyColor = PAL.red;
} else {
// Normal flocking: separation keeps spacing, alignment matches heading,
// cohesion pulls toward the group center using arrive()
let sep = this._separation(others);
let ali = this._alignment(others);
let coh = this._cohesion(others);
sep.mult(1.8);
ali.mult(1.0);
coh.mult(1.2);
this.applyForce(sep);
this.applyForce(ali);
this.applyForce(coh);
// Flow field gives the fish a subtle current to drift with
let flowForce = flow.lookup(this.pos);
flowForce.setMag(this.maxSpeed * 0.6);
let flowSteer = p5.Vector.sub(flowForce, this.vel);
flowSteer.limit(this.maxForce * 0.5);
this.applyForce(flowSteer);
// Isolated fish switch to wander
let neighbors = this._countNeighbors(others, COH_RADIUS);
if (neighbors < 3) {
this.bodyColor = PAL.gold;
this.applyForce(this._wander());
} else {
this.bodyColor = PAL.green;
}
}
}
What made this click for me is that _arrive() is called inside _cohesion(), so even the group behavior uses the same arrive logic I first learned as a single vehicle seeking a target. One method, three behavioral contexts: cohesion toward the group center, the predator arriving at the cursor, and the wander behavior projecting a circle ahead of itself. Reusing the same primitive in different combinations was the most satisfying part of this assignment.
Embedded Sketch
Move your cursor to control the predator. Watch the shoal react. Fish nearest the predator turn red and scatter, isolated fish turn gold and wander, and the rest stay green and hold formation together.
Milestones and Process
Phase 1 — One fish, seek and arrive
I started with a single fish vehicle following the mouse. The goal was to make sure the arrive behavior felt right before scaling up, because arrive is the foundation everything else builds on. At this stage it was just one ellipse with a triangle tail, but the deceleration as it approached the cursor already felt organic.

The main challenge here was getting the arrive slowing to feel natural rather than mechanical. My first attempt applied the speed reduction too early, so the fish would crawl painfully slowly from far away before even reaching the target zone. I had to narrow the slowdown window to only the final 100 pixels, leaving full speed everywhere outside that range, before the motion started to feel right.
Phase 2 — Scaling to a shoal, adding flocking
I scaled from one fish to 40 using a loop and implemented separation, alignment, and cohesion as three separate steering forces composed together.

Tuning the force multipliers was the hardest part of this phase and took the most iteration. My first set of values had separation too weak, so fish constantly clipped through each other and the group looked like a blob rather than a school. Raising it too far in the other direction made the shoal explode outward and never reform. I also had alignment weighted too heavily early on, which made every fish lock into the same heading so rigidly that the group moved like a marching band rather than a living thing. Getting to sep × 1.8, ali × 1.0, coh × 1.2 took many small adjustments and a lot of watching the sketch run.
Phase 3 — Predator, flee, wander, and color states
The predator changed the whole feel of the sketch. Once a dark fish that tracked the mouse was introduced, the shoal became reactive rather than passive. Flee was implemented as a reversed seek: instead of steering toward the target, the fish steers directly away from it with a force multiplied at 2.5× for urgency.

The flee force caused a problem I did not anticipate. With the multiplier too high, fish near the predator would accelerate so violently that they shot across the entire canvas in a single frame and wrapped around to the other side, which looked completely wrong. I had to pair the force multiplier with a capped maxSpeed boost rather than an uncapped acceleration, so the urgency comes through in the speed increase without the motion becoming physically implausible. Getting the flee radius right also took several attempts. Too large and fish were permanently panicking even when the predator was nowhere near them. Too small and the reaction looked delayed and unconvincing.
Phase 4 — Flow field, ocean background, full fish anatomy
The final layer was a Perlin noise flow field that gives the whole canvas a gentle ocean current. Each fish looks up the flow vector at its position and applies it as a weak additional force.

The challenge here was finding the right weight for the flow force relative to the flocking forces. In early versions I had it too strong, and it completely overrode the cohesion and alignment behavior. Fish stopped schooling and just drifted in the same direction like debris, which defeated the whole point. Pulling it back to maxSpeed × 0.6 with a force limit at half of maxForce made it feel like an environmental influence rather than a controlling force — the fish push through it, but you can see them drifting slightly when nothing else is competing for their attention.
Reflection and Ideas for Future Work
The biggest surprise was how little code produces this result. The entire behavioral system, 60 fish with four distinct modes, comes down to three vector additions per frame per fish, each about five lines long. What Reynolds figured out in the 1980s still feels almost unreasonably powerful.
The Ghanaian flag palette was a natural choice for me. I had already used it in my midterm project, and it works well here because each color carries cultural weight while also reading clearly as a data signal. You understand the system’s state at a glance just from the color distribution across the canvas.
Ideas for future work include adding a real food source, a glowing anchor point the shoal seeks when the predator is far away, which would complete the full foraging cycle. Two competing shoals in different colors racing for the same food would also be compelling. Letting individual fish leave a faint pheromone trail that gradually fades would make the paths the school carves through the water visible over time. And sound would add another dimension: low ambient ocean audio that pitches up slightly when the shoal is stressed and fleeing.
References and inspiration:
- The Nature of Code, Chapter 5 (Autonomous Agents) by Daniel Shiffman, which provided the steering force formula the entire system is built on
- Craig Reynolds, Steering Behaviors for Autonomous Characters, the original separation, alignment, and cohesion framework
- Braitenberg Vehicles, and the idea that complex behavior can emerge from extremely simple rules
- Personal memory of fishing communities in the Central Region of Ghana