Model Ant Living Bridges with Matter.js

This article explains how to simulate the emergent phenomenon of army ant living bridges in a 2D environment using the Matter.js physics engine. By representing individual ants as circular rigid bodies equipped with proximity-based self-attraction forces and dynamic constraints, you can create a decentralized multi-agent system where static gaps are spanned automatically without centralized path planning.

The Underlying Mechanism

Ant living bridges emerge from two basic behaviors: attraction toward neighboring ants and physical interlocking when tension or proximity reaches a specific threshold. In Matter.js, this translates to:

  1. Agent Bodies: Independent circular bodies subject to gravity and collision.
  2. Attraction Field: A custom force applied during every engine update tick that pulls nearby ants toward one another.
  3. Dynamic Constraints: Distance-based elastic links (Matter.Constraint) generated dynamically when ants are close enough to latch on, and severed when tensile stress becomes excessive.

Step 1: Environment and Anchor Setup

Before introducing ant agents, define the physical boundaries and the gap that needs to be bridged.

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

const engine = Engine.create();
const world = engine.world;

// Create anchors representing cliffs or branches on either side of a gap
const leftCliff = Bodies.rectangle(150, 300, 200, 40, { isStatic: true });
const rightCliff = Bodies.rectangle(650, 300, 200, 40, { isStatic: true });

Composite.add(world, [leftCliff, rightCliff]);

Step 2: Creating Ant Nodes

Ants should be modeled as small circular bodies with moderate friction and restitution to avoid excessive bouncing. Assign a custom data container to track each ant's active connections.

function createAnt(x, y) {
  const ant = Bodies.circle(x, y, 8, {
    density: 0.002,
    friction: 0.8,
    frictionAir: 0.05,
    restitution: 0.1,
    render: { fillStyle: '#8b4513' }
  });

  // Track connected constraints and max capacity
  ant.plugin = {
    connections: new Set(),
    maxConnections: 3
  };

  return ant;
}

const ants = [];
for (let i = 0; i < 60; i++) {
  const ant = createAnt(100 + Math.random() * 50, 200 + Math.random() * 50);
  ants.push(ant);
  Composite.add(world, ant);
}

Step 3: Implementing Mutual Attraction

To simulate swarm cohesion and pheromone attraction, iterate through pairs of ants within a beforeUpdate event. Apply an attractive force if the distance between them falls within a sensing radius.

const SENSING_RADIUS = 60;
const ATTRACTION_STRENGTH = 0.00005;

Events.on(engine, 'beforeUpdate', () => {
  for (let i = 0; i < ants.length; i++) {
    for (let j = i + 1; j < ants.length; j++) {
      const antA = ants[i];
      const antB = ants[j];

      const delta = Vector.sub(antB.position, antA.position);
      const distance = Vector.magnitude(delta);

      if (distance > 0 && distance < SENSING_RADIUS) {
        const forceMagnitude = ATTRACTION_STRENGTH * (1 - distance / SENSING_RADIUS);
        const force = Vector.mult(Vector.normalise(delta), forceMagnitude);

        Body.applyForce(antA, antA.position, force);
        Body.applyForce(antB, antB.position, Vector.neg(force));
      }
    }
  }
});

Step 4: Dynamically Forming Structural Constraints

When two ants come into physical proximity and have spare connection slots, link them using a Matter.Constraint. This mimics ants grasping each other's legs and mandibles to bear structural loads.

const LATCH_DISTANCE = 20;

Events.on(engine, 'beforeUpdate', () => {
  for (let i = 0; i < ants.length; i++) {
    const antA = ants[i];
    if (antA.plugin.connections.size >= antA.plugin.maxConnections) continue;

    for (let j = i + 1; j < ants.length; j++) {
      const antB = ants[j];
      if (antB.plugin.connections.size >= antB.plugin.maxConnections) continue;

      const distance = Vector.magnitude(Vector.sub(antB.position, antA.position));

      if (distance <= LATCH_DISTANCE) {
        // Prevent duplicate constraints between identical pairs
        const alreadyConnected = [...antA.plugin.connections].some(
          c => c.bodyA === antB || c.bodyB === antB
        );

        if (!alreadyConnected) {
          const joint = Constraint.create({
            bodyA: antA,
            bodyB: antB,
            length: distance,
            stiffness: 0.8,
            damping: 0.1,
            render: { strokeStyle: '#5c2c16', lineWidth: 2 }
          });

          antA.plugin.connections.add(joint);
          antB.plugin.connections.add(joint);
          Composite.add(world, joint);
        }
      }
    }
  }
});

Step 5: Handling Tension and Structural Failure

Living bridges are non-rigid structures that deform under stress. If the load on any given link exceeds the ants' physical grip capacity, break the constraint by removing it from the world.

const BREAKING_STRETCH_RATIO = 1.6;

Events.on(engine, 'afterUpdate', () => {
  const constraints = Composite.allConstraints(world);

  for (let joint of constraints) {
    if (!joint.bodyA || !joint.bodyB) continue;

    const currentDistance = Vector.magnitude(
      Vector.sub(joint.bodyB.position, joint.bodyA.position)
    );

    if (currentDistance > joint.length * BREAKING_STRETCH_RATIO) {
      if (joint.bodyA.plugin) joint.bodyA.plugin.connections.delete(joint);
      if (joint.bodyB.plugin) joint.bodyB.plugin.connections.delete(joint);
      Composite.remove(world, joint);
    }
  }
});

Tuning Bridge Performance

Achieving a stable span requires balancing attraction against gravity: