What's in a collision?
In Box2D it's common to think of bodies colliding with each other, but it's really the shapes which are used to detect when a collision occurs. Collisions can happen in all kinds of ways so they have a lot of information that can be used in the game logic. For example, you might want to know:
- when a collision starts and ends
- what point on the shapes is touching
- the normal vector of the contact between the shapes
- how much energy was involved and how the collision was responded to
Usually collisions happen very quickly, but in this topic we are going to take one collision and slow it riiiight down so we can take a look at the details of what is going on, and all the possible information we can get from it.
The scenario will be two polygon shapes colliding, in a zero-gravity world so we can control it easier. One shape is a stationary box, the other is a triangle moving horizontally towards the box.
This scenario is set up so that the bottom of the triangle will just collide with the top corner of the box. For this topic the finer details of this arrangement are not important, the focus is on what kind of information we can get at each step of the process.
Getting info about collisions
Information about a collision is contained in contact events. From these you can check which two shapes are colliding, and find out about the location and direction of the collision reaction. There are two main ways you can get these contact events from Box2D:
Checking contact data
You can look at all the contacts for a body by getting its contact data:
const contactCapacity = b2Body_GetContactCapacity(bodyId);
const contactData = new Array(contactCapacity);
const contactCount = b2Body_GetContactData(bodyId, contactData, contactCapacity);
for (let i = 0; i < contactCount; i++) {
const contact = contactData[i];
// do something with the contact
}
A very important point to note if you do this, is that the existence of a contact in this data does not mean that the two shapes are actually touching - it only means their AABBs() are touching. If you want to know if the shapes themselves are really touching you can check the contact flags. () AABB = Axis-Aligned-Bounding-Box: The smallest rectangle with horizontal/vertical sides that fully contains a shape.
Contact events
Checking contact data becomes inefficient for large scenarios where many collisions are occurring frequently. Using contact events allows you to have Box2D tell you when something interesting happens, rather than you doing all the work of keeping track of when things start and stop touching. Contact events are provided through the world's event system:
const contactEvents = b2World_GetContactEvents(worldId);
const beginEvents = contactEvents.beginContacts;
const endEvents = contactEvents.endContacts;
const preSolveEvents = contactEvents.preSolve;
const postSolveEvents = contactEvents.postSolve;
Note that depending on what is happening, some events give us more than just the basic contact information. During the world's Step function, when Box2D detects that one of these events has occurred, it will provide the event data through these arrays.
Generally I would recommend using the contact events method. It may seem a little unwieldy at first but is more efficient and more useful in the long run. I have yet to come across a situation where checking contact data would have any great advantage.
Interesting side note: Box2D Events are also available for every body that moved during a world step. This is a much faster way to retrieve their transforms and it eliminates looping through lots of sleeping or stationary bodies.
Either way you get these contacts, they contain the same information. The most fundamental piece of info is which two shapes are colliding, which is obtained from the contact data or event:
const shapeIdA = contact.shapeIdA;
const shapeIdB = contact.shapeIdB;
If you are checking the contact data of a body, you may already know what one of the shapes of the contact will be, but if you are using contact events you will rely on these IDs to find out what is colliding with what. There is no particular ordering of the A and B shapes, so you will often need to have user data set in the shapes or their bodies so you can tell what object the shapes belong to. From these shapes, you can get their body IDs to find the bodies that collided.
Breakdown of a collision
We should start from a point where the AABBs of the shapes are still not overlapping, so we can follow the whole story. Click the 'AABBs' checkbox to see these as purple rectangles around each shape.
Shape AABBs begin to overlap (Broad-phase detection)
Although the shapes themselves are not yet overlapping, at this point a contact is created and added to the contact data for each body. If you are checking this contact data as shown above you will be able to tell that a collision could potentially occur here, but in most cases you don't really care until the shapes themselves really overlap.
Outcome:
- Contact exists but touching flag is not set
Shapes begin to overlap (Narrow-phase detection)
Zooming in on the upper corner of the box, you will see the transition above. It occurs in one time step, which means that the real collision point (as shown by the dotted line in the left image) has been skipped over. This is because Box2D moves all the bodies and then checks for overlaps between them, at least with the default settings. If you want to get the real impact position you can set the body to be a 'bullet' body which will give you the result shown in the bottom image. You can do this by:
// When creating the body:
bodyDef.isBullet = true;
// Or after creating the body:
b2Body_SetBullet(bodyId, true);
Bullet bodies take more CPU time to calculate this accurate collision point, and for many applications they are not necessary. Just be aware that with the default settings, sometimes collisions can be missed completely - for example if the triangle in this example had been moving faster it may have skipped right over the corner of the box! If you have very fast moving bodies that must NOT skip over things like this, for example uh... bullets :) then you will need to set them as bullet bodies.
For the rest of this discussion we will be continuing with the non-bullet body setting.
Outcome:
- Contact touching flag is set
- BeginContact event is generated
Collision points and the normal
At this point we have a contact which is actually touching, so that means we should be able to answer some of the questions at the beginning of this topic. First let's get the location and normal for the contact. For the code sections below, we'll assume that these are either from checking contact data or from a contact event.
Internally, the contact stores the location of the collision point in local coordinates for the bodies involved, and this is usually not so great for us. But we can get the world coordinates of the collision points and normal from the contact data or event.
const pointCount = contact.manifold.pointCount;
const worldPoints = contact.manifold.points;
const normal = new b2Vec2(contact.manifold.normalX, contact.manifold.normalY); // some vectors are separated into X and Y components to help Phaser Box2D execute quickly
// Draw collision points
ctx.beginPath();
for (let i = 0; i < pointCount; i++) {
const point = worldPoints[i];
ctx.rect(point.x, point.y, 2, 2);
}
ctx.fill();
These are the points that will be used in the collision reaction when applying an impulse to push the shapes apart. Although they are not at the exact location where the shapes would first have touched (unless you're using bullet-type bodies), in practice these points are often adequate for use as collision points in game logic.
Next let's show the collision normal, which points from shapeA to shapeB:
// Draw collision normal
const normalLength = 0.1;
const normalStart = new b2Vec2(
worldManifold.points[0].pointX - normalLength * worldManifold.normalX,
worldManifold.points[0].pointY - normalLength * worldManifold.normalY
);
const normalEnd = new b2Vec2(
x: worldManifold.points[0].pointX + normalLength * worldManifold.normalX,
y: worldManifold.points[0].pointY + normalLength * worldManifold.normalY
);
// Draw using canvas 2D context instead of OpenGL
ctx.beginPath();
ctx.strokeStyle = '#FF0000'; // red
ctx.moveTo(normalStart.x, normalStart.y);
ctx.strokeStyle = '#00FF00'; // green
ctx.lineTo(normalEnd.x, normalEnd.y);
ctx.stroke();
It seems that for this collision, the quickest way to resolve the overlap is to apply an impulse that will push the corner of the triangle up and left, and the corner of the square down and right. Please note that the normal is only a direction, it does not have a location and is not connected to either one of the points - I'm just drawing it at the location of points[0] for convenience.
It's important to be aware that the collision normal does not give you the angle that these shapes collided at - remember the triangle was moving horizontally here, right? - it only gives the shortest direction to move the shapes so that they don't overlap.
If you want to know the actual direction that these two corners impacted at, you can use:
const vel1 = b2Body_GetLinearVelocityFromWorldPoint(triangleBody, worldManifold.points[0]); // TODO: this function does not exist in Phaser Box2D v3
const vel2 = b2Body_GetLinearVelocityFromWorldPoint(squareBody, worldManifold.points[0]);
const impactVelocity = new b2Vec2(vel1.x - vel2.x, vel1.y - vel2.y);
... to get the actual relative velocity of the points on each body that collided. For the simple example we are looking at we could also simply take the linear velocity of the triangle because we know the square is stationary and the triangle is not rotating, but the above code will take care of cases when both bodies could be moving or rotating.
Another thing to note is that not every collision will have two of these collision points. I have deliberately chosen a relatively complex example where two corners of a polygon are overlapping, but more common collisions in real situations have only one such point.
Collision response is applied
When shapes are overlapping, Box2D's default behavior is to apply an impulse to each of them to push them apart, but this does not always succeed in a single time step. As shown here, for this particular example the two shapes will be overlapping for three time steps before the 'bounce' is complete and they separate again.
During this time we can step in and customize this behavior if we want to. If you are using the contact listener method, the PreSolve and PostSolve functions of your listener will be repeatedly called in every time step while the shapes are overlapping, giving you a chance to alter the contact before it is processed by the collision response (PreSolve) and to find out what impulses were caused by the collision response after it has been applied (PostSolve).
To make this clearer, here is the output obtained for this example collision by putting a simple console.log statement in the main Step function and each of the contact listener functions:
...
Step
Step
BeginContact
PreSolve
PostSolve
Step
PreSolve
PostSolve
Step
PreSolve
PostSolve
Step
EndContact
Step
Step
...
Outcome:
- PreSolve and PostSolve are called repeatedly
PreSolve and PostSolve
Both PreSolve and PostSolve give you a contact pointer, so we have access to the same points and normal information we just looked at for BeginContact. PreSolve gives us a chance to change the characteristics of the contact before the collision response is calculated, or even to cancel the response altogether, and from PostSolve we can find out what the collision response was.
Here are the alterations you can make to the contact in PreSolve:
// Non-persistent - need to set every time step
b2Contact_SetEnabled(contact, flag);
// These persist for duration of contact (available from v2.2.1)
b2Contact_SetFriction(contact, friction);
b2Contact_SetRestitution(contact, restitution);
Calling SetEnabled(false) will disable the contact, meaning that the collision response that normally would have been applied will be skipped. You can use this to temporarily allow objects to pass through each other. A classic example of this is the one-way wall or platform, where the player is able to pass through an otherwise solid object, depending on various criteria that can only be checked at runtime, like the position of the player and which direction they are heading, etc.
It's important to note that the contact will revert back to being enabled in the next time step, so if you want to disable contacts like this you'll need to call SetEnabled(false) every time step.
As well as the contact pointer, PreSolve has a second parameter from which we can find out info about the collision manifold of the previous time step.
PostSolve is called after the collision response has been calculated and applied. This also has a second parameter, in which we can find information about the impulse that was applied. A common use for this information is to check if the size of the collision response was over a given threshold, perhaps to check if an object should break, etc.
Shapes finish overlapping
The AABBs are still overlapping, so the contact remains in the contact list for the body/world even though the shapes themselves have separated.
Outcome:
- EndContact callback function will be called
- IsTouching() now returns false
Shape AABBs finish overlapping
Outcome:
- Contact is removed from body/world contact list
While the EndContact call to the contact listener passes a contact pointer, at this point the shapes are no longer touching, so there will be no valid manifold information to get. However the EndContact event is still an indispensable part of the contact listener because it allows you to check which shapes/bodies/game objects have ended contact.
Summary
This topic should provide a clear overview of the events going on millisecond-by-millisecond under the hood of a Box2D collision. There's no need to shy away from implementing a contact listener, when the listener usually becomes less work in the long run! Knowing these details allows for a better understanding of what is actually possible, better design, and time saved in implementation.
Credits
This tutorial is adapted from an original piece of work created by Chris Campbell and is used under license.