LibGDX Tutorial - Create a ground and walls using box2D (How To 5)

Finally, in this tutorial we will use the box2D's physics to create a ground and 2 walls. The ground will serve as the place in which our character and all the monsters will stand, move, etc, and the purpose of the walls is to not allow our character to fall.

How to create a ground and walls with box2D

To begin with, create a package with the name " misc " and inside create a class with the name
" WorldMisc ". In here with the help of box2D, we are going to define every element of our world. For example let's create our world itself and a ground;

package com.getest.game.misc;

import com.badlogic.gdx.math.Vector2;
import com.badlogic.gdx.physics.box2d.Body;
import com.badlogic.gdx.physics.box2d.BodyDef;
import com.badlogic.gdx.physics.box2d.PolygonShape;
import com.badlogic.gdx.physics.box2d.World;

public class WorldMisc {

    public  World createWorld() {

        return new World(new Vector2(0, -10), true);

    }

    public Body createGround(World world) {

        BodyDef ground = new BodyDef();
        ground.type = BodyDef.BodyType.StaticBody;
        ground.position.set(new Vector2(0.0F, 0.0F));
        Body body = world.createBody(ground);
        PolygonShape groundShape = new PolygonShape();
        groundShape.setAsBox(16.01F, 2.0F);
        body.createFixture(groundShape, 0.5F);
        body.setUserData(new GroundUserData(2*(16.01F), 4.0F));
        groundShape.dispose();
        return body;

    }
}

The first variable defines the gravity of our world (reminds you of something?) and the second is a performance variable.

Now it's time for the ground. It will be a static body, starting from the position (0, 0), its shape will be a box that will expand 16 units right and left  (we are going to use 16 x 9 virtual resolution) and 2 units top and bottom from the initial position (0, 0). This way, the top side of this box will act as our ground. Finally, we set our grounds user data. User data is a very convenient way to hold some variables that we are going to need. It will give you an error for now, but don't worry, we will create a user data class soon. You might see that in the ground's width i have written 16.01 instead of 16. I did that because with 16 you will be able to see the box's right side (and i haaaate that :P).

We follow the same tactic to create 2 walls;

package com.getest.game.misc;

import com.badlogic.gdx.math.Vector2;
import com.badlogic.gdx.physics.box2d.Body;
import com.badlogic.gdx.physics.box2d.BodyDef;
import com.badlogic.gdx.physics.box2d.PolygonShape;
import com.badlogic.gdx.physics.box2d.World;

public class WorldMisc {

    public  World createWorld() {

        return new World(new Vector2(0, -10), true);
    }

    public Body createGround(World world) {

        BodyDef ground = new BodyDef();
        ground.type = BodyDef.BodyType.StaticBody;
        ground.position.set(new Vector2(0.0F, 0.0F));
        Body body = world.createBody(ground);
        PolygonShape groundShape = new PolygonShape();
        groundShape.setAsBox(16.01F, 2.0F);
        body.createFixture(groundShape, 0.5F);
        body.setUserData(new GroundUserData(2*(16.01F), 4.0F));
        groundShape.dispose();
        return body;

    }

    public Body createLeftWall(World world) {

        BodyDef leftWall = new BodyDef();
        leftWall.type = BodyDef.BodyType.StaticBody;
        leftWall.position.set(new Vector2(-1.0F, 12.0F));
        Body body = world.createBody(leftWall);
        PolygonShape leftWallShape = new PolygonShape();
        leftWallShape.setAsBox(1.0F, 11.0F);
        body.createFixture(leftWallShape, 0.5F);
        body.setUserData(new LeftWallUserData(2.0F, 22.0F));
        leftWallShape.dispose();
        return body;

    }

    public Body createRightWall(World world) {

        BodyDef rightWall = new BodyDef();
        rightWall.type = BodyDef.BodyType.StaticBody;
        rightWall.position.set(new Vector2(17F, 12.0F));
        Body body = world.createBody(rightWall);
        PolygonShape rightWallShape = new PolygonShape();
        rightWallShape.setAsBox(0.999F, 11.0F);
        body.createFixture(rightWallShape, 0.5F);
        body.setUserData(new RightWallUserData(1.998F, 22.0F));
        rightWallShape.dispose();
        return body;

    }
}

We just created 2 tall walls so that their opposite sides can prevent us from going out of the screen. For example the right side of the left wall (those walls are shaped as box ) will prevent us from going out the left side of our screen.

We want a way to differentiate a wall from a ground. For example upon collision we want to be able to know if we just hit a wall or the ground. Create a package with the name " enums " and inside create a java enum with the name " UserDataType ". Paste the code below;

package com.getest.game.enums;

public enum UserDataType {
    GROUND,
    LEFTWALL,
    RIGHTWALL;

    private UserDataType() {

    }
}

Each user data, for example the user data of the ground will have its own user data type. In this case the user data type of ground will be " GROUND ".

Create a package with the name " box2d " and inside create a java class with the name 
" UserData "In here place this code;

package com.getest.game.box2d;

import com.getest.game.enums.UserDataType;

public abstract class UserData {
    protected UserDataType userDataType;
    protected float width;
    protected float height;

    public UserData(float width, float height) {
        this.width = width;
        this.height = height;
    }

    public float getWidth() {
        return width;
    }

    public void setWidth(float width) {
        this.width = width;
    }

    public float getHeight() {
        return height;
    }

    public void setHeight(float height) {
        this.height = height;
    }

    public UserDataType getUserDataType() {
        return userDataType;
    }
}

This is the abstract class of the user data. We just wrote some setters - getters for the width, the height and for the user data type.

