LibGDX Tutorial - Create animations for the enemies and add a background (How To 9)

Even though our game is cool without animation and graphics, lets go one step further and make it even cooler by adding a background and animations to our enemies.

Animation with LibGDX

First, download the packed version of our enemies and name it " enemies.png ". This tutorial took a little bit more than the others to be written, because i wanted to create the animations my self and give them to you.


Enemies frames to be used fro LibGDX animation
Packed enemies

Once again i packed it myself for you, so you will need to create the .atlas file.  I consider that by now, you know how to do it, so just create a " enemies.atlas " file with these lines;


enemies.png
size: 512,512
format: RGBA8888
filter: Linear,Linear
repeat: none
10f
  rotate: false
  xy: 368, 2
  size: 89, 91
  orig: 89, 91
  offset: 0, 0
  index: -1
10g
  rotate: false
  xy: 94, 57
  size: 90, 110
  orig: 90, 110
  offset: 0, 0
  index: -1
11f
  rotate: false
  xy: 186, 2
  size: 89, 91
  orig: 89, 91
  offset: 0, 0
  index: -1
11g
  rotate: false
  xy: 2, 169
  size: 90, 110
  orig: 90, 110
  offset: 0, 0
  index: -1
12f
  rotate: false
  xy: 368, 95
  size: 89, 91
  orig: 89, 91
  offset: 0, 0
  index: -1
12g
  rotate: false
  xy: 94, 169
  size: 90, 110
  orig: 90, 110
  offset: 0, 0
  index: -1
13f
  rotate: false
  xy: 186, 95
  size: 89, 91
  orig: 89, 91
  offset: 0, 0
  index: -1
13g
  rotate: false
  xy: 2, 281
  size: 90, 110
  orig: 90, 110
  offset: 0, 0
  index: -1
14f
  rotate: false
  xy: 277, 2
  size: 89, 91
  orig: 89, 91
  offset: 0, 0
  index: -1
14g
  rotate: false
  xy: 94, 281
  size: 90, 110
  orig: 90, 110
  offset: 0, 0
  index: -1
15f
  rotate: false
  xy: 186, 188
  size: 89, 91
  orig: 89, 91
  offset: 0, 0
  index: -1
15g
  rotate: false
  xy: 2, 393
  size: 90, 110
  orig: 90, 110
  offset: 0, 0
  index: -1
16f
  rotate: false
  xy: 277, 95
  size: 89, 91
  orig: 89, 91
  offset: 0, 0
  index: -1
1g
  rotate: false
  xy: 370, 393
  size: 90, 110
  orig: 90, 110
  offset: 0, 0
  index: -1
16g
  rotate: false
  xy: 370, 393
  size: 90, 110
  orig: 90, 110
  offset: 0, 0
  index: -1
8g
  rotate: false
  xy: 370, 393
  size: 90, 110
  orig: 90, 110
  offset: 0, 0
  index: -1
2g
  rotate: false
  xy: 370, 281
  size: 90, 110
  orig: 90, 110
  offset: 0, 0
  index: -1
3g
  rotate: false
  xy: 278, 393
  size: 90, 110
  orig: 90, 110
  offset: 0, 0
  index: -1
4g
  rotate: false
  xy: 278, 281
  size: 90, 110
  orig: 90, 110
  offset: 0, 0
  index: -1
5g
  rotate: false
  xy: 186, 393
  size: 90, 110
  orig: 90, 110
  offset: 0, 0
  index: -1
6g
  rotate: false
  xy: 186, 281
  size: 90, 110
  orig: 90, 110
  offset: 0, 0
  index: -1
7f
  rotate: false
  xy: 368, 188
  size: 89, 91
  orig: 89, 91
  offset: 0, 0
  index: -1
1f
  rotate: false
  xy: 368, 188
  size: 89, 91
  orig: 89, 91
  offset: 0, 0
  index: -1
