Search code examples
javalibgdxbox2dcollision

Libgdx box2d ContactListener is extremely glitchy


I run into this situation while developing my little 2D side-scrolling platformer game using libgdx. The main problem is that beginContact(Contact contact) isn't called when it clearly needs to. Here are some simplified code snippets to give a quick overview:

ContactListener class which logs every contact between sensor and ground:

public class WorldContactListener implements ContactListener
{
Player player;

public WorldContactListener(Player player, World world)
{
    this.player = player;
    world.setContactListener(this);
}

@Override
public void beginContact(Contact contact)
{
    Object userDataA = contact.getFixtureA().getUserData();
    Object userDataB = contact.getFixtureB().getUserData();

    if((userDataA == Tomb.UserData.GROUND || userDataB == Tomb.UserData.GROUND) 
  && (userDataA == Tomb.UserData.PLAYER_FEET  || userDataB == Tomb.UserData.PLAYER_FEET))
    {   
        Gdx.app.log("collision", "start");
    } 
}

@Override
public void endContact(Contact contact) 
{
    Object userDataA = contact.getFixtureA().getUserData();
    Object userDataB = contact.getFixtureB().getUserData();

    if((userDataA == Tomb.UserData.GROUND || userDataB == Tomb.UserData.GROUND) 
  && (userDataA == Tomb.UserData.PLAYER_FEET  || userDataB == Tomb.UserData.PLAYER_FEET))
    {   
        Gdx.app.log("collision", "stop");
    } 
}

@Override
public void preSolve(Contact contact, Manifold oldManifold) 
{
}

@Override
public void postSolve(Contact contact, ContactImpulse impulse) 
{
}
}

Player creation:

    //Main rectangle:
    BodyDef bodyDef = new BodyDef();
    bodyDef.position.set(spawnCoordinates);
    bodyDef.type = BodyType.DynamicBody;
    body = world.createBody(bodyDef);
    FixtureDef fixtureDef = new FixtureDef();
    PolygonShape shape = new PolygonShape();
    shape.setAsBox(5 / Tomb.PPM, 14 / Tomb.PPM);
    fixtureDef.shape = shape;
    fixtureDef.friction = FRICTION; //1
    fixtureDef.restitution = RESTITUTION; //0
    body.createFixture(fixtureDef).setUserData(Tomb.UserData.PLAYER);

    //Circle-shaped sensor:
    fixtureDef = new FixtureDef();
    CircleShape feet = new CircleShape();
    feet.setPosition(new Vector2(0, -16 / Tomb.PPM));
    feet.setRadius(10 / Tomb.PPM);
    fixtureDef.shape = feet;
    fixtureDef.isSensor = true; 
    body.createFixture(fixtureDef).setUserData(Tomb.UserData.PLAYER_FEET);

Player movements:

private void handleInput(float delta)
{
    if(Gdx.input.isKeyJustPressed(Input.Keys.UP))
    {
        body.applyLinearImpulse(new Vector2(0, JUMP_POWER), body.getWorldCenter(), true);
    }
    if(Gdx.input.isKeyPressed(Input.Keys.RIGHT) && body.getLinearVelocity().x <= MAX_HORIZONTAL_VELOCITY)
    {
        body.applyLinearImpulse(new Vector2(HORIZONTAL_SPEED, 0), body.getWorldCenter(), true);
    }
    if(Gdx.input.isKeyPressed(Input.Keys.LEFT) && body.getLinearVelocity().x >= -MAX_HORIZONTAL_VELOCITY)
    {
        body.applyLinearImpulse(new Vector2(-HORIZONTAL_SPEED, 0), body.getWorldCenter(), true);
    }
}

Initialization of a .tmx map, which is imported from Tiled Map Editor. It's noteworthy that whole terrain is one single PolygonMapObject, so specifically in this case for loop isn't needed.

protected void defineGround(int layerIndex) 
{
    for(PolygonMapObject object : map.getLayers().get(layerIndex).getObjects().getByType(PolygonMapObject.class))
    {
        BodyDef bodyDef = new BodyDef();
        ChainShape chainShape = new ChainShape();
        FixtureDef fixtureDef = new FixtureDef();
        Body body;

        Polygon polygon = object.getPolygon();
        bodyDef.type = BodyType.StaticBody;
        bodyDef.position.set(polygon.getX() / Tomb.PPM, polygon.getY() / Tomb.PPM);
        body = world.createBody(bodyDef);   
        float[] scaledVertices = new float[polygon.getVertices().length];
        for(int i = 0; i < polygon.getVertices().length; i++)
        {
            scaledVertices[i] = polygon.getVertices()[i] / Tomb.PPM;
        }
        chainShape.createChain(scaledVertices);
        fixtureDef.shape = chainShape;
        body.createFixture(fixtureDef).setUserData(Tomb.UserData.GROUND);
    }
}

Finally, self-explaining pics, which show the game screen itself and eclipse console with WorldContactListener logs:

enter image description here enter image description here

enter image description here

Same thing happens when trying to climb up the slope (or absolutely any non-flat surface):

enter image description here enter image description here

I have tried all possible sensor shape and size variations, so this behavior isn't caused by CircleShape's size. What could be the case? Or any workaround which doesn't involve ContactListener?


Solution

  • The deal is that there ale multiple collisions at one time. On your last image u can see that there are still (2-1 + 2-1) = 2 collisions between your player and polygons.

    Here is my example solution: Make some collision counter class with functions incCollision() and decCollision(), and maybe some bool isCollison() that will tell you if there actually is any collision.

    Edit your ContactListener class:

    public void beginContact(Contact contact)
    {
    ...
       if((userDataA == Tomb.UserData.GROUND || userDataB == Tomb.UserData.GROUND) 
          && (userDataA == Tomb.UserData.PLAYER_FEET  || userDataB == Tomb.UserData.PLAYER_FEET))
            {   
                Gdx.app.log("collision", "start");
                counter.incCollision();
            } 
    }
    
    public void endContact(Contact contact)
    {
    ...
    if((userDataA == Tomb.UserData.GROUND || userDataB == Tomb.UserData.GROUND) 
      && (userDataA == Tomb.UserData.PLAYER_FEET  || userDataB == Tomb.UserData.PLAYER_FEET))
        {   
            Gdx.app.log("collision", "stop");
            counter.decCollision();
        } 
    }