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) shapes 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 shapes by setting the contact enabled state to 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.
Basic world setup
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.
Here is some basic setup - keep references to the world objects so we can use them later:
// Create world
const worldDef = b2DefaultWorldDef();
worldDef.gravity = new b2Vec2(0, -10);
const world = CreateWorld({ worldDef: worldDef });
const worldId = world.worldId;
// Create one-way platforms
const platform = CreateBoxPolygon({
position: new b2Vec2(0, 0),
type: b2BodyType.b2_kinematicBody,
size: new b2Vec2(2, 0.2),
density: 10,
friction: 0.7,
worldId: worldId,
preSolve: true, // enable preSolve
userData: { type: 'platform' }
});
const ground = CreateBoxPolygon({
position: new b2Vec2(0, -5),
type: b2BodyType.b2_kinematicBody,
size: new b2Vec2(5, 1),
density: 10,
friction: 0.7,
worldId: worldId,
preSolve: true, // enable preSolve
userData: { type: 'platform' }
});
// create player
const player = CreateBoxPolygon({
position: new b2Vec2(0, -3),
type: b2BodyType.b2_dynamicBody,
size: new b2Vec2(0.5, 1),
density: 10,
friction: 0.7,
worldId: worldId,
userData: { type: 'player' }
});
Handling the simplest case
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:
Here is how a BeginContact could decide whether an incoming contact to the platform should be disabled:
function beginContact (shapeIdA, shapeIdB, manifold) {
const bodyIdA = b2Shape_GetBody(shapeIdA);
const bodyIdB = b2Shape_GetBody(shapeIdB);
if (!b2Body_IsValid(bodyIdA) || !b2Body_IsValid(bodyIdB)) return;
const userDataA = b2Body_GetUserData(bodyIdA);
const userDataB = b2Body_GetUserData(bodyIdB);
const platformBody = userDataA.type === 'platform' ? bodyIdA : userDataB.type === 'platform' ? bodyIdB : null;
const otherBody = userDataA.type === 'player' ? bodyIdA : userDataB.type === 'player' ? bodyIdB : null;
if (!otherBody) return false;
const points = manifold.points;
const totalPoints = points.length;
for (let i = 0; i < totalPoints; i++)
{
const otherVelocity = b2Body_GetLinearVelocity(otherBody);
if (otherVelocity.y > 0)
{
return false; // returning false disables collision
}
}
return true; // returning true enables collision
}
b2World_SetPreSolveCallback(worldId, beginContact, this);
As you can see, most of this code is taken up with the usual chores of finding out which shape is which etc. The two main points to note are: we use b2Body_GetLinearVelocity 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.
General use case
By 'general use 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...
// check if contact points are moving into platform
for (let i = 0; i < totalPoints; i++) {
const platformVelocity = b2Body_GetLinearVelocity(platformBody);
const otherVelocity = b2Body_GetLinearVelocity(otherBody);
const relativeVelocity = b2Body_GetLocalVector(platformBody, b2Sub(otherVelocity, platformVelocity));
if (relativeVelocity.y > 0)
{
return 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:
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 shape. Here is an exaggerated image of this situation:
Suppose that the player body was moving upwards just a tiny 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:
// check if contact points are moving into platform
for (let i = 0; i < totalPoints; i++) {
const platformVelocity = b2Body_GetLinearVelocity(platformBody);
const otherVelocity = b2Body_GetLinearVelocity(otherBody);
const relativeVelocity = b2Body_GetLocalVector(platformBody, b2Sub(otherVelocity, platformVelocity));
if (relativeVelocity.y < -1)
{ // if moving down faster than 1 m/s, handle as before
return true; // point is moving into platform, leave contact solid and exit
}
else if (relativeVelocity.y < 1)
{ // if moving slower than 1 m/s
// borderline case, moving only slightly out of platform
const relativePoint = b2Body_GetLocalPoint(platformBody, points[ i ]);
const platformFaceY = 0.5; // front of platform, from shape definition :(
if (relativePoint.y > platformFaceY - 0.05)
{
return true; // contact point is less than 5cm inside front face of platform
}
}
else
{
return false; // no points are moving into platform, contact should not be solid
}
}
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 shapes 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 shapes 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 :)
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 v3.0.
Testbed test: one-way-walls/index.html
Credits
This tutorial is adapted from an original piece of work created by Chris Campbell and is used under license.