Making a LibGDX Roguelike Survival Game Part 6 – Box2D Collisions #gamedev

[ Full source code for this tutorial ]

Currently our hero can move around disregarding the underlying map, running over grass, cliffs and water, the next step is to impose some restrictions and implement collisions. When the project was created we chose to include Box2D:

Box2D is a 2D physics library. It is one of the most popular physics libraries for 2D games and has been ported to many languages and many different engines, including libgdx.

Using LibGDX we have to create a fair amount of the game code ourselves which can be a good thing but writting your own physics engine will slow your progress significantly, Box2D will do all the heavy lifting for us.

To get started we will need a Box2D World, creating and initialising the world will initialise Box2D.  Our world is top down so we do not need any gravity, the World takes a vector for gravity and also a boolean to set if the world objects sleep. To manage Box2D we will create a new class, as we add more features it will be cleaner to keep all of this together away from the other code.

box2DWorld.java (in a new package “uk.co.carelesslabs.box2d” )

public class Box2DWorld {
  // instance of a B2D World
  public World world;
  // Used to render objects which would be invisible
  private Box2DDebugRenderer debugRenderer;

  public Box2DWorld(){
    // Initialise world with no gravity!
    world = new World(new Vector2(.0f, .0f), true);
    // Initialise debug renderer
    debugRenderer = new Box2DDebugRenderer();
  }
  ...
}

This class has a tick method which will step the world and if debug is enabled draw the world shape outlines.

public void tick(OrthographicCamera camera, Control control){
  // Pass in control to check if debug is true
  if (control.debug) debugRenderer.render(world, camera.combined);
  // step the world forward in time
  world.step(Gdx.app.getGraphics().getDeltaTime(), 6, 2);
  world.clearForces();
}

The call to clearForces() should be done automatically so I will likely remove this call in another commit:

/** Manually clear the force buffer on all bodies. By default, forces are cleared automatically after each call to Step. The /** Manually clear the force buffer on all bodies. By default, forces are cleared automatically after each call to Step. The * default behavior is modified by calling SetAutoClearForces. The purpose of this function is to support sub-stepping. * Sub-stepping is often used to maintain a fixed sized time step under a variable frame-rate. When you perform sub-stepping * you will disable auto clearing of forces and instead call ClearForces after all sub-steps are complete in one pass of your * game loop. {@link #setAutoClearForces(boolean)} */

Next we create a helper class that can create objects for our Box2D world, to get us started we need static boxes that can be used to set a boundary around our island.

Box2DHelper.java 
(package “uk.co.carelesslabs.box2d” )
The helper class allows us to create a Box2D Body, there are several types of body, we will be creating static body types only at the moment. The Box2D site fully explains creating a new body, these bodies will match the size, shape and location of our tiles. For the body to fit over the tile we have to add have the half the width or hight to the x/y position, this is due to the co-ordinates being used as the centre point.

Creating the PolygonShape we have to half the width/height due to Box2D using this value as half the size:

/** Build vertices to represent an axis-aligned box./** Build vertices to represent an axis-aligned box. * @param hx the half-width. * @param hy the half-height. */ public void setAsBox (float hx, float hy) { jniSetAsBox(addr, hx, hy); }

Here is the helper class thus far, the method we will use is static:

public class Box2DHelper {
    public static Body createBody(World world, float width, float height, Vector3 pos, BodyDef.BodyType type) {
        Body body;
        BodyDef bodyDef = new BodyDef();
        bodyDef.position.set(pos.x + width/2, pos.y + height/2);
        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.restitution = 0.4f;

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

        return body;
    }
}

We are ready to add our Box2D class maingame.java

Box2DWorld box2D;

The new variable will need to be initialised within the create() method, a change to Island.java will be made to accept the box2D variable as a parameter:

// Box2D
box2D = new Box2DWorld();

// Island
island = new Island(box2D);

At the bottom of the render method we call the tick method of the Box2DWorld instance:

  ...
  hero.draw(batch);
  batch.end();
  // call tick method to draw debug lines
  // pass in control to check it debug is true
  box2D.tick(camera, control);
}

Island.java
Box2DWorld is needed within the Island class, once the tiles are setup we need to loop through them and check which of them require a collision object making them inpassable.

public Island(Box2DWorld box2D){
  setupTiles();
  codeTiles();
  generateHitboxes(box2D);
}

Before looking at the new generateHitboxes method there is a small fix to Chunk.java and two new methods required in the Tile.java class:

Chunk.java – getTileCode should return 0” and never a null, other wise we could end up with tile codes containing ‘nul’.

Tile.java
isAllWater checks if the tile is water surrounded by water, notIsAllWater returns if the tile is not all water. I prefer to create methods that return the opposite value so they read better, these methods could be better named though!

public boolean isAllWater() {
    return code.equals("000000000");
}

public boolean notIsAllWater() {
    return !isAllWater();
}

With those fixes and additional methods in place we can loop the tiles in the chunk once they are generated and add collision objects where needed.

Looping all of the tiles and checking that they are not passable but not all water we are finding the edge of the island, these are the tiles that should stop the hero moving further, it would be wasteful to create objects for every water tile:

private void generateHitboxes(Box2DWorld box2D) {
  for(ArrayList<Tile> row : chunk.tiles){
    for(Tile tile : row){
      if(tile.isNotPassable() && tile.notIsAllWater()){
        Box2DHelper.createBody(box2D.world, chunk.tileSize, chunk.tileSize, tile.pos, BodyType.StaticBody);
      }
    }
  }
}

Running the game and pressing the backspace key toggles control.debug, when on the debug renderer will draw the world bodies, at the moment the hero is still OP and moves where ever they want:

box2Dworld

Lets add a collision box for the hero so they are trapped on the island, the bodies added to the world for the tiles are not children of the Tile class, once created we cannot access them through the parent (tile), this will be addressed. When it comes to the hero we need to be able to move the body, first add a new variable to Entity class:

public Body body;

The hero will need the box2D variable passed into the public constructor so we can create the body via the world. The body for the hero is dynamic as it will need to move, we dont want the body to be the full height of the hero (wait for the image):

public Hero(Vector3 pos, Box2DWorld box2d){
  type = EntityType.HERO;
  width = 8;
  height = 8;
  this.pos.x = pos.x;
  this.pos.y = pos.y;
  texture = Media.hero;
  // We need to increase the speed!
  speed = 30;
  // Create a new Dynamic body
  body = Box2DHelper.createBody(box2d.world, width, height/2, pos, BodyType.DynamicBody);
}

Next change the update for the Hero class, rather than moving the Entity position we move the Box2D body but setting the linear Velocity, then we set the positon of the Entity to the x and y or the body.

If we were to set the position of the Body using the hero position then we would over ride collisions and force its position:

public void update(Control control) {
  ... 

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

gameclass.java – update the setup of the Hero class as it requires box2D passed in:

hero = new Hero(island.centreTile.pos, box2D);

Running the game and turning on debug we can see the hero now has a collision box which cannot pass through the other bodies:

bodies

Next we will look at adding in trees to an array of entities and sorting these so our hero can be appear in front or behind them.

 

 

 

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