Simulate Pinball Roll-Over Switches in Matter.js

This article explains how to implement pinball lane roll-over switches using Matter.js sensor bodies. By utilizing static sensor fixtures combined with collision event listeners, you can detect when a pinball passes through a specific lane to trigger scores, lights, or game mechanics without affecting the ball's natural trajectory or velocity.

Understanding Sensor Bodies

In traditional 2D physics simulations, dynamic bodies collide and rebound off one another. However, a pinball roll-over switch—whether a wire rollover or a star rollover—should detect the ball passing over it without physically blocking it.

Matter.js provides the isSensor property on rigid bodies. When isSensor: true is set, the body stops generating physical contact responses (meaning bodies pass completely through it), but it continues to register collision detection pairs and dispatch collision events.

Step 1: Defining the Sensor Body

To simulate the switch, create a static rectangular or circular body placed within your lane boundaries and flag it as a sensor. Label the body so it can be easily identified in collision handlers.

const { Bodies, World } = Matter;

// Define the rollover switch
const rollOverSwitch = Bodies.rectangle(200, 350, 40, 10, {
  isStatic: true,
  isSensor: true,
  label: 'rollover_lane_1',
  render: {
    fillStyle: '#ffcc00',
    opacity: 0.5
  }
});

// Define the pinball
const pinball = Bodies.circle(200, 100, 12, {
  label: 'pinball',
  restitution: 0.5,
  density: 0.004
});

World.add(engine.world, [rollOverSwitch, pinball]);

Step 2: Listening for Collision Events

Matter.js dispatches collision events via Matter.Events. To trigger game logic when the ball rolls over the switch, listen to the collisionStart event. Check the collision pairs to confirm whether one body is the switch and the other is the pinball.

const { Events } = Matter;

Events.on(engine, 'collisionStart', (event) => {
  const pairs = event.pairs;

  for (let i = 0; i < pairs.length; i++) {
    const { bodyA, bodyB } = pairs[i];

    const isRollOverTriggered =
      (bodyA.label === 'rollover_lane_1' && bodyB.label === 'pinball') ||
      (bodyB.label === 'rollover_lane_1' && bodyA.label === 'pinball');

    if (isRollOverTriggered) {
      activateRollOverSwitch('lane_1');
    }
  }
});

Step 3: Managing State and Debouncing

Because a ball may trigger multiple collision checks while passing over a wide switch area, you should manage activation state to prevent unintended score multiplications.

const laneStates = {
  lane_1: { lit: false, active: false }
};

function activateRollOverSwitch(laneId) {
  const lane = laneStates[laneId];

  // Prevent repeated triggers during a single pass
  if (lane.active) return;
  lane.active = true;

  // Toggle lane illumination and award points
  lane.lit = !lane.lit;
  addScore(500);

  // Play audio or visual feedback here
  console.log(`${laneId} triggered. Lit: ${lane.lit}`);
}

// Reset the switch trigger once the ball completely leaves the sensor area
Events.on(engine, 'collisionEnd', (event) => {
  const pairs = event.pairs;

  for (let i = 0; i < pairs.length; i++) {
    const { bodyA, bodyB } = pairs[i];

    if (
      (bodyA.label === 'rollover_lane_1' && bodyB.label === 'pinball') ||
      (bodyB.label === 'rollover_lane_1' && bodyA.label === 'pinball')
    ) {
      laneStates.lane_1.active = false;
    }
  }
});

Using static sensor bodies alongside paired collisionStart and collisionEnd events gives you complete control over lane logic while preserving the realistic momentum of the pinball.