Safely Remove Bodies on Collision in Matter.js

Modifying a physics world while the engine is actively resolving collisions often leads to missing collision checks, corrupted arrays, and unexpected runtime errors. When developing games or simulations with Matter.js, you cannot immediately call Composite.remove inside a collision event callback because the engine is midway through its iteration loop. This guide details why this issue occurs and provides the standard, safe pattern for queuing and removing bodies once the engine cycle completes.

Why Immediate Removal Fails

During an update cycle, the Matter.js engine iterates over dynamic lists of bodies, pairs, and collision detectors. If you listen to collisionStart, collisionActive, or collisionEnd events and remove a body directly within that callback:

The Solution: Deferred Removal via afterUpdate

The safest method is to defer the removal of bodies until the current physics step has finished. Matter.js emits an afterUpdate event on the engine after all collision detection, resolution, and position integrations have executed.

Implementation Pattern

  1. Create a Set or an array to act as a removal queue.
  2. In your collision listener, add the bodies marked for deletion to the queue.
  3. In an afterUpdate listener, iterate through the queue, safely remove each body from the world, and clear the queue.

Example Code

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

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

// Create a queue for bodies scheduled for removal
const bodiesToRemove = new Set();

// Listen for collisions
Events.on(engine, 'collisionStart', (event) => {
    const pairs = event.pairs;

    for (let i = 0; i < pairs.length; i++) {
        const { bodyA, bodyB } = pairs[i];

        // Example condition: remove projectiles on impact
        if (bodyA.label === 'projectile') {
            bodiesToRemove.add(bodyA);
        }
        if (bodyB.label === 'projectile') {
            bodiesToRemove.add(bodyB);
        }
    }
});

// Safely process removals after the physics step completes
Events.on(engine, 'afterUpdate', () => {
    if (bodiesToRemove.size === 0) return;

    bodiesToRemove.forEach((body) => {
        Composite.remove(world, body);
    });

    // Clear the queue for the next frame
    bodiesToRemove.clear();
});

Immediate Visual and Physical Disabling

If you must ensure a queued body no longer triggers additional collisions or affects physics during the remainder of the current frame, disable its physical response immediately while waiting for afterUpdate:

By queuing removals for the afterUpdate phase and neutralizing collisions immediately if necessary, your Matter.js simulations remain stable, performant, and error-free.