LibGDX Zombie Bird Tutorial (Flappy Bird Clone/Remake)
Day 6 - Adding Graphics - Welcome to the Necropolis

Thank you for joining me in Day 6. You've been working hard setting up the core framework, but after this section, it will all have been worth it.
It's time to bring Flaps into his natural habitat. In this tutorial, we will create our AssetLoader object, load an animation and a bunch of textures, and use our Renderer to draw our bird and his grim city.
The AssetLoader class
We begin by creating the AssetLoader class in the com.kilobolt.zbhelpers package. (You should have lingering errors from GameRenderer)
We will be creating the following types of objects. (These classes are all included in libGDX):
Texture - you can think of this as an image file. We will combine many images into one Texture file to work with.
TextureRegion - this is a rectangular portion of the Texture. See the image below. This image has multiple texture regions, including the background, the grass, Flaps and the skull.
Animation - we can take multiple texture regions and create an Animation object to specify how to animate Flaps.
Do not download the image below! It has been scaled by a factor of 4, so it will not work with the code. Instead, download the file that I provide a little further down in this lesson.
Texture - you can think of this as an image file. We will combine many images into one Texture file to work with.
TextureRegion - this is a rectangular portion of the Texture. See the image below. This image has multiple texture regions, including the background, the grass, Flaps and the skull.
Animation - we can take multiple texture regions and create an Animation object to specify how to animate Flaps.
Do not download the image below! It has been scaled by a factor of 4, so it will not work with the code. Instead, download the file that I provide a little further down in this lesson.
The Full AssetLoader class:
package com.kilobolt.zbhelpers; import com.badlogic.gdx.Gdx; import com.badlogic.gdx.graphics.Texture; import com.badlogic.gdx.graphics.Texture.TextureFilter; import com.badlogic.gdx.graphics.g2d.Animation; import com.badlogic.gdx.graphics.g2d.TextureRegion; public class AssetLoader { public static Texture texture; public static TextureRegion bg, grass; public static Animation birdAnimation; public static TextureRegion bird, birdDown, birdUp; public static TextureRegion skullUp, skullDown, bar; public static void load() { texture = new Texture(Gdx.files.internal("data/texture.png")); texture.setFilter(TextureFilter.Nearest, TextureFilter.Nearest); bg = new TextureRegion(texture, 0, 0, 136, 43); bg.flip(false, true); grass = new TextureRegion(texture, 0, 43, 143, 11); grass.flip(false, true); birdDown = new TextureRegion(texture, 136, 0, 17, 12); birdDown.flip(false, true); bird = new TextureRegion(texture, 153, 0, 17, 12); bird.flip(false, true); birdUp = new TextureRegion(texture, 170, 0, 17, 12); birdUp.flip(false, true); TextureRegion[] birds = { birdDown, bird, birdUp }; birdAnimation = new Animation(0.06f, birds); birdAnimation.setPlayMode(Animation.PlayMode.LOOP_PINGPONG); skullUp = new TextureRegion(texture, 192, 0, 24, 14); // Create by flipping existing skullUp skullDown = new TextureRegion(skullUp); skullDown.flip(false, true); bar = new TextureRegion(texture, 136, 16, 22, 3); bar.flip(false, true); } public static void dispose() { // We must dispose of the texture when we are finished. texture.dispose(); } }
Let's walk through the code a little bit. It is full of static methods and variables, meaning that we will not be creating instances of the Asset class - there will be only one copy.
It has two methods: the load method and the dispose method.
The load method will be called when the game starts up, and the dispose method will be called when the game is being closed.
Examining load()
Texture
The load method begins by creating a new Texture object with the file texture.png, which I will provide below. It sets its minification and magnification filters (used when resizing to a smaller or larger image) to the enum constant: TextureFilter.Nearest. This is important because when our small pixel art is stretched to a larger size, each pixel will retain its shape rather than becoming blurry!
TextureRegion
We can use our texture to create TextureRegion objects, which require five arguments: the pertinent Texture object and the rectangular boundaries of the desired region on that Texture. This is given as x, y, width and height starting from the top left of the image, so our background image would be 0, 0, 136, 43.
TextureRegions must be flipped, because libGDX assumes a Y Up coordinate system by default. We are using a Y Down coordinate system, and must flip each image (except for skullUp, which can remain upside down)!
Animation
We can create an array of TextureRegion objects and pass it in to the constructor of a new Animation object:
TextureRegion[] birds = { birdDown, bird, birdUp }; // creates an array of TextureRegions
birdAnimation = new Animation(0.06f, birds); // Creates a new Animation in which each frame is 0.06 seconds long, using the above array.
birdAnimation.setPlayMode(Animation.PlayMode.LOOP_PINGPONG); // Sets play mode to be ping pong, in which we will see a bounce.
We gave the Animation 3 frames. It will now change frames every 0.06 seconds (down, middle, up, middle, down ...).
It has two methods: the load method and the dispose method.
The load method will be called when the game starts up, and the dispose method will be called when the game is being closed.
Examining load()
Texture
The load method begins by creating a new Texture object with the file texture.png, which I will provide below. It sets its minification and magnification filters (used when resizing to a smaller or larger image) to the enum constant: TextureFilter.Nearest. This is important because when our small pixel art is stretched to a larger size, each pixel will retain its shape rather than becoming blurry!
TextureRegion
We can use our texture to create TextureRegion objects, which require five arguments: the pertinent Texture object and the rectangular boundaries of the desired region on that Texture. This is given as x, y, width and height starting from the top left of the image, so our background image would be 0, 0, 136, 43.
TextureRegions must be flipped, because libGDX assumes a Y Up coordinate system by default. We are using a Y Down coordinate system, and must flip each image (except for skullUp, which can remain upside down)!
Animation
We can create an array of TextureRegion objects and pass it in to the constructor of a new Animation object:
TextureRegion[] birds = { birdDown, bird, birdUp }; // creates an array of TextureRegions
birdAnimation = new Animation(0.06f, birds); // Creates a new Animation in which each frame is 0.06 seconds long, using the above array.
birdAnimation.setPlayMode(Animation.PlayMode.LOOP_PINGPONG); // Sets play mode to be ping pong, in which we will see a bounce.
We gave the Animation 3 frames. It will now change frames every 0.06 seconds (down, middle, up, middle, down ...).
Download the Texture file
Download the provided texture file below, and place it inside the ZombieBird-android project's assets/data/ folder! This is VERY important.
A note on using images: whenever you update your textures (which you will if you use your own images), you have to clean your projects on Eclipse for it to update immediately.
Do that now, after downloading the file! (You can do this by clicking on Project > Clean > Clean all projects.
A note on using images: whenever you update your textures (which you will if you use your own images), you have to clean your projects on Eclipse for it to update immediately.
Do that now, after downloading the file! (You can do this by clicking on Project > Clean > Clean all projects.

texture.png | |
File Size: | 6 kb |
File Type: | png |

Make sure that you have placed your image inside the correct folder as shown to the left (note we are inside the ZombieBird-android project. You can delete the libgdx.png file that is included with the project.
If you are set, make sure you have cleaned your project and move on.
If you are set, make sure you have cleaned your project and move on.
Calling the Load Method
Now that our AssetLoader is ready (and you have downloaded the texture into the CORRECT FOLDER and cleaned your project), we open up the ZBGame class, so that we can load all the images before the GameScreen is created. We add the line in our create method (before the GameScreen is created):
AssetLoader.load(); (Import com.kilobolt.zbhelpers.AssetLoader)
We must also call AssetLoader.dispose() when the dispose method of our ZBGame is called by our behind-the-scenes platform-dependent code. To do this, we override the existing dispose method from ZBGame's superclass, Game.
That sounds complicated, but all we need to do is this (the full code follows):
AssetLoader.load(); (Import com.kilobolt.zbhelpers.AssetLoader)
We must also call AssetLoader.dispose() when the dispose method of our ZBGame is called by our behind-the-scenes platform-dependent code. To do this, we override the existing dispose method from ZBGame's superclass, Game.
That sounds complicated, but all we need to do is this (the full code follows):
package com.kilobolt.zombiebird; import com.badlogic.gdx.Game; import com.badlogic.gdx.Gdx; import com.kilobolt.screens.GameScreen; import com.kilobolt.zbhelpers.AssetLoader; public class ZBGame extends Game { @Override public void create() { Gdx.app.log("ZBGame", "created"); AssetLoader.load(); setScreen(new GameScreen()); } @Override public void dispose() { super.dispose(); AssetLoader.dispose(); } }
Now that all of our images are loaded, we can start rendering them inside the GameRenderer!
Let's open that up.
To draw a TextureRegion, we need to create a SpriteBatch (just like we had to create a ShapeRenderer). The SpriteBatch draws images for us using the indices provided. (x, y, width and height, typically) Let's remove all the non-essential code from GameRenderer and create this SpriteBatch, as shown below.
Let's open that up.
To draw a TextureRegion, we need to create a SpriteBatch (just like we had to create a ShapeRenderer). The SpriteBatch draws images for us using the indices provided. (x, y, width and height, typically) Let's remove all the non-essential code from GameRenderer and create this SpriteBatch, as shown below.
package com.kilobolt.gameworld; import com.badlogic.gdx.Gdx; import com.badlogic.gdx.graphics.GL20; import com.badlogic.gdx.graphics.OrthographicCamera; import com.badlogic.gdx.graphics.g2d.SpriteBatch; import com.badlogic.gdx.graphics.glutils.ShapeRenderer; public class GameRenderer { private GameWorld myWorld; private OrthographicCamera cam; private ShapeRenderer shapeRenderer; private SpriteBatch batcher; public GameRenderer(GameWorld world) { myWorld = world; cam = new OrthographicCamera(); cam.setToOrtho(true, 137, 204); batcher = new SpriteBatch(); // Attach batcher to camera batcher.setProjectionMatrix(cam.combined); shapeRenderer = new ShapeRenderer(); shapeRenderer.setProjectionMatrix(cam.combined); } public void render() { Gdx.gl.glClearColor(0, 0, 0, 1); Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT); } }
We must change our camera width to 136 and we must change the height to the game height determined by the GameScreen. To do this, we will change our constructor to ask for the gameHeight and the midPointY.
Add these TWO NEW instance variables (keep the other four also) and change the constructor as follows (make sure you change width and height properly to 136 and gameHeight), and store the gameHeight and midPointY variables for future usage:
Add these TWO NEW instance variables (keep the other four also) and change the constructor as follows (make sure you change width and height properly to 136 and gameHeight), and store the gameHeight and midPointY variables for future usage:
package com.kilobolt.gameworld; import com.badlogic.gdx.Gdx; import com.badlogic.gdx.graphics.GL20; import com.badlogic.gdx.graphics.OrthographicCamera; import com.badlogic.gdx.graphics.g2d.SpriteBatch; import com.badlogic.gdx.graphics.glutils.ShapeRenderer; public class GameRenderer { private GameWorld myWorld; private OrthographicCamera cam; private ShapeRenderer shapeRenderer; private SpriteBatch batcher; private int midPointY; private int gameHeight; public GameRenderer(GameWorld world, int gameHeight, int midPointY) { myWorld = world; // The word "this" refers to this instance. // We are setting the instance variables' values to be that of the // parameters passed in from GameScreen. this.gameHeight = gameHeight; this.midPointY = midPointY; cam = new OrthographicCamera(); cam.setToOrtho(true, 136, gameHeight); batcher = new SpriteBatch(); batcher.setProjectionMatrix(cam.combined); shapeRenderer = new ShapeRenderer(); shapeRenderer.setProjectionMatrix(cam.combined); } public void render() { Gdx.gl.glClearColor(0, 0, 0, 1); Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT); } }
Next, We must also add a parameter to the render method:
public void render(float runTime) {
...
}
This value is needed to determine which frame the bird animation should display. The Animation object will use this value (and the previously determined frame duration) to determine which TextureRegion to display.
Now that we have updated the constructor, so we must fix the errors that arise in GameScreen.
Open the GameScreen class and replace the following line:
renderer = new GameRenderer(world);
with this one:
renderer = new GameRenderer(world, (int) gameHeight, midPointY);
We also need to create one extra variable called runTime, which will keep track of how long the game has been running for. We will pass this into the render method of the GameRenderer!
- Create an instance variable called runTime and initialize it at 0.
private float runTime = 0;
Inside the render(float delta) method, increment runTime by delta, and pass it into the render method (where we will use this value to render the animation properly):
@Override
public void render(float delta) {
runTime += delta;
world.update(delta);
renderer.render(runTime);
}
Your GameScreen should look like this:
public void render(float runTime) {
...
}
This value is needed to determine which frame the bird animation should display. The Animation object will use this value (and the previously determined frame duration) to determine which TextureRegion to display.
Now that we have updated the constructor, so we must fix the errors that arise in GameScreen.
Open the GameScreen class and replace the following line:
renderer = new GameRenderer(world);
with this one:
renderer = new GameRenderer(world, (int) gameHeight, midPointY);
We also need to create one extra variable called runTime, which will keep track of how long the game has been running for. We will pass this into the render method of the GameRenderer!
- Create an instance variable called runTime and initialize it at 0.
private float runTime = 0;
Inside the render(float delta) method, increment runTime by delta, and pass it into the render method (where we will use this value to render the animation properly):
@Override
public void render(float delta) {
runTime += delta;
world.update(delta);
renderer.render(runTime);
}
Your GameScreen should look like this:
package com.kilobolt.screens; import com.badlogic.gdx.Gdx; import com.badlogic.gdx.Screen; import com.kilobolt.gameworld.GameRenderer; import com.kilobolt.gameworld.GameWorld; import com.kilobolt.zbhelpers.InputHandler; public class GameScreen implements Screen { private GameWorld world; private GameRenderer renderer; private float runTime; // This is the constructor, not the class declaration public GameScreen() { float screenWidth = Gdx.graphics.getWidth(); float screenHeight = Gdx.graphics.getHeight(); float gameWidth = 136; float gameHeight = screenHeight / (screenWidth / gameWidth); int midPointY = (int) (gameHeight / 2); world = new GameWorld(midPointY); renderer = new GameRenderer(world, (int) gameHeight, midPointY); Gdx.input.setInputProcessor(new InputHandler(world.getBird())); } @Override public void render(float delta) { runTime += delta; world.update(delta); renderer.render(runTime); } @Override public void resize(int width, int height) { } @Override public void show() { Gdx.app.log("GameScreen", "show called"); } @Override public void hide() { Gdx.app.log("GameScreen", "hide called"); } @Override public void pause() { Gdx.app.log("GameScreen", "pause called"); } @Override public void resume() { Gdx.app.log("GameScreen", "resume called"); } @Override public void dispose() { // Leave blank } }
I'm sorry about all the jumping back and forth! We will focus on one method only for the remainder of Day 6. :)
Go back to the GameRenderer, and change your render method to the one below:
Go back to the GameRenderer, and change your render method to the one below:
package com.kilobolt.gameworld; import com.badlogic.gdx.Gdx; import com.badlogic.gdx.graphics.GL20; import com.badlogic.gdx.graphics.OrthographicCamera; import com.badlogic.gdx.graphics.g2d.SpriteBatch; import com.badlogic.gdx.graphics.glutils.ShapeRenderer; import com.badlogic.gdx.graphics.glutils.ShapeRenderer.ShapeType; import com.kilobolt.gameobjects.Bird; import com.kilobolt.zbhelpers.AssetLoader; public class GameRenderer { private GameWorld myWorld; private OrthographicCamera cam; private ShapeRenderer shapeRenderer; private SpriteBatch batcher; private int midPointY; private int gameHeight; public GameRenderer(GameWorld world, int gameHeight, int midPointY) { myWorld = world; // The word "this" refers to this instance. // We are setting the instance variables' values to be that of the // parameters passed in from GameScreen. this.gameHeight = gameHeight; this.midPointY = midPointY; cam = new OrthographicCamera(); cam.setToOrtho(true, 136, gameHeight); batcher = new SpriteBatch(); batcher.setProjectionMatrix(cam.combined); shapeRenderer = new ShapeRenderer(); shapeRenderer.setProjectionMatrix(cam.combined); } public void render(float runTime) { // We will move these outside of the loop for performance later. Bird bird = myWorld.getBird(); // Fill the entire screen with black, to prevent potential flickering. Gdx.gl.glClearColor(0, 0, 0, 1); Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT); // Begin ShapeRenderer shapeRenderer.begin(ShapeType.Filled); // Draw Background color shapeRenderer.setColor(55 / 255.0f, 80 / 255.0f, 100 / 255.0f, 1); shapeRenderer.rect(0, 0, 136, midPointY + 66); // Draw Grass shapeRenderer.setColor(111 / 255.0f, 186 / 255.0f, 45 / 255.0f, 1); shapeRenderer.rect(0, midPointY + 66, 136, 11); // Draw Dirt shapeRenderer.setColor(147 / 255.0f, 80 / 255.0f, 27 / 255.0f, 1); shapeRenderer.rect(0, midPointY + 77, 136, 52); // End ShapeRenderer shapeRenderer.end(); // Begin SpriteBatch batcher.begin(); // Disable transparency // This is good for performance when drawing images that do not require // transparency. batcher.disableBlending(); batcher.draw(AssetLoader.bg, 0, midPointY + 23, 136, 43); // The bird needs transparency, so we enable that again. batcher.enableBlending(); // Draw bird at its coordinates. Retrieve the Animation object from // AssetLoader // Pass in the runTime variable to get the current frame. batcher.draw(AssetLoader.birdAnimation.getKeyFrame(runTime), bird.getX(), bird.getY(), bird.getWidth(), bird.getHeight()); // End SpriteBatch batcher.end(); } }
Try running the code (DesktopLauncher.java) and START CLICKING (otherwise your bird will fall down to oblivion)! It should look like this:
Let's now go back to the render method and see what the logic is there. We always draw the background first, because drawing is done in layers. We begin by drawing some flat colors. We are choosing to use a color filled Shape rather than use a TextureRegion to fill the background.
We draw a temporary green rectangle to show where the grass will be, and a brown one for the dirt.
We then begin the SpriteBatch, again, starting by drawing the background image of the city. On top of that, we retrieve the current TextureRegion of our Animation using our runTime, and draw the bird with blending enabled. Read the comments that I have made inside the render() method!
They are important.
Well, that's a start! Our character Flaps flies again, and the game has started to take shape. Join me in Day 7, where we will start adding the scrollable elements of our game: the Grass and the Pipes.
We draw a temporary green rectangle to show where the grass will be, and a brown one for the dirt.
We then begin the SpriteBatch, again, starting by drawing the background image of the city. On top of that, we retrieve the current TextureRegion of our Animation using our runTime, and draw the bird with blending enabled. Read the comments that I have made inside the render() method!
They are important.
Well, that's a start! Our character Flaps flies again, and the game has started to take shape. Join me in Day 7, where we will start adding the scrollable elements of our game: the Grass and the Pipes.
Source Code for the day.
If you didn't feel like writing that code yourself, download the completed code here:
Download, extract and import into eclipse:
Download, extract and import into eclipse:

day_6.zip | |
File Size: | 10331 kb |
File Type: | zip |
Like us on Facebook to be informed as soon as the next lesson is available.
|
|
comments powered by Disqus