Box2D C++ tutorials - One-way walls
Last edited: July 14 2013
Halt! You should have a good understanding of
the basic tutorials before venturing further.
One-way walls and platforms
Many platform games allow the player to pass through a platform as they jump upwards, and then have it magically become solid so they can stand on it from above. Uh... I don't need to explain this do I? Let's just get started :)
In Box2D all (non-sensor) fixtures are solid no matter which direction they are collided from, so we'll need to figure out a way to alter this behavior. Since we are experts at working with contacts, we know that it's pretty easy to cancel the normal collision response between two fixtures with b2Contact::SetEnabled(false). The question is when to do this. This is still a subject of discussion among Box2D users and there is no standard way to tackle the question, given that it can depend on many other factors in the game.
There are a few ways you could decide when a contact should be disabled, and they all focus on determining which side of the wall/platform the player came from:
- use the velocity of the player body in PreSolve
- look at the contact normal in PreSolve
- check the player velocity in BeginContact
This can be solved by adding more checks, for example using the location of the player body relative to the platform to see if he is close enough to the top to be allowed up onto the platform. But therein lies a lot of manual work to cater for many different cases, and the method using the contact normal in PreSolve has similar issues, and the problem of the player suddenly popping up above the platform when the normal changes.
The BeginContact method requires a little extra setup but it handles the generic case better and is more efficient since it does nothing in PreSolve. The basic idea is to look at the velocity of the contact points when the player first collides with the platform in BeginContact and decide whether or not to enable the collision response, and this decision will remain in effect until an EndContact occurs.
The catch is, we'll need to make a little tweak to Box2D's source code to help this work. It's just a small change though, nothing to worry about.
Change to Box2D code
Since we will be using BeginContact event which only occurs one time per collision, we can only use SetEnabled once to alter the behavior of the contact. The problem is the contact will revert to being enabled again after each step. We could make a note of which contacts we have disabled and then check the list of them every time in PreSolve, but that is kinda inefficient and more work than I can be bothered with today. Or any day actually :)
So we'll just quietly sneak into b2Contact.cpp and comment out the line at the beginning of the Update function which re-enables the contact. After you're done it should look like this:
1 2 | // Re-enable this contact. //m_flags |= e_enabledFlag; |
Note: Obviously you might not want to do this if you are relying on the default behavior for some reason...
Handling the simplest case
First let's take a look at how we could handle the simplest case, where the platform is static and level, and the player can move up through it but not downwards. For the rest of this topic we'll reuse the platform shape, and we'll signify the 'back' (the side from which it can be passed through ) with the small pointed corner, so the flat side should be solid. For the rest of this topic I will be calling the flat side the 'front' or the 'face' of the platform, and when I say 'into the platform' I mean approaching from the front side.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 | //setup platform shape for reuse b2PolygonShape polygonShape; b2Vec2 verts[5]; verts[0].Set( 0, -0.75); verts[1].Set( 2.5, -0.5 ); verts[2].Set( 2.5, 0.5 ); verts[3].Set(-2.5, 0.5 ); verts[4].Set(-2.5, -0.5); polygonShape.Set( verts, 5 ); //kinematic platform { b2BodyDef bodyDef; bodyDef.type = b2_kinematicBody; bodyDef.position.Set(0,10); m_platformBody = m_world->CreateBody(&bodyDef); m_platformFixture = m_platformBody->CreateFixture( &polygonShape, 0 ); } //dynamic box { b2BodyDef bodyDef; bodyDef.type = b2_dynamicBody; bodyDef.position.Set(0,15); b2Body* body = m_world->CreateBody(&bodyDef); polygonShape.SetAsBox( 0.5, 0.5 ); b2FixtureDef fixtureDef; fixtureDef.shape = &polygonShape; m_world->CreateBody( &bodyDef )->CreateFixture( &polygonShape, 1 ); } |

