Fix Matter.js Tunneling on Fast Air Hockey Strikes

In fast-paced 2D physics games like air hockey, high-velocity collisions between a mallet and a puck often cause tunneling—where objects pass right through one another because their displacement per frame exceeds their bounding dimensions. Because Matter.js uses discrete collision detection rather than continuous collision detection (CCD), handling these intense impulses requires specific architectural adjustments. This guide covers practical, performance-friendly techniques to prevent tunneling in Matter.js, including physics sub-stepping, engine iteration tuning, swept raycasting, and velocity clamping.

Increase Engine Sub-Stepping

The most effective way to eliminate tunneling in Matter.js is to decrease the time delta of each physics step by running multiple smaller updates per frame. By default, Engine.update(engine, delta) runs once per rendering frame. If a mallet moves 40 pixels in a single 16.6ms step, it can jump completely past a puck with a radius of 15 pixels.

Dividing the frame's elapsed time into smaller increments ensures the displacement per step remains smaller than the collision geometry:

const subSteps = 4;
const subDelta = delta / subSteps;

for (let i = 0; i < subSteps; i++) {
  Matter.Engine.update(engine, subDelta);
}

Sub-stepping keeps body movements small enough for discrete collision algorithms to register overlaps reliably without changing your rendering loop.

Adjust Position and Velocity Iterations

Matter.js relies on an iterative solver to resolve constraints and collisions. Increasing the solver iterations tightens the engine's response to dynamic impacts and reduces penetration artifacts:

engine.positionIterations = 12; // Default is 6
engine.velocityIterations = 8;  // Default is 4

While increasing iterations alone will not prevent an object from missing a collision entirely if it travels too far in a single step, it prevents extreme overlaps from resolving unpredictably when high forces push bodies into one another.

Implement Raycasting and Swept Volumes

When players move the mallet via mouse or touch input, the mallet effectively teleports between frames rather than moving through physics forces. This makes standard collision resolution fail even with sub-stepping.

To fix this, cast a ray or check a swept shape along the movement path from the previous frame to the current frame using Matter.Query.ray:

  1. Store the previous position of the mallet and puck before updating.
  2. In a beforeUpdate event, check for an intersection between the mallet’s travel path and the puck’s bounding circle.
  3. If an intersection is found, calculate the exact point of impact along the ray.
  4. Position the puck at the contact point and manually apply an impulse based on the mallet's incoming speed and direction using Body.setVelocity().

Thicken Colliders with Invisible Padding

For table boundaries and fast-moving circular bodies, geometric thickness acts as a buffer against tunneling.

Clamp Maximum Velocity

High-impact strikes can produce exponential force transfers, sending the puck across the board at speeds exceeding hundreds of pixels per frame. Enforce a terminal velocity on the puck inside the beforeUpdate hook:

Matter.Events.on(engine, 'beforeUpdate', () => {
  const maxSpeed = 25; // Define based on minimum body radius
  const speed = Matter.Vector.magnitude(puck.velocity);

  if (speed > maxSpeed) {
    const clampedVelocity = Matter.Vector.mult(
      Matter.Vector.normalise(puck.velocity),
      maxSpeed
    );
    Matter.Body.setVelocity(puck, clampedVelocity);
  }
});

Keep maxSpeed lower than the puck's diameter divided by the physics time step to ensure it can never skip past a static or dynamic body without at least one frame of overlap.