Create Crushing Piston Traps in Matter.js
Crushing piston traps are a staple mechanic in 2D platformers and physics-based puzzle games. This guide explains how to construct periodic slamming pistons using Matter.js by configuring kinematic bodies, scripting an asymmetric timing loop for a rapid slam and slow retract, and handling the crushing forces that interact with the player or dynamic objects.
1. Configure the Piston Body
A crushing piston must push other bodies without being displaced or
slowed down by collisions. The best approach is to use a non-static body
driven explicitly via kinematics (Matter.Body.setVelocity
and Matter.Body.setPosition).
const { Engine, Render, Runner, Bodies, Composite, Events, Body } = Matter;
// Create the piston head
const piston = Bodies.rectangle(400, 100, 120, 60, {
isStatic: false,
frictionAir: 0,
restitution: 0,
density: 1 // High density ensures solid contact
});
// Create base/ceiling to anchor the visual context
const ceiling = Bodies.rectangle(400, 40, 200, 40, { isStatic: true });
const ground = Bodies.rectangle(400, 500, 800, 40, { isStatic: true });
Composite.add(world, [piston, ceiling, ground]);2. Implement the Periodic Cycle
Crushing pistons create tension through asymmetric movement: they
retract slowly, pause briefly at the top, and slam down rapidly. You can
manage this state machine inside Matter.js's beforeUpdate
event listener.
const PISTON_CONFIG = {
retractedY: 100,
extendedY: 440,
slamSpeed: 18,
retractSpeed: 2,
pauseDuration: 60 // frames to wait at the apex
};
let state = 'PAUSED'; // 'SLAM', 'RETRACT', 'PAUSED'
let pauseTimer = 0;
Events.on(engine, 'beforeUpdate', () => {
if (state === 'PAUSED') {
Body.setVelocity(piston, { x: 0, y: 0 });
pauseTimer++;
if (pauseTimer >= PISTON_CONFIG.pauseDuration) {
pauseTimer = 0;
state = 'SLAM';
}
} else if (state === 'SLAM') {
if (piston.position.y < PISTON_CONFIG.extendedY) {
Body.setVelocity(piston, { x: 0, y: PISTON_CONFIG.slamSpeed });
} else {
// Reached maximum extension
Body.setPosition(piston, { x: piston.position.x, y: PISTON_CONFIG.extendedY });
Body.setVelocity(piston, { x: 0, y: 0 });
state = 'RETRACT';
}
} else if (state === 'RETRACT') {
if (piston.position.y > PISTON_CONFIG.retractedY) {
Body.setVelocity(piston, { x: 0, y: -PISTON_CONFIG.retractSpeed });
} else {
// Reached top position
Body.setPosition(piston, { x: piston.position.x, y: PISTON_CONFIG.retractedY });
Body.setVelocity(piston, { x: 0, y: 0 });
state = 'PAUSED';
}
}
});3. Restrict Horizontal Drift
Because the piston is non-static, dynamic forces or angled collisions
can introduce slight horizontal drift over time. Lock the horizontal
axis by continuously overriding the X coordinates and rotation inside
the beforeUpdate loop:
Body.setPosition(piston, { x: 400, y: piston.position.y });
Body.setAngle(piston, 0);
Body.setAngularVelocity(piston, 0);4. Detecting Crushed Entities
A standard collision does not automatically mean a player or object
is destroyed. To detect an actual "crush," listen to the
collisionActive event to check if an entity is
simultaneously in contact with both the downward-moving piston and the
static floor beneath it:
Events.on(engine, 'collisionActive', (event) => {
const pairs = event.pairs;
pairs.forEach((pair) => {
const bodies = [pair.bodyA, pair.bodyB];
const isPistonInvolved = bodies.includes(piston);
if (isPistonInvolved && state === 'SLAM') {
const target = bodies.find((b) => b !== piston && !b.isStatic);
if (target) {
// Check if target is close to the ground boundary
const distanceToGround = ground.position.y - ground.bounds.max.y;
if (target.position.y >= PISTON_CONFIG.extendedY - 40) {
// Trigger crushing logic (e.g., remove body, spawn particles)
Composite.remove(world, target);
}
}
}
});
});