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
The typical complications that arise with the first two of these are caused by not having quite enough information to work with. For example let's say you get a call to PreSolve so you know that the player and a platform are overlapping. You check the velocity of the player body and find that it is moving down - if he is stepping on it from above the platform should be solid... but he might also have jumped up from below and not quite made it far enough so is now falling back down again, in which case the platform should not be solid.

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;
That's it!
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. One-way walls Here is some basic setup - keep a reference to the fixture and body in a class variable so we can use them later:
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 );
  }
All we need to do in BeginContact is check whether each contact point is moving up or down. If either of them is moving down, the platform should be solid. Just as a reminder that there could be two contact points, and also to help explain why we need to check both of them, consider this case where the box is rotating as it hits the platform: One-way walls Obviously in this exaggerated screenshot the left corner is not touching the platform so there would be no contact point there, but imagine that this angle is much smaller and both corners of the box are still touching the platform whilst the box is rotating - rare, but it can happen.

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);
  }
As you can see, most of this code is taken up with the usual chores of finding out which fixture is which etc. The two main points to note are: we use GetLinearVelocityFromWorldPoint instead of the velocity of the body itself, so that we can check the direction of movement of the relevant parts that collided with the platform. This is necessary if you allow your player body to rotate.

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: One-way walls At the red line there is a point where we abruptly switch between saying the platform is solid or not. For the most part this is fine, the problem comes about when we have a body approaching from a very shallow angle, almost right along the red line, and it is moving upwards but we want the platform to be solid. But when would we ever want that?

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: One-way walls Suppose that the player body was moving upwards just a tiiiny bit at it hits the next tile. Our check in BeginContact would disable that contact and the player would fall through the ground. So unfortunately, simply checking the approach velocity is not quite enough - we'll need to use some other information as well.

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);
Here we do the extra check when the approach velocity is less than 1 m/s in or out of the platform, and allow the platform to stay solid if the contact point is within 5cm of the top of the platform... and that is the annoying part. Until now we could treat all of our one-sided fixtures in the same way, but for this extra check we now need to know the 'height' of the front face of the platform. Sure, we know this because we set the fixtures up ourselves, but it's extra work to have this info required in the contact listener.


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 :) One-way walls

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