LibGDX Zombie Bird Tutorial (Flappy Bird Clone/Remake)
Day 8 - Collision Detection and Sound Effects
Welcome back to your libGDX Flappy Bird tutorial! Today, we are going to add collision detection so that our game knows when our bird dies.
In Flappy Bird, death occurs in one of two ways. You either make contact with the ground or you hit one of the pipes.
We will be implementing the latter type of death in this tutorial (collision with a pipe), and we will be playing a sound effect when this happens! Let's get started. I'm picking up from where we left off in Day 7 of this series. If you want to follow along, feel free to download the source code on that page and come back.
In Flappy Bird, death occurs in one of two ways. You either make contact with the ground or you hit one of the pipes.
We will be implementing the latter type of death in this tutorial (collision with a pipe), and we will be playing a sound effect when this happens! Let's get started. I'm picking up from where we left off in Day 7 of this series. If you want to follow along, feel free to download the source code on that page and come back.
Fixing Some Errors
If you've been using my source code, we need to fix a small mistake. I accidentally copied the same if statement two times, so we must remove one.
Open up the Bird class, scroll down to the update method and remove one of these repeated if statements
(this may not be present in your own code):
Open up the Bird class, scroll down to the update method and remove one of these repeated if statements
(this may not be present in your own code):
if (velocity.y > 200) { velocity.y = 200; } if (velocity.y > 200) { velocity.y = 200; }
Discussing Collision
I originally thought about using a rotating polygon to handle collision, but after experimenting, I discovered that a circle is much easier to implement and equally as effective. So, we are going to go with the simpler solution.
The idea is that the circle will always be at centered at the same point. It does not need to rotate. This collision circle will always cover the bird's head. Because of the way the bird moves, the rear of the bird is extremely unlikely to make contact with the pipes, so we will not prioritize checking that area.
The Pipe object, in our game, comprises both the upper and lower pipes. Each of these pipes will be treated using two rectangles. One rectangle will cover the skull, the other rectangle will cover the base.
When checking for collision, we simply have to use the built-in Intersector class, which as a method for checking collision between rectangles and circles. Once this collision is detected, we will inform the our game that this happened, and we will tell all of our scrolling objects to stop.
The idea is that the circle will always be at centered at the same point. It does not need to rotate. This collision circle will always cover the bird's head. Because of the way the bird moves, the rear of the bird is extremely unlikely to make contact with the pipes, so we will not prioritize checking that area.
The Pipe object, in our game, comprises both the upper and lower pipes. Each of these pipes will be treated using two rectangles. One rectangle will cover the skull, the other rectangle will cover the base.
When checking for collision, we simply have to use the built-in Intersector class, which as a method for checking collision between rectangles and circles. Once this collision is detected, we will inform the our game that this happened, and we will tell all of our scrolling objects to stop.
Bird Class - Bounding Circle
We will begin by quickly creating the bounding Circle for our Bird. Open up the Bird class.
- Create an instance variable of Circle (import com.badlogic.gdx.math.Circle;) called boundingCircle.
private Circle boundingCircle;
- Initialize it in the constructor with the no values:
boundingCircle = new Circle();
We must set the circle's coordinates each time that the Bird moves. The Bird moves when we add the scaled velocity to the position, so add the following in bold directly below this line -> position.add(velocity.cpy().scl(delta)).
boundingCircle.set(position.x + 9, position.y + 6, 6.5f);
- Add a getter method for the boundingCircle.
Here is what your Bird class should look like:
- Create an instance variable of Circle (import com.badlogic.gdx.math.Circle;) called boundingCircle.
private Circle boundingCircle;
- Initialize it in the constructor with the no values:
boundingCircle = new Circle();
We must set the circle's coordinates each time that the Bird moves. The Bird moves when we add the scaled velocity to the position, so add the following in bold directly below this line -> position.add(velocity.cpy().scl(delta)).
boundingCircle.set(position.x + 9, position.y + 6, 6.5f);
- Add a getter method for the boundingCircle.
Here is what your Bird class should look like:
package com.kilobolt.gameobjects; import com.badlogic.gdx.math.Circle; import com.badlogic.gdx.math.Vector2; public class Bird { private Vector2 position; private Vector2 velocity; private Vector2 acceleration; private float rotation; private int width; private int height; 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(); } 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()) { 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; } public Circle getBoundingCircle() { return boundingCircle; } }
Next, we are going to make sure that our Circle is positioned correctly. Open up the GameRenderer and add these four lines of code at the very bottom of the render method. We are going to draw the boundingCircle.
shapeRenderer.begin(ShapeType.Filled);
shapeRenderer.setColor(Color.RED);
shapeRenderer.circle(bird.getBoundingCircle().x, bird.getBoundingCircle().y, bird.getBoundingCircle().radius);
shapeRenderer.end();
With that, our game will now look like this:
shapeRenderer.begin(ShapeType.Filled);
shapeRenderer.setColor(Color.RED);
shapeRenderer.circle(bird.getBoundingCircle().x, bird.getBoundingCircle().y, bird.getBoundingCircle().radius);
shapeRenderer.end();
With that, our game will now look like this:
Pipe Class - Bounding Rectangles
Now that we have the Bird's bounding circle, we must create the four Rectangle objects to represent each of our Pipe objects as shown below. Open the Pipe class.
Here's the Relevant Math
There's going to be a lot of seemingly arbitrary pixel width and height values in this section, so please refer to the diagram below. Also read the comments that I have made in my code carefully, so that you can follow my logic.
We are going to implement the above diagram in our code.
- Create the following instance variables (import com.badlogic.gdx.math.Rectangle).
private Rectangle skullUp, skullDown, barUp, barDown;
- We need a constant to represent the 45 pixel vertical gap between the upper and lower pipe, and some other constants:
public static final int VERTICAL_GAP = 45;
public static final int SKULL_WIDTH = 24;
public static final int SKULL_HEIGHT = 11;
- We also need to know where the ground is, so that we can provide a height for our Lower Bar (as shown in the diagram above). Create one more instance variable:
private float groundY;
- Make the below changes IN BOLD to the constructor so that we can initialize skullUp, skullDown, barUp, barDown and groundY:
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;
}
- Now add the getter methods for the four rectangle objects, so that we can access them when we check for collision:
- Create the following instance variables (import com.badlogic.gdx.math.Rectangle).
private Rectangle skullUp, skullDown, barUp, barDown;
- We need a constant to represent the 45 pixel vertical gap between the upper and lower pipe, and some other constants:
public static final int VERTICAL_GAP = 45;
public static final int SKULL_WIDTH = 24;
public static final int SKULL_HEIGHT = 11;
- We also need to know where the ground is, so that we can provide a height for our Lower Bar (as shown in the diagram above). Create one more instance variable:
private float groundY;
- Make the below changes IN BOLD to the constructor so that we can initialize skullUp, skullDown, barUp, barDown and groundY:
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;
}
- Now add the getter methods for the four rectangle objects, so that we can access them when we check for collision:
public Rectangle getSkullUp() { return skullUp; } public Rectangle getSkullDown() { return skullDown; } public Rectangle getBarUp() { return barUp; } public Rectangle getBarDown() { return barDown; }
As with the Bird's boundingCircle, we must update each of the four rectangles whenever our Pipe object's position is updated. At the moment, our Pipe does not have an update method. It is inheriting the update method from the Scrollable class. We could try to update our four rectangles in our Scrollable class, but the easier way to do this is to @Override the update method (review what this means in Day 7 if you have forgotten).
By calling super, we are calling the original update method that belongs to the Scrollable class. Any code that follows the super call is additional functionality. In this case, we simply update our four rectangles.
With the Math and measurements described in the image above, here is how we would update our upper bar and the lower bar rectangles (this is what your Pipe class should now look like):
By calling super, we are calling the original update method that belongs to the Scrollable class. Any code that follows the super call is additional functionality. In this case, we simply update our four rectangles.
With the Math and measurements described in the image above, here is how we would update our upper bar and the lower bar rectangles (this is what your Pipe class should now look like):
package com.kilobolt.gameobjects; import java.util.Random; 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; // 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; } public Rectangle getSkullUp() { return skullUp; } public Rectangle getSkullDown() { return skullDown; } public Rectangle getBarUp() { return barUp; } public Rectangle getBarDown() { return barDown; } }
Updating the ScrollHandler
We have changed the constructor for our Pipe objects, so we must include the additional argument whenever we create a Pipe object in ScrollHandler. Change the following lines:
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);
To this:
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);
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);
To this:
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);
Now let's go back to the GameRenderer and add some lines of code at the end of the Render method.
This is just temporary code for testing. Just copy and paste as shown below.
This is just temporary code for testing. Just copy and paste as shown below.
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(); shapeRenderer.begin(ShapeType.Filled); shapeRenderer.setColor(Color.RED); shapeRenderer.circle(bird.getBoundingCircle().x, bird.getBoundingCircle().y, bird.getBoundingCircle().radius); /* * Excuse the mess below. Temporary code for testing bounding * rectangles. */ // Bar up for pipes 1 2 and 3 shapeRenderer.rect(pipe1.getBarUp().x, pipe1.getBarUp().y, pipe1.getBarUp().width, pipe1.getBarUp().height); shapeRenderer.rect(pipe2.getBarUp().x, pipe2.getBarUp().y, pipe2.getBarUp().width, pipe2.getBarUp().height); shapeRenderer.rect(pipe3.getBarUp().x, pipe3.getBarUp().y, pipe3.getBarUp().width, pipe3.getBarUp().height); // Bar down for pipes 1 2 and 3 shapeRenderer.rect(pipe1.getBarDown().x, pipe1.getBarDown().y, pipe1.getBarDown().width, pipe1.getBarDown().height); shapeRenderer.rect(pipe2.getBarDown().x, pipe2.getBarDown().y, pipe2.getBarDown().width, pipe2.getBarDown().height); shapeRenderer.rect(pipe3.getBarDown().x, pipe3.getBarDown().y, pipe3.getBarDown().width, pipe3.getBarDown().height); // Skull up for Pipes 1 2 and 3 shapeRenderer.rect(pipe1.getSkullUp().x, pipe1.getSkullUp().y, pipe1.getSkullUp().width, pipe1.getSkullUp().height); shapeRenderer.rect(pipe2.getSkullUp().x, pipe2.getSkullUp().y, pipe2.getSkullUp().width, pipe2.getSkullUp().height); shapeRenderer.rect(pipe3.getSkullUp().x, pipe3.getSkullUp().y, pipe3.getSkullUp().width, pipe3.getSkullUp().height); // Skull down for Pipes 1 2 and 3 shapeRenderer.rect(pipe1.getSkullDown().x, pipe1.getSkullDown().y, pipe1.getSkullDown().width, pipe1.getSkullDown().height); shapeRenderer.rect(pipe2.getSkullDown().x, pipe2.getSkullDown().y, pipe2.getSkullDown().width, pipe2.getSkullDown().height); shapeRenderer.rect(pipe3.getSkullDown().x, pipe3.getSkullDown().y, pipe3.getSkullDown().width, pipe3.getSkullDown().height); shapeRenderer.end(); }
Next, we run our game! This is what it should look like:
We have the necessary bounding boxes to test for collision. Now we simply have to add the logic.
Checking for Collision
Checking for collision requires multiple classes working together.
- The ScrollHandler has access to all the Pipes and their bounding rectangles, so it should be the one doing the actual collision checking.
- The GameWorld needs to know when the collision occurs, so that it can handle the collision (get current score, make the bird stop, play sound, etc).
- The GameRenderer, once the Bird dies, needs to be able to react (show the score, display a flash).
We will start in the GameWorld class. We will add the following in bold to our update method.
public void update(float delta) {
bird.update(delta);
scroller.update(delta);
if (scroller.collides(bird)) {
// Clean up on game over
scroller.stop();
}
}
We now need to create two methods in our ScrollHandler: the stop and the collides. Let's go to the ScrollHandler class and add the following methods:
public void stop() {
frontGrass.stop();
backGrass.stop();
pipe1.stop();
pipe2.stop();
pipe3.stop();}
// Return true if ANY pipe hits the bird.
public boolean collides(Bird bird) {
return (pipe1.collides(bird) || pipe2.collides(bird) || pipe3.collides(bird));
}
This is the full class:
- The ScrollHandler has access to all the Pipes and their bounding rectangles, so it should be the one doing the actual collision checking.
- The GameWorld needs to know when the collision occurs, so that it can handle the collision (get current score, make the bird stop, play sound, etc).
- The GameRenderer, once the Bird dies, needs to be able to react (show the score, display a flash).
We will start in the GameWorld class. We will add the following in bold to our update method.
public void update(float delta) {
bird.update(delta);
scroller.update(delta);
if (scroller.collides(bird)) {
// Clean up on game over
scroller.stop();
}
}
We now need to create two methods in our ScrollHandler: the stop and the collides. Let's go to the ScrollHandler class and add the following methods:
public void stop() {
frontGrass.stop();
backGrass.stop();
pipe1.stop();
pipe2.stop();
pipe3.stop();}
// Return true if ANY pipe hits the bird.
public boolean collides(Bird bird) {
return (pipe1.collides(bird) || pipe2.collides(bird) || pipe3.collides(bird));
}
This is the full class:
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, 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) { return (pipe1.collides(bird) || pipe2.collides(bird) || pipe3 .collides(bird)); } 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, to fix errors, we need to make two changes. Our Scrollable objects (Pipe and Grass) need to be able to stop, so we will create a stop method inside the Scrollable Class.
- Open up Scrollable and add the following method:
public void stop() {
velocity.x = 0;
}
- Next, our Pipe objects need to individually check if they collided with the Bird, so we need to add that method. Open up the Pipe class, and add the following method:
- Open up Scrollable and add the following method:
public void stop() {
velocity.x = 0;
}
- Next, our Pipe objects need to individually check if they collided with the Bird, so we need to add that method. Open up the Pipe class, and add the following method:
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; }
In this method, we begin by checking if the position.x is less than bird.getX + bird.getWidth, because otherwise, collision is impossible. This is a very cheap check (it does not take much toll on performance). Most of the time, this condition will fail, and we will not have to perform the more expensive checks.
If this if statement does evalute to true, we do the much more expensive Intersector.overlaps() calls (which returns true if the circle argument object collides with a rectangle argument object). We will return true if any of the four rectangle collides with the bird's circle.
Here are the updated Pipe and Scrollable classes:
If this if statement does evalute to true, we do the much more expensive Intersector.overlaps() calls (which returns true if the circle argument object collides with a rectangle argument object). We will return true if any of the four rectangle collides with the bird's circle.
Here are the updated Pipe and Scrollable classes:
Updated Pipe Class
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; // 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; } 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; } }
Updated Scrollable class
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; } public void stop() { velocity.x = 0; } // 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; } }
Testing our code!
Now that we have completed all the collision checking code, run your game to make sure that it's working! One the Bird collides with one of the pipes, all the scrolling should stop. We will do some more sophisticated death handling in Day 9. For now, let's try playing a sound file when the bird dies.
Download the Sound File
Here's a sound file that I created using bfxr.

