LibGDX Tutorial - Create moving enemies (How To 8)

After we finish these tutorials and our final game is created, i will upload the code on github. Even though everything we do is inspired by my first game Black Dodger which is not an open - source project, i thought it would be very nice to give you the whole getest project and make it open - source. (In a way it already is). But now lets move on the main subject.

How to create moving enemies with box2D and libGDX

A moving hero without some enemies to bother is kinda pointless, so in the How To 8 tutorial we are going to implement 2 enemies to worry about.

For the first step, open the " enums " package and inside create an enum java class with the name
" EnemyType ". In here will keep the information of our enemies.

package com.getest.game.enums;

import com.badlogic.gdx.math.Vector2;

public enum EnemyType {

    GROUND1(  1F, 1F, 18F, 2.5F, 0.5F, new Vector2(-7.5F,0F)),
    GROUND2(  1F, 1F, -2.0F, 2.5F, 0.5F, new Vector2(7.5F,0F)),
    FLYING1(  1F, 1F, 18F, 3.6F, 0.5F, new Vector2(-7.5F,0F)),
    FLYING2(  1F, 1F, -2.0F, 3.6F, 0.5F, new Vector2(7.5F,0F));

    private float width;
    private float height;
    private float x;
    private float y;
    private float density;
    private Vector2 velocity;

    private EnemyType(float width, float height, float x, float y, float density, Vector2 velocity) {
        this.width = width;
        this.height = height;
        this.x = x;
        this.y = y;
        this.density = density;
        this.velocity = velocity;
    }

    public float getWidth() {
        return width;
    }

    public float getHeight() {
        return height;
    }

    public float getX() {
        return x;
    }

    public float getY() {
        return y;
    }

    public float getDensity() {
        return density;
    }

    public Vector2 getVelocity() {
        return velocity;
    }

}

I said that we will create 2 enemies and yet we created 4. That's because the 2 of them are actually identical with the other 2, with the only difference being the side from which they will come. We need the same enemy to be able to come either from left or right. So, we just defined the width and the height, the position, the density and the velocity of our enemies and we also created the getters and the setters.

The first enemy will come from the ground, so will jump over it, and the second will fly over the ground and will slide under it.

" How do we decide which of those 4 enemies to create? "

Inside the " misc " package, create a java class with the name " RandomMisc ". The purpose of this class is to select a random enemy from the enum class.

package com.getest.game.misc;

import com.getest.game.enums.EnemyType;
import java.util.Random;

public class RandomMisc {

    public RandomMisc() {
    }

    public static EnemyType getRandomEnemyType() {
        RandomMisc.RandomEnum randomEnum = new RandomMisc.RandomEnum(EnemyType.class);
        return (EnemyType)randomEnum.random();
    }

    private static class RandomEnum<E extends Enum> {

        private static final Random RND = new Random();
        private final E[] values;

        public RandomEnum(Class<E> token) {
            values = token.getEnumConstants();
        }

        public E random() {
            return values[RND.nextInt(values.length)];
        }
    }


}

And now, once again we follow a known procedure, just like we did in the How To 6 and How To 7 tutorials to create our hero.

In the " WorldMisc " class, we create the function that will produce a random enemy from the above 4, when called.

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.FixtureDef;
import com.badlogic.gdx.physics.box2d.PolygonShape;
import com.badlogic.gdx.physics.box2d.World;
import com.getest.game.box2d.GroundUserData;
import com.getest.game.box2d.HeroUserData;
import com.getest.game.box2d.LeftWallUserData;
import com.getest.game.box2d.RightWallUserData;
import com.getest.game.enums.EnemyType;

public class WorldMisc {
    
    //Rest of the code

    public  Body createHero(World world) {
        BodyDef hero = new BodyDef();
        hero.type = BodyDef.BodyType.DynamicBody;
        hero.position.set(new Vector2(8.0F, 2F));
        PolygonShape heroShape = new PolygonShape();
        heroShape.setAsBox(0.4F, 0.8F);
        Body body = world.createBody(hero);
        FixtureDef heroFixture = new FixtureDef();
        heroFixture.density = 0.5F;
        heroFixture.shape = heroShape;
        heroFixture.friction = 0.0F;
        body.createFixture(heroFixture);
        body.setGravityScale(5F);
        body.setFixedRotation(true);
        body.resetMassData();
        body.setUserData(new HeroUserData(0.8F, 1.6F));
        heroShape.dispose();
        return body;
    }