2f
  rotate: false
  xy: 368, 188
  size: 89, 91
  orig: 89, 91
  offset: 0, 0
  index: -1
3f
  rotate: false
  xy: 368, 188
  size: 89, 91
  orig: 89, 91
  offset: 0, 0
  index: -1
4f
  rotate: false
  xy: 368, 188
  size: 89, 91
  orig: 89, 91
  offset: 0, 0
  index: -1
5f
  rotate: false
  xy: 368, 188
  size: 89, 91
  orig: 89, 91
  offset: 0, 0
  index: -1
6f
  rotate: false
  xy: 368, 188
  size: 89, 91
  orig: 89, 91
  offset: 0, 0
  index: -1
8f
  rotate: false
  xy: 368, 188
  size: 89, 91
  orig: 89, 91
  offset: 0, 0
  index: -1
7g
  rotate: false
  xy: 94, 393
  size: 90, 110
  orig: 90, 110
  offset: 0, 0
  index: -1
9f
  rotate: false
  xy: 277, 188
  size: 89, 91
  orig: 89, 91
  offset: 0, 0
  index: -1
9g
  rotate: false
  xy: 2, 57
  size: 90, 110
  orig: 90, 110
  offset: 0, 0
  index: -1

In our assets folder of our project, create a folder with the name " inGame " and inside copy and paste those 2 files, the .png and the .atlas.
Download the background below, name it " backGround.png " and place it inside the " inGame " folder.

Game background
In game background

Note;; Once again i need to remind you have to download all the files i am giving you in their original size. To do that, you must first click on the image and then download it.

Animation is simple to understand. Multiple frames are shown in a sequence. For example, our animation for the enemies consists of 16 frames and will saw them twice a second, thus i will use a 32 fps animation.

We have to make some modifications to our existing classes. First of all, we modify our
" EnemyType " enum class.

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), new  String[] {"1g","2g","3g","4g","5g","6g","7g","8g","9g","10g","11g","12g","13g","14g","15g","16g"}),
    GROUND2(  1F, 1F, -2.0F, 2.5F, 0.5F, new Vector2(7.5F,0F), new  String[] {"1g","2g","3g","4g","5g","6g","7g","8g","9g","10g","11g","12g","13g","14g","15g","16g"}),
    FLYING1(  1F, 1F, 18F, 3.6F, 0.5F, new Vector2(-7.5F,0F), new  String[] {"1f","2f","3f","4f","5f","6f","7f","8f","9f","10f","11f","12f","13f","14f","15f","16f"}),
    FLYING2(  1F, 1F, -2.0F, 3.6F, 0.5F, new Vector2(7.5F,0F), new  String[] {"1f","2f","3f","4f","5f","6f","7f","8f","9f","10f","11f","12f","13f","14f","15f","16f"});
    private float width;
    private float height;
    private float x;
    private float y;
    private float density;
    private Vector2 velocity;
    private String[] regions;

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

    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;
    }

    public String[] getRegions() {
        return regions;
    }

}

We added a String for every enemy (and the getter for that String), which contains the names of all the frames for the animation of the specific enemy. You might have noticed that the 2 ground enemies have the same frame sequence and the same goes for 2 flying enemies.

Basically, we have the animation for the enemies that move right, but we only need to mirror this animation and then we' ll also have the animation for the enemies that move left. LibGDX gives us this feature and it's great, because we save up space. 

The second step is to modify the " EnemyUserData " class to also be able to save the above String, which we generally call texture region,

package com.getest.game.box2d;

import com.getest.game.enums.UserDataType;

public class EnemyUserData extends UserData {
    private float x,y;
    private String[] textureRegions;

    public EnemyUserData(float width, float height, float x, float y, String[] textureRegions) {
        super(width, height);
        userDataType = UserDataType.ENEMY;
        this.x = x;
        this.y = y;
        this.textureRegions = textureRegions;
    }

    public float getX() {return x;}

    public float getY() {return y;}

    public String[] getTextureRegions() {return  textureRegions;}
}

