# 2D Platformer: add ability to jump on top of other character

I want to add ability to jump onto a head of other character/player, but still be able to go through them horizontally. There is one useful tutorial about one-way platforms, but that approach doesn't allow player to go through a platform from a side.

I tried to add sensor to foot and head of characters and enable contact in preSolve method, but it doesn't look to work very well (sometimes bodies are thrown out from each other with additional impulse).

Do you have any ideas? Also it would be nice to not change sources of box2d as it was done in tutorial as I'm using it from LibGDX.

Brief:

The solution is not in collision listeners, but in collision filters. Collision filters have a shouldCollide method that you can implement to return false if you want to disable collision or true to allow it. Collision filters are executed before collision listeners.

You can use, for example the linear velocity of the player sprite to decide if you allow or not the collision with the enemy/platform during that frame of the simulation. For example, if your player sprite has a linear velocity with a negative y (pointing down, angle isn't important) then allow the collision by returning true, if it is 0 or positive do not allow it by returning false.

Demonstration:

The following example needs some notes to be correctly analyzed as some "not so common" design decisions were taken. As I used a simple String variable to hold the game level, having y pointing down in the positive axis was more convenient, so I inverted the physics world. This means that gravity is Vector2(0f, 10f) the opposite of the examples in the box2d manual where gravity is Vector2(0f, -10f). This may create some confusion when trying to understand the following code.

Another thing that may be not so clear at first sight is the formulas to transform the y axis to the coordinate system of the graphics API. I know I could use matrices to invert the coordinates of the graphics API but I found simpler this method.

What matters most is the implementation of the shouldCollide method from the ContactFilter interface.

To run the example provided, some assets are needed. They are public domain. Filenames that the game expects included. They must be placed in directory core/assets.

hard_block.png

platform.png

platform_left.png

platform_left_corner.png

platform_left_corner_background.png

platform_right.png

platform_right_corner.png

platform_right_corner_background.png

platform_up.png

The player sprite is controlled by the Left, Up and Right keys. There is no jump, the Up key adds impulse to fly like using a jet pack so you can fly through the game level.

Use gdx-setup.jar to create a new libGDX project. Don't forget to enable box2d.

In the MyGDXGame.java file, replace everything for this (com.mygdx.game was used as package name, rename it if you used a different name in the libGDX wizard):

package com.mygdx.game;

public class MyGdxGame extends ApplicationAdapter implements ContactFilter {
public class Entity
{
public String mName = "";
public int mContacts = 0;
public Fixture mFixture = null;
}

public class TileDef {
Texture image;
//BodyDef body;
}

double mCurrentTime = 0;
double mTimeStep = 1.0/60.0;
double mMaxFrameTime = mTimeStep * 4;
SpriteBatch batch;
World mWorld;
Vector2 mGravity;
String mLevel;
int mLevelWidth;
int mLevelHeight;
int mTileSize = 32; // pixels
int mCameraLeft = 0;
int mCameraTop = 0;
float mBackgroundColor [] = {222f/255f, 243f/255f, 250f/255f, 1f};
Entity mPlayerEntity;

TileDef mTiles [];

@Override
public boolean shouldCollide(Fixture fa, Fixture fb) {
Fixture player_fixture = null;
Fixture tile_fixture = null;
int tile_x = 0;
int tile_y = 0;

Fixture fixtures [] = {fa, fb};

for (int f = 0; f < fixtures.length; ++f)
{
if (fixtures[f].getBody().getType() == BodyType.DynamicBody)
{
// Then this is the player
player_fixture = fixtures[f];
} else
{
// Then this is a solid tile of the game level
tile_fixture = fixtures[f];
Vector2 vec = fixtures[f].getBody().getPosition();
tile_x = (int)vec.x;
tile_y = (int)vec.y;
}
}

char tile_type = mLevel.charAt(tile_y * mLevelWidth + tile_x);

if (tile_type == '-' || tile_type == 'o' || tile_type == 'O' || tile_type == '<' || tile_type == '>')
{
// Remember, positive y is down, negative y is up

if (player_fixture.getBody().getLinearVelocity().y <= 0 ||
player_fixture.getBody().getPosition().y >= tile_fixture.getBody().getPosition().y)
return false;
}

return true;
}

public float getBoxWidth(PolygonShape shape) {
float result = 0f;

if (shape.getVertexCount() != 4)
return 0f;

Vector2 vec1 = new Vector2();
Vector2 vec2 = new Vector2();

for (int i = 0; i < 4; ++i)
{
shape.getVertex(i, vec1);

for (int j = 0; j < 4; ++j)
{
shape.getVertex(j, vec2);

if (vec2.y == vec1.y)
{
float d = Math.abs(vec2.x - vec1.x);
if (d > result)
result = d;
}
}
}

return result;
}

public float getBoxHeight(PolygonShape shape) {
float result = 0f;

if (shape.getVertexCount() != 4)
return 0f;

Vector2 vec1 = new Vector2();
Vector2 vec2 = new Vector2();

for (int i = 0; i < 4; ++i)
{
shape.getVertex(i, vec1);

for (int j = 0; j < 4; ++j)
{
shape.getVertex(j, vec2);

if (vec2.x == vec1.x)
{
float d = Math.abs(vec2.y - vec1.y);
if (d > result)
result = d;
}
}
}

return result;
}

@Override
public void create () {
batch = new SpriteBatch();

// For compatibiliy with our level format, positive y is down.
// That means that we need a positive gravity vector, the opposite
// of the examples in the Box2D manual.
mGravity = new Vector2(0, 10);

// Create the physical world for our simulation
mWorld = new World(mGravity, true);
mWorld.setContactFilter(this);

//mTileShape = new PolygonShape();

// Define a test level

// A brief on our level format
//   '#' is impassable tile
//   '(' and ')' are tiles passable by the side
//   '-' is a plataform that only collide with player when falling, you can pass if coming from below and you cannot if coming for above.
//   'P' is the starting point of the player. After the player sprite is created it will be replaced by an empty tile ' '

mLevel = "####################################################";
mLevelWidth = mLevel.length();
mLevel +="#                                                  #";
mLevel +="#                ############                      #";
mLevel +="#                                                  #";
mLevel +="#   ######                                         #";
mLevel +="#                                  <------>        #";
mLevel +="#                                  (******)        #";
mLevel +="#                                  (******)        #";
mLevel +="#                                  (******)        #";
mLevel +="#          <----->             #################   #";
mLevel +="#          (*****)                                 #";
mLevel +="#          (****O--->                              #";
mLevel +="#          (****(***)                              #";
mLevel +="#     <---------o***)                              #";
mLevel +="# P   (*********)***)                              #";
mLevel +="#   <----o******)***)     ####                     #";
mLevel +="#   (****)******)***)   ######       #####         #";
mLevel +="####################################################";
mLevelHeight = mLevel.length() / mLevelWidth;

// Build the tile palette
mTiles = new TileDef [256];
mTiles['#'] = new TileDef();
mTiles['#'].image = new Texture("hard_block.png");

mTiles['('] = new TileDef();
mTiles['('].image = new Texture("platform_left.png");

mTiles[')'] = new TileDef();
mTiles[')'].image = new Texture("platform_right.png");

mTiles['-'] = new TileDef();
mTiles['-'].image = new Texture("platform_up.png");

mTiles['*'] = new TileDef();
mTiles['*'].image = new Texture("platform.png");

mTiles['<'] = new TileDef();
mTiles['<'].image = new Texture("platform_left_corner.png");

mTiles['O'] = new TileDef();
mTiles['O'].image = new Texture("platform_left_corner_background.png");

mTiles['>'] = new TileDef();
mTiles['>'].image = new Texture("platform_right_corner.png");

mTiles['o'] = new TileDef();
mTiles['o'].image = new Texture("platform_right_corner_background.png");

// Physicalize world
for (int tile_y = 0; tile_y < mLevelHeight; ++tile_y)
{
char prev_tile_type = '\0';
Body prev_body = null;

for (int tile_x = 0; tile_x < mLevelWidth; ++tile_x)
{
char tile_type = mLevel.charAt(tile_y * mLevelWidth + tile_x);

if (tile_type == ' ')
{
// We do nothing
prev_body = null;
} else
if (tile_type == 'P')
{
prev_body = null;

// Create the player and replace for an empty tile ' '
mLevel = mLevel.replace('P', ' ');

BodyDef bdef = new BodyDef();
bdef.type = BodyType.DynamicBody;

// As in classic platformers, we want the player sprite
// to modify its position in response to forces, not
// its rotation. Other objects of the game, like falling
// boxes, may be fully physicalized.
bdef.fixedRotation = true;
bdef.position.x = tile_x + 0.5f;
bdef.position.y = tile_y + 0.5f;

// Note: for this example we use the tile shape to create
// the player fixture too. But the idea is that in the
// real game entities like the player will have varying
// shapes.
Body body = mWorld.createBody(bdef);

FixtureDef fd = new FixtureDef();
fd.density = 1;
fd.friction = 0.3f;
fd.shape = new PolygonShape();
((PolygonShape)fd.shape).setAsBox(0.5f, 0.5f);

Fixture f = body.createFixture(fd);

mPlayerEntity = new Entity();
mPlayerEntity.mName = "Player";
mPlayerEntity.mFixture = f;
} else
if ("<->oO#".indexOf(tile_type) > -1)
{
// If the tile is part of a continuous line of the same
// type, then expand the previous body, do not create
// a new body. This is necessary to avoid a glitch many
// physics engines have when the world is built on blocks.
// The player sprite may collide with the side of the
// next tile even if the expected result is that it stays
// stepts into the next tile, resulting in the player
// sprite getting stuck between tiles.

if (prev_tile_type == tile_type || ("<->oO".indexOf(tile_type) > -1 && "<->oO".indexOf(prev_tile_type) > -1))
{
float prev_width = getBoxWidth((PolygonShape)prev_body.getFixtureList().first().getShape());
float prev_height = getBoxHeight((PolygonShape)prev_body.getFixtureList().first().getShape());
prev_body.destroyFixture(prev_body.getFixtureList().first());
PolygonShape shape = new PolygonShape();
shape.setAsBox((prev_width + 1f) / 2f, prev_height / 2f);
prev_body.createFixture(shape, 0);
} else
{
float vertical_extent = 0.5f; // half the desired height
if ("<->oO".indexOf(tile_type) > -1)
{
vertical_extent = 0.03f / 2f; // a very slim box
}

BodyDef bdef = new BodyDef();
bdef.allowSleep = true;
bdef.type = BodyType.StaticBody;
bdef.fixedRotation = true;
bdef.position.set(tile_x + 0.5f, tile_y + vertical_extent);
Body body = mWorld.createBody(bdef);

PolygonShape shape = new PolygonShape();
shape.setAsBox(0.5f, vertical_extent);
body.createFixture(shape, 0);

prev_body = body;
}
}

prev_tile_type = tile_type;
}
}

mCurrentTime = TimeUtils.millis() / 1000.0;
}

@Override
public void render () {
Vector2 linear_vel = mPlayerEntity.mFixture.getBody().getLinearVelocity();

double new_time = TimeUtils.millis() / 1000.0;
double frame_time = new_time - mCurrentTime;
mCurrentTime = new_time;

if (frame_time > mMaxFrameTime)
frame_time = mMaxFrameTime;

while (frame_time >= mTimeStep)
{
if (Gdx.input.isKeyPressed(Input.Keys.LEFT))
{
if (linear_vel.x > -10f)
mPlayerEntity.mFixture.getBody().applyForceToCenter(-10f, 0f, true);
}

if (Gdx.input.isKeyPressed(Input.Keys.RIGHT))
{
if (linear_vel.x < 10f)
mPlayerEntity.mFixture.getBody().applyForceToCenter(10f, 0f, true);
}

if (Gdx.input.isKeyPressed(Input.Keys.UP))
{
mPlayerEntity.mFixture.getBody().applyForceToCenter(0, -20f, true);
}

mWorld.step((float)mTimeStep, 6, 2);
frame_time -= mTimeStep;

mCameraLeft = (int)(mPlayerEntity.mFixture.getBody().getPosition().x * (float)mTileSize - (float)Gdx.graphics.getWidth() / 2f);
if (mCameraLeft < 0)
mCameraLeft = 0;
if (mCameraLeft + Gdx.graphics.getWidth() > mLevelWidth * mTileSize)
mCameraLeft = (int)(mLevelWidth * mTileSize - Gdx.graphics.getWidth());

mCameraTop = (int)(mPlayerEntity.mFixture.getBody().getPosition().y * (float)mTileSize - (float)Gdx.graphics.getHeight() / 2f);
if (mCameraTop < 0)
mCameraTop = 0;
if (mCameraTop + Gdx.graphics.getHeight() > mLevelHeight * mTileSize)
mCameraTop = (int)(mLevelHeight * mTileSize - Gdx.graphics.getHeight());
}

Gdx.gl.glClearColor(mBackgroundColor[0],
mBackgroundColor[1],
mBackgroundColor[2],
mBackgroundColor[3]);

Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT);
batch.begin();

// If you don't understand what happens with the y coordinate during
// rendering, remember that we are using a positive y for down then
// you will understand the formulas.

for (int y = 0; y < Gdx.graphics.getHeight() / mTileSize + 2; ++y)
{
int y_offset = mCameraTop - mCameraTop / 32 * 32;

for (int x = 0; x < Gdx.graphics.getWidth() / mTileSize + 1; ++x)
{
int x_offset = mCameraLeft - mCameraLeft / mTileSize * mTileSize;

int tile = (y + mCameraTop / 32) * mLevelWidth + (x + mCameraLeft / 32);
if (tile >= mLevel.length())
break;

if (tile < 0)
continue;

char tile_char = mLevel.charAt(tile);

if (tile_char != ' ')
{
batch.draw(mTiles[mLevel.charAt(tile)].image, x * mTileSize - x_offset, Gdx.graphics.getHeight() - y * mTileSize + y_offset);
}
}
}

// Draw the player sprite
if (mPlayerEntity != null)
{
Vector2 pos = mPlayerEntity.mFixture.getBody().getPosition();
batch.draw(mTiles['#'].image, ((pos.x - 0.5f) * (float)mTileSize) - (float)mCameraLeft, Gdx.graphics.getHeight() - ((pos.y - 0.5f) * (float)mTileSize) + (float)mCameraTop);
}

batch.end();
}
}


The example has some issues and if you insist enough when playing with it you will trigger them sooner or later.

Conclusion: I was testing box2d with the intention to use it in a platformer and reached the conclusion that box2d is what you need when making a clone of Angry Birds, but is not the library for a platformer game like Mario Bros (IMHO).

I find simpler to just implement AABB and any needed functionality to detect collisions and calculate penetration vectors between two AABB. box2d doesn't come in a way that let you just use it for collision detection, it is meant to completely control the physics simulation of the game (as probably most physics engines/libraries). So I would only enable a full physics simulation at specific parts of a game level, and use a simpler collision detection during the rest of the game, applying the gravity in the main loop.

I think that the most simple way of getting around your problem is as follows. I assume that your player entity helps you keep track of his state. Maybe it's running, or climbing or whatever you might have used. If you add to make sure that the only time you will ever check if the player sprite is on top of an enemy is when the player's state is falling, your problem should be solved.

I hope this helps.