Kilobolt
  • Home
  • Tutorials
    • Game Development Tutorial >
      • Unit 1: Beginning Java >
        • Before you begin...
        • Day 1: Setting Up
        • Day 2: Java Basics
        • Day 3: More Basics
        • Day 4: Java Math
        • Day 5: More Math
        • Day 6: If... else...
        • Day 7: More Control Flow
        • Day 8: Looping
        • Day 9: More on Looping
        • Day 10: Inheritance, Interface
        • Day 11: Threads and Graphics
      • Unit 2: Creating a Game I >
        • Day 1: Foundations
        • Day 2: Basic Framework
        • Day 3: Taking User Input
        • Day 4: Enter the Robot
        • Day 5: Background and Sprites
        • Day 6: Adding Enemies
        • Day 7: Shooting Bullets
        • Day 8: Animations
        • Day 9: 2D-Arrays
        • Day 10: Painting the Tilemap
      • Unit 3: Creating a Game II >
        • Day 1: Level Creation - Part 1
        • Day 2: Level Creation - Part 2
        • Day 3: Level Creation - Part 3
        • Collision Detection Basics
        • Day 4: Collision Detection Part 1
        • Day 5: Collision Detection Part 2
        • Day 6: Collision Detection Part 3
        • Day 7: Health System & Death
        • Day 8: Basic AI & Final Touches
      • Unit 4: Android Game Development >
        • Day 1: Introduction to Android
        • Day 2: Setting up for Development
        • Day 3: Creating our First Android Application
        • Day 4: Parts of an Android Application
        • Day 5: The Android Game Framework: Part I
        • Day 6: The Android Game Framework: Part II
        • Create an Android Game From Scratch (or port your existing game)
        • Day 7: Creating an Android Game (From Start to Finish)
      • Reference Sheet
    • Zombie Bird Tutorial (Flappy Bird Remake) >
      • Unit 1: Building the Game >
        • Introduction
        • Day 1: Flappy Bird - An In-depth Analysis
        • Day 2: Setting up libGDX
        • Day 3: Understanding the libGDX Framework
        • Day 4: GameWorld and GameRenderer and the Orthographic Camera
        • Day 5: The Flight of the Dead - Adding the Bird
        • Day 6: Adding Graphics - Welcome to the Necropolis
        • Day 7: The Grass, the Bird and the Skull Pipe
        • Day 8: Collision Detection and Sound Effects
        • Day 9: Finishing Gameplay and Basic UI
        • Day 10: GameStates and High Score
        • Day 11: Supporting iOS/Android + SplashScreen, Menus and Tweening
        • Day 12: Completed UI & Source Code
    • Android Application Development Tutorial >
      • Unit 1: Writing Basic Android Apps >
        • Before you begin...
        • Day 1: Android 101
        • Day 2: Getting to Know the Android Project
        • Day 3: The Development Machine
        • Day 4: Building a Music App - Part 1: Building Blocks
        • Day 5: Building a Music App - Part 2: Intents
        • Day 6: Building a Music App - Part 3: Activity Lifecycles
  • Forum
  • About Us
    • Contact Us
  • Our Games
    • TUMBL: FallDown
  • Facebook
  • Twitter

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.

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):
        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.
Picture

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:
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:
Picture

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.
Picture

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.
Picture
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:
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):
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);


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.
    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:
Picture
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:
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:
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:

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 File

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.
Picture

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:
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!
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:
day_8.zip
File Size: 10277 kb
File Type: zip
Download File

Like us on Facebook to be informed as soon as the next lesson is available.
Go to Day 9!
comments powered by Disqus
© 2014 Kilobolt, LLC. All rights reserved.