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 4While 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:
- Store the previous position of the mallet and puck before updating.
- In a
beforeUpdateevent, check for an intersection between the mallet’s travel path and the puck’s bounding circle. - If an intersection is found, calculate the exact point of impact along the ray.
- 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.
- Thick Rink Boundaries: Make the static boundary walls significantly thicker than visible on screen. Position the outer edges far beyond the viewable arena.
- Compound Mallet Bodies: Give the mallet an invisible outer collision circle set as a sensor or secondary fixture to detect impending collisions early and apply corrective forces before deep overlap occurs.
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.