dead.wav | |
File Size: | 16 kb |
File Type: | wav |
Download this file into your ZombieBird-android project's assets/data folder.
Make sure that you COPY it in there, do NOT create a LINK.
Make sure that you COPY it in there, do NOT create a LINK.
Updating the AssetLoader
Now that we have a sound file, we must create a Sound object using this file in our AssetLoader. Sound objects are stored in memory and can be loaded once and used quickly (this is appropriate for short sound files, which have a very small file size).
- Create this instance variable in AssetLoader:
public static Sound dead;
- Initialize it as below in the load method:
dead = Gdx.audio.newSound(Gdx.files.internal("data/dead.wav"));
Here's the full class:
- Create this instance variable in AssetLoader:
public static Sound dead;
- Initialize it as below in the load method:
dead = Gdx.audio.newSound(Gdx.files.internal("data/dead.wav"));
Here's the full class:
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.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; public static void load() { texture = new Texture(Gdx.files.internal("data/texture.png")); texture.setFilter(TextureFilter.Nearest, TextureFilter.Nearest); bg = new TextureRegion(texture, 0, 0, 136, 43); bg.flip(false, true); grass = new TextureRegion(texture, 0, 43, 143, 11); grass.flip(false, true); birdDown = new TextureRegion(texture, 136, 0, 17, 12); birdDown.flip(false, true); bird = new TextureRegion(texture, 153, 0, 17, 12); bird.flip(false, true); birdUp = new TextureRegion(texture, 170, 0, 17, 12); birdUp.flip(false, true); TextureRegion[] birds = { birdDown, bird, birdUp }; birdAnimation = new Animation(0.06f, birds); birdAnimation.setPlayMode(Animation.PlayMode.LOOP_PINGPONG); skullUp = new TextureRegion(texture, 192, 0, 24, 14); // Create by flipping existing skullUp skullDown = new TextureRegion(skullUp); skullDown.flip(false, true); bar = new TextureRegion(texture, 136, 16, 22, 3); bar.flip(false, true); dead = Gdx.audio.newSound(Gdx.files.internal("data/dead.wav")); } public static void dispose() { // We must dispose of the texture when we are finished. texture.dispose(); } }
Playing the Sound File
Now that we have our Sound object, we can play it in our game. Open up the GameWorld, and create the following instance variable:
private boolean isAlive = true;
Then make the following changes to the update method:
public void update(float delta) {
bird.update(delta);
scroller.update(delta);
if (isAlive && scroller.collides(bird)) {
scroller.stop();
AssetLoader.dead.play();
isAlive = false;
}
}
Now, when your Bird dies, it will play a sound file (just once)! Here's the updated GameWorld. Try running the game!
private boolean isAlive = true;
Then make the following changes to the update method:
public void update(float delta) {
bird.update(delta);
scroller.update(delta);
if (isAlive && scroller.collides(bird)) {
scroller.stop();
AssetLoader.dead.play();
isAlive = false;
}
}
Now, when your Bird dies, it will play a sound file (just once)! Here's the updated GameWorld. Try running the game!
package com.kilobolt.gameworld; 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 boolean isAlive = true; 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); if (scroller.collides(bird) && isAlive) { scroller.stop(); AssetLoader.dead.play(); isAlive = false; } } public Bird getBird() { return bird; } public ScrollHandler getScroller() { return scroller; } }
Finally, you can remove all the red rectangles and circles from the GameRenderer, now that we know collision is working properly. Here's what your GameRenderer should look like:
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're almost done!
Our game is almost fully implemented. In Day 9, we will finish up the rest of the gameplay and start adding some UI. Let's finish strong!
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_8.zip | |
File Size: | 10277 kb |
File Type: | zip |
Like us on Facebook to be informed as soon as the next lesson is available.
|
|
comments powered by Disqus