When bodies interact in the physics scene, Box2D handles collision detection and response automatically. However, we often need to trigger game events when specific collisions occur (e.g., player touching a monster, ball bouncing sound effects).
While we query body positions every frame for rendering, collisions occur less frequently. Checking for collisions every frame would be inefficient. Instead, Box2D provides callback functions that notify us when collisions actually happen.
Callback timing
Collisions are detected during b2World_Step()
. When a collision occurs, control transfers to your callback function before returning to complete the physics step.
Important: Don't modify the physics scene during callbacks since the world may still be updating and you can break the simulation. Trying this will often encounter a 'world is locked' assertion.
To handle collision callbacks, use the contact events from b2World_GetContactEvents()
:
// Get contact events after stepping the world
const contactEvents = b2World_GetContactEvents(worldId);
// Process begin contact events
for (const event of contactEvents.beginContacts) {
// Called when two shapes begin touching
handleBeginContact(event);
}
// Process end contact events
for (const event of contactEvents.endContacts) {
// Called when two shapes cease touching
handleEndContact(event);
}
Callback information
The contact event contains collision details. To get the colliding shapes:
// Contact event contains the colliding shape IDs
const shapeIdA = event.shapeIdA;
const shapeIdB = event.shapeIdB;
// Get the bodies that own these shapes
const bodyIdA = b2Shape_GetBody(shapeIdA);
const bodyIdB = b2Shape_GetBody(shapeIdB);
Use b2Body_GetUserData()
on bodies to access associated game objects. Remember that collisions occur between shapes, not bodies directly.
Example Implementation
Here's how to change a ball's color on collision:
// Ball class
class Ball {
constructor() {
this.contacting = false;
}
startContact() {
this.contacting = true;
}
endContact() {
this.contacting = false;
}
render(ctx) {
// red when contacting, white when not contacting
ctx.fillStyle = this.contacting ? '#ff0000' : '#ffffff';
ctx.fill();
}
}
// Store ball reference in body user data
b2Body_SetUserData(bodyId, ball);
Contact event handlers:
function handleBeginContact(event) {
// Check if shape A was a ball
const bodyIdA = b2Shape_GetBody(event.shapeIdA);
const ballA = b2Body_GetUserData(bodyIdA);
if (ballA) {
ballA.startContact();
}
// Check if shape B was a ball
const bodyIdB = b2Shape_GetBody(event.shapeIdB);
const ballB = b2Body_GetUserData(bodyIdB);
if (ballB) {
ballB.startContact();
}
}
function handleEndContact(event) {
// Check if shape A was a ball
const bodyIdA = b2Shape_GetBody(event.shapeIdA);
const ballA = b2Body_GetUserData(bodyIdA);
if (ballA) {
ballA.endContact();
}
// Check if shape B was a ball
const bodyIdB = b2Shape_GetBody(event.shapeIdB);
const ballB = b2Body_GetUserData(bodyIdB);
if (ballB) {
ballB.endContact();
}
}
Process contact events:
// In game loop after stepping physics
const contactEvents = b2World_GetContactEvents(worldId);
for (const event of contactEvents.beginContacts) {
handleBeginContact(event);
}
for (const event of contactEvents.endContacts) {
handleEndContact(event);
}
That's it. Now when the ball touches a wall it should appear red, and when it moves away again it should return to white.
This seems to work ok, especially for a simple scene like this one, but there is a problem with this method. Imagine the ball is in a corner so that it is touching two walls at the same time. Now it slides along one wall, out of the corner. The ball returns to white, even though it is still touching a wall! What's happening here is that when the ball moves out of the corner, an EndContact occurs for the wall that is no longer touching, so the contacting flag variable of the ball gets set to false. Fortunately it's easy to fix - instead of using a boolean to store a simple on/off state, we need to store an integer, and increment/decrement it to count the number of other fixtures currently being touched. When the integer reaches zero, we are no longer touching any walls.
Multiple contact example:
class Ball {
constructor() {
this.numContacts = 0;
}
startContact() {
this.numContacts++;
}
endContact() {
this.numContacts--;
}
render(ctx) {
// if we are touching anything, color red, otherwise white
ctx.fillStyle = this.numContacts > 0 ? '#ff0000' : '#ffffff';
ctx.fill();
}
}
A "tag" game example:
How about one more example to demonstrate how we can use the relationship between the contacting objects. So far all we've done is look at whether a ball touched something, now we will look at what it touched. Let's set up a scene where only one ball is red, and when it hits another ball the red will pass onto the other ball, kind of like playing tag where one ball is 'it'.
Add a boolean variable to the Ball class to track whether it is currently 'it', and set up a function that takes two balls and switches their 'it' status.
class Ball {
constructor() {
this.isIt = false;
}
render(ctx) {
ctx.fillStyle = this.isIt ? '#ff0000' : '#ffffff';
ctx.fill();
}
}
function handleContact(ball1, ball2) {
const temp = ball1.isIt;
ball1.isIt = ball2.isIt;
ball2.isIt = temp;
}
Contact handler for tag game:
function handleBeginContact(event) {
// Check if both shapes were balls
const bodyA = b2Shape_GetBody(event.shapeIdA);
const bodyB = b2Shape_GetBody(event.shapeIdB);
const ballA = b2Body_GetUserData(bodyA);
const ballB = b2Body_GetUserData(bodyB);
if (ballA && ballB) {
handleContact(ballA, ballB);
}
}
To start the game:
const balls = [];
// create some balls into this array and add some velocity to get them moving
...
// then make the first ball 'it'
balls[0].isIt = true;
// don't forget to call the render function for each ball in your update loop!
Credits
This tutorial is adapted from an original piece of work created by Chris Campbell and is used under license.