Here is how a BeginContact could decide whether an incoming contact to the platform should be disabled.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 | void BeginContact(b2Contact* contact) { b2Fixture* fixtureA = contact->GetFixtureA(); b2Fixture* fixtureB = contact->GetFixtureB(); //check if one of the fixtures is the platform b2Fixture* platformFixture = NULL; b2Fixture* otherFixture = NULL; if ( fixtureA == m_platformFixture ) { platformFixture = fixtureA; otherFixture = fixtureB; } else if ( fixtureB == m_platformFixture ) { platformFixture = fixtureB; otherFixture = fixtureA; } if ( !platformFixture ) return; b2Body* platformBody = platformFixture->GetBody(); b2Body* otherBody = otherFixture->GetBody(); int numPoints = contact->GetManifold()->pointCount; b2WorldManifold worldManifold; contact->GetWorldManifold( &worldManifold ); //check if contact points are moving downward for (int i = 0; i < numPoints; i++) { b2Vec2 pointVel = otherBody->GetLinearVelocityFromWorldPoint( worldManifold.points[i] ); if ( pointVel.y < 0 ) return;//point is moving down, leave contact solid and exit } //no points are moving downward, contact should not be solid contact->SetEnabled(false); } void EndContact(b2Contact* contact) { //reset the default state of the contact in case it comes back for more contact->SetEnabled(true); } |
The other important thing is to return the contact to the default (enabled) state when an EndContact occurs. This is necessary because contacts exists as long as the AABBs of two fixtures continue to overlap, even if the fixtures themselves do not overlap. If the player jumps just high enough to clear the top of the platform, but not high enough for the AABBs to separate, the contact will remain disabled and he will fall back down again.
Handling the general case
By 'general case', I mean situations where the player and platform bodies can both be rotated or moving around. This is a little tricker but still not too hard. We can't just check the y-component of the velocity of the contact point directly, we need to convert that velocity into a relative velocity from the point of view of the platform, and then check it. Since the platform itself can be moving or rotating, this means we'll also need to take into account the velocity of the contact point in the platform.
We only need to change the last part of BeginContact. Here goes...
1 2 3 4 5 6 7 8 9 10 11 12 13 | //check if contact points are moving into platform for (int i = 0; i < numPoints; i++) { b2Vec2 pointVelPlatform = platformBody->GetLinearVelocityFromWorldPoint( worldManifold.points[i] ); b2Vec2 pointVelOther = otherBody->GetLinearVelocityFromWorldPoint( worldManifold.points[i] ); b2Vec2 relativeVel = platformBody->GetLocalVector( pointVelOther - pointVelPlatform ); if ( relativeVel.y < 0 ) return;//point is moving into platform, leave contact solid and exit } //no points are moving into platform, contact should not be solid contact->SetEnabled(false); |
Close but no cigar
Unfortunately when I tried this in a more game-like scenario, there was one rather annoying little issue that showed up when the other body approaches the platform at a very shallow angle. Consider how we are handling things so far, by checking the vertical component of the approach velocity:

Actually, for a platform game using tiles we want that almost ALL the time. Even if the player body is moving along flat ground, it is moving up and down a tiny bit as the collision response works to keep it on top of the ground fixture. Here is an exaggerated image of this situation:

For the other information, I decided to use the location of the contact point to decide whether the player should be allowed to stand on the platform when approaching from a very shallow angle like this. The relevant part of BeginContact now looks like this:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | //check if contact points are moving into platform for (int i = 0; i < numPoints; i++) { b2Vec2 pointVelPlatform = platformBody->GetLinearVelocityFromWorldPoint( worldManifold.points[i] ); b2Vec2 pointVelOther = otherBody->GetLinearVelocityFromWorldPoint( worldManifold.points[i] ); b2Vec2 relativeVel = platformBody->GetLocalVector( pointVelOther - pointVelPlatform ); if ( relativeVel.y < -1 ) //if moving down faster than 1 m/s, handle as before return;//point is moving into platform, leave contact solid and exit else if ( relativeVel.y < 1 ) { //if moving slower than 1 m/s //borderline case, moving only slightly out of platform b2Vec2 relativePoint = platformBody->GetLocalPoint( worldManifold.points[i] ); float platformFaceY = 0.5f;//front of platform, from fixture definition :( if ( relativePoint.y > platformFaceY - 0.05 ) return;//contact point is less than 5cm inside front face of platfrom } else ;//moving up faster than 1 m/s } //no points are moving into platform, contact should not be solid contact->SetEnabled(false); |
Improvements
This method takes the rather simplistic assumption that the 'front' face of the platform/wall always has the normal (0,1) in the local coordinates of the body, and our check results in approximately a 180 degree range around the front face in which the platform/wall will be solid. If you wanted to restrict or extend the range in which the wall is solid, you could use a method like that in the conveyor belts topic to check if the contact was inside the range you are interested in.
As I mentioned earlier, there does not seem to be any cut and dried solution for handling one-sided walls in Box2D. I think this one does ok, but it's not as free from tweak-requiring as I had hoped. Check out the downloads below for a scene involving a bunch of things commonly encountered in platform games. You may find the occasional glitch here and there :)

Source code
Here is the source code for those who would like to try it out for themselves. This is a 'test' for the testbed, based on Box2D v2.3.0.
Testbed test (simple scene): iforce2d_OneWayWalls.h
Testbed test (platforms scene): iforce2d_OneWayWalls_demo.h
Linux binary
Windows binary
YouTube video