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:
- Boids and predators are represented as non-colliding circular dynamic bodies using collision filters.
- Flocking and evasion behaviors are computed manually each tick as vector forces.
- Computed steering vectors are applied to bodies using
Matter.Body.applyForce(). - Maximum velocities are clamped to simulate aerodynamic drag and terminal flight velocity.
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:
- Separation: Steer away from crowded local flockmates.
- Alignment: Steer toward the average heading of local flockmates.
- Cohesion: Steer toward the average position of local flockmates.
- 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.