Velocity-Based Collision Damage in Matter.js

This article explains how to implement a dynamic damage system in Matter.js by calculating the impact velocity between colliding physics bodies. While Matter.js handles rigid body dynamics out of the box, it does not include a native health or damage system. By intercepting collision events, computing the relative velocity of the colliding bodies, and taking collision normals into account, you can determine impact intensity and apply proportional damage to custom entity properties.

1. Attaching Custom Properties to Bodies

Before processing damage, assign health tracking and damage configuration directly to the Matter.js body objects or attach a reference to a parent game entity:

const player = Matter.Bodies.rectangle(100, 100, 50, 50);
player.health = 100;
player.damageThreshold = 4; // Minimum speed required to take damage
player.damageMultiplier = 5; // Scales velocity into hit points lost

const obstacle = Matter.Bodies.circle(100, 300, 30);
obstacle.health = 50;
obstacle.damageThreshold = 3;
obstacle.damageMultiplier = 2;

2. Listening to Collision Events

Use the collisionStart event from the Matter.Events module. This triggers on the initial frame two bodies make contact, preventing multiple calculations for a single impact:

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

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

3. Calculating Impact Velocity Along the Collision Normal

Relying solely on the scalar difference between two body speeds can cause false readings, such as bodies scraping alongside each other receiving maximum damage. To find the true impact force, calculate the relative velocity vector and project it onto the collision normal using a dot product.

function handleCollisionDamage(bodyA, bodyB, collision) {
    // 1. Calculate relative velocity vector (vA - vB)
    const relativeVelocity = {
        x: bodyA.velocity.x - bodyB.velocity.x,
        y: bodyA.velocity.y - bodyB.velocity.y
    };

    // 2. Normal vector provided by Matter.js collision pair
    const normal = collision.normal;

    // 3. Dot product: project relative velocity along the collision normal
    const impactSpeed = Math.abs(
        relativeVelocity.x * normal.x + relativeVelocity.y * normal.y
    );

    // 4. Apply damage to both bodies
    applyDamage(bodyA, impactSpeed);
    applyDamage(bodyB, impactSpeed);
}

4. Computing and Applying Damage

Using the impactSpeed, check if the speed exceeds the body's defined threshold. If it does, subtract the calculated damage from the body's health:

function applyDamage(body, impactSpeed) {
    if (!body.health || impactSpeed < body.damageThreshold) {
        return;
    }

    const effectiveImpact = impactSpeed - body.damageThreshold;
    const damage = Math.round(effectiveImpact * body.damageMultiplier);

    body.health -= damage;

    if (body.health <= 0) {
        body.health = 0;
        onBodyDestroyed(body);
    }
}

function onBodyDestroyed(body) {
    Matter.Composite.remove(engine.world, body);
}

5. Incorporating Mass (Optional)

To create a more realistic system where heavy objects deal more damage than light ones, factor the mass of the opposing body into the calculation:

function applyMassBasedDamage(targetBody, opposingBody, impactSpeed) {
    if (!targetBody.health || impactSpeed < targetBody.damageThreshold) {
        return;
    }

    // Heavy objects exert higher momentum on impact
    const massFactor = opposingBody.mass / targetBody.mass;
    const effectiveImpact = (impactSpeed - targetBody.damageThreshold) * massFactor;
    const damage = Math.max(0, Math.round(effectiveImpact * targetBody.damageMultiplier));

    targetBody.health -= damage;
}

This ensures that high-speed collisions deal significant damage, light scrapes are ignored via the threshold, and large masses appropriately influence the impact.