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:
- New Collision: Present in the current frame, but not present in the previous frame.
- Ongoing Collision: Present in both the current frame and the previous frame.
- Ended Collision: Present in the previous frame, but not present in the current frame.
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
- Garbage Collection: Creating string keys and new
Setinstances every frame can trigger garbage collection overhead. For performance-critical applications, consider pooling objects or using numeric hashing instead of string concatenation. - User Pointers: To identify what game
objects are colliding (e.g., player, enemy, projectile), assign unique
IDs to your game entities and store them in the custom user pointer of
the
btCollisionObjectviauserObjectPointerorsetUserPointer().