Build Goal Pocket Trigger Zones in Matter.js

This guide explains how to implement goal pocket trigger zones in Matter.js to accurately detect and register scoring events. By configuring rigid bodies as non-colliding sensors and attaching custom event listeners to the physics engine, you can detect when a ball or puck enters a designated pocket without disrupting the natural physical movement of other objects on the board.

1. Define the Goal Pocket as a Sensor Body

In Matter.js, standard bodies produce solid physical collisions that bounce objects away. To create an invisible trigger zone, set the isSensor property to true. A sensor body detects intersections with other objects without imparting any physical forces back onto them.

const { Bodies, Composite } = Matter;

// Create the goal trigger zone
const goalPocket = Bodies.circle(400, 550, 30, {
  isSensor: true,
  isStatic: true,
  label: 'goalPocket',
  render: {
    fillStyle: 'transparent',
    strokeStyle: '#00ff00',
    lineWidth: 2
  }
});

// Create the scoring object (e.g., a ball)
const ball = Bodies.circle(400, 100, 15, {
  restitution: 0.8,
  label: 'ball'
});

Composite.add(engine.world, [goalPocket, ball]);

2. Listen for Collision Events

Matter.js dispatches collision lifecycle events through Matter.Events. To register scoring the instant an object crosses into the trigger area, bind a handler to the collisionStart event.

Iterate through event.pairs to inspect colliding bodies:

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];

    // Check if one body is the goal pocket and the other is the ball
    const isGoalA = bodyA.label === 'goalPocket';
    const isGoalB = bodyB.label === 'goalPocket';
    const isBallA = bodyA.label === 'ball';
    const isBallB = bodyB.label === 'ball';

    if ((isGoalA && isBallB) || (isGoalB && isBallA)) {
      const scoringBall = isBallA ? bodyA : bodyB;
      handleScore(scoringBall);
    }
  }
});

3. Handle Scoring Logic and Prevent Double Counting

When a fast-moving object intersects multiple sub-steps or vibrates within a pocket, it can trigger multiple collisions if not properly managed. To handle this, implement a state flag, debounce check, or immediately remove/reset the ball once scored:

let score = 0;

function handleScore(ballBody) {
  score += 1;
  console.log(`Goal scored! Current score: ${score}`);

  // Prevent repeated collisions by resetting or removing the ball
  Matter.Body.setVelocity(ballBody, { x: 0, y: 0 });
  Matter.Body.setPosition(ballBody, { x: 400, y: 100 });
}

4. Optimize Detection with Collision Filtering

If your game world contains multiple non-scoring objects (such as obstacles, walls, or players), use collision categories and masks so the trigger zone only checks for the ball:

const CATEGORY_DEFAULT = 0x0001;
const CATEGORY_BALL = 0x0002;
const CATEGORY_GOAL = 0x0004;

// Ball setup
ball.collisionFilter = {
  category: CATEGORY_BALL,
  mask: CATEGORY_DEFAULT | CATEGORY_GOAL
};

// Goal pocket setup (only detects the ball)
goalPocket.collisionFilter = {
  category: CATEGORY_GOAL,
  mask: CATEGORY_BALL
};

This ensures the physics engine avoids checking calculations between the trigger zone and irrelevant static elements, keeping scoring detection fast and accurate.