AI Boid Flocking Simulator - Emergent Swarm Behavior Watch realistic bird/fish swarms with separati
This sketch simulates a flock of colorful triangular boids that swarm, scatter, and regroup using the classic separation/alignment/cohesion rules pioneered by Craig Reynolds. Left-clicking spawns a new cluster of panicked boids and right-clicking drops a red predator that hunts the nearest boid, all rendered over a soft gradient background with trailing motion streaks.
Make boids fully opaque — Removing the transparency makes each boid a solid, crisp color instead of letting the trails blend through it.
Create a super-tight swarm — Boosting the cohesion multiplier pulls every boid much more strongly toward the group's center, producing a dense, ball-like swarm.
Supersize the predator — Doubling the predator's size makes it look far more menacing and easier to spot as it chases boids across the screen.
This sketch brings a living swarm to life on screen: dozens of small colored triangles glide, regroup, and scatter exactly like a flock of birds or school of fish. It's a textbook implementation of Craig Reynolds' boids algorithm, built entirely from p5.Vector math - each boid steers itself using three simple rules (separation, alignment, cohesion) that combine into surprisingly organic group behavior. On top of that, the sketch layers predator-avoidance forces, a panic response triggered by mouse clicks, HSB color blending, and a gradient background redrawn every frame with low alpha to create smooth motion trails.
The code is split into p5.js lifecycle functions (setup, draw, windowResized), input handlers (mousePressed, keyPressed), small helper functions for drawing the gradient and spawning groups, and two ES6 classes - Boid and Predator - that each encapsulate their own position, velocity, and behavior methods. Studying it teaches you how to structure an object-oriented simulation in p5.js, how vector steering forces are calculated and combined, and how per-pixel alpha blending can fake trailing effects without a shader.
⚙️ How It Works
When the page loads, setup() creates a full-window canvas, switches to HSB color mode, paints a solid gradient background once, and seeds the world with 120 Boid objects at random positions.
Every frame, draw() first paints a very faint copy of the gradient over the whole canvas instead of clearing it, which is what produces the soft trailing streaks behind every moving shape.
Still inside draw(), each Predator updates its position (chasing the nearest boid) and each Boid calculates steering forces from its neighbors, then updates its position and wraps around any edge it crosses.
A boid's flock() method blends four separate forces - separation (avoid crowding), alignment (match neighbor heading), cohesion (move toward the group's center), and predator avoidance - each weighted differently before being applied.
Left-clicking calls spawnBoidGroup() to add a burst of new boids around the cursor and also starts a 180-frame panic timer; while panicTimer is active every boid gets an extra outward force pushing it away from the click point.
Right-clicking drops a new Predator that actively hunts the closest boid, and pressing C or X instantly empties the boids or predators array to reset the scene.
🎓 Concepts You'll Learn
Boids / flocking algorithmSteering behaviors (separation, alignment, cohesion)p5.Vector mathObject-oriented classes in p5.jsHSB color mode and alpha blendingMotion trails via low-alpha overlaysEmergent collective behavior
📝 Code Breakdown
setup()
setup() runs once when the sketch starts. It's the right place to configure the canvas, color mode, and starting state before the animation loop takes over.
🔬 This loop seeds the flock. What happens visually if you drop the count to 10? What about pushing it to 500 - does the frame rate hold up?
for (let i = 0; i < 120; i++) {
boids.push(new Boid(random(width), random(height)));
}
function setup() {
canvas = createCanvas(windowWidth, windowHeight);
// Disable right-click menu over the canvas so we can use RIGHT mouse
canvas.elt.oncontextmenu = () => false;
// Use HSB so colors are easy to control
colorMode(HSB, 360, 100, 100, 100);
// Gradient colors (top -> bottom)
gradientTop = color(210, 70, 25); // deep blue
gradientBottom = color(280, 60, 10); // violet
// Draw a full-intensity gradient once to initialize background
drawInitialGradient();
// Seed some initial boids so the scene isn't empty
for (let i = 0; i < 120; i++) {
boids.push(new Boid(random(width), random(height)));
}
}
Line-by-line explanation (8 lines)
🔧 Subcomponents:
for-loopSeed Initial Boidsfor (let i = 0; i < 120; i++) {
Creates 120 Boid objects at random positions so the flock isn't empty at launch
canvas = createCanvas(windowWidth, windowHeight);
Makes the canvas fill the entire browser window and stores a reference to it
canvas.elt.oncontextmenu = () => false;
Disables the browser's normal right-click context menu so right-click can be used to place predators instead
colorMode(HSB, 360, 100, 100, 100);
Switches p5's color system to Hue/Saturation/Brightness, which makes it easy to pick a range of hues for boids and gradients
gradientTop = color(210, 70, 25); // deep blue
Defines the color used at the top of the background gradient
gradientBottom = color(280, 60, 10); // violet
Defines the color used at the bottom of the background gradient
drawInitialGradient();
Paints a fully opaque gradient once so the canvas isn't blank before the first frame
for (let i = 0; i < 120; i++) {
Loops 120 times to create the starting population of boids
Creates a new Boid at a random x,y position and adds it to the global boids array
draw()
draw() is the animation loop that p5.js calls roughly 60 times per second. Everything that needs to move or update over time - trails, timers, predators, boids - happens here in a fixed order.
🔬 This loop is the heartbeat of the simulation. What happens if you comment out the boid.flock() line entirely - do the boids still look alive, or just drift randomly?
for (let boid of boids) {
boid.flock(boids, predators);
if (panicTimer > 0 && panicCenter) {
boid.panic(panicCenter);
}
boid.update();
boid.edges();
boid.show();
}
function draw() {
// Motion trails: instead of a solid background, overlay a faint gradient
drawGradientOverlay(8); // small alpha -> long trails
// Panic timer countdown
if (panicTimer > 0) {
panicTimer--;
if (panicTimer === 0) panicCenter = null;
}
// Update + draw predators
for (let predator of predators) {
predator.update();
predator.edges();
predator.show();
}
// Update + draw boids
for (let boid of boids) {
boid.flock(boids, predators);
if (panicTimer > 0 && panicCenter) {
boid.panic(panicCenter);
}
boid.update();
boid.edges();
boid.show();
}
}
Decrements the panic timer each frame and clears the panic center once it reaches zero
for-loopUpdate & Draw Predatorsfor (let predator of predators) {
Runs the update/edges/show cycle for every predator on screen
for-loopUpdate & Draw Boidsfor (let boid of boids) {
Runs flocking, panic, movement, and rendering for every boid on screen
drawGradientOverlay(8); // small alpha -> long trails
Paints a nearly-transparent gradient over the whole frame instead of clearing it, which lets previous frames faintly show through and creates trailing streaks
if (panicTimer > 0) {
Only runs the countdown logic while a panic burst is active
panicTimer--;
Counts the panic timer down by one frame
if (panicTimer === 0) panicCenter = null;
Once the timer expires, clears the panic center so boids stop reacting to it
for (let predator of predators) {
Loops over every predator currently placed on the canvas
predator.update();
Moves the predator toward the nearest boid
predator.edges();
Wraps the predator around the screen edges if it goes off-canvas
predator.show();
Draws the predator as a red triangle
for (let boid of boids) {
Loops over every boid currently in the flock
boid.flock(boids, predators);
Calculates and applies the combined separation/alignment/cohesion/avoidance steering forces
if (panicTimer > 0 && panicCenter) {
Checks whether a panic burst is currently active
boid.panic(panicCenter);
Applies an extra outward force pushing the boid away from the click point
boid.update();
Applies the accumulated forces to move the boid
boid.edges();
Wraps the boid around the screen edges if it leaves the canvas
boid.show();
Draws the boid as an oriented, colored triangle
windowResized()
windowResized() is a p5.js callback that automatically fires whenever the browser window changes size, letting you keep the canvas responsive.
function windowResized() {
resizeCanvas(windowWidth, windowHeight);
drawInitialGradient();
}
Line-by-line explanation (2 lines)
resizeCanvas(windowWidth, windowHeight);
Resizes the canvas to match the browser window whenever it's resized
drawInitialGradient();
Repaints a fresh, fully opaque gradient so the resized canvas doesn't show a stretched or blank background
mousePressed()
mousePressed() is a p5.js input callback that fires once whenever any mouse button is clicked. Checking the mouseButton variable lets one function handle multiple distinct interactions.
🔬 This block spawns boids AND triggers panic in one click. What happens if you delete the panicTimer line so left-click only spawns boids without scaring anyone?
Checks for either uppercase or lowercase C being pressed
boids = [];
Replaces the boids array with an empty one, instantly removing every boid from the scene
} else if (key === 'X' || key === 'x') {
Checks for either uppercase or lowercase X being pressed
predators = [];
Replaces the predators array with an empty one, removing every predator
spawnBoidGroup()
This helper demonstrates how to scatter multiple objects around a point using p5.Vector.random2D() combined with a random magnitude, a common pattern for particle bursts.
function spawnBoidGroup(x, y, count) {
for (let i = 0; i < count; i++) {
const offset = p5.Vector.random2D().mult(random(0, 30));
boids.push(new Boid(x + offset.x, y + offset.y));
}
}
Line-by-line explanation (3 lines)
🔧 Subcomponents:
for-loopSpawn Loopfor (let i = 0; i < count; i++) {
Creates 'count' new boids scattered in a small radius around (x, y)
Creates a random direction vector and scales it by a random distance up to 30 pixels, so boids don't all spawn on exactly the same spot
boids.push(new Boid(x + offset.x, y + offset.y));
Creates a new Boid offset from the click point and adds it to the shared boids array
drawInitialGradient()
This function fakes a vertical gradient the same way you'd see in graphic design tools - by drawing many thin horizontal lines, each a slightly different interpolated color.
function drawInitialGradient() {
strokeWeight(1);
for (let y = 0; y < height; y++) {
const t = y / max(height - 1, 1);
const c = lerpColor(gradientTop, gradientBottom, t);
c.setAlpha(100);
stroke(c);
line(0, y, width, y);
}
}
Line-by-line explanation (7 lines)
🔧 Subcomponents:
for-loopRow-by-Row Gradientfor (let y = 0; y < height; y++) {
Draws one horizontal line per pixel row, interpolating its color between the top and bottom gradient colors
strokeWeight(1);
Sets the line thickness to 1 pixel so each row draws a crisp single-pixel-tall stripe
for (let y = 0; y < height; y++) {
Loops once for every vertical pixel row on the canvas
const t = y / max(height - 1, 1);
Converts the row number into a 0-to-1 fraction representing how far down the canvas this row is
const c = lerpColor(gradientTop, gradientBottom, t);
Blends between the top and bottom colors based on that fraction, producing a smooth gradient
c.setAlpha(100);
Sets this color fully opaque (100 is max alpha in this HSB color mode) so it completely covers the canvas
stroke(c);
Sets the line color for the row that's about to be drawn
line(0, y, width, y);
Draws a horizontal line spanning the full width at this row's y position
drawGradientOverlay()
Instead of calling background() to fully clear the canvas each frame, this function paints an almost-transparent copy of the background on top. Because it never fully erases, previous positions of every shape linger faintly, creating a comet-like trail effect.
function drawGradientOverlay(alphaVal) {
strokeWeight(1);
for (let y = 0; y < height; y++) {
const t = y / max(height - 1, 1);
const c = lerpColor(gradientTop, gradientBottom, t);
c.setAlpha(alphaVal); // 0–100 in HSB alpha range
stroke(c);
line(0, y, width, y);
}
}
Line-by-line explanation (7 lines)
🔧 Subcomponents:
for-loopRow-by-Row Faint Overlayfor (let y = 0; y < height; y++) {
Repaints a nearly-transparent gradient over the whole canvas each frame, letting previous frames fade rather than vanish
strokeWeight(1);
Sets each drawn line to 1 pixel thick
for (let y = 0; y < height; y++) {
Loops through every row of the canvas, same as the initial gradient function
const t = y / max(height - 1, 1);
Calculates this row's position as a 0-to-1 fraction down the canvas
const c = lerpColor(gradientTop, gradientBottom, t);
Blends the top/bottom gradient colors for this row
c.setAlpha(alphaVal); // 0–100 in HSB alpha range
Uses a low transparency value passed in from draw() so this overlay only slightly darkens/tints what's beneath it instead of erasing it
stroke(c);
Applies the semi-transparent color to the upcoming line
line(0, y, width, y);
Draws the horizontal line for this row, partially blending with whatever was drawn in previous frames
Boid constructor
The constructor runs once when 'new Boid(x, y)' is called, setting up all the per-instance properties (position, velocity, appearance) that make every boid slightly unique.
Stores the boid's location as a p5.Vector so it can be used in vector math
this.velocity = p5.Vector.random2D();
Gives the boid a random starting direction as a unit-length vector
this.velocity.setMag(random(1, 3));
Scales that direction to a random speed between 1 and 3 pixels per frame
this.acceleration = createVector(0, 0);
Starts with zero acceleration; forces will be added to this each frame
this.maxSpeed = random(2.0, 3.6);
Gives each boid a slightly different top speed so the flock doesn't move in perfect lockstep
this.maxForce = 0.05;
Limits how sharply the boid can steer each frame, preventing unrealistic instant direction changes
this.size = random(4, 7);
Randomizes the boid's rendered size slightly for visual variety
this.hue = random(160, 320);
Picks a random hue between teal and purple for this boid's color
this.sat = random(60, 100);
Picks a random saturation so colors range from muted to vivid
this.bri = random(60, 100);
Picks a random brightness so boids aren't all the same shade
applyForce()
This is the standard 'force accumulator' pattern used in physics-style sketches: instead of directly changing velocity, you add forces to acceleration and let update() apply them uniformly.
Adds an incoming force vector to the boid's total acceleration for this frame; multiple forces can stack before update() applies them
Boid update()
This update() method implements the classic physics integration pattern: acceleration changes velocity, velocity changes position, then acceleration resets for the next frame's fresh set of forces.
Wraps the boid to the opposite vertical edge when it exits the canvas
const margin = 10;
Allows the boid to travel slightly past the edge before wrapping, so it disappears smoothly instead of popping
if (this.position.x > width + margin) this.position.x = -margin;
If the boid exits the right edge, teleport it just off the left edge
else if (this.position.x < -margin) this.position.x = width + margin;
If the boid exits the left edge, teleport it just off the right edge
if (this.position.y > height + margin) this.position.y = -margin;
If the boid exits the bottom edge, teleport it just off the top
else if (this.position.y < -margin) this.position.y = height + margin;
If the boid exits the top edge, teleport it just off the bottom
flock()
flock() is the conductor that blends together separation, alignment, cohesion, and predator avoidance into one combined steering behavior - the weights here define the flock's overall personality.
🔬 These four multipliers balance the whole flock's personality. What happens if you set alignment's multiplier much higher than separation's - do the boids start moving in tight, orderly lines like a squadron?
Computes the predator-avoidance force and triples its strength so fleeing always overrides normal flocking
this.applyForce(sep);
Adds the separation force to this frame's acceleration
this.applyForce(ali);
Adds the alignment force to this frame's acceleration
this.applyForce(coh);
Adds the cohesion force to this frame's acceleration
this.applyForce(avoidPred);
Adds the predator-avoidance force to this frame's acceleration
separation()
Separation is the 'personal space' rule of flocking - without it, boids would collapse into a single overlapping clump. Notice how this is the only rule with the extra div(d), which makes the push stronger the closer a neighbor gets.
🔬 desiredSeparation defines how much personal space each boid demands. What happens to the flock's density if you raise it to 80?
const desiredSeparation = 25;
const steering = createVector(0, 0);
let total = 0;
separation(boids) {
const desiredSeparation = 25;
const steering = createVector(0, 0);
let total = 0;
for (let other of boids) {
if (other === this) continue;
const d = p5.Vector.dist(this.position, other.position);
if (d > 0 && d < desiredSeparation) {
const diff = p5.Vector.sub(this.position, other.position);
diff.normalize();
diff.div(d); // stronger for close neighbors
steering.add(diff);
total++;
}
}
if (total > 0) {
steering.div(total);
steering.setMag(this.maxSpeed);
steering.sub(this.velocity);
steering.limit(this.maxForce * 1.5);
}
return steering;
}
Line-by-line explanation (14 lines)
🔧 Subcomponents:
for-loopScan Nearby Neighborsfor (let other of boids) {
Checks every other boid to find ones close enough to push away from
conditionalToo Close Checkif (d > 0 && d < desiredSeparation) {
Only reacts to neighbors within the personal-space radius
const desiredSeparation = 25;
Sets the personal-space radius in pixels - neighbors closer than this trigger avoidance
for (let other of boids) {
Loops through every boid in the flock to check its distance
if (other === this) continue;
Skips comparing the boid to itself
const d = p5.Vector.dist(this.position, other.position);
Measures the distance between this boid and the other one
if (d > 0 && d < desiredSeparation) {
Only reacts if the other boid is inside the personal-space radius (and not at the exact same spot)
Calculates a vector pointing away from the other boid
diff.normalize();
Reduces that vector to length 1 so only its direction matters so far
diff.div(d); // stronger for close neighbors
Divides by distance so closer neighbors produce a stronger push
steering.add(diff);
Accumulates this push into the total steering vector
total++;
Counts how many neighbors contributed to this force
steering.div(total);
Averages the accumulated pushes across all nearby neighbors
steering.setMag(this.maxSpeed);
Scales the desired direction to the boid's top speed
steering.sub(this.velocity);
Converts the desired velocity into a steering force by subtracting the current velocity (this is the standard Reynolds steering formula)
steering.limit(this.maxForce * 1.5);
Caps how strong this force can be, giving separation a slightly higher force limit than the other rules
alignment()
Alignment makes boids match the average direction of their neighbors, which is what produces the smooth, synchronized turning you see in real bird flocks.
alignment(boids) {
const perceptionRadius = 60;
const steering = createVector(0, 0);
let total = 0;
for (let other of boids) {
if (other === this) continue;
const d = p5.Vector.dist(this.position, other.position);
if (d < perceptionRadius) {
steering.add(other.velocity);
total++;
}
}
if (total > 0) {
steering.div(total);
steering.setMag(this.maxSpeed);
steering.sub(this.velocity);
steering.limit(this.maxForce);
}
return steering;
}
Line-by-line explanation (11 lines)
🔧 Subcomponents:
for-loopScan Neighbor Velocitiesfor (let other of boids) {
Gathers the velocity of every nearby boid to average their heading
const perceptionRadius = 60;
Defines how far away a neighbor's heading still counts toward alignment
for (let other of boids) {
Checks every boid in the flock
if (other === this) continue;
Skips itself
const d = p5.Vector.dist(this.position, other.position);
Measures distance to the other boid
if (d < perceptionRadius) {
Only counts neighbors within the perception radius
steering.add(other.velocity);
Adds the neighbor's velocity to a running total
total++;
Counts how many neighbors contributed
steering.div(total);
Averages the neighbors' velocities to find the group's average heading
steering.setMag(this.maxSpeed);
Scales the desired heading to top speed
steering.sub(this.velocity);
Turns the desired velocity into a steering force
steering.limit(this.maxForce);
Caps how strongly this force can turn the boid
cohesion()
Cohesion pulls each boid gently toward the average position of its neighbors, which is what keeps the flock from drifting apart into scattered individuals.
cohesion(boids) {
const perceptionRadius = 70;
const steering = createVector(0, 0);
let total = 0;
for (let other of boids) {
if (other === this) continue;
const d = p5.Vector.dist(this.position, other.position);
if (d < perceptionRadius) {
steering.add(other.position);
total++;
}
}
if (total > 0) {
steering.div(total);
steering.sub(this.position);
steering.setMag(this.maxSpeed);
steering.sub(this.velocity);
steering.limit(this.maxForce);
}
return steering;
}
Line-by-line explanation (8 lines)
🔧 Subcomponents:
for-loopScan Neighbor Positionsfor (let other of boids) {
Gathers the position of every nearby boid to find the group's center of mass
const perceptionRadius = 70;
Defines how far away a neighbor still counts toward the perceived group center
for (let other of boids) {
Checks every boid in the flock
steering.add(other.position);
Adds the neighbor's position to a running total, used to find the average location
steering.div(total);
Averages all nearby positions to find the local center of mass
steering.sub(this.position);
Turns that center-of-mass point into a direction vector pointing from this boid toward it
steering.setMag(this.maxSpeed);
Scales that direction to top speed
steering.sub(this.velocity);
Converts it into a steering force relative to current velocity
steering.limit(this.maxForce);
Caps the force strength
avoidPredators()
This method uses the same steering-force pattern as separation, alignment, and cohesion, but with a much larger perception radius and force limit so predator-avoidance can overpower normal flocking.
avoidPredators(predators) {
const perceptionRadius = 130;
const steering = createVector(0, 0);
let total = 0;
for (let predator of predators) {
const d = p5.Vector.dist(this.position, predator.position);
if (d < perceptionRadius) {
const diff = p5.Vector.sub(this.position, predator.position);
diff.normalize();
diff.div(d * 0.5 + 1); // very strong when close
steering.add(diff);
total++;
}
}
if (total > 0) {
steering.div(total);
steering.setMag(this.maxSpeed * 1.4);
steering.sub(this.velocity);
steering.limit(this.maxForce * 3.0);
}
return steering;
}
Line-by-line explanation (11 lines)
🔧 Subcomponents:
for-loopScan Nearby Predatorsfor (let predator of predators) {
Checks every predator to see if it's close enough to flee from
Calculates a vector pointing away from the predator
diff.normalize();
Reduces it to a pure direction
diff.div(d * 0.5 + 1); // very strong when close
Scales the push so it grows dramatically stronger as the predator gets closer
steering.add(diff);
Accumulates this fleeing force
total++;
Counts how many predators contributed
steering.setMag(this.maxSpeed * 1.4);
Lets the fleeing direction exceed the boid's normal top speed by 40%, simulating an adrenaline burst
steering.limit(this.maxForce * 3.0);
Allows this force to be three times stronger than normal steering forces, since survival takes priority
panic()
panic() is only called while panicTimer is active, layering a temporary, distance-weighted fleeing force on top of the normal flocking behavior triggered by every left-click.
🔬 The '7' here multiplies how violently boids flee. What happens if you lower it to 2 - does the panic burst look calmer or almost unnoticeable?
Adds a small random jitter so the flight looks chaotic rather than perfectly radial
this.applyForce(dir);
Applies the combined panic-and-jitter force to the boid's acceleration
Boid show()
Using velocity.heading() to rotate the shape is what makes each boid visually point in its direction of travel - a small but essential touch that sells the illusion of purposeful movement.
Gives the predator a random initial direction at speed 1.5
this.maxSpeed = 2.6;
Sets the predator's top speed, slightly faster than an average boid
this.maxForce = 0.08;
Sets how sharply the predator can steer, higher than a boid's default for more aggressive turning
this.size = 11;
Sets the predator's base size, larger than any boid so it reads visually as a threat
Predator update()
Unlike the Boid class, Predator has no separate applyForce/acceleration system - it computes and applies its steering force directly within update() each frame, a simpler variant of the same steering pattern.
update() {
let steering = createVector(0, 0);
// Lightly seek nearest boid (if any) to keep them interesting
const target = this.findNearestBoid();
if (target) {
const desired = p5.Vector.sub(target.position, this.position);
desired.setMag(this.maxSpeed);
steering = p5.Vector.sub(desired, this.velocity);
steering.limit(this.maxForce);
}
// Add a bit of wander
steering.add(p5.Vector.random2D().mult(0.02));
this.velocity.add(steering);
this.velocity.limit(this.maxSpeed);
this.position.add(this.velocity);
}
Line-by-line explanation (10 lines)
🔧 Subcomponents:
conditionalSeek Nearest Boidif (target) {
Steers the predator toward the closest boid, if one exists
const target = this.findNearestBoid();
Finds the closest boid to chase
if (target) {
Only steers toward a target if at least one boid exists
Allows the predator to travel a bit further off-screen than a boid before wrapping
if (this.position.x > width + margin) this.position.x = -margin;
Wraps the predator from the right edge to the left
else if (this.position.x < -margin) this.position.x = width + margin;
Wraps the predator from the left edge to the right
if (this.position.y > height + margin) this.position.y = -margin;
Wraps the predator from the bottom edge to the top
else if (this.position.y < -margin) this.position.y = height + margin;
Wraps the predator from the top edge to the bottom
Predator show()
Reusing the same triangle-and-rotate rendering pattern as Boid.show() but with a bright red fill and sharper proportions instantly communicates 'danger' without needing any images or sprites.
Each boid loops over the entire boids array for every one of the three flocking rules, giving the simulation O(n²) complexity per frame. With hundreds of boids (easily reached via repeated left-clicks), this can noticeably drop the frame rate.
💡 Use a spatial partitioning structure (a grid or quadtree) to only check boids in nearby cells, or combine the three neighbor loops into a single shared pass that computes all three sums at once.
BUGspawnBoidGroup() / mousePressed()
The boids array has no upper limit. Repeatedly left-clicking keeps adding 22 boids every time with no cap, so the simulation can grow unbounded and eventually stutter or crash the tab.
💡 Add a MAX_BOIDS constant and skip spawning (or remove the oldest boids) once boids.length exceeds it.
STYLEBoid class (separation/alignment/cohesion/avoidPredators)
Perception radii (25, 60, 70, 130) and force multipliers (1.6, 1.0, 0.8, 3.0, 1.5) are hard-coded as magic numbers scattered across multiple methods, making the flocking behavior harder to tune consistently.
💡 Move these into named constants near the top of the file (e.g. SEPARATION_RADIUS, ALIGNMENT_RADIUS) so all the tunable 'personality' knobs live in one place.
FEATUREPredator class
Predators are permanent once placed - there's no way to remove a single predator, only clear all of them at once with X.
💡 Add a click-to-remove interaction (e.g. clicking near an existing predator with a modifier key removes just that one) for more precise scene control.
What visual experience does the AI Boid Flocking Simulator create?
The sketch visually simulates realistic swarms of birds or fish, showcasing emergent behaviors as they flock together, while displaying a vibrant gradient background.
How can users interact with the AI Boid Flocking Simulator?
Users can interact by clicking the left mouse button to spawn new groups of boids and trigger a panic response, causing the boids to scatter from the click location.
What creative coding concepts are demonstrated in this sketch?
This sketch demonstrates flocking behavior, emergent systems, and particle motion, illustrating how simple rules can create complex group dynamics.