Ammo.js New vs Ongoing Collision Detection

This article explains the standard technique for distinguishing between a newly initiated collision and an ongoing contact in the ammo.js physics engine. Since ammo.js does not provide native event listeners for collision states, developers must implement a state-tracking system using contact manifolds and unique object pairs to accurately monitor collision phases across simulation steps.

The Core Concept: State Tracking

Ammo.js (a direct port of Bullet Physics) does not fire “onCollisionStart” or “onCollisionPersist” events. Instead, it maintains a list of active contact manifolds during each physics tick.

To differentiate between a new collision and an ongoing one, you must manually track the state of colliding pairs across frames. By comparing the active collisions of the current frame with those from the previous frame, you can classify the collision state:


Step-by-Step Implementation

1. Generate Unique Pair Keys

To track collisions, you need a reliable way to identify a pair of colliding bodies. You can generate a unique string key using the memory pointers of the two btCollisionObject instances. Using Ammo.getPointer() ensures the key remains consistent regardless of the order in which the dispatcher processes the bodies.

function getCollisionKey(bodyA, bodyB) {
    const ptrA = Ammo.getPointer(bodyA);
    const ptrB = Ammo.getPointer(bodyB);
    
    // Sort pointers to ensure the key is identical for (A, B) and (B, A)
    return ptrA < ptrB ? `${ptrA}_${ptrB}` : `${ptrB}_${ptrA}`;
}

2. Maintain Collision Sets

Keep two collections (typically Set objects) in your application state: one for the previous frame’s collisions and one for the current frame’s collisions.

let previousCollisions = new Set();

3. Query the Dispatcher Every Frame

During your game loop, after calling dynamicsWorld.stepSimulation(), iterate through all contact manifolds to find active collisions. A manifold only represents an active collision if it has one or more contact points with a distance less than or equal to zero.

function processCollisions() {
    const currentCollisions = new Set();
    const dispatcher = dynamicsWorld.getDispatcher();
    const numManifolds = dispatcher.getNumManifolds();

    for (let i = 0; i < numManifolds; i++) {
        const manifold = dispatcher.getManifoldByIndexInternal(i);
        const numContacts = manifold.getNumContacts();

        if (numContacts === 0) continue;

        let hasActualContact = false;
        for (let j = 0; j < numContacts; j++) {
            const contactPoint = manifold.getContactPoint(j);
            if (contactPoint.getDistance() <= 0) {
                hasActualContact = true;
                break;
            }
        }

        if (hasActualContact) {
            const body0 = Ammo.castObject(manifold.getBody0(), Ammo.btCollisionObject);
            const body1 = Ammo.castObject(manifold.getBody1(), Ammo.btCollisionObject);
            
            const pairKey = getCollisionKey(body0, body1);
            currentCollisions.add(pairKey);

            // Store references to the bodies if you need to access them later
            collisionRegistry[pairKey] = { body0, body1 };
        }
    }

    determineCollisionStates(currentCollisions);
}

4. Differentiate the States

Once the currentCollisions set is populated, compare it against previousCollisions to trigger your game logic.

function determineCollisionStates(currentCollisions) {
    // 1. Check for New and Ongoing Collisions
    currentCollisions.forEach(pairKey => {
        const bodies = collisionRegistry[pairKey];
        
        if (previousCollisions.has(pairKey)) {
            // Ongoing Collision (Collision Stay)
            onCollisionOngoing(bodies.body0, bodies.body1);
        } else {
            // New Collision (Collision Start)
            onCollisionStart(bodies.body0, bodies.body1);
        }
    });

    // 2. Check for Ended Collisions
    previousCollisions.forEach(pairKey => {
        if (!currentCollisions.has(pairKey)) {
            const bodies = collisionRegistry[pairKey];
            // Ended Collision (Collision End)
            onCollisionEnd(bodies.body0, bodies.body1);
            
            // Clean up registry
            delete collisionRegistry[pairKey];
        }
    });

    // 3. Update the state for the next frame
    previousCollisions = currentCollisions;
}

Performance Considerations