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.
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.
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.
" 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.
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;
- 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!
- 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;
The only extra part is that we added the background.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
}
" 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
Post a Comment