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 ]
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.
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); }
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;
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; }
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); } }
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.