LibGDX Zombie Bird Tutorial (Flappy Bird Clone/Remake)
Day 9 - Finishing Gameplay and Basic UI
Welcome to Day 9. In this tutorial, we are going to finish up the gameplay by adding collision to the ground and handling death. Then, we are going to implement the score keeping and setup a BitmapFont to display the score!
We are picking up exactly where we left off in Day 8. If you want to follow along, feel free to download the source code on that page.
We are picking up exactly where we left off in Day 8. If you want to follow along, feel free to download the source code on that page.
A Couple More Sound Effects
We need to add a sound effect for the flapping of wings and another sound effect for scoring a point.
- Create two new Sound instance variables called flap and coin, and initialize them inside the load method using:
flap = Gdx.audio.newSound(Gdx.files.internal("data/flap.wav"));
coin = Gdx.audio.newSound(Gdx.files.internal("data/coin.wav"));
When we are done with our Sound objects, we must dispose of them. So down in the dispose method, add the following code:
dead.dispose();
flap.dispose();
coin.dispose();
Download the following files into the assets/data folder in your ZombieGame-android project:
- Create two new Sound instance variables called flap and coin, and initialize them inside the load method using:
flap = Gdx.audio.newSound(Gdx.files.internal("data/flap.wav"));
coin = Gdx.audio.newSound(Gdx.files.internal("data/coin.wav"));
When we are done with our Sound objects, we must dispose of them. So down in the dispose method, add the following code:
dead.dispose();
flap.dispose();
coin.dispose();
Download the following files into the assets/data folder in your ZombieGame-android project:
![]()
|
![]()
|
Adding Text!
Just to be proactive, we are going to add a .font file generated using Hiero. Hiero converts a text file into a .png Texture image similar to the one for our game. It also generates a .fnt configuration file which libGDX can read and figure out where each letter (or TextureRegion) exists.
I have generated these files for you to download below. I will show you how to implement them into our game.
I have generated these files for you to download below. I will show you how to implement them into our game.
Download the four files below:
![]()
![]()
|
![]()
![]()
|
I encourage you to open up each of these files and see what they look like! Place all four files inside the assets/data folder in your ZombieGame-android project.
We can use this pair of .fnt and .png files to create a BitmapFont object, which allows us to draw Strings to the SpriteBatch in our GameRenderer without creating a new String object each time. The BitmapFont will use the .fnt file to determine where each letter and number is, so we don't have to do any work!
To create these fonts in your AssetLoader:
1. Create the following instance variables:
public static BitmapFont font, shadow;
2. Add the following inside the load method. These will load the files and change them to an appropriate size for us to use:
font = new BitmapFont(Gdx.files.internal("data/text.fnt"));
font.setScale(.25f, -.25f);
shadow = new BitmapFont(Gdx.files.internal("data/shadow.fnt"));
shadow.setScale(.25f, -.25f);
3. Add these lines to the dispose method:
font.dispose();
shadow.dispose();
We can use this pair of .fnt and .png files to create a BitmapFont object, which allows us to draw Strings to the SpriteBatch in our GameRenderer without creating a new String object each time. The BitmapFont will use the .fnt file to determine where each letter and number is, so we don't have to do any work!
To create these fonts in your AssetLoader:
1. Create the following instance variables:
public static BitmapFont font, shadow;
2. Add the following inside the load method. These will load the files and change them to an appropriate size for us to use:
font = new BitmapFont(Gdx.files.internal("data/text.fnt"));
font.setScale(.25f, -.25f);
shadow = new BitmapFont(Gdx.files.internal("data/shadow.fnt"));
shadow.setScale(.25f, -.25f);
3. Add these lines to the dispose method:
font.dispose();
shadow.dispose();
This is what your AssetLoader should look like now:
package com.kilobolt.ZBHelpers; import com.badlogic.gdx.Gdx; import com.badlogic.gdx.audio.Sound; 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.BitmapFont; 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 Sound dead, flap, coin; public static BitmapFont font, shadow; 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.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); dead = Gdx.audio.newSound(Gdx.files.internal("data/dead.wav")); flap = Gdx.audio.newSound(Gdx.files.internal("data/flap.wav")); coin = Gdx.audio.newSound(Gdx.files.internal("data/coin.wav")); font = new BitmapFont(Gdx.files.internal("data/text.fnt")); font.setScale(.25f, -.25f); shadow = new BitmapFont(Gdx.files.internal("data/shadow.fnt")); shadow.setScale(.25f, -.25f); } public static void dispose() { // We must dispose of the texture when we are finished. texture.dispose(); // Dispose sounds dead.dispose(); flap.dispose(); coin.dispose(); font.dispose(); shadow.dispose(); } }
The Ground Hurts
We next move on to the GameWorld class.
1. Begin by deleting the instance variable isAlive. We will be revamping how we handle death.
2. We want our character to die when he hits the ground. We will be implementing this now.
Rather than creating a rectangle object representing the collision box for our grass objects and updating them, we are just going to treat the ground as a static box for simplicity.
- Start by creating a new instance variable:
(import com.badlogic.gdx.math)
private Rectangle ground;
-Initialize it in the constructor as:
ground = new Rectangle(0, midPointY + 66, 136, 11);
- Next, we are going to change our update method to the following:
1. Begin by deleting the instance variable isAlive. We will be revamping how we handle death.
2. We want our character to die when he hits the ground. We will be implementing this now.
Rather than creating a rectangle object representing the collision box for our grass objects and updating them, we are just going to treat the ground as a static box for simplicity.
- Start by creating a new instance variable:
(import com.badlogic.gdx.math)
private Rectangle ground;
-Initialize it in the constructor as:
ground = new Rectangle(0, midPointY + 66, 136, 11);
- Next, we are going to change our update method to the following:
public void update(float delta) { // Add a delta cap so that if our game takes too long // to update, we will not break our collision detection. if (delta > .15f) { delta = .15f; } bird.update(delta); scroller.update(delta); if (scroller.collides(bird) && bird.isAlive()) { scroller.stop(); bird.die(); AssetLoader.dead.play(); } if (Intersector.overlaps(bird.getBoundingCircle(), ground)) { scroller.stop(); bird.die(); bird.decelerate(); } }
Fixing our Bird
Now we must fix our errors in the Bird class.
- We need to create an instance variable called:
private boolean isAlive
- We initialize this in the constructor:
isAlive = true;
- Next, generate a getter for this:
public boolean isAlive() {
return isAlive;
}
- Now that we have defined isAlive, we make a small change to our shouldntFlap method. We don't want our bird to animate when it is dead (granted it is a zombie, but it is still very awkward):
public boolean shouldntFlap() {
return velocity.y > 70 || !isAlive;
}
This method will return true if either of the two conditions above are true (meaning that if the bird has a high y velocity or is dead, we will not be flapping).
- We make another small change in the onClick method, which should only work if our bird is alive:
public void onClick() {
if (isAlive) {
AssetLoader.flap.play();
velocity.y = -140;
}
}
You may need to import: com.kilobolt.ZBHelpers.AssetLoader;
- We are going to add two new methods: die and decelerate.These are pretty straightforward:
public void die() {
isAlive = false;
velocity.y = 0;
}
public void decelerate() {
// We want the bird to stop accelerating downwards once it is dead.
acceleration.y = 0;
}
- Lastly, add a new condition to the final if statement inside the update method:
if (isFalling() || !isAlive) {
...
}
This is so that if our bird dies while facing upwards, it will point its nose to the ground just like the bird in the game Flappy Bird.
The completed Bird class is as follows:
- We need to create an instance variable called:
private boolean isAlive
- We initialize this in the constructor:
isAlive = true;
- Next, generate a getter for this:
public boolean isAlive() {
return isAlive;
}
- Now that we have defined isAlive, we make a small change to our shouldntFlap method. We don't want our bird to animate when it is dead (granted it is a zombie, but it is still very awkward):
public boolean shouldntFlap() {
return velocity.y > 70 || !isAlive;
}
This method will return true if either of the two conditions above are true (meaning that if the bird has a high y velocity or is dead, we will not be flapping).
- We make another small change in the onClick method, which should only work if our bird is alive:
public void onClick() {
if (isAlive) {
AssetLoader.flap.play();
velocity.y = -140;
}
}
You may need to import: com.kilobolt.ZBHelpers.AssetLoader;
- We are going to add two new methods: die and decelerate.These are pretty straightforward:
public void die() {
isAlive = false;
velocity.y = 0;
}
public void decelerate() {
// We want the bird to stop accelerating downwards once it is dead.
acceleration.y = 0;
}
- Lastly, add a new condition to the final if statement inside the update method:
if (isFalling() || !isAlive) {
...
}
This is so that if our bird dies while facing upwards, it will point its nose to the ground just like the bird in the game Flappy Bird.
The completed Bird class is as follows:
package com.kilobolt.GameObjects; import com.badlogic.gdx.math.Circle; import com.badlogic.gdx.math.Vector2; import com.kilobolt.ZBHelpers.AssetLoader; public class Bird { private Vector2 position; private Vector2 velocity; private Vector2 acceleration; private float rotation; private int width; private int height; private boolean isAlive; private Circle boundingCircle; public Bird(float x, float y, int width, int height) { this.width = width; this.height = height; position = new Vector2(x, y); velocity = new Vector2(0, 0); acceleration = new Vector2(0, 460); boundingCircle = new Circle(); isAlive = true; } public void update(float delta) { velocity.add(acceleration.cpy().scl(delta)); if (velocity.y > 200) { velocity.y = 200; } position.add(velocity.cpy().scl(delta)); // Set the circle's center to be (9, 6) with respect to the bird. // Set the circle's radius to be 6.5f; boundingCircle.set(position.x + 9, position.y + 6, 6.5f); // Rotate counterclockwise if (velocity.y < 0) { rotation -= 600 * delta; if (rotation < -20) { rotation = -20; } } // Rotate clockwise if (isFalling() || !isAlive) { rotation += 480 * delta; if (rotation > 90) { rotation = 90; } } } public boolean isFalling() { return velocity.y > 110; } public boolean shouldntFlap() { return velocity.y > 70 || !isAlive; } public void onClick() { if (isAlive) { AssetLoader.flap.play(); velocity.y = -140; } } public void die() { isAlive = false; velocity.y = 0; } public void decelerate() { acceleration.y = 0; } public float getX() { return position.x; } public float getY() { return position.y; } public float getWidth() { return width; } public float getHeight() { return height; } public float getRotation() { return rotation; } public Circle getBoundingCircle() { return boundingCircle; } public boolean isAlive() { return isAlive; } }
Run your code!
The game should now be fully playable with full collision and death. We are now going to implement a scoring system.
Keeping Score - The score integer
In Flappy Bird, you score a point when your Bird passes about half way thorough each vertical pair of Pipes. We are going to emulate this behavior and keep score. We need to create an integer that keeps track of our current score. We will do this in the GameWorld.
Open up the GameWorld class
- Create a new instance variable:
private int score = 0;
- Next, create these these getter and increment methods:
public int getScore() {
return score;
}
public void addScore(int increment) {
score += increment;
}
Your GameWorld should look like this for now:
Open up the GameWorld class
- Create a new instance variable:
private int score = 0;
- Next, create these these getter and increment methods:
public int getScore() {
return score;
}
public void addScore(int increment) {
score += increment;
}
Your GameWorld should look like this for now:
package com.kilobolt.GameWorld; import com.badlogic.gdx.math.Intersector; import com.badlogic.gdx.math.Rectangle; import com.kilobolt.GameObjects.Bird; import com.kilobolt.GameObjects.ScrollHandler; import com.kilobolt.ZBHelpers.AssetLoader; public class GameWorld { private Bird bird; private ScrollHandler scroller; private Rectangle ground; private int score = 0; public GameWorld(int midPointY) { bird = new Bird(33, midPointY - 5, 17, 12); // The grass should start 66 pixels below the midPointY scroller = new ScrollHandler(midPointY + 66); ground = new Rectangle(0, midPointY + 66, 137, 11); } public void update(float delta) { // Add a delta cap so that if our game takes too long // to update, we will not break our collision detection. if (delta > .15f) { delta = .15f; } bird.update(delta); scroller.update(delta); if (scroller.collides(bird) && bird.isAlive()) { scroller.stop(); bird.die(); AssetLoader.dead.play(); } if (Intersector.overlaps(bird.getBoundingCircle(), ground)) { scroller.stop(); bird.die(); bird.decelerate(); } } public Bird getBird() { return bird; } public ScrollHandler getScroller() { return scroller; } public int getScore() { return score; } public void addScore(int increment) { score += increment; } }
Increment the Score
The logic for incrementing our score will be in our ScrollHandler class. Let's open it.
We need a reference to our GameWorld so that we can change its score. So, we are going to ask for a GameWorld object inside our constructor, and store the passed in parameter as an instance variable called gameWorld. Make sure you import GameWorld (com.kilobolt.GameWorld.GameWorld).
This is the new constructor:
We need a reference to our GameWorld so that we can change its score. So, we are going to ask for a GameWorld object inside our constructor, and store the passed in parameter as an instance variable called gameWorld. Make sure you import GameWorld (com.kilobolt.GameWorld.GameWorld).
This is the new constructor:
public ScrollHandler(GameWorld gameWorld, float yPos) { this.gameWorld = gameWorld; frontGrass = new Grass(0, yPos, 143, 11, SCROLL_SPEED); backGrass = new Grass(frontGrass.getTailX(), yPos, 143, 11, SCROLL_SPEED); pipe1 = new Pipe(210, 0, 22, 60, SCROLL_SPEED, yPos); pipe2 = new Pipe(pipe1.getTailX() + PIPE_GAP, 0, 22, 70, SCROLL_SPEED, yPos); pipe3 = new Pipe(pipe2.getTailX() + PIPE_GAP, 0, 22, 60, SCROLL_SPEED, yPos); }
Quickly go back to the GameWorld and update our ScrollHandler constructor call to the following:
scroller = new ScrollHandler(this, midPointY + 66);
The general logic will be this:
- If a pipe's midpoint with respect to x is lesser than bird's beak, we are going to add 1 to our score.
- Since we do not want this to repeat, we need to keep a boolean variable for EACH Pipe called isScored. Only if isScored is false, will we add one to the score (Setting isScored to true in the process. If we just need to change isScored back to false when the Pipe resets, then this will work the next time the Pipe returns).
This will be our new collides method that implements the above logic:
scroller = new ScrollHandler(this, midPointY + 66);
The general logic will be this:
- If a pipe's midpoint with respect to x is lesser than bird's beak, we are going to add 1 to our score.
- Since we do not want this to repeat, we need to keep a boolean variable for EACH Pipe called isScored. Only if isScored is false, will we add one to the score (Setting isScored to true in the process. If we just need to change isScored back to false when the Pipe resets, then this will work the next time the Pipe returns).
This will be our new collides method that implements the above logic:
public boolean collides(Bird bird) { if (!pipe1.isScored() && pipe1.getX() + (pipe1.getWidth() / 2) < bird.getX() + bird.getWidth()) { addScore(1); pipe1.setScored(true); AssetLoader.coin.play(); } else if (!pipe2.isScored() && pipe2.getX() + (pipe2.getWidth() / 2) < bird.getX() + bird.getWidth()) { addScore(1); pipe2.setScored(true); AssetLoader.coin.play(); } else if (!pipe3.isScored() && pipe3.getX() + (pipe3.getWidth() / 2) < bird.getX() + bird.getWidth()) { addScore(1); pipe3.setScored(true); AssetLoader.coin.play(); } return (pipe1.collides(bird) || pipe2.collides(bird) || pipe3 .collides(bird)); }
To solve all the errors that we have just introduced, we must do the following:
1. Import AssetLoader (com.kilobolt.ZBHelpers.AssetLoader).
2. Create the following addScore method:
private void addScore(int increment) {
gameWorld.addScore(increment);
}
3. We must now move on to our Pipe class and implement our boolean: isScored.
This is the completed ScrollHandler:
1. Import AssetLoader (com.kilobolt.ZBHelpers.AssetLoader).
2. Create the following addScore method:
private void addScore(int increment) {
gameWorld.addScore(increment);
}
3. We must now move on to our Pipe class and implement our boolean: isScored.
This is the completed ScrollHandler:
package com.kilobolt.GameObjects; import com.kilobolt.GameWorld.GameWorld; import com.kilobolt.ZBHelpers.AssetLoader; public class ScrollHandler { private Grass frontGrass, backGrass; private Pipe pipe1, pipe2, pipe3; public static final int SCROLL_SPEED = -59; public static final int PIPE_GAP = 49; private GameWorld gameWorld; public ScrollHandler(GameWorld gameWorld, float yPos) { this.gameWorld = gameWorld; frontGrass = new Grass(0, yPos, 143, 11, SCROLL_SPEED); backGrass = new Grass(frontGrass.getTailX(), yPos, 143, 11, SCROLL_SPEED); pipe1 = new Pipe(210, 0, 22, 60, SCROLL_SPEED, yPos); pipe2 = new Pipe(pipe1.getTailX() + PIPE_GAP, 0, 22, 70, SCROLL_SPEED, yPos); pipe3 = new Pipe(pipe2.getTailX() + PIPE_GAP, 0, 22, 60, SCROLL_SPEED, yPos); } public void update(float delta) { // Update our objects frontGrass.update(delta); backGrass.update(delta); pipe1.update(delta); pipe2.update(delta); pipe3.update(delta); // Check if any of the pipes are scrolled left, // and reset accordingly if (pipe1.isScrolledLeft()) { pipe1.reset(pipe3.getTailX() + PIPE_GAP); } else if (pipe2.isScrolledLeft()) { pipe2.reset(pipe1.getTailX() + PIPE_GAP); } else if (pipe3.isScrolledLeft()) { pipe3.reset(pipe2.getTailX() + PIPE_GAP); } // Same with grass if (frontGrass.isScrolledLeft()) { frontGrass.reset(backGrass.getTailX()); } else if (backGrass.isScrolledLeft()) { backGrass.reset(frontGrass.getTailX()); } } public void stop() { frontGrass.stop(); backGrass.stop(); pipe1.stop(); pipe2.stop(); pipe3.stop(); } public boolean collides(Bird bird) { if (!pipe1.isScored() && pipe1.getX() + (pipe1.getWidth() / 2) < bird.getX() + bird.getWidth()) { addScore(1); pipe1.setScored(true); AssetLoader.coin.play(); } else if (!pipe2.isScored() && pipe2.getX() + (pipe2.getWidth() / 2) < bird.getX() + bird.getWidth()) { addScore(1); pipe2.setScored(true); AssetLoader.coin.play(); } else if (!pipe3.isScored() && pipe3.getX() + (pipe3.getWidth() / 2) < bird.getX() + bird.getWidth()) { addScore(1); pipe3.setScored(true); AssetLoader.coin.play(); } return (pipe1.collides(bird) || pipe2.collides(bird) || pipe3 .collides(bird)); } private void addScore(int increment) { gameWorld.addScore(increment); } public Grass getFrontGrass() { return frontGrass; } public Grass getBackGrass() { return backGrass; } public Pipe getPipe1() { return pipe1; } public Pipe getPipe2() { return pipe2; } public Pipe getPipe3() { return pipe3; } }
Pipe Class - isScored boolean
- Create a new instance variable:
private boolean isScored = false;
This boolean is set to true in the ScrollHandler when a point has been awarded to the player for passing the current Pipe object.
- Update the reset method so that isScored is set to false when the Pipe resets:
@Override
public void reset(float newX) {
super.reset(newX);
height = r.nextInt(90) + 15;
isScored = false;
}
Next, we just have to create the getter and setter methods:
public boolean isScored() {
return isScored;
}
public void setScored(boolean b) {
isScored = b;
}
Your Pipe class should now look like this:
private boolean isScored = false;
This boolean is set to true in the ScrollHandler when a point has been awarded to the player for passing the current Pipe object.
- Update the reset method so that isScored is set to false when the Pipe resets:
@Override
public void reset(float newX) {
super.reset(newX);
height = r.nextInt(90) + 15;
isScored = false;
}
Next, we just have to create the getter and setter methods:
public boolean isScored() {
return isScored;
}
public void setScored(boolean b) {
isScored = b;
}
Your Pipe class should now look like this:
package com.kilobolt.GameObjects; import java.util.Random; import com.badlogic.gdx.math.Intersector; import com.badlogic.gdx.math.Rectangle; public class Pipe extends Scrollable { private Random r; private Rectangle skullUp, skullDown, barUp, barDown; public static final int VERTICAL_GAP = 45; public static final int SKULL_WIDTH = 24; public static final int SKULL_HEIGHT = 11; private float groundY; private boolean isScored = false; // When Pipe's constructor is invoked, invoke the super (Scrollable) // constructor public Pipe(float x, float y, int width, int height, float scrollSpeed, float groundY) { super(x, y, width, height, scrollSpeed); // Initialize a Random object for Random number generation r = new Random(); skullUp = new Rectangle(); skullDown = new Rectangle(); barUp = new Rectangle(); barDown = new Rectangle(); this.groundY = groundY; } @Override public void update(float delta) { // Call the update method in the superclass (Scrollable) super.update(delta); // The set() method allows you to set the top left corner's x, y // coordinates, // along with the width and height of the rectangle barUp.set(position.x, position.y, width, height); barDown.set(position.x, position.y + height + VERTICAL_GAP, width, groundY - (position.y + height + VERTICAL_GAP)); // Our skull width is 24. The bar is only 22 pixels wide. So the skull // must be shifted by 1 pixel to the left (so that the skull is centered // with respect to its bar). // This shift is equivalent to: (SKULL_WIDTH - width) / 2 skullUp.set(position.x - (SKULL_WIDTH - width) / 2, position.y + height - SKULL_HEIGHT, SKULL_WIDTH, SKULL_HEIGHT); skullDown.set(position.x - (SKULL_WIDTH - width) / 2, barDown.y, SKULL_WIDTH, SKULL_HEIGHT); } @Override public void reset(float newX) { // Call the reset method in the superclass (Scrollable) super.reset(newX); // Change the height to a random number height = r.nextInt(90) + 15; isScored = false; } public Rectangle getSkullUp() { return skullUp; } public Rectangle getSkullDown() { return skullDown; } public Rectangle getBarUp() { return barUp; } public Rectangle getBarDown() { return barDown; } public boolean collides(Bird bird) { if (position.x < bird.getX() + bird.getWidth()) { return (Intersector.overlaps(bird.getBoundingCircle(), barUp) || Intersector.overlaps(bird.getBoundingCircle(), barDown) || Intersector.overlaps(bird.getBoundingCircle(), skullUp) || Intersector .overlaps(bird.getBoundingCircle(), skullDown)); } return false; } public boolean isScored() { return isScored; } public void setScored(boolean b) { isScored = b; } }
Run the code!
You should hear the Coin.wav file play each time that you score a point by passing a pair of Pipes. But we don't want to just hear our score go up. We want to see our score change!
So we are going to render some text to the screen for this.
So we are going to render some text to the screen for this.
Displaying the Score in GameRenderer
Displaying text is very simple. Between our batcher.begin() and batcher.end() calls, we must include a line like this:
AssetLoader.shadow.draw(batcher, "hello world", x, y);
A BitmapFont object, as shown above, has a draw method that takes in the current SpriteBatch, a String, and the x and y coordinates to draw the text at.
- Add the following lines of code right before the batcher.end() call in the render method:
... rest of render method
// Convert integer into String
String score = myWorld.getScore() + "";
// Draw shadow first
AssetLoader.shadow.draw(batcher, "" + myWorld.getScore(), (136 / 2) - (3 * score.length()), 12);
// Draw text
AssetLoader.font.draw(batcher, "" + myWorld.getScore(), (136 / 2) - (3 * score.length() - 1), 11);
batcher.end();
}
We are first converting the score integer to a String, so that we can draw it using our BitmapFont. We calculate the appropriate X location by checking the length of the score, so that we can center the score as best we can.
Here's the full GameRenderer class:
AssetLoader.shadow.draw(batcher, "hello world", x, y);
A BitmapFont object, as shown above, has a draw method that takes in the current SpriteBatch, a String, and the x and y coordinates to draw the text at.
- Add the following lines of code right before the batcher.end() call in the render method:
... rest of render method
// Convert integer into String
String score = myWorld.getScore() + "";
// Draw shadow first
AssetLoader.shadow.draw(batcher, "" + myWorld.getScore(), (136 / 2) - (3 * score.length()), 12);
// Draw text
AssetLoader.font.draw(batcher, "" + myWorld.getScore(), (136 / 2) - (3 * score.length() - 1), 11);
batcher.end();
}
We are first converting the score integer to a String, so that we can draw it using our BitmapFont. We calculate the appropriate X location by checking the length of the score, so that we can center the score as best we can.
Here's the full GameRenderer class:
package com.kilobolt.GameWorld; import com.badlogic.gdx.Gdx; import com.badlogic.gdx.graphics.Color; import com.badlogic.gdx.graphics.GL20; import com.badlogic.gdx.graphics.OrthographicCamera; import com.badlogic.gdx.graphics.g2d.Animation; import com.badlogic.gdx.graphics.g2d.SpriteBatch; import com.badlogic.gdx.graphics.g2d.TextureRegion; import com.badlogic.gdx.graphics.glutils.ShapeRenderer; import com.badlogic.gdx.graphics.glutils.ShapeRenderer.ShapeType; import com.badlogic.gdx.math.Rectangle; import com.kilobolt.GameObjects.Bird; import com.kilobolt.GameObjects.Grass; import com.kilobolt.GameObjects.Pipe; import com.kilobolt.GameObjects.ScrollHandler; 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; // Game Objects private Bird bird; private ScrollHandler scroller; private Grass frontGrass, backGrass; private Pipe pipe1, pipe2, pipe3; // Game Assets private TextureRegion bg, grass; private Animation birdAnimation; private TextureRegion birdMid, birdDown, birdUp; private TextureRegion skullUp, skullDown, bar; public GameRenderer(GameWorld world, int gameHeight, int midPointY) { myWorld = world; 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); // Call helper methods to initialize instance variables initGameObjects(); initAssets(); } private void initGameObjects() { bird = myWorld.getBird(); scroller = myWorld.getScroller(); frontGrass = scroller.getFrontGrass(); backGrass = scroller.getBackGrass(); pipe1 = scroller.getPipe1(); pipe2 = scroller.getPipe2(); pipe3 = scroller.getPipe3(); } private void initAssets() { bg = AssetLoader.bg; grass = AssetLoader.grass; birdAnimation = AssetLoader.birdAnimation; birdMid = AssetLoader.bird; birdDown = AssetLoader.birdDown; birdUp = AssetLoader.birdUp; skullUp = AssetLoader.skullUp; skullDown = AssetLoader.skullDown; bar = AssetLoader.bar; } private void drawGrass() { // Draw the grass batcher.draw(grass, frontGrass.getX(), frontGrass.getY(), frontGrass.getWidth(), frontGrass.getHeight()); batcher.draw(grass, backGrass.getX(), backGrass.getY(), backGrass.getWidth(), backGrass.getHeight()); } private void drawSkulls() { // Temporary code! Sorry about the mess :) // We will fix this when we finish the Pipe class. batcher.draw(skullUp, pipe1.getX() - 1, pipe1.getY() + pipe1.getHeight() - 14, 24, 14); batcher.draw(skullDown, pipe1.getX() - 1, pipe1.getY() + pipe1.getHeight() + 45, 24, 14); batcher.draw(skullUp, pipe2.getX() - 1, pipe2.getY() + pipe2.getHeight() - 14, 24, 14); batcher.draw(skullDown, pipe2.getX() - 1, pipe2.getY() + pipe2.getHeight() + 45, 24, 14); batcher.draw(skullUp, pipe3.getX() - 1, pipe3.getY() + pipe3.getHeight() - 14, 24, 14); batcher.draw(skullDown, pipe3.getX() - 1, pipe3.getY() + pipe3.getHeight() + 45, 24, 14); } private void drawPipes() { // Temporary code! Sorry about the mess :) // We will fix this when we finish the Pipe class. batcher.draw(bar, pipe1.getX(), pipe1.getY(), pipe1.getWidth(), pipe1.getHeight()); batcher.draw(bar, pipe1.getX(), pipe1.getY() + pipe1.getHeight() + 45, pipe1.getWidth(), midPointY + 66 - (pipe1.getHeight() + 45)); batcher.draw(bar, pipe2.getX(), pipe2.getY(), pipe2.getWidth(), pipe2.getHeight()); batcher.draw(bar, pipe2.getX(), pipe2.getY() + pipe2.getHeight() + 45, pipe2.getWidth(), midPointY + 66 - (pipe2.getHeight() + 45)); batcher.draw(bar, pipe3.getX(), pipe3.getY(), pipe3.getWidth(), pipe3.getHeight()); batcher.draw(bar, pipe3.getX(), pipe3.getY() + pipe3.getHeight() + 45, pipe3.getWidth(), midPointY + 66 - (pipe3.getHeight() + 45)); } public void render(float runTime) { Gdx.gl.glClearColor(0, 0, 0, 1); Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT); 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); shapeRenderer.end(); batcher.begin(); batcher.disableBlending(); batcher.draw(bg, 0, midPointY + 23, 136, 43); // 1. Draw Grass drawGrass(); // 2. Draw Pipes drawPipes(); batcher.enableBlending(); // 3. Draw Skulls (requires transparency) drawSkulls(); if (bird.shouldntFlap()) { batcher.draw(birdMid, bird.getX(), bird.getY(), bird.getWidth() / 2.0f, bird.getHeight() / 2.0f, bird.getWidth(), bird.getHeight(), 1, 1, bird.getRotation()); } else { batcher.draw(birdAnimation.getKeyFrame(runTime), bird.getX(), bird.getY(), bird.getWidth() / 2.0f, bird.getHeight() / 2.0f, bird.getWidth(), bird.getHeight(), 1, 1, bird.getRotation()); } // Convert integer into String String score = myWorld.getScore() + ""; // Draw shadow first AssetLoader.shadow.draw(batcher, "" + myWorld.getScore(), (136 / 2) - (3 * score.length()), 12); // Draw text AssetLoader.font.draw(batcher, "" + myWorld.getScore(), (136 / 2) - (3 * score.length() - 1), 11); batcher.end(); } }
We now have a working score system and have displayed some text on the screen! In Day 10, we will implement GameStates so that we can handle restarting the game. After that, we will try to add some additional UI!
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_9.zip | |
File Size: | 10412 kb |
File Type: | zip |
Like us on Facebook to be informed as soon as the next lesson is available.
|
|