Finally, inside the same package, create 3 more classes with names " GroundUserData ",
" LeftWallUserData "" RightWallUserData ". Place these parts of codes in each respectively;

package com.getest.game.box2d;

import com.getest.game.enums.UserDataType;

public class GroundUserData extends UserData {
    public GroundUserData(float width, float height) {
        super(width, height);
        userDataType = UserDataType.GROUND;
    }
}

package com.getest.game.box2d;

import com.getest.game.enums.UserDataType;

public class LeftWallUserData extends UserData {
    public LeftWallUserData(float width, float height) {
        super(width, height);
        userDataType = UserDataType.LEFTWALL;
    }
}

package com.getest.game.box2d;

import com.getest.game.enums.UserDataType;

public class RightWallUserData extends UserData {
    public RightWallUserData(float width, float height) {
        super(width, height);
        userDataType = UserDataType.RIGHTWALL;
    }
}

What we did extra in each of these classes, was to define the user data type for each one.

Go back to the " WorldMisc " class and import everything that we have created. There shouldn't be any errors now.

" Hey, we have to actually call these methods "

If that's what you thinking right now then you are totally right. First, create a package with the name
 " stages " and inside create a class with the name " GameStage ". Paste this code;

package com.getest.game.stages;

import com.badlogic.gdx.graphics.OrthographicCamera;
import com.badlogic.gdx.physics.box2d.Body;
import com.badlogic.gdx.physics.box2d.Box2DDebugRenderer;
import com.badlogic.gdx.physics.box2d.World;
import com.badlogic.gdx.scenes.scene2d.Stage;
import com.badlogic.gdx.utils.viewport.ExtendViewport;
import com.getest.game.camera.AndroidCamera;
import com.getest.game.misc.WorldMisc;

public class GameStage extends Stage{

    private float accumulator, TIME_STEP;
    private Box2DDebugRenderer renderer;
    private WorldMisc wrl;
    private World world;
    private Body ground,leftWall,rightWall;

    public GameStage(){

        super(new ExtendViewport(16f, 9f, new AndroidCamera(16f, 9f)));
        accumulator = 0.0F;
        TIME_STEP = 1/300F; // Recomended by libgdx

        setupWorld();
    }

    private void setupWorld() {

        wrl = new WorldMisc();
        world = wrl.createWorld();
        renderer = new Box2DDebugRenderer();
        setupGround();
        setupLeftWall();
        setupRightWall();

    }

    private void setupGround(){
        ground = wrl.createGround(world);
    }

    private void setupLeftWall(){
        leftWall = wrl.createLeftWall(world);
    }

    private void setupRightWall(){
        rightWall = wrl.createRightWall(world);
    }

    @Override
    public void act(float delta) {

        super.act(delta);
        accumulator += delta;

        while (accumulator >= delta) {
            world.step(TIME_STEP, 8, 4);
            accumulator -= TIME_STEP;
        }
    }

    @Override
    public void draw() {

        super.draw();
        renderer.render(world, getViewport().getCamera().combined);

    }
}

Remember the stage from scen2D? We have gone one step deeper by creating a class that extends it, so that we can create our ground, walls etc. We call the methods we previously created to create our world, ground and walls. Once again,we create our camera and as a viewport we choose Extended and a virtual resolution of 16 x 9. I always prefer working with low values for my game worlds and with big when creating menus. Read Common Problems 1 and Common Problems 2, if you want to know why i choose these values. For now, don't worry about the extended viewport (it is the method that keeps the aspect ratio on all devices and shows a bit more of the world to the players with a bigger or smaller aspect ratio).

The box2d debug renderer is for debugging purposes, with that enabled we can see all of the bodies we have created. About the act method, its purpose is to step the simulation and this is a big topic that we will try to understand in another tutorial.

Finally, create a " GameScreen " class inside the screens package with the code below, in order to use the stage we have just created.

package com.getest.game.screens;

import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.Screen;
import com.badlogic.gdx.graphics.GL20;
import com.getest.game.stages.GameStage;

public class GameScreen implements Screen {

    private GameStage gameStage;

    public GameScreen() {
    }

    @Override
    public void show() {
        gameStage = new GameStage();
    }

    @Override
    public void render(float delta) {

        Gdx.gl.glClearColor(0.0F, 0.0F, 0.0F, 1.0F);
        Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT);
        gameStage.draw();
        gameStage.act(delta);

    }
    
    @Override
    public void resize(int width, int height) {
      gameStage.getViewport().update(width,height,true);
 }
@Override public void pause() { } @Override public void resume() { } @Override public void hide() { dispose(); } @Override public void dispose() { gameStage.dispose(); } }

Don't forget to call the game screen when clicking the play button. Go back to our
  " MainMenuScreen " class and replace this line of code;

((Game)Gdx.app.getApplicationListener()).setScreen(new SplashScreen());

with this;

((Game)Gdx.app.getApplicationListener()).setScreen(new GameScreen());

Run the game and you must get a result like this (if of course your device has a 16 x 9 aspect ratio, otherwise your world might be extended!

Note;; I insist on reading the Common Problems 1 and Common Problems 2 tutorials if you haven't already.

LibGDX and Box2D world with ground and walls
Box2D world with ground and walls

That's it for today's tutorial, feel free to ask any question in the comments below!

Comments

  1. Can you explain a little more about why the enum of UserDataType is needed? It's not fully clear to me.

    ReplyDelete
    Replies
    1. It's just a way to store all the types of our elements. We are going to use those types to make further checks. For example, in the next tutorials we create our hero, we implement his moves and we create the enemies. We will use those types to check if our hero is/has touching/touched the ground or the enemies. Feel free to ask anything else!

      Delete

Post a Comment