AI Boids Flocking - Emergent Swarm Intelligence Watch 100 birds flock together using Craig Reynolds
This sketch simulates a flock of 100 triangular birds ('boids') that move around the screen following three simple local rules — separation, alignment, and cohesion — plus a fourth rule that makes them flee the mouse cursor like a predator. No bird is told to flock; the group behavior emerges purely from each boid reacting to its nearby neighbors.
Make trails linger longer — Lowering the background overlay's alpha leaves a longer glowing trail behind each boid instead of fading quickly.
Grow the birds — Increasing this.size makes each triangle bigger, so you can clearly see individual boids and their orientation.
Make the flock clump into one tight blob — Boosting the cohesion weight relative to separation pulls boids much closer together, creating a dense swarm ball.
This sketch brings Craig Reynolds' classic 'boids' algorithm to life: 100 small colorful triangles glide around the canvas, banking and turning as they form swirling, fish-like swarms. Each triangle is rotated to face its direction of travel and colored by its heading using HSB color mode, so the whole flock ripples with shifting hues. Move your mouse into the canvas and the boids scatter away from it like fish fleeing a predator, then regroup once you move away.
The code is organized around a single Boid class that stores each bird's position, velocity, and acceleration as p5.Vector objects, plus a handful of small methods — align(), cohesion(), separation(), and fleePredator() — that each return a steering force. The global setup() and draw() functions simply create the flock and loop over every boid each frame, calling flock(), update(), edges(), and show(). Studying this sketch teaches you how independent local rules on many objects can create complex, organic group motion without any central controller.
⚙️ How It Works
When the sketch loads, setup() creates a full-window canvas, switches to HSB color mode so hue can represent direction, and spawns 100 Boid objects at random positions, each with a random starting velocity.
Every frame, draw() paints a mostly-transparent black rectangle over the whole canvas instead of a solid background — this lets old frames fade slowly instead of vanishing instantly, producing soft motion trails behind every boid.
For each boid, flock() calculates four steering vectors — alignment (match neighbors' direction), cohesion (move toward the local flock's center), separation (avoid crowding), and predator avoidance (flee the mouse) — then blends them together with different strengths and adds the result to the boid's acceleration.
update() applies that acceleration to the boid's velocity, caps the velocity at maxSpeed, and moves the boid's position forward, then resets acceleration to zero so it can be recalculated fresh next frame.
edges() teleports any boid that drifts off one side of the screen back onto the opposite side, so the flock lives in a seamless, wrap-around world instead of bouncing off walls.
Finally show() draws each boid as a triangle rotated to match its velocity's heading and colored by that same heading angle mapped to a hue, so you can visually read each bird's direction from its color.
🎓 Concepts You'll Learn
Object-oriented programming with ES6 classesVector math with p5.Vector (add, sub, limit, setMag, heading)Steering behaviors (separation, alignment, cohesion)Emergent behavior from simple local rulesHSB color mode mapped from direction angleEdge wrapping for continuous spaceAlpha-blended backgrounds for motion trails
📝 Code Breakdown
setup()
setup() runs exactly once when the sketch starts. It's the right place to configure the canvas, set the color system, and populate arrays like boids before the animation loop begins.
function setup() {
createCanvas(windowWidth, windowHeight);
// Use HSB so we can map heading → hue easily
colorMode(HSB, 360, 100, 100, 100);
background(0); // solid black to start
// Create initial flock
for (let i = 0; i < NUM_BOIDS; i++) {
boids.push(new Boid(random(width), random(height)));
}
}
Line-by-line explanation (5 lines)
🔧 Subcomponents:
for-loopSpawn Initial Flockfor (let i = 0; i < NUM_BOIDS; i++) {
boids.push(new Boid(random(width), random(height)));
}
Creates NUM_BOIDS new Boid objects at random positions and adds them to the boids array
createCanvas(windowWidth, windowHeight);
Creates a canvas that fills the entire browser window.
colorMode(HSB, 360, 100, 100, 100);
Switches p5's color system from RGB to HSB (hue, saturation, brightness, alpha), with hue ranging 0-360, so we can later color boids based on a 0-360 degree angle.
background(0); // solid black to start
Paints the canvas solid black once, giving the trail effect in draw() a clean starting point.
for (let i = 0; i < NUM_BOIDS; i++) {
Loops NUM_BOIDS times (100 by default) to build the initial flock.
Creates a new Boid at a random x/y position on the canvas and adds it to the global boids array.
draw()
draw() runs continuously (about 60 times per second) and is where all animation happens. Using a translucent background instead of a solid one is a classic p5.js trick for creating trail/motion-blur effects cheaply.
🔬 This loop runs the whole simulation for each boid every frame. What happens if you comment out boid.edges() — where do the boids go when they leave the screen?
for (let boid of boids) {
boid.flock(boids);
boid.update();
boid.edges();
boid.show();
}
function draw() {
// Draw a translucent background for fading trails
// (alpha = 20/100 → about 20% opaque, 80% previous frame visible)
background(0, 0, 0, 20);
// Optional: visualize predator (mouse) as a faint circle
noFill();
stroke(0, 0, 100, 30); // light, semi-transparent
strokeWeight(2);
circle(mouseX, mouseY, 80);
// Update and render boids
for (let boid of boids) {
boid.flock(boids);
boid.update();
boid.edges();
boid.show();
}
}
Line-by-line explanation (9 lines)
🔧 Subcomponents:
for-loopUpdate and Render Every Boidfor (let boid of boids) {
boid.flock(boids);
boid.update();
boid.edges();
boid.show();
}
For each boid, calculates steering forces, moves it, wraps it around screen edges, and draws it
background(0, 0, 0, 20);
Instead of fully clearing the canvas, this draws a mostly-transparent black rectangle over everything. Because it's only 20% opaque, previous frames don't fully disappear, which creates fading motion trails behind each boid.
noFill();
Turns off fill so the next shape (the predator circle) is just an outline.
stroke(0, 0, 100, 30); // light, semi-transparent
Sets the outline color to a faint, semi-transparent white (in HSB: 0 hue, 0 saturation, 100 brightness, 30 alpha).
circle(mouseX, mouseY, 80);
Draws a faint circle at the mouse position to visualize the 'predator' zone that scares boids away.
for (let boid of boids) {
Loops through every boid in the flock to update and draw it this frame.
boid.flock(boids);
Asks this boid to calculate its steering forces based on all other boids, and add them to its acceleration.
boid.update();
Applies the accumulated acceleration to velocity and moves the boid's position.
boid.edges();
Wraps the boid to the opposite side of the canvas if it has drifted off an edge.
boid.show();
Draws the boid as a colored, rotated triangle at its current position.
windowResized()
windowResized() is a special p5.js function that p5 calls automatically for you whenever the browser window is resized — you don't need to call it yourself.
function windowResized() {
resizeCanvas(windowWidth, windowHeight);
background(0); // reset background on resize
}
Line-by-line explanation (2 lines)
resizeCanvas(windowWidth, windowHeight);
A built-in p5.js callback that fires automatically whenever the browser window changes size; this resizes the canvas to match the new window dimensions.
background(0); // reset background on resize
Repaints the canvas solid black so old trail residue doesn't look stretched or distorted after the resize.
constructor(x, y)
The constructor runs once each time you write `new Boid(x, y)`. It sets up all the per-boid state — position, velocity, acceleration, and tuning values — that the other methods will read and modify every frame.
constructor(x, y) {
this.position = createVector(x, y);
// Start with random direction & speed
const angle = random(TWO_PI);
const speed = random(1.5, 3.0);
this.velocity = p5.Vector.fromAngle(angle).mult(speed);
this.acceleration = createVector(0, 0);
this.maxSpeed = 3.0;
this.maxForce = 0.08; // max steering force
this.size = 5; // use for triangle size and edge wrapping
}
Line-by-line explanation (8 lines)
this.position = createVector(x, y);
Stores the boid's starting position as a p5.Vector, which bundles x and y into one object with useful math methods.
const angle = random(TWO_PI);
Picks a random direction between 0 and 2π radians (a full circle).
const speed = random(1.5, 3.0);
Picks a random starting speed between 1.5 and 3.0.
Builds a velocity vector pointing in the random angle, then scales it by the random speed — this is how the boid gets a random initial direction and speed in one line.
this.acceleration = createVector(0, 0);
Starts acceleration at zero; it will be filled in each frame by the flocking rules.
this.maxSpeed = 3.0;
The fastest this boid is ever allowed to travel, used to cap velocity each frame.
this.maxForce = 0.08; // max steering force
Limits how strongly any steering behavior can change the boid's direction, keeping turns smooth instead of instant.
this.size = 5; // use for triangle size and edge wrapping
Controls both the drawn triangle's size and the margin used when wrapping the boid around screen edges.
flock(boids)
flock() is the brain of each boid: it gathers four independent steering opinions, weights them by importance, and combines them into one acceleration. Changing these weights is the single most powerful way to change the flock's overall personality.
🔬 These four numbers are the 'personality' of the flock. What happens to the flock's shape if you set cohesion.mult(0.8) up to 2.5, making boids much more eager to clump together?
Gets a steering vector that nudges this boid to match its neighbors' average direction.
const cohesion = this.cohesion(boids);
Gets a steering vector that pulls this boid toward the center of nearby flockmates.
const separation = this.separation(boids);
Gets a steering vector that pushes this boid away from flockmates that are too close.
const avoidPredator = this.fleePredator();
Gets a steering vector that pushes the boid away from the mouse if it's within threat range.
alignment.mult(1.0);
Scales the alignment force — 1.0 means it's applied at full strength.
cohesion.mult(0.8);
Slightly weakens the cohesion force compared to alignment, so boids group together less aggressively than they match direction.
separation.mult(1.5);
Boosts the separation force above the others, so avoiding collisions takes priority over grouping.
avoidPredator.mult(2.0);
Strongly amplifies the fleeing force so boids clearly scatter from the mouse.
this.acceleration.add(alignment);
Adds the weighted alignment force into this frame's total acceleration.
this.acceleration.add(cohesion);
Adds the weighted cohesion force.
this.acceleration.add(separation);
Adds the weighted separation force.
this.acceleration.add(avoidPredator);
Adds the weighted predator-avoidance force, completing this boid's combined steering decision for the frame.
update()
update() implements basic Euler integration — a simple physics pattern where acceleration changes velocity and velocity changes position, one small step per frame. This same three-line pattern powers countless particle and physics simulations.
update() {
// Integrate acceleration into velocity and position
this.velocity.add(this.acceleration);
this.velocity.limit(this.maxSpeed);
this.position.add(this.velocity);
// Reset acceleration each frame
this.acceleration.mult(0);
}
Line-by-line explanation (4 lines)
this.velocity.add(this.acceleration);
Adds this frame's acceleration (the combined steering forces) into the velocity, changing speed and direction.
this.velocity.limit(this.maxSpeed);
Clamps the velocity's magnitude so the boid never exceeds maxSpeed, no matter how strong the steering forces were.
this.position.add(this.velocity);
Moves the boid by adding velocity to its position — this is the actual motion you see on screen.
this.acceleration.mult(0);
Resets acceleration to zero so next frame's flock() call starts with a clean slate instead of stacking forces forever.
edges()
This function creates a 'toroidal' or wrap-around world — think of the classic Asteroids game. Instead of bouncing off walls, boids that leave one side instantly reappear on the opposite side, keeping the flock feeling infinite.
Teleports the boid to the opposite vertical edge when it drifts off screen
const margin = this.size * 2;
Gives the boid a little buffer beyond the visible canvas edge before it wraps, so it doesn't pop out of view too abruptly.
if (this.position.x > width + margin) this.position.x = -margin;
If the boid goes off the right edge, teleport it to just off the left edge.
if (this.position.x < -margin) this.position.x = width + margin;
If the boid goes off the left edge, teleport it to just off the right edge.
if (this.position.y > height + margin) this.position.y = -margin;
If the boid goes off the bottom edge, teleport it to just above the top edge.
if (this.position.y < -margin) this.position.y = height + margin;
If the boid goes off the top edge, teleport it to just below the bottom edge.
align(boids)
align() is one of Reynolds' three original boid rules. By nudging each boid toward the average velocity of its neighbors, the whole flock naturally starts moving in the same direction without any boid knowing the group's overall heading.
align(boids) {
const perceptionRadius = 70;
const steering = createVector(0, 0);
let total = 0;
for (let other of boids) {
const d = p5.Vector.dist(this.position, other.position);
if (other !== this && 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 (10 lines)
🔧 Subcomponents:
for-loopSum Neighbor Velocitiesfor (let other of boids) {
const d = p5.Vector.dist(this.position, other.position);
if (other !== this && d < perceptionRadius) {
steering.add(other.velocity);
total++;
}
}
Adds up the velocities of every nearby boid (within perceptionRadius) to later compute their average heading
const perceptionRadius = 70;
Only boids within 70 pixels count as 'nearby neighbors' for alignment purposes.
const steering = createVector(0, 0);
Starts an empty vector that will accumulate the total steering force.
const d = p5.Vector.dist(this.position, other.position);
Measures the straight-line distance between this boid and another boid.
if (other !== this && d < perceptionRadius) {
Skips comparing the boid to itself, and only considers boids close enough to matter.
steering.add(other.velocity);
Accumulates the neighbor's velocity into the running total.
total++;
Counts how many neighbors were found, needed to compute an average afterward.
steering.div(total);
Divides the summed velocities by the neighbor count to get the average heading of the local flock.
steering.setMag(this.maxSpeed);
Scales that average direction so it has a magnitude equal to maxSpeed — turning it into a 'desired velocity'.
steering.sub(this.velocity);
Subtracts the boid's current velocity from the desired one, producing a steering force that nudges current velocity toward the desired velocity (this is the classic 'Reynolds steering' formula: desired - current).
steering.limit(this.maxForce);
Caps how strong this steering force can be, so alignment never causes an instant, unrealistic turn.
cohesion(boids)
cohesion() is the 'stick together' rule. Without it, boids would align and avoid each other but drift apart into separate groups; cohesion is what pulls scattered boids back into a single flock.
cohesion(boids) {
const perceptionRadius = 70;
const steering = createVector(0, 0);
let total = 0;
for (let other of boids) {
const d = p5.Vector.dist(this.position, other.position);
if (other !== this && d < perceptionRadius) {
steering.add(other.position);
total++;
}
}
if (total > 0) {
steering.div(total);
// Steer toward that center point
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-loopSum Neighbor Positionsfor (let other of boids) {
const d = p5.Vector.dist(this.position, other.position);
if (other !== this && d < perceptionRadius) {
steering.add(other.position);
total++;
}
}
Adds up the positions of every nearby boid to later find their average location (the local flock center)
const perceptionRadius = 70;
Defines the neighborhood radius used to decide which boids count toward the local center of mass.
steering.add(other.position);
Accumulates the neighbor's position, building toward an average position.
total++;
Counts neighbors so the sum can be turned into an average.
steering.div(total);
Divides the summed positions by the count to get the average position — the center of the local flock.
steering.sub(this.position);
Converts that center point into a direction vector by subtracting this boid's own position (pointing from 'me' toward 'the center').
steering.setMag(this.maxSpeed);
Scales that direction to maxSpeed to get a desired velocity pointing toward the group center.
steering.sub(this.velocity);
Subtracts current velocity to get the actual steering force needed (desired minus current, the standard steering formula).
steering.limit(this.maxForce);
Caps the force so the pull toward the center is gentle rather than instant.
separation(boids)
separation() is the collision-avoidance rule. It's what stops all 100 boids from converging into one overlapping blob — without it, cohesion alone would pull the whole flock into a single point.
🔬 Dividing by d*d makes repulsion much stronger at very short range. What happens to how the flock packs together if you change diff.div(d * d || 1) to diff.div(d || 1) (dividing by distance instead of distance-squared)?
let diff = p5.Vector.sub(this.position, other.position);
// Stronger repulsion when very close
diff.div(d * d || 1); // avoid division by zero
steering.add(diff);
separation(boids) {
const desiredSeparation = 25;
const steering = createVector(0, 0);
let total = 0;
for (let other of boids) {
const d = p5.Vector.dist(this.position, other.position);
if (other !== this && d < desiredSeparation) {
let diff = p5.Vector.sub(this.position, other.position);
// Stronger repulsion when very close
diff.div(d * d || 1); // avoid division by zero
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 (6 lines)
🔧 Subcomponents:
for-loopSum Repulsion From Close Neighborsfor (let other of boids) {
const d = p5.Vector.dist(this.position, other.position);
if (other !== this && d < desiredSeparation) {
let diff = p5.Vector.sub(this.position, other.position);
// Stronger repulsion when very close
diff.div(d * d || 1); // avoid division by zero
steering.add(diff);
total++;
}
}
Builds a push-away vector from every boid that's crowding too close, weighted so closer boids push harder
const desiredSeparation = 25;
Sets the minimum comfortable distance boids want to keep from each other — closer than this triggers repulsion.
if (other !== this && d < desiredSeparation) {
Only reacts to neighbors that are uncomfortably close (closer than desiredSeparation), ignoring itself and distant boids.
let diff = p5.Vector.sub(this.position, other.position);
Creates a vector pointing away from the crowding neighbor, from their position toward this boid's position.
diff.div(d * d || 1); // avoid division by zero
Divides the push vector by distance-squared, so boids that are extremely close get pushed away much harder than ones just barely inside the radius. The `|| 1` fallback prevents a crash if distance is ever exactly zero.
steering.add(diff);
Adds this neighbor's push contribution to the total repulsion force.
steering.limit(this.maxForce * 1.5);
Allows separation to push 50% harder than the normal maxForce, since avoiding collisions is treated as more urgent than aligning or grouping.
fleePredator()
This method is not part of Reynolds' original three rules, but it's a natural extension: any local rule that returns a small steering vector can be added to flock() alongside alignment, cohesion, and separation, which shows how extensible the boids model is.
🔬 map() here turns distance into a fleeing intensity between 2.0 and 0.3. What happens if you flip those numbers to map(d, 0, threatRadius, 0.3, 2.0), making boids flee harder the FARTHER they are from the mouse?
// Stronger when closer to the predator
const scale = map(d, 0, threatRadius, 2.0, 0.3);
flee.mult(scale);
fleePredator() {
const predatorPos = createVector(mouseX, mouseY);
const d = p5.Vector.dist(this.position, predatorPos);
const threatRadius = 120;
if (d < threatRadius) {
let flee = p5.Vector.sub(this.position, predatorPos);
flee.setMag(this.maxSpeed);
flee.sub(this.velocity);
flee.limit(this.maxForce * 3.0);
// Stronger when closer to the predator
const scale = map(d, 0, threatRadius, 2.0, 0.3);
flee.mult(scale);
return flee;
}
return createVector(0, 0);
}
Line-by-line explanation (10 lines)
🔧 Subcomponents:
conditionalThreat Range Checkif (d < threatRadius) {
let flee = p5.Vector.sub(this.position, predatorPos);
flee.setMag(this.maxSpeed);
flee.sub(this.velocity);
flee.limit(this.maxForce * 3.0);
// Stronger when closer to the predator
const scale = map(d, 0, threatRadius, 2.0, 0.3);
flee.mult(scale);
return flee;
}
Only applies a fleeing force if the mouse ('predator') is within threatRadius pixels of this boid
const predatorPos = createVector(mouseX, mouseY);
Treats the current mouse position as the location of a 'predator' the boids should avoid.
const d = p5.Vector.dist(this.position, predatorPos);
Measures how far this boid currently is from the mouse.
if (d < threatRadius) {
Only reacts if the mouse is close enough to be considered a threat; otherwise no fleeing force is needed.
let flee = p5.Vector.sub(this.position, predatorPos);
Creates a vector pointing away from the predator, from the mouse toward the boid.
flee.setMag(this.maxSpeed);
Scales that direction to full maxSpeed, representing the boid's desired 'escape velocity'.
flee.sub(this.velocity);
Subtracts current velocity to get the actual steering adjustment needed (the standard steering formula again).
flee.limit(this.maxForce * 3.0);
Allows fleeing to be up to 3x stronger than normal steering forces, since escaping a predator should override other behaviors.
const scale = map(d, 0, threatRadius, 2.0, 0.3);
Remaps the distance to the predator into a multiplier between 2.0 (very close, extra strong flee) and 0.3 (at the edge of threat range, weak flee).
flee.mult(scale);
Applies that distance-based scale, so fleeing is much more urgent right next to the mouse than at the edge of the threat radius.
return createVector(0, 0);
If the predator is out of range, returns a zero-length vector meaning 'no fleeing force needed'.
show()
show() demonstrates push()/translate()/rotate()/pop(), one of the most important patterns in p5.js for drawing many objects that each need their own position and rotation without affecting each other.
🔬 These three vertices define the triangle's shape. What happens if you add a fourth vertex to turn it into a diamond, or change the nose distance from this.size * 2 to this.size * 4 for a sharper arrow shape?
beginShape();
vertex(this.size * 2, 0); // nose
vertex(-this.size, this.size); // back bottom
vertex(-this.size, -this.size); // back top
endShape(CLOSE);
show() {
const theta = this.velocity.heading();
// Map direction (-PI..PI) to hue (0..360)
const hueVal = map(theta, -PI, PI, 0, 360);
push();
translate(this.position.x, this.position.y);
rotate(theta);
// Gradient-like effect via HSB hue based on direction
fill(hueVal, 80, 100, 80); // colorful body
stroke(hueVal, 80, 100, 100); // matching outline
strokeWeight(1.3);
// Triangle pointing in direction of motion (nose to the right)
beginShape();
vertex(this.size * 2, 0); // nose
vertex(-this.size, this.size); // back bottom
vertex(-this.size, -this.size); // back top
endShape(CLOSE);
pop();
}
Line-by-line explanation (13 lines)
const theta = this.velocity.heading();
Gets the angle (in radians) that the boid's velocity vector is pointing — its current direction of travel.
const hueVal = map(theta, -PI, PI, 0, 360);
Converts that angle (which ranges from -π to π) into a hue value from 0 to 360 degrees, so direction can be visualized as color.
push();
Saves the current drawing style and coordinate system so the upcoming translate/rotate only affects this boid.
translate(this.position.x, this.position.y);
Moves the coordinate origin to the boid's position, so all following drawing commands are relative to the boid.
rotate(theta);
Rotates the coordinate system to match the boid's heading, so the triangle drawn next will automatically point in its direction of travel.
fill(hueVal, 80, 100, 80); // colorful body
Sets the triangle's fill color using the direction-based hue, with high saturation and brightness and slight transparency.
stroke(hueVal, 80, 100, 100); // matching outline
Sets a fully opaque outline in the same hue family, giving the triangle a defined edge.
beginShape();
Starts defining a custom polygon shape.
vertex(this.size * 2, 0); // nose
Places the triangle's pointed 'nose' vertex out in front (along the rotated x-axis), giving the shape a clear direction indicator.
vertex(-this.size, this.size); // back bottom
Places one back corner of the triangle behind and below the nose.
vertex(-this.size, -this.size); // back top
Places the other back corner behind and above the nose, completing the triangle outline.
endShape(CLOSE);
Finishes the shape and connects the last vertex back to the first, closing the triangle.
pop();
Restores the saved coordinate system and style, so the translate/rotate done for this boid doesn't affect anything drawn afterward.
📦 Key Variables
NUM_BOIDSnumber
The total number of boids created at startup; controls how dense or sparse the flock is.
const NUM_BOIDS = 100;
boidsarray
Holds every Boid object in the simulation; setup() fills it, and draw() loops over it every frame to update and render each boid.
let boids = [];
🔧 Potential Improvements (4)
Here are some ways this code could be enhanced:
PERFORMANCEalign(), cohesion(), separation()
Each of these three methods independently loops over every other boid and recomputes p5.Vector.dist() for the same pair, giving roughly 3 x 100 x 100 = 30,000 distance calculations per frame.
💡 Combine the three loops into one shared neighbor-scanning loop inside flock() that computes distance once per pair and accumulates alignment, cohesion, and separation sums together, cutting the distance calculations by two-thirds.
STYLEalign() and cohesion()
Both methods declare an identical `const perceptionRadius = 70;`, duplicating a 'magic number' in two places that must be kept in sync manually.
💡 Move perceptionRadius to a single class-level property like `this.perceptionRadius = 70;` set in the constructor, or a shared constant, so it only needs to be changed once.
FEATUREflock() / general simulation
The neighbor-weighting numbers (1.0, 0.8, 1.5, 2.0) are hard-coded directly in flock(), making it hard to experiment with flock 'personality' without editing code each time.
💡 Expose these weights as top-level variables (or even simple UI sliders using p5.dom) so viewers can tune alignment/cohesion/separation/predator strength live without editing source.
PERFORMANCEOverall O(n^2) neighbor search
With NUM_BOIDS at 100 the O(n^2) neighbor checks are manageable, but increasing the flock size (as suggested by the NUM_BOIDS tunable) will slow the sketch down quickly since cost grows quadratically.
💡 For larger flocks, use a spatial partitioning structure (a grid or quadtree) to only check boids in nearby cells instead of comparing against the entire array every frame.
What visual experience does the AI Boids Flocking simulation offer?
The simulation visually showcases 100 triangle-shaped birds flocking together, creating dynamic and fluid movements that resemble natural swarming behavior.
How can users interact with the Boids Flocking simulation?
Users can interact with the sketch by moving their mouse, which is represented as a predator, influencing the flock's behavior as the boids attempt to avoid it.
What creative coding concepts are illustrated in the AI Boids Flocking sketch?
The sketch demonstrates emergent swarm intelligence through the implementation of flocking behaviors like separation, alignment, and cohesion, inspired by Craig Reynolds' Boids model.