and then, we modify the " WorldMisc " class and particularly the function that creates randomly the enemies, in order for this String to actually be passed to each enemies user data.

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.EnemyUserData;
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 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(), enemyType.getRegions());
        body.setLinearVelocity(enemyType.getVelocity());
        body.setUserData(userData);
        shape.dispose();
        return body;

    }
}

After you 've completed the above steps, go to the " GameActors " class and modify it to look like this;

package com.getest.game.actors;

import com.badlogic.gdx.math.Rectangle;
import com.badlogic.gdx.physics.box2d.Body;
import com.badlogic.gdx.scenes.scene2d.Actor;
import com.getest.game.box2d.UserData;

public abstract class GameActors extends Actor {

    protected Body body;
    protected UserData userData;
    protected Rectangle screenRectangle;

    public GameActors(Body body) {
        this.body = body;
        this.userData = (UserData)body.getUserData();
        screenRectangle = new Rectangle();
    }

    public abstract UserData getUserData();

    @Override
    public void act(float delta) {
        super.act(delta);
        if (body.getUserData() != null) {
            updateRectangle();
        } else {
            remove();
        }
    }

    private void updateRectangle() {
        screenRectangle.x = (body.getPosition().x - userData.getWidth() / 2);
        screenRectangle.y = (body.getPosition().y - userData.getHeight() / 2);
        screenRectangle.width = (userData.getWidth());
        screenRectangle.height = (userData.getHeight());
    }
}

We added 2 things;

  1. We created a function that defines a rectangle around a body's position. For example, if the body belongs to an enemy, then in each step we will have a rectangle around its position. We are going to attach the animation on this rectangle and the result will be a body with an animation on it. It's basically an illusion, because what happens ( the collisions for example ) concerns the bodies ( those boxes that we have created) but the user will only see the animation!
  2. We have overridden the act method, so that this rectangle will be updated in each step of the simulation. If it doesn't exist (probably it would have gone out of our game bounds) then we remove it.

It's time to create the animations, so go to the " EnemyActor " which now, should like this;

package com.getest.game.actors;

import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.graphics.g2d.Animation;
import com.badlogic.gdx.graphics.g2d.Batch;
import com.badlogic.gdx.graphics.g2d.TextureAtlas;
import com.badlogic.gdx.graphics.g2d.TextureRegion;
import com.badlogic.gdx.physics.box2d.Body;
import com.getest.game.box2d.EnemyUserData;

public class EnemyActor extends GameActors {

    private Animation<TextureRegion> animation;
    private TextureRegion[] runningFrames;
    private TextureAtlas textureAtlas;
    private float stateTimeGroundRight,stateTimeGroundLeft,stateTimeAirLeft,stateTimeAirRight;

    public EnemyActor(Body body) {
        super(body);
        textureAtlas = new TextureAtlas("inGame/enemies.atlas");
        runningFrames = new TextureRegion[getUserData().getTextureRegions().length];

        for (int i = 0; i < getUserData().getTextureRegions().length; i++) {
            String path = getUserData().getTextureRegions()[i];
            runningFrames[i] = textureAtlas.findRegion(path); 
       }
        animation = new Animation<TextureRegion>(0.03125f, runningFrames);
        stateTimeGroundRight = 0f;
        stateTimeGroundLeft = 0f;
        stateTimeAirLeft = 0f;
        stateTimeAirRight = 0f;
    }

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

    public void act(float delta) {
        super.act(delta);
    }

    public Body getBody(){return body;}

