この章ではBoids(ボイド)という鳥の群れの動きをシミュレーションするためのアルゴリズムを扱う。鳥だけではなく魚などの動きにも適用できるので、Boidsではなく抽象的にフロッキングや群行動と呼ばれることも多い。

Boidsの機能は、前章で扱ったVehicleクラスを拡張する形で定義を行う。

視界範囲

我々が混雑している場所を歩くとき、他の人にぶつからないように歩く位置や速度を調整するが、群れの中にいる鳥も同じようなことを行う。ただし、相手が視界に入っていない場合や距離が離れている場合は配慮をする必要がないので、群れ全体ではなく近くにいる数匹の鳥だけを意識すればいい。自分の近くにいる鳥に注目して、相手が近づいてきたら離れたり、逆に注目している集団の中に近づこうとする。このような動きを実装するとリアルな鳥の群れの動きを作ることができ、これがBoidsのアルゴリズムの核になる。

相手の鳥を意識しないといけない領域のことをここでは視界範囲と呼ぶ。視界範囲としては、視野角と距離を設定して、その中に群れの鳥一匹一匹が入っているかを計算するのがいいのだが、コードがややこしくなるので、ここでは自身の位置から見て一定の半径以内を視界範囲と定義したい。

const Vehicle = {
  inSight(v, other) {
    const d = dist(v.position.x, v.position.y, other.position.x, other.position.y);
    return d < 100;
  }
};

Vehicle.inSight(v, other)v からみて other が視えているかいないかを boolean として返す。前述した通り、自身の位置から見て一定の半径以内を視界範囲と定義したいので、2点間距離を取り、それが半径以内かどうかを調べればいい。

次に、Boidsで定義されている3つの動作に関するルールについて解説する。

ルール その1 - 結合

const Vehicle = {
  cohesion(v, boids) {
    let avgPos = createVector();
    let n = 0;
    for (const other of boids) {
      if (v === other || !Vehicle.inSight(v, other)) continue;
      avgPos.add(other.position);
      n++;
    }
    if (n > 0) {
      avgPos.div(n);
      Vehicle.seek(v, avgPos);
    }
  }
};

群れからはぐれないように視界範囲にいる鳥の中央に移動しようとする動きを結合と呼ぶ。

ルール その2 - 分離

const Vehicle = {
  separation(v, boids) {
    for (const other of boids) {
      if (v === other || !Vehicle.inSight(v, other)) continue;
      const d = dist(v.position.x, v.position.y, other.position.x, other.position.y);
      if (d < 50) {
        Vehicle.flee(v, other.position);
      }
    }
  }
};

視界範囲にいる鳥が自身にあまりにも近づいてきた場合は、その鳥の位置と逆方向に逃げる動きを分離と呼ぶ。

ルール その3 - 整列

const Vehicle = {
  align(v, vehicles) {
    const avgVel = createVector();
    let n = 0;
    for (const other of vehicles) {
      if (v === other || !Vehicle.inSight(v, other)) continue;
      avgVel.add(other.velocity);
      n++;
    }
    if (n > 0) {
      avgVel.div(n);
      avgVel.limit(v.maxSpeed);
      v.acceleration.add(p5.Vector.sub(avgVel, v.velocity));
    }
  }
};

鳥の群れはだいたい同じ方向に移動するので、自身が視界範囲にいる鳥に合わせて向き(速度)を合わせる動きを整列と呼ぶ。

フロック

function draw() {
  clear();

  vehicles.forEach((v) => {
    Vehicle.flock(v, vehicles);
    Vehicle.update(v);
    Vehicle.draw(v, "#aaa");
  });
}

const Vehicle = {
  flock(v, boids) {
    Vehicle.align(v, boids);
    Vehicle.cohesion(v, boids);
    Vehicle.separation(v, boids);
  }
};

3つのルールの関数から得た力の合計を加速度にして、加速度から速度を、速度から位置を更新する。

Boidsの実装例

let vehicles;

function setup() {
  createCanvas(windowWidth, windowHeight);

  vehicles = [];
  for (let i = 0; i < 50; i++) {
    const position = createVector(random(width), random(height));
    const velocity = p5.Vector.random2D();
    velocity.setMag(random(2, 4));
    const maxSpeed = 4;
    const maxForce = 2;
    vehicles.push(Vehicle.create(position, velocity, maxSpeed, maxForce));
  }
}

function draw() {
  clear();

  vehicles.forEach((v) => {
    Vehicle.flock(v, vehicles);
    Vehicle.update(v);
    Vehicle.draw(v, "#aaa");
  });
}

const Vehicle = {
  create: function (position, velocity, maxSpeed, maxForce) {
    return { position, velocity, acceleration: createVector(), maxSpeed, maxForce };
  },

  update: function (v) {
    v.acceleration.limit(v.maxForce);
    v.velocity.add(v.acceleration);
    v.velocity.limit(v.maxSpeed);
    v.position.add(v.velocity);
    v.acceleration.set(0);

    Vehicle.adjustEdge(v);
  },

  adjustEdge: function (v) {
    if (v.position.x < 0) {
      v.position.x = 0;
      v.velocity.x *= -1;
    } else if (v.position.x >= width) {
      v.position.x = width - 1;
      v.velocity.x *= -1;
    }

    if (v.position.y < 0) {
      v.position.y = 0;
      v.velocity.y *= -1;
    } else if (v.position.y >= height) {
      v.position.y = height - 1;
      v.velocity.y *= -1;
    }
  },

  draw: function (v, strokeColor) {
    push();
    noFill();
    strokeWeight(2);
    stroke(strokeColor);
    translate(v.position);
    rotate(v.velocity.heading());
    beginShape();
    const r = 8;
    vertex(r * 2, 0);
    vertex(-r, r);
    vertex(-r, -r);
    endShape(CLOSE);
    pop();
  },

  seek: function (v, target) {
    const tv = p5.Vector.sub(target, v.position);
    tv.limit(v.maxSpeed);
    const force = p5.Vector.sub(tv, v.velocity);
    v.acceleration.add(force);
  },

  flee: function (v, target) {
    const tv = p5.Vector.sub(target, v.position);
    tv.limit(v.maxSpeed);
    const force = p5.Vector.sub(tv, v.velocity);
    v.acceleration.sub(force);
  },

  inSight(v, other) {
    const d = dist(v.position.x, v.position.y, other.position.x, other.position.y);
    return d < 100;
  },

  align(v, vehicles) {
    const avgVel = createVector();
    let n = 0;
    for (const other of vehicles) {
      if (v === other || !Vehicle.inSight(v, other)) continue;
      avgVel.add(other.velocity);
      n++;
    }
    if (n > 0) {
      avgVel.div(n);
      avgVel.limit(v.maxSpeed);
      v.acceleration.add(p5.Vector.sub(avgVel, v.velocity));
    }
  },

  separation(v, boids) {
    for (const other of boids) {
      if (v === other || !Vehicle.inSight(v, other)) continue;
      const d = dist(v.position.x, v.position.y, other.position.x, other.position.y);
      if (d < 50) {
        Vehicle.flee(v, other.position);
      }
    }
  },

  cohesion(v, boids) {
    let avgPos = createVector();
    let n = 0;
    for (const other of boids) {
      if (v === other || !Vehicle.inSight(v, other)) continue;
      avgPos.add(other.position);
      n++;
    }
    if (n > 0) {
      avgPos.div(n);
      Vehicle.seek(v, avgPos);
    }
  },

  flock(v, boids) {
    Vehicle.align(v, boids);
    Vehicle.cohesion(v, boids);
    Vehicle.separation(v, boids);
  },
};