LibGDX Zombie Bird Tutorial (Flappy Bird Clone/Remake)
Day 7 - The Grass, the Bird and the Skull Pipe
Welcome to Day 7! In today's lesson we will learn how to rotate our bird, and how to scroll the grass and the skull pipe. This will involve making some changes to the Bird class, and creating new classes to represent our grass and the pipe.
Let's get started!
Let's get started!
Rotating the Bird
Before we get to the code, let's examine the Bird's rotating behavior first. In Flappy Bird the bird has two primary states. The bird is either rising after a touch or the bird is falling. In those two states, the bird rotates as follows:
Rotation is controlled using the Y velocity. In our game, the bird accelerates downwards due to gravity (meaning that the velocity increases). When our velocity is negative (meaning that the bird is moving upwards), the bird will start rotating counterclockwise. When our velocity is higher than some positive value, the bird will start to rotate clockwise (we don't start rotating until the Bird starts speeding up a bit).
Animations
One thing we have to pay attention to is the animation. The bird should not flap his wings when falling. Instead, his wings should go in the middle position. Once he starts rising, he will start flapping again.
Let's implement all of these in code:
- We begin by creating two methods. We are going to use experimentally determined hard-coded values to create the following methods anywhere inside the Bird class:
public boolean isFalling() {
return velocity.y > 110;
}
public boolean shouldntFlap() {
return velocity.y > 70;
}
We will use the isFalling method to decide when the bird should begin rotating downwards.
We will use the shouldntFlap method to determine when the bird should stop animating.
- Next, we need to make some changes to the update method. Recall that we have a float called rotation, which will keep track of how much our bard has rotated. We will say that a positive change in rotation is a clockwise rotation and that a negative change in rotation is a counterclockwise rotation.
Add these two blocks of code to the end of the update method. They will handle the counterclockwise and clockwise rotations (rising and falling).
Animations
One thing we have to pay attention to is the animation. The bird should not flap his wings when falling. Instead, his wings should go in the middle position. Once he starts rising, he will start flapping again.
Let's implement all of these in code:
- We begin by creating two methods. We are going to use experimentally determined hard-coded values to create the following methods anywhere inside the Bird class:
public boolean isFalling() {
return velocity.y > 110;
}
public boolean shouldntFlap() {
return velocity.y > 70;
}
We will use the isFalling method to decide when the bird should begin rotating downwards.
We will use the shouldntFlap method to determine when the bird should stop animating.
- Next, we need to make some changes to the update method. Recall that we have a float called rotation, which will keep track of how much our bard has rotated. We will say that a positive change in rotation is a clockwise rotation and that a negative change in rotation is a counterclockwise rotation.
Add these two blocks of code to the end of the update method. They will handle the counterclockwise and clockwise rotations (rising and falling).
// Rotate counterclockwise if (velocity.y < 0) { rotation -= 600 * delta; if (rotation < -20) { rotation = -20; } } // Rotate clockwise if (isFalling()) { rotation += 480 * delta; if (rotation > 90) { rotation = 90; } }
Note that we scale our rotation by delta, so that the bird will rotate at the same rate even if the game slows down (or speeds up, for that matter).
Both of these if statements have some kind of cap on rotation. If we rotate too far, our game will correct that for us.
Here is what your full Bird class should look like:
Both of these if statements have some kind of cap on rotation. If we rotate too far, our game will correct that for us.
Here is what your full Bird class should look like:
package com.kilobolt.gameobjects; import com.badlogic.gdx.math.Vector2; public class Bird { private Vector2 position; private Vector2 velocity; private Vector2 acceleration; private float rotation; // For handling bird rotation private int width; private int height; 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); } public void update(float delta) { velocity.add(acceleration.cpy().scl(delta)); if (velocity.y > 200) { velocity.y = 200; } position.add(velocity.cpy().scl(delta)); // Rotate counterclockwise if (velocity.y < 0) { rotation -= 600 * delta; if (rotation < -20) { rotation = -20; } } // Rotate clockwise if (isFalling()) { rotation += 480 * delta; if (rotation > 90) { rotation = 90; } } } public boolean isFalling() { return velocity.y > 110; } public boolean shouldntFlap() { return velocity.y > 70; } public void onClick() { velocity.y = -140; } 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; } }
Great. Now that we've gotten that taken care of, all we need to do is go to the GameRenderer and use the shouldntFlap method to determine whether the bird should animate or not.
Caution
As mentioned in Day 5 (Reiterated here because of its importance!)
Every time you create a new Object, you allocate a little bit of memory in RAM for that object (specifically in the Heap). Once your Heap fills up, a subroutine called a Garbage Collector will come in and clean up your memory to ensure that you do not run out of space. This is great, except when you are building a game. Every time the garbage collector comes into play, your game will stutter for several essential milliseconds. To avoid garbage collection, you need to avoid allocating new objects whenever possible
I've recently discovered that the method Vector2.cpy() creates a new instance of a Vector2 rather than recycling one instance. This means that at 60 FPS, calling Vector2.cpy() would allocate 60 new Vector2 objects every second, which would cause the Java garbage collector to get involved every frequently.
Just keep this in mind as you go through the remainder of Unit 1. We will solve this issue later in the series.
Every time you create a new Object, you allocate a little bit of memory in RAM for that object (specifically in the Heap). Once your Heap fills up, a subroutine called a Garbage Collector will come in and clean up your memory to ensure that you do not run out of space. This is great, except when you are building a game. Every time the garbage collector comes into play, your game will stutter for several essential milliseconds. To avoid garbage collection, you need to avoid allocating new objects whenever possible
I've recently discovered that the method Vector2.cpy() creates a new instance of a Vector2 rather than recycling one instance. This means that at 60 FPS, calling Vector2.cpy() would allocate 60 new Vector2 objects every second, which would cause the Java garbage collector to get involved every frequently.
Just keep this in mind as you go through the remainder of Unit 1. We will solve this issue later in the series.
Cleaning up GameRenderer
To achieve high performance in games, you have to minimize the work that you do in your game loop. In Day 6, we wrote some code that violates this principle. Have a look at the render method. We had this line of code:
Bird bird = myWorld.getBird();
Each time that the render method was called (about 60 times per second), we were asking our game to find myWorld, then find its Bird object, return it to the GameRenderer to put it on the stack as a local variable.
Instead of doing that, we are going to retrieve it once when the GameRenderer is first created then store the Bird object as an instance variable.
We will be doing the same thing with our TextureRegions from AssetLoader and for any future objects that we create.
- Begin by adding these instance variables to GameRenderer (importing as necessary-- use com.badlogic.gdx.graphics.g2d.Animation for animation):
// Game Objects
private Bird bird;
// Game Assets
private TextureRegion bg, grass;
private Animation birdAnimation;
private TextureRegion birdMid, birdDown, birdUp;
private TextureRegion skullUp, skullDown, bar;
Next, we will initialize these in the constructor. But rather than doing all the work in the constructor and making the code messy, we are going to create two helper methods that will initialize them for us:
- Add the following two methods to the GameRenderer:
Bird bird = myWorld.getBird();
Each time that the render method was called (about 60 times per second), we were asking our game to find myWorld, then find its Bird object, return it to the GameRenderer to put it on the stack as a local variable.
Instead of doing that, we are going to retrieve it once when the GameRenderer is first created then store the Bird object as an instance variable.
We will be doing the same thing with our TextureRegions from AssetLoader and for any future objects that we create.
- Begin by adding these instance variables to GameRenderer (importing as necessary-- use com.badlogic.gdx.graphics.g2d.Animation for animation):
// Game Objects
private Bird bird;
// Game Assets
private TextureRegion bg, grass;
private Animation birdAnimation;
private TextureRegion birdMid, birdDown, birdUp;
private TextureRegion skullUp, skullDown, bar;
Next, we will initialize these in the constructor. But rather than doing all the work in the constructor and making the code messy, we are going to create two helper methods that will initialize them for us:
- Add the following two methods to the GameRenderer:
private void initGameObjects() { bird = myWorld.getBird(); } 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; }
We then call these methods in the constructor:
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(); }
Lastly, we must make changes to the render method. Specifically, we will remove any references to AssetLoader, and remove the line of code that says:
Bird bird = myWorld.getBird().
Secondly, we will modify our bird drawing calls so that we can handle rotations properly. Also change any references to AssetLoader (such as AssetLoader.bg) Here's how you might do that:
Bird bird = myWorld.getBird().
Secondly, we will modify our bird drawing calls so that we can handle rotations properly. Also change any references to AssetLoader (such as AssetLoader.bg) Here's how you might do that:
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); batcher.enableBlending(); 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()); } batcher.end(); }
And your full GameRenderer should look like this:
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.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.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; // Game Objects private Bird bird; // 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(); } 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); batcher.enableBlending(); 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()); } batcher.end(); } private void initGameObjects() { bird = myWorld.getBird(); } 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; } }
Now try running the code! Your bird will flap his wings and rotate, exactly as we intended.
Creating the Scrollable Classes
We will now implement the grass and the pipes in our game:
The grass and the pipes move at the same speed to the left. We will work with the following parameters:
- We will use 3 pipes (each column will be considered one pipe object).
- We will use 2 long strips of grass, attached to each other horizontally.
- When each pipe or grass scrolls to the left (is no longer visible), we will reset it.
- To reset the grass, we will simply attach it to the tail (the end) of the other piece.
- To reset the pipe, we place it at a fixed distance from the pipe that is furthest back, and reset its height.
Notice that pipes and grass behave in very similar ways. Because of this, we can take their common traits and create a Scrollable Superclass with them, and then use inheritance to create the more specific Pipe and Grass objects.
The reseting part can be tricky.
For example, if we have three pipes:
- Pipe 1, when resetting, should go behind Pipe 3.
- Pipe 2, when resetting, should go behind Pipe 1.
- Pipe 3, when resetting, should go behind Pipe 2.
Rather than give each of these objects a reference to the other pipes, we will simply create a ScrollHandler object that will keep a reference to all of these objects and determine where each should go. This will make things a lot easier.
We begin by creating a Scrollable class (inside com.kilobolt.gameobjects) which we will use to define all the shared properties of the Pipe and the Grass objects.
The shared properties include:
Instance variables:
position, velocity, width, height, and the boolean isScrolledLeft to determine when the Scrollable object is no longer visible and should be reset.
Methods:
update and reset, and getter methods for various instance variables above.
Here's the full code (it's very straightforward). There's really nothing we haven't seen before, except for the reset method. Please have a look at the comments:
- We will use 3 pipes (each column will be considered one pipe object).
- We will use 2 long strips of grass, attached to each other horizontally.
- When each pipe or grass scrolls to the left (is no longer visible), we will reset it.
- To reset the grass, we will simply attach it to the tail (the end) of the other piece.
- To reset the pipe, we place it at a fixed distance from the pipe that is furthest back, and reset its height.
Notice that pipes and grass behave in very similar ways. Because of this, we can take their common traits and create a Scrollable Superclass with them, and then use inheritance to create the more specific Pipe and Grass objects.
The reseting part can be tricky.
For example, if we have three pipes:
- Pipe 1, when resetting, should go behind Pipe 3.
- Pipe 2, when resetting, should go behind Pipe 1.
- Pipe 3, when resetting, should go behind Pipe 2.
Rather than give each of these objects a reference to the other pipes, we will simply create a ScrollHandler object that will keep a reference to all of these objects and determine where each should go. This will make things a lot easier.
We begin by creating a Scrollable class (inside com.kilobolt.gameobjects) which we will use to define all the shared properties of the Pipe and the Grass objects.
The shared properties include:
Instance variables:
position, velocity, width, height, and the boolean isScrolledLeft to determine when the Scrollable object is no longer visible and should be reset.
Methods:
update and reset, and getter methods for various instance variables above.
Here's the full code (it's very straightforward). There's really nothing we haven't seen before, except for the reset method. Please have a look at the comments:
package com.kilobolt.gameobjects; import com.badlogic.gdx.math.Vector2; public class Scrollable { // Protected is similar to private, but allows inheritance by subclasses. protected Vector2 position; protected Vector2 velocity; protected int width; protected int height; protected boolean isScrolledLeft; public Scrollable(float x, float y, int width, int height, float scrollSpeed) { position = new Vector2(x, y); velocity = new Vector2(scrollSpeed, 0); this.width = width; this.height = height; isScrolledLeft = false; } public void update(float delta) { position.add(velocity.cpy().scl(delta)); // If the Scrollable object is no longer visible: if (position.x + width < 0) { isScrolledLeft = true; } } // Reset: Should Override in subclass for more specific behavior. public void reset(float newX) { position.x = newX; isScrolledLeft = false; } // Getters for instance variables public boolean isScrolledLeft() { return isScrolledLeft; } public float getTailX() { return position.x + width; } public float getX() { return position.x; } public float getY() { return position.y; } public int getWidth() { return width; } public int getHeight() { return height; } }
Now that we have a generic Scrollable class, we can create subclasses using inheritance. Create the following two classes also inside com.kilobolt.gameobjects:
Pipe
package com.kilobolt.gameobjects; import java.util.Random; public class Pipe extends Scrollable { private Random r; // When Pipe's constructor is invoked, invoke the super (Scrollable) // constructor public Pipe(float x, float y, int width, int height, float scrollSpeed) { super(x, y, width, height, scrollSpeed); // Initialize a Random object for Random number generation r = new Random(); } @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; } }
Grass
package com.kilobolt.gameobjects; public class Grass extends Scrollable { // When Grass's constructor is invoked, invoke the super (Scrollable) // constructor public Grass(float x, float y, int width, int height, float scrollSpeed) { super(x, y, width, height, scrollSpeed); } }
The two above classes (when we say Pipe p = new Pipe(...) or Grass g = new Grass(...)) will create themselves using the Scrollable constructor (by inheritance), and also add some of their own properties (in the case of Pipe, a new instance variable and a new method).
What is @Override and super?
In inheritance, the subclasses have access to all the methods in the superclass. This means that Pipe and Grass are both able to reset even if we don't define the method specifically inside the Pipe and Grass classes. This is because they are extensions of Scrollable which does have the reset method.
For instance, we could do something like this:
Grass g = new Grass(...);
g.reset(); // Reset method from scrollable is called.
However, if we want more specific behavior than the one described in the superclass, we can use: @Override, which basically says: use this reset method inside the subclass rather than the reset method in the superclass. We are doing this inside the Pipe class - we are replacing the original reset method from the superclass with a more specific one in the subclass.
So, when we do this:
Pipe p = new Pipe(...);
p.reset(); // Reset method from Pipe called.
So what is super?
Even when Overriding, the subclass still has a reference to the superclass's original reset method. Calling super.reset(...) inside the Overridden reset will mean that both reset methods are called.
Why do we need a Grass class?
Grass, above, is really a worthless class right now, because it has no properties of its own. But we will later add some properties for collision detection and thats why it gets its own class.
Now that our Scrollable classes are ready, we can implement the ScrollHandler, which will take care of creating these Grass and Pipe objects, updating them, and handling reset.
Create the new ScrollHandler class inside com.kilobolt.gameobjects.
We will start with simple stuff:
- We need a constructor that receives a y position to know where we need to create our ground (where the grass and the bottom pipes will begin).
- We need five instance variables: 2 for two Grass objects and 3 for three Pipe objects (for now, we will treat each column of pipes as one Pipe object).
- We need getters for all of these.
- We need an update method.
What is @Override and super?
In inheritance, the subclasses have access to all the methods in the superclass. This means that Pipe and Grass are both able to reset even if we don't define the method specifically inside the Pipe and Grass classes. This is because they are extensions of Scrollable which does have the reset method.
For instance, we could do something like this:
Grass g = new Grass(...);
g.reset(); // Reset method from scrollable is called.
However, if we want more specific behavior than the one described in the superclass, we can use: @Override, which basically says: use this reset method inside the subclass rather than the reset method in the superclass. We are doing this inside the Pipe class - we are replacing the original reset method from the superclass with a more specific one in the subclass.
So, when we do this:
Pipe p = new Pipe(...);
p.reset(); // Reset method from Pipe called.
So what is super?
Even when Overriding, the subclass still has a reference to the superclass's original reset method. Calling super.reset(...) inside the Overridden reset will mean that both reset methods are called.
Why do we need a Grass class?
Grass, above, is really a worthless class right now, because it has no properties of its own. But we will later add some properties for collision detection and thats why it gets its own class.
Now that our Scrollable classes are ready, we can implement the ScrollHandler, which will take care of creating these Grass and Pipe objects, updating them, and handling reset.
Create the new ScrollHandler class inside com.kilobolt.gameobjects.
We will start with simple stuff:
- We need a constructor that receives a y position to know where we need to create our ground (where the grass and the bottom pipes will begin).
- We need five instance variables: 2 for two Grass objects and 3 for three Pipe objects (for now, we will treat each column of pipes as one Pipe object).
- We need getters for all of these.
- We need an update method.
package com.kilobolt.gameobjects; public class ScrollHandler { // ScrollHandler will create all five objects that we need. private Grass frontGrass, backGrass; private Pipe pipe1, pipe2, pipe3; // ScrollHandler will use the constants below to determine // how fast we need to scroll and also determine // the size of the gap between each pair of pipes. // Capital letters are used by convention when naming constants. public static final int SCROLL_SPEED = -59; public static final int PIPE_GAP = 49; // Constructor receives a float that tells us where we need to create our // Grass and Pipe objects. public ScrollHandler(float yPos) { } public void update(float delta) { } // The getters for our five instance variables 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; } }
Now we just have to work on the constructor and the update method. Inside the constructor, we will initialize our Scrollable objects:
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);
pipe2 = new Pipe(pipe1.getTailX() + PIPE_GAP, 0, 22, 70, SCROLL_SPEED);
pipe3 = new Pipe(pipe2.getTailX() + PIPE_GAP, 0, 22, 60, SCROLL_SPEED);
The logic here is simple. Remember that our Scrollable objects' constructors want : x, y, width, height and scroll speed. We pass each of these in.
The backGrass object should be attached to the tail of the frontGrass, so we create it at the frontGrass' tail.
The Pipe objects follow a similar logic, except that we add the PIPE_GAP to create a gap of 49 pixels (experimentally determined).
We will now complete our update method, in which we will call the update methods for all five of these objects. In addition, we will use a similar logic to the one in the constructor above to reset our objects. The full code follows:
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);
pipe2 = new Pipe(pipe1.getTailX() + PIPE_GAP, 0, 22, 70, SCROLL_SPEED);
pipe3 = new Pipe(pipe2.getTailX() + PIPE_GAP, 0, 22, 60, SCROLL_SPEED);
The logic here is simple. Remember that our Scrollable objects' constructors want : x, y, width, height and scroll speed. We pass each of these in.
The backGrass object should be attached to the tail of the frontGrass, so we create it at the frontGrass' tail.
The Pipe objects follow a similar logic, except that we add the PIPE_GAP to create a gap of 49 pixels (experimentally determined).
We will now complete our update method, in which we will call the update methods for all five of these objects. In addition, we will use a similar logic to the one in the constructor above to reset our objects. The full code follows:
package com.kilobolt.gameobjects; 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; public ScrollHandler(float yPos) { 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); pipe2 = new Pipe(pipe1.getTailX() + PIPE_GAP, 0, 22, 70, SCROLL_SPEED); pipe3 = new Pipe(pipe2.getTailX() + PIPE_GAP, 0, 22, 60, SCROLL_SPEED); } 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 Grass getFrontGrass() { return frontGrass; } public Grass getBackGrass() { return backGrass; } public Pipe getPipe1() { return pipe1; } public Pipe getPipe2() { return pipe2; } public Pipe getPipe3() { return pipe3; } }
Our Scrollable objects' classes are now setup! Our next two steps will be as follows:
1. Create the ScrollHandler object inside the GameWorld (this will automatically create its five objects)
2. Render the ScrollHandlers' five objects inside the GameRenderer.
1. Create the ScrollHandler object inside the GameWorld (this will automatically create its five objects)
2. Render the ScrollHandlers' five objects inside the GameRenderer.
1. Creating the ScrollHandler object
Open up GameWorld. We are going to make a quick change.
- Add an instance variable (import accordingly):
private ScrollHandler scroller
- Initialize the scroller in the constructor (we include the Y coordinate of where the grass should begin, which is 66 pixels below the midPointY):
scroller = new ScrollHandler(midPointY + 66);
- Call the Scroller update method inside the GameWorld update method :
scroller.update(delta);
- Create a getter for the Scroller (return the ScrollHandler scroll).
The full code is:
- Add an instance variable (import accordingly):
private ScrollHandler scroller
- Initialize the scroller in the constructor (we include the Y coordinate of where the grass should begin, which is 66 pixels below the midPointY):
scroller = new ScrollHandler(midPointY + 66);
- Call the Scroller update method inside the GameWorld update method :
scroller.update(delta);
- Create a getter for the Scroller (return the ScrollHandler scroll).
The full code is:
package com.kilobolt.gameworld; import com.kilobolt.gameobjects.Bird; import com.kilobolt.gameobjects.ScrollHandler; public class GameWorld { private Bird bird; private ScrollHandler scroller; 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); } public void update(float delta) { bird.update(delta); scroller.update(delta); } public Bird getBird() { return bird; } public ScrollHandler getScroller() { return scroller; } }
2. Rendering the ScrollHandler's Objects
We begin by creating six instance variables:
1 for the ScrollHandler
5 for the 3 pipes + 2 grass objects.
- Add the following in BOLD below the declaration of the Bird object.
// Game Objects
private Bird bird;
private ScrollHandler scroller;
private Grass frontGrass, backGrass;
private Pipe pipe1, pipe2, pipe3;
- Import the following:import com.kilobolt.gameobjects.Bird;
import com.kilobolt.gameobjects.Grass;
import com.kilobolt.gameobjects.Pipe;
- Now we must initialize them in the initGameObjects method, which should now look like this:
1 for the ScrollHandler
5 for the 3 pipes + 2 grass objects.
- Add the following in BOLD below the declaration of the Bird object.
// Game Objects
private Bird bird;
private ScrollHandler scroller;
private Grass frontGrass, backGrass;
private Pipe pipe1, pipe2, pipe3;
- Import the following:import com.kilobolt.gameobjects.Bird;
import com.kilobolt.gameobjects.Grass;
import com.kilobolt.gameobjects.Pipe;
- Now we must initialize them in the initGameObjects method, which should now look like this:
private void initGameObjects() { bird = myWorld.getBird(); scroller = myWorld.getScroller(); frontGrass = scroller.getFrontGrass(); backGrass = scroller.getBackGrass(); pipe1 = scroller.getPipe1(); pipe2 = scroller.getPipe2(); pipe3 = scroller.getPipe3(); }
Next, we simply have to render these objects in our render method. As the Pipe object is not complete yet, we are going to just add some placeholder code that will change later. Let's create some helper methods so that we can keep the code neat:
The pixel values (widths/heights/etc) you see have been added after careful measurements. I thought you would prefer seeing hard coded values to deriving them yourself!
The pixel values (widths/heights/etc) you see have been added after careful measurements. I thought you would prefer seeing hard coded values to deriving them yourself!
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)); }
Now we just have to call these methods in the appropriate places in our render method. Here's the full code below:
I labeled the three changes inside the render method using 1... 2... 3.
I labeled the three changes inside the render method using 1... 2... 3.
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.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.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()); } batcher.end(); } }
We've made a lot of progress in Day 7! Let's see what our game looks like now:
Thank you very much for reading, and congratulations! We now have everything that we need to render. Time to start working on collision.
Join me in Day 8!
Join me in Day 8!
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_7.zip | |
File Size: | 10337 kb |
File Type: | zip |
Like us on Facebook to be informed as soon as the next lesson is available.
|
|
comments powered by Disqus