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);
        }
      }
    }
  });
});