Building a Scoring System with Matter.js Collisions

This guide explains how to implement a functional scoring system using Matter.js collision events. You will learn how to assign custom identifiers to physics bodies, listen for collision events using the Matter.js event system, filter specific interactions between objects, and reliably update the score while preventing duplicate points.

1. Labeling Matter.js Bodies

To track which objects trigger score changes, identify them by setting custom properties on their body definitions. The standard way is using the label property or custom attributes like scoreValue.

const player = Matter.Bodies.circle(100, 100, 20, {
    label: 'player'
});

const coin = Matter.Bodies.circle(300, 300, 15, {
    label: 'coin',
    isStatic: true,
    scoreValue: 10
});

const hazard = Matter.Bodies.rectangle(200, 400, 50, 50, {
    label: 'hazard',
    isStatic: true,
    scoreValue: -5
});

Matter.Composite.add(engine.world, [player, coin, hazard]);

2. Listening for collisionStart Events

Matter.js provides collision event hooks via the Matter.Events module. The collisionStart event fires during the tick in which two bodies first make contact.

let currentScore = 0;

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

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

        handleCollision(bodyA, bodyB);
    }
});

3. Handling Pairs and Updating the Score

Because collisions involve two bodies where the order of bodyA and bodyB is arbitrary, verify both combinations to see if a scoring interaction occurred.

function handleCollision(bodyA, bodyB) {
    // Identify if the player collided with a scoring object
    let target = null;

    if (bodyA.label === 'player' && targetLabels.includes(bodyB.label)) {
        target = bodyB;
    } else if (bodyB.label === 'player' && targetLabels.includes(bodyA.label)) {
        target = bodyA;
    }

    if (target) {
        processScore(target);
    }
}

const targetLabels = ['coin', 'hazard'];

function processScore(targetBody) {
    if (targetBody.scoreValue) {
        currentScore += targetBody.scoreValue;
        updateScoreDisplay(currentScore);

        // Remove the item from the world if it's a consumable like a coin
        if (targetBody.label === 'coin') {
            Matter.Composite.remove(engine.world, targetBody);
        }
    }
}

function updateScoreDisplay(score) {
    document.getElementById('score-board').innerText = `Score: ${score}`;
}

4. Preventing Duplicate Score Triggers

If an object is not removed immediately, multiple collision events might trigger across consecutive frames or between sub-steps. To ensure a single body only rewards points once, use a boolean flag or a Set to track processed items.

const collectedBodies = new Set();

function processScoreSafe(targetBody) {
    if (collectedBodies.has(targetBody.id)) {
        return;
    }

    collectedBodies.add(targetBody.id);
    currentScore += targetBody.scoreValue || 0;
    updateScoreDisplay(currentScore);

    // Schedule removal from physics world
    Matter.Composite.remove(engine.world, targetBody);
}

Using unique body identifiers (targetBody.id) ensures that your scoring logic remains accurate, decoupled from rendering rates, and free of multi-trigger bugs.