Ai

On emergent behaviour, visualising the Boids Algorithm

On emergent behaviour, visualising the Boids Algorithm
On emergent behaviour, visualising the Boids Algorithm.

The Boids algorithm, developed by Craig Reynolds in 1986, simulates the flocking behavior of birds and other animals. It is based on three simple rules that govern the movement of each individual “boid” (a term derived from “bird-oid object”).

The thing I find fascinating is the emergent complex-looking behaviour from simple rules. The overall effect as shown in the simulation below is a beautiful, dynamic flocking motion that appears natural and lifelike. It’s a great example of how simple local interactions can lead to complex global patterns.

I personally really enjoy looking at the overall movement and get a sense of calmness from it. Yet, in the back of my mind, I’m also aware it is just a simulation based on bare simple rules.

Try the simulation below. Move your mouse around to see how the boids react to it.

Code

You can view the full code by inspecting the HTML file above. The key parts of the algorithm are laid out below:

  1. Separation: Steer to avoid crowding local flockmates. The objective is to maintain a comfortable distance from nearby boids. You don’t have large flocks of birds flying into each other!
  2. Alignment: Steer towards the average heading of local flockmates. This encourages the boids to align their direction of movement with their neighbours. In my opinion, this is one of the most interesting rules as it influences your own decision (direction in this case) based on the behaviour of others around you.
  3. Cohesion: Steer to move toward the average position of local flockmates. This rule encourages boids to move closer together, promoting group cohesion and preventing them from drifting apart. So it’s not just about flying in the same direction, but also staying together as a group.

Finally, we add an avoid function to steer away from a target (in this case, the mouse position) to demonstrate obstacle avoidance and how it affects the flocking behaviour.

// Rule 1: Separation - Steer to avoid crowding local flockmates
separation(boids) {
    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 < this.perceptionRadius / 2) {
            // Calculate vector pointing away from neighbor
            let diff = p5.Vector.sub(this.position, other.position);
            diff.div(d * d); // Weight by distance
            steering.add(diff);
            total++;
        }
    }
    if (total > 0) {
        steering.div(total);
        steering.setMag(this.maxSpeed);
        steering.sub(this.velocity);
        steering.limit(this.maxForce);
    }
    return steering;
}

// Rule 2: Alignment - Steer towards the average heading of local flockmates
align(boids) {
    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 < this.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;
}

// Rule 3: Cohesion - Steer to move toward the average position of local flockmates
cohesion(boids) {
    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 < this.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;
}

// A method to steer away from a target
avoid(target, avoidanceRadius) {
    let steering = createVector();
    let d = dist(this.position.x, this.position.y, target.x, target.y);
    if (d < avoidanceRadius) {
        let diff = p5.Vector.sub(this.position, target);
        diff.div(d * d); // Weight by distance
        steering.add(diff);
        steering.setMag(this.maxSpeed);
        steering.sub(this.velocity);
        steering.limit(this.maxForce * 1.5); // Give avoidance a bit more power
    }
    return steering;
}

Each boid calculates the steering forces based on these three rules and updates its velocity and position accordingly. It is important to note, that these calculations are done for each boid in relation to its local neighbours, which is what leads to the emergent flocking behaviour. There is no communication besides a boid seeing another boid within a certain radius.

Conclusion

Can you think of a situation where we as humans exhibit similar behaviour where we follow our own local rules and produce emergent behaviour? I think being able to breakdown something that looks complex into simple building blocks is incredibly powerful.