Bird Flocking with Predators in Matter.js

This article explains how to simulate Craig Reynolds' classic flocking behavior (Boids) with dynamic predator evasion using the Matter.js 2D physics engine. By combining separation, alignment, and cohesion with high-priority repulsion vectors away from moving predator bodies, you can achieve natural aerial navigation and evasive maneuvers within a rigid-body physics simulation.

1. Architectural Overview

Standard Matter.js simulations rely on rigid body collisions. For natural flocking, however, birds (boids) navigate through steering forces rather than hard physical impacts.

To achieve this in Matter.js:

2. Setting Up Bodies and Collision Filters

Initialize your boids and predators as sensor-like bodies so they pass through one another without triggering default impulse resolution:

const { Engine, Render, Runner, Bodies, Composite, Body, Vector } = Matter;

const engine = Engine.create({ gravity: { x: 0, y: 0 } }); // Zero gravity for top-down/aerial view
const world = engine.world;

// Disable standard collisions among boids and predators
const FLOCK_GROUP = Body.nextGroup(true);

function createBoid(x, y) {
  return Bodies.circle(x, y, 5, {
    collisionFilter: { group: FLOCK_GROUP },
    frictionAir: 0.02
  });
}

function createPredator(x, y) {
  return Bodies.circle(x, y, 15, {
    collisionFilter: { group: FLOCK_GROUP },
    render: { fillStyle: 'red' }
  });
}

3. Calculating Steering Behaviors

Flocking relies on three core rules plus an evasion rule:

  1. Separation: Steer away from crowded local flockmates.
  2. Alignment: Steer toward the average heading of local flockmates.
  3. Cohesion: Steer toward the average position of local flockmates.
  4. Predator Avoidance: Strongly steer away from any predator entering a threat radius.
function getSteeringForces(boid, boids, predators) {
  const perceptionRadius = 60;
  const predatorRadius = 150;

  let separation = { x: 0, y: 0 };
  let alignment = { x: 0, y: 0 };
  let cohesion = { x: 0, y: 0 };
  let predatorAvoidance = { x: 0, y: 0 };
  let totalNeighbors = 0;

  // Flocking calculations
  for (const other of boids) {
    if (other === boid) continue;

    const d = Vector.magnitude(Vector.sub(boid.position, other.position));
    if (d < perceptionRadius && d > 0) {
      // Separation (inversely proportional to distance)
      const diff = Vector.normalise(Vector.sub(boid.position, other.position));
      separation = Vector.add(separation, Vector.div(diff, d));

      // Alignment
      alignment = Vector.add(alignment, other.velocity);

      // Cohesion
      cohesion = Vector.add(cohesion, other.position);

      totalNeighbors++;
    }
  }

  // Predator evasion
  for (const predator of predators) {
    const d = Vector.magnitude(Vector.sub(boid.position, predator.position));
    if (d < predatorRadius && d > 0) {
      // Strong repulsion vector weighted higher than flocking behaviors
      const fleeVector = Vector.normalise(Vector.sub(boid.position, predator.position));
      const forceMagnitude = (predatorRadius - d) / predatorRadius; // Stronger when closer
      predatorAvoidance = Vector.add(predatorAvoidance, Vector.mult(fleeVector, forceMagnitude));
    }
  }

  if (totalNeighbors > 0) {
    alignment = Vector.div(alignment, totalNeighbors);
    cohesion = Vector.sub(Vector.div(cohesion, totalNeighbors), boid.position);
  }

  return {
    separation: Vector.mult(Vector.normalise(separation), 0.0015),
    alignment: Vector.mult(Vector.normalise(alignment), 0.001),
    cohesion: Vector.mult(Vector.normalise(cohesion), 0.0008),
    evasion: Vector.mult(Vector.normalise(predatorAvoidance), 0.006) // Heavy weight for survival
  };
}

4. Running the Simulation Loop

Hook into Matter.js’s beforeUpdate event to compute steering forces, apply them, clamp maximum speeds, and rotate the boid toward its velocity vector.

Matter.Events.on(engine, 'beforeUpdate', () => {
  const maxSpeed = 4;

  for (const boid of boidList) {
    const forces = getSteeringForces(boid, boidList, predatorList);
    
    // Sum forces
    let netForce = Vector.add(forces.separation, forces.alignment);
    netForce = Vector.add(netForce, forces.cohesion);
    netForce = Vector.add(netForce, forces.evasion);

    // Apply the resulting force
    Body.applyForce(boid, boid.position, netForce);

    // Clamp velocity
    const speed = Vector.magnitude(boid.velocity);
    if (speed > maxSpeed) {
      const clampedVelocity = Vector.mult(Vector.normalise(boid.velocity), maxSpeed);
      Body.setVelocity(boid, clampedVelocity);
    }

    // Orient the body to face its movement direction
    if (speed > 0.1) {
      Body.setAngle(boid, Math.atan2(boid.velocity.y, boid.velocity.x));
    }
  }

  // Update predator trajectory (e.g., chasing nearest boid or user cursor)
  updatePredators(predatorList, boidList);
});

5. Moving the Predator

To make the predator actively disrupt the flock, update its velocity toward the centroid of the flock or the nearest boid:

function updatePredators(predators, boids) {
  const predatorSpeed = 2.5;

  for (const predator of predators) {
    let nearest = null;
    let shortestDistance = Infinity;

    for (const boid of boids) {
      const d = Vector.magnitude(Vector.sub(predator.position, boid.position));
      if (d < shortestDistance) {
        shortestDistance = d;
        nearest = boid;
      }
    }

    if (nearest) {
      const chaseDirection = Vector.normalise(Vector.sub(nearest.position, predator.position));
      Body.setVelocity(predator, Vector.mult(chaseDirection, predatorSpeed));
    }
  }
}

By keeping the predator speed slightly slower than the boids' maximum evasion speed, the flock splits realistically around the predator's body and reforms dynamically once the threat passes.