    public static Body createEnemy(World world) {
        EnemyType enemyType = RandomMisc.getRandomEnemyType();
        BodyDef bodyDef = new BodyDef();
        bodyDef.type = BodyDef.BodyType.KinematicBody;
        bodyDef.position.set(new Vector2(enemyType.getX(), enemyType.getY()));
        PolygonShape shape = new PolygonShape();
        shape.setAsBox(enemyType.getWidth() / 2, enemyType.getHeight() / 2);
        Body body = world.createBody(bodyDef);
        body.createFixture(shape, enemyType.getDensity());
        body.resetMassData();
        EnemyUserData userData = new EnemyUserData(enemyType.getWidth(), enemyType.getHeight(), enemyType.getX(), enemyType.getY());        body.setLinearVelocity(enemyType.getVelocity());
        body.setUserData(userData);
        shape.dispose();
        return body;
    }
}

As you can see, with the help of the " RandomMisc " class we choose a random enemy from the enum class and then we define his position, width, height, velocity and density.

I have also presented the function that creates our hero, because i added one line, the

body.setFixedRotation(true);

That way, when an enemy hits our hero in a weird angle, his rotation will be fixed. You can try not to add this line and see what happens!

We also have to create the user data type for the enemies and then create their user data. So, first of all,  the " UserDataType " class now looks like;

package com.getest.game.enums;

public enum UserDataType {
    GROUND,
    LEFTWALL,
    RIGHTWALL,
    ENEMY,
    HERO;

    private UserDataType() {

    }
}

and then we create a class with the name " EnemyUserData ", inside our " box2d " package;

package com.getest.game.box2d;

import com.getest.game.enums.UserDataType;

public class EnemyUserData extends UserData {

    private float x,y;

    public EnemyUserData(float width, float height, float x, float y) {

        super(width, height);
        userDataType = UserDataType.ENEMY;
        this.x = x;
        this.y = y;

    }

    public float getX() {return x;}

    public float getY() {return y;}
}

Don't forget to go back to the " WorldMisc " class and import the enemy user data class that we have just created.

Inside the " actors " package, we also create a " EnemyActor " java class with the below code;

package com.getest.game.actors;

import com.badlogic.gdx.physics.box2d.Body;
import com.getest.game.box2d.EnemyUserData;

public class EnemyActor extends GameActors {

    public EnemyActor(Body body) {
        super(body);
    }

    public EnemyUserData getUserData() {
        return (EnemyUserData) userData;
    }
}

It is time to actually call the function that creates our enemies! This tutorial is ez and small.

Wrong (even thought i hope it is easy)


When we create something and that something is not any more needed, then we must destroy it, just like we did with our graphics (after we change a screen, we dispose everything). The same goes with the box2D bodies that we are creating. When an enemy goes out of our screen, then why keep it? The problem won't be big right now, because we are not using animations for our enemies, but imagine what could happen if we had created 100 enemies with animations. Our device would have a hard time.

Inside the " WorldMisc " package, we modify our " BodyMisc " class and we basically add a function that understands if a body belongs to an enemy or not and a class that checks whether an enemy is out of the screen or not.

package com.getest.game.misc;

import com.badlogic.gdx.physics.box2d.Body;
import com.getest.game.box2d.UserData;
import com.getest.game.enums.UserDataType;

public class BodyMisc {

    public BodyMisc(){

    }

    //Rest of the code

    public static boolean bodyInBounds(Body body) {

        UserData userData = (UserData)body.getUserData();
        switch(userData.getUserDataType()) {
            case HERO:
            case ENEMY:
                return body.getPosition().x + userData.getWidth() / 2.0F > -4.0F && body.getPosition().x + userData.getWidth() / 2.0F < 20F;
            default:
                return true;
        }
    }

    public static boolean bodyIsEnemy(Body body) {
        UserData userData = (UserData)body.getUserData();
        return userData != null && userData.getUserDataType() == UserDataType.ENEMY;
    }
}

We just considered that out of bounds means that an enemy is 4 units of our own virtual resolution (16 x 9 ) out of the screen, on the x - axis.

