Making a LibGDX Roguelike Survival Game Part 8 – Collision Listeners & Interacting #gamedev

When our Hero collides with certain bodies within our Island we want to be able to interact with them in some way. Most of the bodies exist only to stop us walking through objects, some are set as sensors, we can walk through them but our game is aware of when we come into contact with them and when we lose that contact.  Here is a gif (quite long) explaining how we can handle those collisions. We will go into more detail of the code but hopefully this helps explain the process:

[ Full source code for this tutorial ]

collision_anim

Box2D currently stops the player passing through the boundaries on the Island and walking over trees, to add interactions to objects we can implement listeners to handle collisions.

Before we look at using the Box2D callbacks for collisions we need to add some variables and methods to the Entity class. We have already added a body which acts as a collision hitbox, we add a new body called sensor, this will be a body which can be passed over.

Entity.java

public class Entity implements Comparable<Entity> {
    public int hashcode; // to be explained.
    public Body sensor; // A trigger hitbox
    public boolean remove;

When a collision occurs on the body of an Entity we will call the collision method, the first param of entity is the object that was collided with and begin is true if the hit-boxes started to overlap or false is the collision has ended (They no longer overlap).

The interact method will be overrode to handle the player using an object in some way.

The removeBodies method will be used to remove an entities bodies from the world, cutting down a tree for example would remove it from the Island, we would no longer need the bodies for collisions, leaving them in place would leave behind invisible blocks.

public void collision(Entity entity, boolean begin){}

public void interact(){}
public void removeBodies(Box2DWorld box2D) {
    if(sensor != null) box2D.world.destroyBody(sensor);
    if(body != null) box2D.world.destroyBody(body);
}

Control.java

Add a new class boolean called interact, this is set to true on key up of ‘E’, this is the ‘use’ key.  Once an interaction is processed it will set this value back to false.

// ACTIONS
public boolean interact;
...
public boolean keyUp(int keycode) {
  switch (keycode) {
    case Keys.E:
      interact = true;
      break;

Box2DHelper.java

A new method called createSensor which is almost identical to the createBody method creates hit boxes that are used like triggers. Our Hero will be able to pass over these hitboxes; the key line of code is fixtureDef.isSensor = true;  :

public static Body createSensor(World world, float width, float height, float xOffset, float yOffset, Vector3 pos, BodyDef.BodyType type) {
        Body body;
        BodyDef bodyDef = new BodyDef();
        bodyDef.position.x = pos.x + xOffset;
        bodyDef.position.y = pos.y + yOffset;
        bodyDef.angle = 0;
        bodyDef.fixedRotation = true;
        bodyDef.type = type;
        body = world.createBody(bodyDef);

        FixtureDef fixtureDef = new FixtureDef();
        PolygonShape boxShape = new PolygonShape();
        boxShape.setAsBox(width / 2, height / 2);

        fixtureDef.shape=boxShape;
        fixtureDef.isSensor = true;

        body.createFixture(fixtureDef);
        boxShape.dispose();

        return body;
    }

Box2DWorld.java

There are significant changes to the Box2DWorld class, first we add a HashMap to store the Island entities, you may wonder why we store two entity arrays (two arrays with the same entities), one for drawing/logic and this new array for dealing with collisions. A HashMap stores key and value, we set the key to the body hashcode and the value as the Entity. When a collision (contact) occurs we get access to the two fixtures present in the collision, these fixtures are the bodies (body and sensor) that are defined within Entity, by using the hashcode for the key we can select the correct Entity from the array without having to loop.

Given the Hero body has a code of 001 and the Tree sensor has a code of 333, when a contact between the two occur we get fixtureA and fixtureB from the contact, lets say A is the body of the Hero and B is the sensor of the Tree. We can look up the two entities by using the hash codes of the fixtures (bodies).

public class Box2DWorld {
  public World world;
  private Box2DDebugRenderer debugRenderer;
  // New array to hold entities
  private HashMap<Integer, Entity> entityMap;

  public Box2DWorld(){
    world = new World(new Vector2(.0f, .0f), true);
    debugRenderer = new Box2DDebugRenderer();
    // Init the new entity array
    entityMap = new HashMap<Integer, Entity>();

    // Setup the world contacts listeners
    world.setContactListener(new ContactListener() {
      @Override
      public void beginContact(Contact contact) {
        Fixture fixtureA = contact.getFixtureA();
        Fixture fixtureB = contact.getFixtureB();

        process_collisions(fixtureA, fixtureB, true);
      }

      @Override
      public void endContact(Contact contact) {
        Fixture fixtureA = contact.getFixtureA();
        Fixture fixtureB = contact.getFixtureB();

        process_collisions(fixtureA, fixtureB, false);
      }

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

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

  public void tick(OrthographicCamera camera, Control control){
  if (control.debug) debugRenderer.render(world, camera.combined);
    world.step(Gdx.app.getGraphics().getDeltaTime(), 6, 2);
    world.clearForces();
  }

  // Method to clear down all bodies
  public void clearAllBodies() {
    Array<Body> bodies = new Array<Body>;();
    world.getBodies(bodies);
      for(Body b: bodies){
      world.destroyBody(b);
    }

    entityMap.clear();
  }

  // check if the two bodies colliding exist in our array of entities
  // check that only one entity is a sensor
  // When entityA is a sensor then entityB is the player as its the only
  // moving object.
  // We want to call the Entity method collision for the hero:
  // ... Hero.collision(Tree, true)
  private void process_collisions(Fixture aFixture, Fixture bFixture, boolean begin) {
    Entity entityA = entityMap.get(aFixture.hashCode());
    Entity entityB = entityMap.get(bFixture.hashCode());

    if(entityA != null && entityB != null){
      if(aFixture.isSensor() && !bFixture.isSensor()){
        entityB.collision(entityA, begin);
      } else if(bFixture.isSensor() && !aFixture.isSensor()){
        entityA.collision(entityB, begin);
      }
    }
  }

  // Pass in Island entities and copy them into a new
  // array that has a key (the hashcode of the entity).
  public void populateEntityMap(ArrayList<Entity> entities){
    entityMap.clear();
    for(Entity e: entities){
      entityMap.put(e.hashcode, e);
    }
  }

  // When a tree/hero is added to the island we
  // track it in the entity array
  public void addEntityToMap(Entity entity){
    entityMap.put(entity.hashcode, entity);
  }

  // removes an entity from the array
  public void removeEntityToMap(Entity entity){
    entityMap.remove(entity.hashcode, entity);
  }
}

Hero.java

A new ArrayList of entities is added to keep track of the entities currently in contact with the hero hitbox (Body), the player may be overlapping more than one tree for example, but we will only allow interactions with one at a time.

When collisions or contacts occur with other entities they will be added to this array, when those contacts are broken (collision is no longer true) the entities will be removed from this array. When a collision occurs with the hero body then we use the hashcode of the body which we get via the contact > fixture to select the hero from the hashmap and call the collision method.

When the hero update is called we pass in control and check if interact is true, if it is and the interactEntity array has items then we call the interact method on that entity:

// Class variable. Array of entities currently overlapping/coliding with.
ArrayList<Entity> interactEntities;

body = Box2DHelper.createBody(box2d.world, width/2, height/2, width/4, 0, pos, BodyType.DynamicBody);
// Set hashcode to that of the bodies fixture
// Our body has a single fixture
hashcode = body.getFixtureList().get(0).hashCode();
// init the Entity Array
interactEntities = new ArrayList<Entity>();

public void update(Control control) {
        dirX = 0;
        dirY = 0;

        if (control.down)  dirY = -1;
        if (control.up)    dirY = 1;
        if (control.left)  dirX = -1;
        if (control.right) dirX = 1;    

        body.setLinearVelocity(dirX * speed, dirY * speed);
        pos.x = body.getPosition().x - width/2;
        pos.y = body.getPosition().y - height/4;

        // If interact key pressed and interactEntities present interact with first in list.
        if(control.interact && interactEntities.size() > 0){
        	interactEntities.get(0).interact();
        }

        // Reset interact
        control.interact = false;
    }

    @Override
    public void collision(Entity entity, boolean begin)
    	if(begin){
    	    // Hero entered hitbox
    	    interactEntities.add(entity);
    	} else {
    	    // Hero Left hitbox
    	    interactEntities.remove(entity);
    	}
    }

Tree.java
The sensor for the tree is setup to be bigger than the body, the body will only stop the player walking through it making it a solid object. The sensor will trigger collision callbacks, by setting the entity hashcode to that of the sensor (Body) it will be found in the hashmap of entities and we can trigger interactions with it.

On interacting with a tree we set remove to true:

public Tree(Vector3 pos, Box2DWorld box2d){
    ...
    body = Box2DHelper.createBody(box2d.world, width/2, height/2, width/4, 0, pos, BodyDef.BodyType.StaticBody);
    sensor = Box2DHelper.createSensor(box2d.world, width, height*.85f, width/2, height/3, pos, BodyDef.BodyType.DynamicBody);
    hashcode = sensor.getFixtureList().get(0).hashCode();
}

@Override
public void interact(){
    remove = true;
}

gameclass.java
The create method passes the island entities in the the box2D class instance to setup the hashmap of entities, these entities are copied, the point toward the same on entity so changing an entity in the arraylist or the hashmap array has the same outcome.

After reseting the map we have to re-populate the hashmap, the last line in the render loop is clearing out any entities that have remove set to true.

@Override
public void create() {
    ....
    // HashMap of Entities for collisions
    box2D.populateEntityMap(island.entities);
}

@Override
public void render () {
    Gdx.gl.glClearColor(0, 0, 0, 0);
    Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT);

    // GAME LOGIC
    if(control.reset){
        island.reset(box2D);
        hero.reset(box2D,island.getCentrePosition());
        island.entities.add(hero);
        // re-populate hashmap
        box2D.populateEntityMap(island.entities);
        control.reset = false;
    }

    ...
    island.clearRemovedEntities(box2D);
}

Island.java
The new method clearRemoverEntities uses an iterator to loop through the objects, this is a safe way to loop and change an array without causing errors:

public void clearRemovedEntities(Box2DWorld box2D) {
    Iterator<Entity> it = entities.iterator();
    while(it.hasNext()) {
        Entity e = it.next();
        if(e.remove){
            e.removeBodies(box2D);
            box2D.removeEntityToMap(e);

            it.remove();
        }
    }
}

So now we have Box2D listeners calling our collision methods everytime a contact takes place between two bodies. We filter out which contacts we are interesting in and can pass that interaction occurance to an entity to be dealt with.

At the moment we simply remove a tree when the Hero interacts with it, we will later add HP and allow many hits before it is removed also add items to the hero when it is removed as we should get resources for downing a tree.

 

Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s