p5js Boids

Okay, sketch 2: Boids!

The basic idea is to create a bunch of particles (the Boids in this case) and apply to them each a series of simple, limited rules that rely neither on communcation between the Boids nor a global controller and see what behaviors you can generate. Specifically, can you replicate the flocking behavior found in birds, since birds can obviously fly together without hitting one another and also without some lead bird giving orders.

Something like this:

For this case, there are three rules:

  • seperation - Fly away from any Boids that are too close to you (to avoid collision)
  • alignment - Align yourself to fly in the same direction as any Boids in your field of vision
  • cohesion - Fly towards the center point of the Boids you can see

Let’s turn that into code:


let gui;
let params = {
    boidCount: 10, boidCountMin: 1,
    boidVision: 50, boidVisionMin: 1, boidVisionMax: 100,
    displayVision: false,

    maximumVelocity: 2, maximumVelocityMin: 1, maximumVelocityMax: 10, maximumVelocityStep: 0.1,
    fade: 0.05, fadeMin: 0, fadeMax: 1, fadeStep: 0.01,

    seperationForce: 1, seperationForceMin: -1, seperationForceMax: 10, seperationForceStep: 0.1,
    alignmentForce: 2, alignmentForceMin: -1, alignmentForceMax: 10, alignmentForceStep: 0.1,
    cohesionForce: 2, cohesionForceMin: -1, cohesionForceMax: 10, cohesionForceStep: 0.1
};

var boids = [];

class Boid {
  constructor(p, v, c) {
    this.p = p;
    if (!p) this.p = createVector(random(width), random(height));

    this.v = v;
    if (!v) this.v = createVector(
      random(-params["maximumVelocity"], params["maximumVelocity"]),
      random(-params["maximumVelocity"], params["maximumVelocity"])
    );
    this.v = this.v.limit(params["maximumVelocity"]);

    this.c = c;
    if (!c) this.c = color(random(256), random(256), random(256));
  }

  update() {
    // Fix boid count
    while (boids.length < params["boidCount"]) boids.push(new Boid());
    while (boids.length > params["boidCount"]) boids.pop();

    // Seperation
    for (var other of boids) {
      if (this.p.dist(other.p) < params["boidVision"]) {
        this.v =
          p5.Vector.sub(this.p, other.p)
          .normalize()
          .mult(params["seperationForce"] * params["maximumVelocity"])
          .add(this.v);
      }
    }

    // Calculate the average boid of the local flock
    var flock = []
    for (other of boids) {
      if (this != other && this.p.dist(other.p) < params["boidVision"]) {
        flock.push(other);
      }
    }

    if (flock.length > 0) {
      var average = flock.reduce((average, boid) => new Boid(
        p5.Vector.add(average.p, boid.p),
        p5.Vector.add(average.v, boid.v),
      ));
      average.p.div(flock.length);
      average.v.div(flock.length);

      // Alignment
      this.v.add(p5.Vector.mult(average.v, params["alignmentForce"]));

      // Cohesion
      this.v =
        p5.Vector.sub(average.p, this.p)
        .normalize()
        .mult(params["cohesionForce"] * params["maximumVelocity"])
        .add(this.v);
    }

    // Clamp and move
    this.v.limit(params["maximumVelocity"]);
    this.p.add(this.v);

    this.p.x = (this.p.x + width) % width;
    this.p.y = (this.p.y + height) % height;
  }

  draw() {
    fill(this.c);
    noStroke();
    ellipse(this.p.x, this.p.y, 2, 2);
    
    noFill();
    stroke(this.c);
    if (params["displayVision"]) {
      ellipse(this.p.x, this.p.y, params["boidVision"], params["boidVision"]);
    }
  }
}

function setup() {
  createCanvas(400, 400);
  gui = createGuiPanel('boids');
  gui.addObject(params);
  gui.setPosition(420, 0);

  for (var i = 0; i < params["boidCount"]; i++) {
    boids.push(new Boid());
  }
}

function draw() {
  fill(color(256, 256, 256, 256 * params["fade"]));
  noStroke();
  rect(0, 0, width, height);

  boids.forEach((boid) => boid.update());
  boids.forEach((boid) => boid.draw());
}

function keyTyped() {
  if (key === 's') {
    saveCanvas('photo', 'png');
  }

  if (key == 'c') {
    clear();
  }
}

I’ve added the p5.gui.js library for creating mostly automatic GUIs with the variables of the simulation, so you can add/remove Boids, change the forces around (even make them negative), and other things. Pretty fun to play with!

You can do some pretty crazy things with these. One setting I found looks awfully like a cell:

Weird!