Now we are ready to go to our " GameStage " and call the function that creates our enemies! We will do that be creating a timer that will call the function every once in a while, continusly. Then we will check if an enemy is out of bounds and if it is, we will destroy it.

package com.getest.game.stages;

import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.graphics.OrthographicCamera;
import com.badlogic.gdx.graphics.g2d.TextureAtlas;
import com.badlogic.gdx.physics.box2d.Body;
import com.badlogic.gdx.physics.box2d.Box2DDebugRenderer;
import com.badlogic.gdx.physics.box2d.Contact;
import com.badlogic.gdx.physics.box2d.ContactImpulse;
import com.badlogic.gdx.physics.box2d.ContactListener;
import com.badlogic.gdx.physics.box2d.Manifold;
import com.badlogic.gdx.physics.box2d.World;
import com.badlogic.gdx.scenes.scene2d.InputEvent;
import com.badlogic.gdx.scenes.scene2d.Stage;
import com.badlogic.gdx.scenes.scene2d.ui.ImageButton;
import com.badlogic.gdx.scenes.scene2d.ui.Skin;
import com.badlogic.gdx.scenes.scene2d.utils.ActorGestureListener;
import com.badlogic.gdx.utils.Array;
import com.badlogic.gdx.utils.Timer;
import com.badlogic.gdx.utils.viewport.ExtendViewport;
import com.getest.game.actors.EnemyActor;
import com.getest.game.actors.GroundActor;
import com.getest.game.actors.HeroActor;
import com.getest.game.actors.LeftWallActor;
import com.getest.game.actors.RightWallActor;
import com.getest.game.camera.AndroidCamera;
import com.getest.game.misc.WorldMisc;
import com.getest.game.misc.BodyMisc;

public class GameStage extends Stage implements ContactListener{

    private float accumulator, TIME_STEP;
    private Box2DDebugRenderer renderer;
    private WorldMisc wrl;
    private World world;
    private GroundActor ground;
    private LeftWallActor leftWall;
    private RightWallActor rightWall;
    private HeroActor hero;
    private Boolean right = false, left = false;
    private Skin leftButtonSkin, rightButtonSkin, jumpButtonSkin, slideButtonSkin;
    private ImageButton leftButton, rightButton, jumpButton, slideButton;
    private Timer timerEnemy = new Timer();

