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 underlying collections shift indices while the engine is still looping through them.
- Other collision pairs involving the removed body may still attempt
to resolve within the same tick, resulting in
TypeError: Cannot read properties of undefinedor broken constraint calculations. - Sleeping bodies or broadphase collision trees can become desynchronized from the actual world state.
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
- Create a
Setor an array to act as a removal queue. - In your collision listener, add the bodies marked for deletion to the queue.
- In an
afterUpdatelistener, 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:
- Disable Collisions: Set
body.collisionFilter.mask = 0. This stops any further collisions from registering against it in the current frame. - Stop Motion: Set
Matter.Body.setStatic(body, true)or setbody.isSensor = true. - Hide from Renderer: If using the built-in
Matter.Render, setbody.render.visible = false.
By queuing removals for the afterUpdate phase and
neutralizing collisions immediately if necessary, your Matter.js
simulations remain stable, performant, and error-free.