LibGDX Zombie Bird Tutorial (Flappy Bird Clone/Remake)
Day 10 - GameStates and High Score
In this tutorial, we are going to discuss GameStates, which we will use to implement a 'start on touch,' pausing, and restarting. Then we will make use of the libGDX preferences to keep track of the high score. At the end of Day 10, you will have a fully featured Flappy Bird clone.
In Day 11, we will add our final touches, adding splash screens and etc. Then we will move on to Unit 2, where we will add the most requested features such as AdMob and Google Play integration!
If you are ready, lets get started.
In Day 11, we will add our final touches, adding splash screens and etc. Then we will move on to Unit 2, where we will add the most requested features such as AdMob and Google Play integration!
If you are ready, lets get started.
Quick-fix - Ceiling collisions
Forgot to add ceiling collision for our bird. Update the update method inside the Bird class!
public void update(float delta) { velocity.add(acceleration.cpy().scl(delta)); if (velocity.y > 200) { velocity.y = 200; } // CEILING CHECK if (position.y < -13) { position.y = -13; velocity.y = 0; } 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; } } }
Adding GameStates
The idea behind GameStates is to divide our game into multiple 'states' such as "RUNNING" or "GAMEOVER". Then we will use if statements or switches to control the flow of our game depending on what the state is.
An easy way to implement GameStates is to create an Enum, which is just a variable that can only take certain values that have been defined for it. I will let you read up on that here, if you are interested. If you prefer to see the code, keep reading!
We add our enum to our GameWorld. Add the following code somewhere inside the GameWorld class:
public enum GameState {
READY, RUNNING, GAMEOVER
}
- Now we can create GameState variables just like any other variables. Create an instance variable:
private GameState currentState;
- Initialize it in the constructor:
currentState = GameState.READY;
An easy way to implement GameStates is to create an Enum, which is just a variable that can only take certain values that have been defined for it. I will let you read up on that here, if you are interested. If you prefer to see the code, keep reading!
We add our enum to our GameWorld. Add the following code somewhere inside the GameWorld class:
public enum GameState {
READY, RUNNING, GAMEOVER
}
- Now we can create GameState variables just like any other variables. Create an instance variable:
private GameState currentState;
- Initialize it in the constructor:
currentState = GameState.READY;
We next need to make a couple of changes to our update method.
In fact, we are going to change its name to updateRunning.
Once you have made this change, create a new update method and an updateReady method as shown below:
In fact, we are going to change its name to updateRunning.
Once you have made this change, create a new update method and an updateReady method as shown below:
public void update(float delta) { switch (currentState) { case READY: updateReady(delta); break; case RUNNING: default: updateRunning(delta); break; } } private void updateReady(float delta) { // Do nothing for now }
The update method now checks the current state of the game before determining which more specific update method to call.
Next, we must change our GameState when our Bird dies. We will consider the Bird fully dead when he hits the ground. Inside the updateRunning method (changed from the original name of update, like I asked you to above), add this inside the final if statement:
currentState = GameState.GAMEOVER;
And finally, add these various methods, which will handle GameState changes, restarting, and so on.
isReady simply returns true if the currentState is GameState.READY. start changes the currentState to GameState.RUNNING.
restart is a more interesting - it resets all the instance variables that have changed since the game started! This is how we will handle restarting the game.
When the restart method is called, we will cascade down to the other objects and call their onRestart methods. The result should be that every instance variable that has changed throughout the flow of the game will be reset - like dominos.
What are the arguments that we are passing into the onRestart methods? These are all the initial values of variables that may have changed when our game was running. For example, our Bird's y position may have changed, so we pass in the original y position.
Next, we must change our GameState when our Bird dies. We will consider the Bird fully dead when he hits the ground. Inside the updateRunning method (changed from the original name of update, like I asked you to above), add this inside the final if statement:
currentState = GameState.GAMEOVER;
And finally, add these various methods, which will handle GameState changes, restarting, and so on.
isReady simply returns true if the currentState is GameState.READY. start changes the currentState to GameState.RUNNING.
restart is a more interesting - it resets all the instance variables that have changed since the game started! This is how we will handle restarting the game.
When the restart method is called, we will cascade down to the other objects and call their onRestart methods. The result should be that every instance variable that has changed throughout the flow of the game will be reset - like dominos.
What are the arguments that we are passing into the onRestart methods? These are all the initial values of variables that may have changed when our game was running. For example, our Bird's y position may have changed, so we pass in the original y position.
public void restart() { currentState = GameState.READY; score = 0; bird.onRestart(midPointY - 5); scroller.onRestart(); currentState = GameState.READY; }
We also need access to the midPointY, which is currently not being stored as an instance variable in the constructor. Let's change that.
Add this instance variable:
public int midPointY;
Initialize it in the constructor:
this.midPointY = midPointY;
In case you got lost somewhere, here's the full GameWorld class:
Add this instance variable:
public int midPointY;
Initialize it in the constructor:
this.midPointY = midPointY;
In case you got lost somewhere, here's the full GameWorld class:
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; private int midPointY; private GameState currentState; public enum GameState { READY, RUNNING, GAMEOVER } public GameWorld(int midPointY) { currentState = GameState.READY; this.midPointY = midPointY; bird = new Bird(33, midPointY - 5, 17, 12); // The grass should start 66 pixels below the midPointY scroller = new ScrollHandler(this, midPointY + 66); ground = new Rectangle(0, midPointY + 66, 137, 11); } public void update(float delta) { switch (currentState) { case READY: updateReady(delta); break; case RUNNING: default: updateRunning(delta); break; } } private void updateReady(float delta) { // Do nothing for now } public void updateRunning(float delta) { 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(); currentState = GameState.GAMEOVER; } } public Bird getBird() { return bird; } public ScrollHandler getScroller() { return scroller; } public int getScore() { return score; } public void addScore(int increment) { score += increment; } public boolean isReady() { return currentState == GameState.READY; } public void start() { currentState = GameState.RUNNING; } public void restart() { currentState = GameState.READY; score = 0; bird.onRestart(midPointY - 5); scroller.onRestart(); currentState = GameState.READY; } public boolean isGameOver() { return currentState == GameState.GAMEOVER; } }
We will now add the onRestart methods to our Bird and Scroller objects. Let's start with the easier one - the Bird object.
Resetting the Bird
Create the onRestart method for our bird. Inside, we must undo all the changes that were done during the course of the gameplay:
public void onRestart(int y) { rotation = 0; position.y = y; velocity.x = 0; velocity.y = 0; acceleration.x = 0; acceleration.y = 460; isAlive = true; }
That's it! Here's the full class:
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 void onRestart(int y) { rotation = 0; position.y = y; velocity.x = 0; velocity.y = 0; acceleration.x = 0; acceleration.y = 460; isAlive = true; } 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; } }
OnRestart - ScrollHandler
We must now go to the ScrollHandler class and create a similar method, resetting all the instance variables of the ScrollHandler class! Notice that we call some more non-existent onRestart methods. We will go deeper and complete them all.
public void onRestart() { frontGrass.onRestart(0, SCROLL_SPEED); backGrass.onRestart(frontGrass.getTailX(), SCROLL_SPEED); pipe1.onRestart(210, SCROLL_SPEED); pipe2.onRestart(pipe1.getTailX() + PIPE_GAP, SCROLL_SPEED); pipe3.onRestart(pipe2.getTailX() + PIPE_GAP, SCROLL_SPEED); }
Grass Class onRestart
This is really simple. We just need to move the grass back to their original positions and reset their speed back to SCROLL_SPEED:
package com.kilobolt.GameObjects; public class Grass extends Scrollable { public Grass(float x, float y, int width, int height, float scrollSpeed) { super(x, y, width, height, scrollSpeed); } public void onRestart(float x, float scrollSpeed) { position.x = x; velocity.x = scrollSpeed; } }
Pipe onRestart
Pipe is slightly more complicated, but still very simple. Add this method:
public void onRestart(float x, float scrollSpeed) {
velocity.x = scrollSpeed;
reset(x);
}
public void onRestart(float x, float scrollSpeed) {
velocity.x = scrollSpeed;
reset(x);
}
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 void onRestart(float x, float scrollSpeed) { velocity.x = scrollSpeed; reset(x); } 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; } }
And we are done! Let's look back at what we've accomplished. We started by adding GameStates in the GameWorld class. We then added a restart method, which asked Bird to restart and ScrollHandler to restart, which in turn asked its three Pipe objects and two Grass objects to restart. Now all we need to do is let the game know when to restart.
Go to the InputHandler
Our InputHandler needs a reference to our GameWorld object, so that we can determine what the current GameState is and handle touches correctly. Rather than adding another argument to the constructor, I'm going to change the existing constructor to be this:
public InputHandler(GameWorld myWorld) {
// myBird now represents the gameWorld's bird.
this.myWorld = myWorld;
myBird = myWorld.getBird();
}
- Of course create an instance variable for myWorld (above the constructor): private GameWorld myWorld;
- Don't forget to import!
Next, we are going to update our touchDown method:
public InputHandler(GameWorld myWorld) {
// myBird now represents the gameWorld's bird.
this.myWorld = myWorld;
myBird = myWorld.getBird();
}
- Of course create an instance variable for myWorld (above the constructor): private GameWorld myWorld;
- Don't forget to import!
Next, we are going to update our touchDown method:
@Override public boolean touchDown(int screenX, int screenY, int pointer, int button) { if (myWorld.isReady()) { myWorld.start(); } myBird.onClick(); if (myWorld.isGameOver()) { // Reset all variables, go to GameState.READ myWorld.restart(); } return true; }
Here's the full class!
package com.kilobolt.ZBHelpers; import com.badlogic.gdx.InputProcessor; import com.kilobolt.GameObjects.Bird; import com.kilobolt.GameWorld.GameWorld; public class InputHandler implements InputProcessor { private Bird myBird; private GameWorld myWorld; // Ask for a reference to the Bird when InputHandler is created. public InputHandler(GameWorld myWorld) { // myBird now represents the gameWorld's bird. this.myWorld = myWorld; myBird = myWorld.getBird(); } @Override public boolean touchDown(int screenX, int screenY, int pointer, int button) { if (myWorld.isReady()) { myWorld.start(); } myBird.onClick(); if (myWorld.isGameOver()) { // Reset all variables, go to GameState.READ myWorld.restart(); } return true; } @Override public boolean keyDown(int keycode) { return false; } @Override public boolean keyUp(int keycode) { return false; } @Override public boolean keyTyped(char character) { return false; } @Override public boolean touchUp(int screenX, int screenY, int pointer, int button) { return false; } @Override public boolean touchDragged(int screenX, int screenY, int pointer) { return false; } @Override public boolean mouseMoved(int screenX, int screenY) { return false; } @Override public boolean scrolled(int amount) { return false; } }
Fixing GameScreen
Of course, since we changed the constructor for our InputHandler, we need to update our GameScreen, where we initialized the InputHandler using its constructor.
Change this line:
Gdx.input.setInputProcessor(new InputHandler(world.getBird()));
To this line:
Gdx.input.setInputProcessor(new InputHandler(world));
The full class should be:
Change this line:
Gdx.input.setInputProcessor(new InputHandler(world.getBird()));
To this line:
Gdx.input.setInputProcessor(new InputHandler(world));
The full class should be:
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)); } @Override public void render(float delta) { runTime += delta; world.update(delta); renderer.render(runTime); } @Override public void resize(int width, int height) { System.out.println("GameScreen - resizing"); } @Override public void show() { System.out.println("GameScreen - show called"); } @Override public void hide() { System.out.println("GameScreen - hide called"); } @Override public void pause() { System.out.println("GameScreen - pause called"); } @Override public void resume() { System.out.println("GameScreen - resume called"); } @Override public void dispose() { // Leave blank } }
Changing GameRenderer
Our restarting code is setup! Now when the game starts, it will start in the ready state, in which nothing will happen. We will tap the screen to start the game. When our bird dies, we will enter the game over state, at which time we can tap the screen again to restart.
No buttons yet, but it's a start!
Now to make this more intuitive, we are going to make changes to the Game Renderer, displaying some useful information.
Change your GameRenderer render method to the one shown below:
No buttons yet, but it's a start!
Now to make this more intuitive, we are going to make changes to the Game Renderer, displaying some useful information.
Change your GameRenderer render method to the one shown below:
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()); } // TEMPORARY CODE! We will fix this section later: if (myWorld.isReady()) { // Draw shadow first AssetLoader.shadow.draw(batcher, "Touch me", (136 / 2) - (42), 76); // Draw text AssetLoader.font.draw(batcher, "Touch me", (136 / 2) - (42 - 1), 75); } else { if (myWorld.isGameOver()) { AssetLoader.shadow.draw(batcher, "Game Over", 25, 56); AssetLoader.font.draw(batcher, "Game Over", 24, 55); AssetLoader.shadow.draw(batcher, "Try again?", 23, 76); AssetLoader.font.draw(batcher, "Try again?", 24, 75); } // 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(); } }
Completed Gameplay!
Our gameplay is now finished. Let's wrap things up by implementing high score!
Implementing High Score
The easiest way to store small data for a libGDX game is to use a Preferences object. A Preferences object maps key-value pairs. This means that you can store some key and a corresponding value, and you can pull the values out!
Let's see an example:
// We specify the name of the Preferences File
Preferences prefs = Gdx.app.getPreferences("PreferenceName");// We store the value 10 with the key of "highScore"
prefs.putInteger("highScore", 10);
prefs.flush(); // This saves the preferences file.
A week later, you try running:
System.out.println(prefs.getInteger("highScore"));
The resulting value will be 10!
So, the three important methods you need to know are:
1. put...
2. get...
3. and flush (for saving).
You could store other things like:
putBoolean("soundEnabled", true); // getBoolean("soundEnabled") retrieves a boolean.
putString("playerName", "James");
Let's implement this for high score.
Open up the AssetLoader class.
- Create an static variable in the header:
public static Preferences prefs;
Inside the load method, add the following lines of code:
// Create (or retrieve existing) preferences file
prefs = Gdx.app.getPreferences("ZombieBird");
// Provide default high score of 0
if (!prefs.contains("highScore")) {
prefs.putInteger("highScore", 0);
}
Now we can access the file prefs from anywhere in the game! Let's create some helper methods so that we don't have to deal with preferences outside of AssetLoader.
Add these methods:
// Receives an integer and maps it to the String highScore in prefs
public static void setHighScore(int val) {
prefs.putInteger("highScore", val);
prefs.flush();
}
// Retrieves the current high score
public static int getHighScore() {
return prefs.getInteger("highScore");
}
Here's the full class:
Let's see an example:
// We specify the name of the Preferences File
Preferences prefs = Gdx.app.getPreferences("PreferenceName");// We store the value 10 with the key of "highScore"
prefs.putInteger("highScore", 10);
prefs.flush(); // This saves the preferences file.
A week later, you try running:
System.out.println(prefs.getInteger("highScore"));
The resulting value will be 10!
So, the three important methods you need to know are:
1. put...
2. get...
3. and flush (for saving).
You could store other things like:
putBoolean("soundEnabled", true); // getBoolean("soundEnabled") retrieves a boolean.
putString("playerName", "James");
Let's implement this for high score.
Open up the AssetLoader class.
- Create an static variable in the header:
public static Preferences prefs;
Inside the load method, add the following lines of code:
// Create (or retrieve existing) preferences file
prefs = Gdx.app.getPreferences("ZombieBird");
// Provide default high score of 0
if (!prefs.contains("highScore")) {
prefs.putInteger("highScore", 0);
}
Now we can access the file prefs from anywhere in the game! Let's create some helper methods so that we don't have to deal with preferences outside of AssetLoader.
Add these methods:
// Receives an integer and maps it to the String highScore in prefs
public static void setHighScore(int val) {
prefs.putInteger("highScore", val);
prefs.flush();
}
// Retrieves the current high score
public static int getHighScore() {
return prefs.getInteger("highScore");
}
Here's the full class:
package com.kilobolt.ZBHelpers; import com.badlogic.gdx.Gdx; import com.badlogic.gdx.Preferences; 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; private static Preferences prefs; 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); // Create (or retrieve existing) preferences file prefs = Gdx.app.getPreferences("ZombieBird"); if (!prefs.contains("highScore")) { prefs.putInteger("highScore", 0); } } public static void setHighScore(int val) { prefs.putInteger("highScore", val); prefs.flush(); } public static int getHighScore() { return prefs.getInteger("highScore"); } 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(); } }
Now we can go back to GameWorld and start saving those high scores!
Let's begin by adding a fourth enum constant:
public enum GameState {
READY, RUNNING, GAMEOVER, HIGHSCORE
}
Now, we just need to add one if statement to where we handle our death (update method, when we check for collision between Bird and Ground). We simply check whether our current score is better than the previous high score, and if it is, we update the high score:
Let's begin by adding a fourth enum constant:
public enum GameState {
READY, RUNNING, GAMEOVER, HIGHSCORE
}
Now, we just need to add one if statement to where we handle our death (update method, when we check for collision between Bird and Ground). We simply check whether our current score is better than the previous high score, and if it is, we update the high score:
if (Intersector.overlaps(bird.getBoundingCircle(), ground)) { scroller.stop(); bird.die(); bird.decelerate(); currentState = GameState.GAMEOVER; if (score > AssetLoader.getHighScore()) { AssetLoader.setHighScore(score); currentState = GameState.HIGHSCORE; } }
Create one more method that we can use to check if we are currently in the HIGHSCORE state:
public boolean isHighScore() {
return currentState == GameState.HIGHSCORE;
}
Now we can head to our GameRenderer and handle our new HIGHSCORE gameState, and display the high score at the end of the game:
Updated GameRenderer:
NOTE: This is temporary code used just for illustration:
public boolean isHighScore() {
return currentState == GameState.HIGHSCORE;
}
Now we can head to our GameRenderer and handle our new HIGHSCORE gameState, and display the high score at the end of the game:
Updated GameRenderer:
NOTE: This is temporary code used just for illustration:
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()); } // TEMPORARY CODE! We will fix this section later: if (myWorld.isReady()) { // Draw shadow first AssetLoader.shadow.draw(batcher, "Touch me", (136 / 2) - (42), 76); // Draw text AssetLoader.font .draw(batcher, "Touch me", (136 / 2) - (42 - 1), 75); } else { if (myWorld.isGameOver() || myWorld.isHighScore()) { if (myWorld.isGameOver()) { AssetLoader.shadow.draw(batcher, "Game Over", 25, 56); AssetLoader.font.draw(batcher, "Game Over", 24, 55); AssetLoader.shadow.draw(batcher, "High Score:", 23, 106); AssetLoader.font.draw(batcher, "High Score:", 22, 105); String highScore = AssetLoader.getHighScore() + ""; // Draw shadow first AssetLoader.shadow.draw(batcher, highScore, (136 / 2) - (3 * highScore.length()), 128); // Draw text AssetLoader.font.draw(batcher, highScore, (136 / 2) - (3 * highScore.length() - 1), 127); } else { AssetLoader.shadow.draw(batcher, "High Score!", 19, 56); AssetLoader.font.draw(batcher, "High Score!", 18, 55); } AssetLoader.shadow.draw(batcher, "Try again?", 23, 76); AssetLoader.font.draw(batcher, "Try again?", 24, 75); // Convert integer into String String score = myWorld.getScore() + ""; // Draw shadow first AssetLoader.shadow.draw(batcher, score, (136 / 2) - (3 * score.length()), 12); // Draw text AssetLoader.font.draw(batcher, score, (136 / 2) - (3 * score.length() - 1), 11); } // 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(); } }
Let's quickly update our update method in GameWorld to this:
public void update(float delta) { switch (currentState) { case READY: updateReady(delta); break; case RUNNING: updateRunning(delta); break; default: break; } }
The very last thing we need to do is change one line in the InputHandler class to this:
if (myWorld.isGameOver() || myWorld.isHighScore()) {
// Reset all variables, go to GameState.READ
myWorld.restart();
}
That's it for Day 10! We will continue to build on this in Day 11. Thank you for reading
if (myWorld.isGameOver() || myWorld.isHighScore()) {
// Reset all variables, go to GameState.READ
myWorld.restart();
}
That's it for Day 10! We will continue to build on this in Day 11. Thank you for reading
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:
There's a small bug in the Source code! You have to manually change one line in the InputHandler to this:
if (myWorld.isGameOver() || myWorld.isHighScore()) {
// Reset all variables, go to GameState.READ
myWorld.restart();
}
and also update your GameWorld's update method to this:
Download, extract and import into eclipse:
There's a small bug in the Source code! You have to manually change one line in the InputHandler to this:
if (myWorld.isGameOver() || myWorld.isHighScore()) {
// Reset all variables, go to GameState.READ
myWorld.restart();
}
and also update your GameWorld's update method to this:
public void update(float delta) { switch (currentState) { case READY: updateReady(delta); break; case RUNNING: updateRunning(delta); break; default: break; } }

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