    public GameStage(){

        super(new ExtendViewport(16f, 9f, new OrthographicCamera(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();
        Gdx.input.setInputProcessor(this);
        world.setContactListener(this);
        setupGround();
        setupLeftWall();
        setupRightWall();
        setupHero();
        setupButtons();
        createEnemy();
    }

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

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

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

    private void setupHero() {
        hero = new HeroActor(wrl.createHero(world));
        addActor(hero);
    }

    private void setupButtons() {

        leftButtonSkin = new Skin(Gdx.files.internal("skins/leftButton.json"), new TextureAtlas(Gdx.files.internal("skins/inGameButtons.atlas")));
        rightButtonSkin = new Skin(Gdx.files.internal("skins/rightButton.json"), new TextureAtlas(Gdx.files.internal("skins/inGameButtons.atlas")));
        jumpButtonSkin = new Skin(Gdx.files.internal("skins/jumpButton.json"), new TextureAtlas(Gdx.files.internal("skins/inGameButtons.atlas")));
        slideButtonSkin = new Skin(Gdx.files.internal("skins/slideButton.json"), new TextureAtlas(Gdx.files.internal("skins/inGameButtons.atlas")));
        leftButton = new ImageButton(leftButtonSkin);
        rightButton = new ImageButton(rightButtonSkin);
        jumpButton = new ImageButton(jumpButtonSkin);
        slideButton = new ImageButton(slideButtonSkin);
        leftButton.setSize(2.35F, 5F);
        rightButton.setSize(2.35F, 5F);
        jumpButton.setSize(2.35F, 5F);
        slideButton.setSize(2.35F, 5F);
        leftButton.setPosition(1.25F, -0.5F);
        rightButton.setPosition(4.0F, -0.5F); 
        jumpButton.setPosition(12.75F, -0.5F);
        slideButton.setPosition(10F, -0.5F);
        addActor(leftButton);
        addActor(rightButton);
        addActor(jumpButton);
        addActor(slideButton);

        leftButton.addListener(new ActorGestureListener() {
            public void touchDown(InputEvent event, float x, float y, int pointer, int button) {
                left = true;
            }

            public void touchUp(InputEvent event, float x, float y, int pointer, int button) {
                left = false;
            }
        });

        rightButton.addListener(new ActorGestureListener() {
            public void touchDown(InputEvent event, float x, float y, int pointer, int button) {
                right = true;
            }

            public void touchUp(InputEvent event, float x, float y, int pointer, int button) {
                right = false;
            }
        });

        jumpButton.addListener(new ActorGestureListener() {
            public void touchDown(InputEvent event, float x, float y, int pointer, int button) {
                    hero.jump();
            }

        });
        slideButton.addListener(new ActorGestureListener() {
            public void touchDown(InputEvent event, float x, float y, int pointer, int button) {
                    hero.dodge();
            }

        });
    }


    private void createEnemy() {
        Timer.Task task = new Timer.Task() {
            public void run() {
                EnemyActor enemy = new EnemyActor(wrl.createEnemy(world));         //I am setting up useless tasks when not needed on some worlds. Fix that.                addActor(enemy);            }
            }
        };        
        timerEnemy.scheduleTask(task, 1.4F, 4);
    }

    @Override
    public void act(float delta) {

        super.act(delta);
        Array<Body> bodies = new Array<Body>(world.getBodyCount());
        world.getBodies(bodies);
        accumulator += delta;
        while (accumulator >= delta) {
            world.step(TIME_STEP, 8, 4);
            accumulator -= TIME_STEP;
        }

        for(Body body : bodies){
            update(body);
        }

        if (left) {
            hero.moveLeft();
        } else if (right) {
            hero.moveRight();
        } else {
            hero.moveStop();
        }

    }

    private void update(Body body) {
        if((!BodyMisc.bodyInBounds(body) && BodyMisc.bodyIsEnemy(body))){
            world.destroyBody(body);
        }
    }

    @Override
    public void draw() {
        super.draw();
        renderer.render(world, getViewport().getCamera().combined);
    }

    @Override
    public void beginContact(Contact contact) {
        Body a = contact.getFixtureA().getBody();
        Body b = contact.getFixtureB().getBody();
        if((BodyMisc.bodyIsHero(a) && (BodyMisc.bodyIsGround(b) )) || ((BodyMisc.bodyIsGround(a) && (BodyMisc.bodyIsHero(b))))) {
            hero.landed();
        }

    }

    @Override
    public void endContact(Contact contact) {

    }

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

    }

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

    }
}

Sorry for showing the whole class, but we are in key point and i wanted everything to be perfect. 

What we did was to create the function createEnemy() which calls the function that actually creates randomly our enemies. This is done with a timer and every 4 seconds another enemy will be created.

Then, in the act() function we take all the active bodies of our world and if the bodies belong to an enemy and are out of bounds, then we destroy them. We always destroy a body AFTER we step the simulation.

Run the game and play!



When watching the video (or when playing the game yourself) you might notice that you can get hit by the enemies. You could do the following for homework; Modify the contact functions and when you get hit by an enemy, change the screen to the splash screen. Or even better, do that when you get hit 3 times by an enemy. This exactly, is the concept of lives or a life bar.

Feel free to ask any questions in the comments below. Next time, we will implement some graphics and animations.

Comments

  1. I think you meant for your createEnemy method to be like this:

    private void createEnemy() {
    Timer.Task task = new Timer.Task() {
    public void run() {
    EnemyActor enemy = new EnemyActor(wrl.createEnemy(world));
    //I am setting up useless tasks when not needed on some worlds.
    // Fix that.
    addActor(enemy);
    }
    };
    timerEnemy.scheduleTask(task, 1.4F, 4);
    }

    Instead, the addActor(enemy) and the closing bracket of the run() method were being commented out, leaving the brackets unbalanced. The only reason I knew to look for it was that timerEnemy did not have a .scheduleTask() method available.

    ReplyDelete
    Replies
    1. Thanks for the contributions man, really appreciate it. Btw, you can also find the whole project in github https://github.com/Sk0uF/Gamengineering-Tutorials

      Delete

Post a Comment