    @Override
    public void draw(Batch batch, float parentAlpha) {

        super.draw(batch, parentAlpha);
        if (getUserData().getX() < 0 && getUserData().getY() == 2.5F) {

            batch.draw(animation.getKeyFrame(stateTimeGroundRight, true), screenRectangle.x - screenRectangle.width / 2 +0.5F , screenRectangle.y - screenRectangle.height / 2 +0.4F, (float)1.2*screenRectangle.getWidth(), (float)1.2*screenRectangle.getHeight());
            stateTimeGroundRight += Gdx.graphics.getDeltaTime();
        }
        else if(getUserData().getX() >0 && getUserData().getY() == 2.5F){
            batch.draw(animation.getKeyFrame(stateTimeGroundLeft, true), screenRectangle.x - screenRectangle.width / 2 +1.4F , screenRectangle.y - screenRectangle.height / 2 +0.4f, -(float)1.2*screenRectangle.getWidth(), (float)1.2*screenRectangle.getHeight());
            stateTimeGroundLeft += Gdx.graphics.getDeltaTime();
        }
        else if(getUserData().getX() <0 && getUserData().getY() == 3.6F){
            batch.draw(animation.getKeyFrame(stateTimeAirRight, false), screenRectangle.x - screenRectangle.width / 2 +0.5F, screenRectangle.y - screenRectangle.height / 2 +0.4F, (float)1.2*screenRectangle.getWidth(), (float)1.2*screenRectangle.getHeight());
            stateTimeAirRight += Gdx.graphics.getDeltaTime();
        }
        else{
            batch.draw(animation.getKeyFrame(stateTimeAirLeft, false), screenRectangle.x - screenRectangle.width / 2 +1.4F , screenRectangle.y - screenRectangle.height / 2 +0.4F, -(float)1.2*screenRectangle.getWidth(), (float)1.2*screenRectangle.getHeight());
            stateTimeAirLeft += Gdx.graphics.getDeltaTime();
        }
    }
}

We simply created the animation for our enemy. Each frame will be seen for 0.00325 seconds, which actually comes from the division 1/32. That means we are going to see 32 frames per second (fps).

Next, we override the draw method to actually draw the animation. Because our enemy is randomly selected, we need a way to know which of the 4 it is. To solve that problem we use the enemy's position. The animation of the ground enemy loops, because we want to recreate a running animation but the animation of the flying enemy is only played once (that's the way i created them).

For the enemies that come from the left side of the screen, we use a negative width, which actually is the mirror of the original animation. We also made the animation bigger by 20% from the original, for the collision to look better in the eye and calibrated the position a little bit to perfectly match our needs.

The final touch is to add a background and now comes the tricky part. We all agree that we can't use a fit viewport because almost everyone hates black bars, so the best solution here is to use the extend viewport, which keeps the aspect ratio and allows the player to see a little bit more of the game world. Go to our " GameStage " class and modify it to look like this;

package com.getest.game.stages;

import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.graphics.Texture;
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.Image;
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.BodyMisc;
import com.getest.game.misc.WorldMisc;

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 Texture backGroundTexture;
    private Image backGroundImage;
    private Timer timerEnemy = new Timer();

    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();
        Gdx.input.setInputProcessor(this);
        world.setContactListener(this);
        setupBackGround();
        setupGround();
        setupLeftWall();
        setupRightWall();
        setupHero();
        setupButtons();
        createEnemy();
    }

    private void setupBackGround(){
        backGroundTexture = new Texture("inGame/backGround.png");
        backGroundTexture.setFilter(Texture.TextureFilter.Linear, Texture.TextureFilter.Linear);
        backGroundImage = new Image(backGroundTexture);
        backGroundImage.setSize(19.5F, 12F);
        backGroundImage.setPosition(-1.75F,0);
        addActor(backGroundImage);
    }
    
    //Rest of the code

}

The only extra part is that we added the background.

" But, what is this weird size and placement of the background? "

Fair question! This actually is the way we are going to follow to implement resolution and aspect ratio independence. In the next tutorial i will explain furthermore the logic behind. You already know some techniques that you can follow to achieve resolution and aspect ratio independence but in the next tutorial you will once and for all understand how to implement them using libGDX.

I almost forgot, run the game and play around! The hero is missing but you probably can see his body! Don't worry, we will soon add him too.



Feel free to ask any questions in the comments below!

Comments