Inspiration & Concept
The inspiration for this project came from Robert Hodgin and Aditya Anantharaman. Robert Hodgin inspired the concept of creating a tree and birds flocking around it, while Aditya Anantharaman provided insights on how to implement a flocking mechanism.
Sketch
Code
let seed; let flock = []; let centerTree; function setup() { createCanvas(windowWidth, windowHeight); angleMode(DEGREES); seed = Date.now(); randomSeed(seed); noStroke(); // Initialize the tree centerTree = new Tree(width / 2, height - 100); // Initialize the flock for (let i = 0; i < 200; i++) { flock.push(new Boid(random(width), random(height))); } } function draw() { background(255); centerTree.display(); // Update and display each boid in the flock for (let boid of flock) { boid.flock(flock); boid.update(); boid.edges(); boid.show(); } } // Tree class class Tree { constructor(x, y) { this.x = x; this.y = y; } display() { push(); translate(this.x, this.y); // Start drawing the branches of the tree with an initial length of 25 branch(25); pop(); } } function branch(len) { push(); // Recursive branching if the length is greater than 5 if (len > 5) { let newLen = len * 0.8; // Set the stroke weight relative to the branch length to create a tapering effect strokeWeight(map(len, 10, 120, 1, 8)); stroke(0); line(0, 0, 0, -len); // Move to the end of the current branch to create the next branch translate(0, -len); rotate(20); branch(newLen); rotate(-40); branch(newLen); } pop(); } // Boid class for flocking behavior class Boid { constructor(x, y) { // Set the initial position of the boid this.position = createVector(x, y); // Assign a random initial velocity to each boid this.velocity = p5.Vector.random2D(); // Set the magnitude of the velocity to control the initial speed this.velocity.setMag(random(2, 4)); // Initialize acceleration with zero vector this.acceleration = createVector(); // Maximum steering force to apply, controls agility this.maxForce = 0.2; // Maximum speed of the boid this.maxSpeed = 4; // Size of the boid, represented as a dot this.size = random(2, 6); } // Handles wrapping of boids across the edges of the canvas to keep them in view edges() { if (this.position.x > width) { this.position.x = 0; } else if (this.position.x < 0) { this.position.x = width; } if (this.position.y > height) { this.position.y = height; } else if (this.position.y < 0) { this.position.y = height; } } // Alignment behavior: steer towards the average heading of nearby boids align(boids) { let perceptionRadius = 75; let steering = createVector(); let total = 0; for (let other of boids) { let d = dist( this.position.x, this.position.y, other.position.x, other.position.y ); 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; } // Cohesion behavior: steer towards the average position of nearby boids cohesion(boids) { let perceptionRadius = 75; let steering = createVector(); let total = 0; for (let other of boids) { let d = dist( this.position.x, this.position.y, other.position.x, other.position.y ); if (other != this && 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; } // Separation behavior: steer away from nearby boids to avoid crowding separation(boids) { let perceptionRadius = 24; let steering = createVector(); let total = 0; for (let other of boids) { let d = dist( this.position.x, this.position.y, other.position.x, other.position.y ); if (other != this && d < perceptionRadius) { let diff = p5.Vector.sub(this.position, other.position); diff.div(d * d); steering.add(diff); total++; } } if (total > 0) { steering.div(total); steering.setMag(this.maxSpeed); steering.sub(this.velocity); steering.limit(this.maxForce); } return steering; } // Apply the three flocking rules to determine the boid's acceleration flock(boids) { let alignment = this.align(boids); let cohesion = this.cohesion(boids); let separation = this.separation(boids); // Apply the three simple flocking rules alignment.mult(1.0); // Alignment: move in the average direction of nearby boids cohesion.mult(1.0); // Cohesion: move towards the average position of nearby boids and occasionally towards the tree separation.mult(1.5); // Repulsion: move away if too close to neighbors this.acceleration.add(alignment); this.acceleration.add(cohesion); this.acceleration.add(separation); // Dynamic change: Modify size to create tension and release effect this.size = map(sin(frameCount * 0.05), -1, 1, 1, 4); } update() { this.position.add(this.velocity); this.velocity.add(this.acceleration); this.velocity.limit(this.maxSpeed); // Reset acceleration to 0 for the next frame to avoid compounding effects this.acceleration.mult(0); } show() { fill(0); ellipse(this.position.x, this.position.y, this.size); } }
Setting the Scene
The project begins by defining a canvas that spans the entire window. A central tree is positioned at the bottom center of the canvas, and a flock of 200 birds (represented as dots) is initialized at random positions. The code uses randomSeed(seed) to ensure the system’s behavior remains consistent across different runs by fixing the random values generated for the tree and flock.
function setup() { createCanvas(windowWidth, windowHeight); angleMode(DEGREES); seed = Date.now(); randomSeed(seed); noStroke(); centerTree = new Tree(width / 2, height - 100); for (let i = 0; i < 200; i++) { flock.push(new Boid(random(width), random(height))); } }
The setup() function also initializes the tree and the flock. The tree will remain fixed, while the birds will exhibit a behavior similar to a flock moving around and reacting to each other and their surroundings.
Flocking Behavior of Birds
The flock of birds is represented by instances of the Boid class. Each bird follows three simple rules that are inspired by Aditya Anantharaman work:
- Alignment: Birds attempt to move in the average direction of their neighbors within a certain perception radius. This helps ensure the flock is cohesive.
- Cohesion: Birds steer towards the average position of nearby birds to maintain the flock’s unity.
- Separation: Birds steer away if they come too close to their neighbors, preventing clustering and collisions.
Each bird’s behavior is implemented using three methods: align(), cohesion(), and separation(). These methods collectively determine the bird’s acceleration.
flock(boids) { let alignment = this.align(boids); let cohesion = this.cohesion(boids); let separation = this.separation(boids); alignment.mult(1.0); cohesion.mult(1.0); separation.mult(1.5); this.acceleration.add(alignment); this.acceleration.add(cohesion); this.acceleration.add(separation); }
In the flock() method, the calculated steering forces for alignment, cohesion, and separation are combined and applied to each bird’s acceleration. The mult() function is used to control the influence of each behavior, making separation slightly more influential to avoid overcrowding.
Dynamic Movement and Visual Appeal
One of the features that makes this flocking system visually appealing is the dynamic change in the size of each bird. The size varies sinusoidally, creating a rhythmic pulsation that adds a sense of tension and release to the movement of the flock:
this.size = map(sin(frameCount * 0.05), -1, 1, 1, 4);
The sin() function is used to create a smooth oscillation effect, making the birds appear to “breathe” as they move. This subtle addition contributes to the natural, lifelike quality of the simulation.
Future Improvements
Moving forward, there are several potential improvements to enhance this project further. One idea is to add environmental changes, such as wind or obstacles, that the flock must navigate around. Adding interactivity, like allowing users to click and influence the direction of the birds or the growth of the tree, would also make the experience more immersive.