Chapter 11: Collision Detection, Pickups, and Bullets

So far, we have implemented the main visual aspects of our game. We have a controllable character running around in an arena full of zombies that chase them. The problem is that they don't interact with each other. A zombie can wander right through the player without leaving a scratch. We need to detect collisions between the zombies and the player.

If the zombies are going to be able to injure and eventually kill the player, it is only fair that we give the player some bullets for their gun. We will then need to make sure that the bullets can hit and kill the zombies.

At the same time, if we are writing collision detection code for bullets, zombies, and the player, it would be a good time to add a class for health and ammo pickups as well.

Here is what we will do and the order in which we will cover things in this chapter:

  • Shooting Bullets
  • Adding a crosshair and hiding the mouse pointer
  • Spawning pickups
  • Detecting collisions

Let's start with the Bullet class.

Coding the Bullet class

We will use the SFML RectangleShape class to visually represent a bullet. We will code a Bullet class that has a RectangleShape member, as well as other member data and functions. Then, we will add bullets to our game in a few steps, as follows:

  1. First, we will code the Bullet.h file. This will reveal all the details of the member data and the prototypes for the functions.
  2. Next, we will code the Bullet.cpp file, which, of course, will contain the definitions for all the functions of the Bullet class. As we step through this, I will explain exactly how an object of the Bullet type will work and be controlled.
  3. Finally, we will declare a whole array full of bullets in the main function. We will also implement a control scheme for shooting, managing the player's remaining ammo, and reloading.

Let's get started with step 1.

Coding the Bullet header file

To make the new header file, right-click Header Files in the Solution Explorer and select Add | New Item.... In the Add New Item window, highlight (by left-clicking) Header File (.h), and then, in the Name field, type Bullet.h.

Add the following private member variables, along with the Bullet class declaration, to the Bullet.h file. We can then run through them and explain what they are for:

#pragma once

#include <SFML/Graphics.hpp>

using namespace sf;

class Bullet

{

private:

    // Where is the bullet?

    Vector2f m_Position;

    // What each bullet looks like

    RectangleShape m_BulletShape;

    // Is this bullet currently whizzing through the air

    bool m_InFlight = false;

    // How fast does a bullet travel?

    float m_BulletSpeed = 1000;

    // What fraction of 1 pixel does the bullet travel,

    // Horizontally and vertically each frame?

    // These values will be derived from m_BulletSpeed

    float m_BulletDistanceX;

    float m_BulletDistanceY;

    

    // Some boundaries so the bullet doesn't fly forever

    float m_MaxX;

    float m_MinX;

    float m_MaxY;

    float m_MinY;

// Public function prototypes go here

};

In the previous code, the first member is a Vector2f called m_Position, which will hold the bullet's location in the game world.

Next, we declare a RectangleShape called m_BulletShape as we are using a simple non-texture graphic for each bullet, a bit like we did for the time-bar in Timber!!!.

The code then declares a Boolean, m_InFlight, which will keep track of whether the bullet is currently whizzing through the air or not. This will allow us to decide whether we need to call its update function each frame and whether we need to run collision detection checks.

The float variable, m_BulletSpeed, will (you can probably guess) hold the speed in pixels per second that the bullet will travel at. It is initialized to the value of 1000, which is a little arbitrary, but it works well.

Next, we have two more float variables, m_BulletDistanceX and m_BulletDistanceY. As the calculations to move a bullet are a little more complex than those used to move a zombie or the player, we will benefit from having these two variables, which we will perform calculations on. They will be used to decide the horizontal and vertical changes in the bullet's position in each frame.

Finally, we have four more float variables (m_MaxX, m_MinX, m_MaxY, and m_MinY), which will later be initialized to hold the maximum and minimum and horizontal and vertical positions for the bullet.

It is likely that the need for some of these variables is not immediately apparent, but it will become clearer when we see each of them in action in the Bullet.cpp file.

Now, add all the public function prototypes to the Bullet.h file:

// Public function prototypes go here

public:

    // The constructor

    Bullet();

    // Stop the bullet

    void stop();

    // Returns the value of m_InFlight

    bool isInFlight();

    // Launch a new bullet

    void shoot(float startX, float startY,

        float xTarget, float yTarget);

    // Tell the calling code where the bullet is in the world

    FloatRect getPosition();

    // Return the actual shape (for drawing)

    RectangleShape getShape();

    // Update the bullet each frame

    void update(float elapsedTime);

};

Let's run through each of the functions in turn, and then we can move on to coding their definitions.

First, we have the Bullet function, which is, of course, the constructor. In this function, we will set up each Bullet instance, ready for action.

The stop function will be called when the bullet has been in action but needs to stop.

The isInFlight function returns a Boolean and will be used to test whether a bullet is currently in flight or not.

The shoot function's use is given away by its name, but how it will work deserves some discussion. For now, just note that it has four float parameters that will be passed in. The four values represent the starting (where the player is) horizontal and vertical position of the bullet, as well as the vertical and horizontal target position (where the crosshair is).

The getPosition function returns a FloatRect that represents the location of the bullet. This function will be used to detect collisions with zombies. You might remember from Chapter 10, Pointers, the Standard Template Library, and Texture Management, that zombies also had a getPosition function.

Following on, we have the getShape function, which returns an object of the RectangleShape type. As we have discussed, each bullet is represented visually by a RectangleShape object. The getShape function, therefore, will be used to grab a copy of the current state of RectangleShape in order to draw it.

Finally, and hopefully as expected, there is the update function, which has a float parameter that represents the fraction of a second that has passed since the last time update was called. The update method will change the position of the bullet each frame.

Let's look at and code the function definitions.

Coding the Bullet source file

Now, we can create a new .cpp file that will contain the function definitions. Right-click Source Files in the Solution Explorer and select Add | New Item.... In the Add New Item window, highlight (by left-clicking) C++ File (.cpp), and then, in the Name field, type Bullet.cpp. Finally, click the Add button. We are now ready to code the class.

Add the following code, which is for the include directives and the constructor. We know it is a constructor because the function has the same name as the class:

#include "bullet.h"

// The constructor

Bullet::Bullet()

{

    m_BulletShape.setSize(sf::Vector2f(2, 2));

}

The only thing that the Bullet constructor needs to do is set the size of m_BulletShape, which is the RectangleShape object. The code sets the size to two pixels by two pixels.

Next, we will code the more substantial shoot function. Add the following code to the Bullet.cpp file and study it, and then we can talk about it:

void Bullet::shoot(float startX, float startY,

    float targetX, float targetY)

{

    // Keep track of the bullet

    m_InFlight = true;

    m_Position.x = startX;

    m_Position.y = startY;

    // Calculate the gradient of the flight path

    float gradient = (startX - targetX) / (startY - targetY);

    // Any gradient less than 1 needs to be negative

    if (gradient < 0)

    {

        gradient *= -1;

    }

    // Calculate the ratio between x and y

    float ratioXY = m_BulletSpeed / (1 + gradient);

    // Set the "speed" horizontally and vertically

    m_BulletDistanceY = ratioXY;

    m_BulletDistanceX = ratioXY * gradient;

    

    // Point the bullet in the right direction

    if (targetX < startX)

    {

        m_BulletDistanceX *= -1;

    }

    if (targetY < startY)

    {

        m_BulletDistanceY *= -1;

    }

    

    // Set a max range of 1000 pixels

    float range = 1000;

    m_MinX = startX - range;

    m_MaxX = startX + range;

    m_MinY = startY - range;

    m_MaxY = startY + range;

    

    // Position the bullet ready to be drawn

    m_BulletShape.setPosition(m_Position);

}

In order to demystify the shoot function, we will split it up and talk about the code we have just added in chunks.

First, let's remind ourselves about the signature. The shoot function receives the starting and target horizontal and vertical positions of a bullet. The calling code will supply these based on the position of the player sprite and the position of the crosshair. Here it is again:

void Bullet::shoot(float startX, float startY,

    float targetX, float targetY)

Inside the shoot function, we set m_InFlight to true and position the bullet using the startX and startY parameters. Here is that piece of code again:

// Keep track of the bullet

m_InFlight = true;

m_Position.x = startX;

m_Position.y = startY;

Now, we use a bit of trigonometry to determine the gradient of travel for a bullet. The progression of the bullet, both horizontally and vertically, must vary based on the slope of the line that's created by drawing between the start and target of a bullet. The rate of change cannot be the same or very steep shots will arrive at the horizontal location before the vertical location, and vice versa for shallow shots.

The code that follows derives the gradient based on the equation of a line. Then, it checks whether the gradient is less than zero and if it is, multiplies it by -1. This is because the start and target coordinates that are passed in can be negative or positive, and we always want the amount of progression each frame to be positive. Multiplying by -1 simply makes the negative number into its positive equivalent because a minus multiplied by a minus gives a positive. The actual direction of travel will be handled in the update function by adding or subtracting the positive values we arrive at in this function.

Next, we calculate a ratio of horizontal to vertical distance by dividing our bullet's speed (m_BulletSpeed) by one, plus the gradient. This will allow us to change the bullet's horizontal and vertical position by the correct amount each frame, based on the target the bullet is heading toward.

Finally, in this part of the code, we assign the values to m_BulletDistanceY and m_BulletDistanceX:

// Calculate the gradient of the flight path

float gradient = (startX - targetX) / (startY - targetY);

// Any gradient less than zero needs to be negative

if (gradient < 0)

{

    gradient *= -1;

}

// Calculate the ratio between x and y

float ratioXY = m_BulletSpeed / (1 + gradient);

// Set the "speed" horizontally and vertically

m_BulletDistanceY = ratioXY;

m_BulletDistanceX = ratioXY * gradient;

The following code is much more straightforward. We simply set a maximum horizontal and vertical location that the bullet can reach. We don't want a bullet carrying on forever. In the update function, we will see whether a bullet has passed its maximum or minimum locations:

// Set a max range of 1000 pixels in any direction

float range = 1000;

m_MinX = startX - range;

m_MaxX = startX + range;

m_MinY = startY - range;

m_MaxY = startY + range;

The following code moves the sprite that represents the bullet to its starting location. We use the setPosition function of Sprite, as we have often done before:

// Position the bullet ready to be drawn

m_BulletShape.setPosition(m_Position);

Next, we have four straightforward functions. Let's add the stop, isInFlight, getPosition, and getShape functions:

void Bullet::stop()

{

    m_InFlight = false;

}

bool Bullet::isInFlight()

{

    return m_InFlight;

}

FloatRect Bullet::getPosition()

{

    return m_BulletShape.getGlobalBounds();

}

RectangleShape Bullet::getShape()

{

    return m_BulletShape;

}

The stop function simply sets the m_InFlight variable to false. The isInFlight function returns whatever the value of this same variable currently is. So, we can see that shoot sets the bullet going, stop makes it stop, and isInFlight informs us what the current state is.

The getPosition function returns a FloatRect. We will see how we can use the FloatRect from each game object to detect collisions soon.

Finally, for the previous code, getShape returns a RectangleShape so that we can draw the bullet once each frame.

The last function we need to implement before we can start using Bullet objects is update. Add the following code, study it, and then we can talk about it:

void Bullet::update(float elapsedTime)

{

    // Update the bullet position variables

    m_Position.x += m_BulletDistanceX * elapsedTime;

    m_Position.y += m_BulletDistanceY * elapsedTime;

    // Move the bullet

    m_BulletShape.setPosition(m_Position);

    // Has the bullet gone out of range?

    if (m_Position.x < m_MinX || m_Position.x > m_MaxX ||

        m_Position.y < m_MinY || m_Position.y > m_MaxY)

    {

        m_InFlight = false;

    }

}

In the update function, we use m_BulletDistanceX and m_BulletDistanceY, multiplied by the time since the last frame to move the bullet. Remember that the values of the two variables were calculated in the shoot function and represent the gradient (ratio to each other) that's required to move the bullet at just the right angle. Then, we use the setPosition function to actually move RectangleShape.

The last thing we do in update is a test to see whether the bullet has moved beyond its maximum range. The slightly convoluted if statement checks m_Position.x and m_Position.y against the maximum and minimum values that were calculated in the shoot function. These maximum and minimum values are stored in m_MinX, m_MaxX, m_MinY, and m_MaxY. If the test is true, then m_InFlight is set to false.

The Bullet class is done. Now, we will look at how we can shoot some in the main function.

Making the bullets fly

We will make the bullets usable by following these six steps:

  1. Add the necessary include directive for the Bullet class.
  2. Add some control variables and an array to hold some Bullet instances.
  3. Handle the player pressing R to reload.
  4. Handle the player pressing the left mouse button to fire a bullet.
  5. Update all bullets that are in flight in each frame.
  6. Draw the bullets that are in flight in each frame.

Including the Bullet class

Add the include directive to make the Bullet class available:

#include <SFML/Graphics.hpp>

#include "ZombieArena.h"

#include "Player.h"

#include "TextureHolder.h"

#include "Bullet.h"

using namespace sf;

Let's move on to the next step.

Control variables and the bullet array

Here are some variables to keep track of clip sizes, spare bullets, bullets, the remaining bullets in the clip, the current rate of fire (starting at one per second), and the time when the last bullet was fired.

Add the following highlighted code. Then, we can move on and see all these variables in action throughout the rest of this section:

// Prepare for a horde of zombies

int numZombies;

int numZombiesAlive;

Zombie* zombies = NULL;

// 100 bullets should do

Bullet bullets[100];

int currentBullet = 0;

int bulletsSpare = 24;

int bulletsInClip = 6;

int clipSize = 6;

float fireRate = 1;

// When was the fire button last pressed?

Time lastPressed;

// The main game loop

while (window.isOpen())

Next, let's handle what happens when the player presses the R keyboard key, which is used for reloading a clip.

Reloading the gun

Now, we will handle the player input related to shooting bullets. First, we will handle pressing the R key to reload the gun. We will do so with an SFML event.

Add the following highlighted code. It is shown with lots of context to make sure the code goes in the right place. Study the code and then we can talk about it:

// Handle events

Event event;

while (window.pollEvent(event))

{

    if (event.type == Event::KeyPressed)

    {

        // Pause a game while playing

        if (event.key.code == Keyboard::Return &&

            state == State::PLAYING)

        {

            state = State::PAUSED;

        }

        // Restart while paused

        else if (event.key.code == Keyboard::Return &&

            state == State::PAUSED)

        {

            state = State::PLAYING;

            // Reset the clock so there isn't a frame jump

            clock.restart();

        }

        // Start a new game while in GAME_OVER state

        else if (event.key.code == Keyboard::Return &&

            state == State::GAME_OVER)

        {

            state = State::LEVELING_UP;

        }

        if (state == State::PLAYING)

        {

            // Reloading

            if (event.key.code == Keyboard::R)

            {

                if (bulletsSpare >= clipSize)

                {

                    // Plenty of bullets. Reload.

                    bulletsInClip = clipSize;

                    bulletsSpare -= clipSize;                            

                }

                else if (bulletsSpare > 0)

                {

                    // Only few bullets left

                    bulletsInClip = bulletsSpare;

                    bulletsSpare = 0;                            

                }

                else

                {

                    // More here soon?!

                }

            }

        }

    }

}// End event polling

The previous code is nested within the event handling part of the game loop (while(window.pollEvent)), within the block that only executes when the game is actually being played (if(state == State::Playing)). It is obvious that we don't want the player reloading when the game has finished or is paused, and wrapping the new code as we've described achieves this.

In the new code itself, the first thing we do is test for the R key being pressed with if (event.key.code == Keyboard::R). Once we have detected that the R key was pressed, the remaining code is executed. Here is the structure of the if, else if, and else blocks:

if(bulletsSpare >= clipSize)

    ...

else if(bulletsSpare > 0)

    ...

else

    ...

The previous structure allows us to handle three possible scenarios, as shown here:

  • The player has pressed R and they have more bullets spare than the clip can take. In this scenario, the clip is refilled, and the number of spare bullets is reduced.
  • The player has some spare bullets but not enough to fill the clip completely. In this scenario, the clip is filled with as many spare bullets as the player has and the number of spare bullets is set to zero.
  • The player has pressed R but they have no spare bullets at all. For this scenario, we don't actually need to alter the variables. However, we will play a sound effect here when we implement the sound in Chapter 13, Sound Effects, File I/O, and Finishing the Game, so we will leave the empty else block ready.

Now, let's shoot a bullet.

Shooting a bullet

Here, we will handle the left mouse button being clicked to fire a bullet. Add the following highlighted code and study it carefully:

    if (Keyboard::isKeyPressed(Keyboard::D))

    {

        player.moveRight();

    }

    else

    {

        player.stopRight();

    }

    // Fire a bullet

    if (Mouse::isButtonPressed(sf::Mouse::Left))

    {

        if (gameTimeTotal.asMilliseconds()

            - lastPressed.asMilliseconds()

            > 1000 / fireRate && bulletsInClip > 0)

        {

            // Pass the centre of the player

            // and the centre of the cross-hair

            // to the shoot function

            bullets[currentBullet].shoot(

                player.getCenter().x, player.getCenter().y,

                mouseWorldPosition.x, mouseWorldPosition.y);

            currentBullet++;

            if (currentBullet > 99)

            {

                currentBullet = 0;

            }

            lastPressed = gameTimeTotal;

            bulletsInClip--;

        }

    }// End fire a bullet

}// End WASD while playing

All the previous code is wrapped in an if statement that executes whenever the left mouse button is pressed, that is, if (Mouse::isButtonPressed(sf::Mouse::Left)). Note that the code will execute repeatedly, even if the player just holds down the button. The code we will go through now controls the rate of fire.

In the preceding code, we then check whether the total time elapsed in the game (gameTimeTotal) minus the time the player last shot a bullet (lastPressed) is greater than 1,000, divided by the current rate of fire and that the player has at least one bullet in the clip. We use 1,000 because this is the number of milliseconds in a second.

If this test is successful, the code that actually fires a bullet is executed. Shooting a bullet is easy because we did all the hard work in the Bullet class. We simply call shoot on the current bullet from the bullets array. We pass in the player's and the cross-hair's current horizontal and vertical locations. The bullet will be configured and set in flight by the code in the shoot function of the Bullet class.

All we must do is keep track of the array of bullets. We incremented the currentBullet variable. Then, we need to check to see whether we fired the last bullet (99) with the if (currentBullet > 99) statement. If it was the last bullet, we set currentBullet to zero. If it wasn't the last bullet, then the next bullet is ready to go whenever the rate of fire permits it and the player presses the left mouse button.

Finally, in the preceding code, we store the time that the bullet was fired into lastPressed and decrement bulletsInClip.

Now, we can update every bullet, each frame.

Updating the bullets each frame

Add the following highlighted code to loop through the bullets array, check whether the bullet is in flight, and if it is, call its update function:

    // Loop through each Zombie and update them

    for (int i = 0; i < numZombies; i++)

    {

        if (zombies[i].isAlive())

        {

            zombies[i].update(dt.asSeconds(), playerPosition);

        }

    }

    // Update any bullets that are in-flight

    for (int i = 0; i < 100; i++)

    {

        if (bullets[i].isInFlight())

        {

            bullets[i].update(dtAsSeconds);

        }

    }

}// End updating the scene

Finally, we will draw all the bullets.

Drawing the bullets each frame

Add the following highlighted code to loop through the bullets array, check whether the bullet is in flight, and if it is, draw it:

/*

 **************

 Draw the scene

 **************

 */

if (state == State::PLAYING)

{

    window.clear();

    // set the mainView to be displayed in the window

    // And draw everything related to it

    window.setView(mainView);

    // Draw the background

    window.draw(background, &textureBackground);

    // Draw the zombies

    for (int i = 0; i < numZombies; i++)

    {

        window.draw(zombies[i].getSprite());

    }

    for (int i = 0; i < 100; i++)

    {

        if (bullets[i].isInFlight())

        {

            window.draw(bullets[i].getShape());

        }

    }

    // Draw the player

    window.draw(player.getSprite());

}

Run the game to try out the bullets. Notice that you can fire six shots before you need to press R to reload. The obvious things that are missing is some visual indicator of the number of bullets in the clip and the number of spare bullets. Another problem is that the player can very quickly run out of bullets, especially since the bullets have no stopping power whatsoever. They fly straight through the zombies. Add to this that the player is expected to aim at a mouse pointer instead of a precision crosshair and it is clear that we have work to do.

In the next chapter, we will give visual feedback through a HUD. We will replace the mouse cursor with a crosshair next and then spawn some pickups to replenish bullets and health after that. Finally, in this chapter, we will handle collision detection to make the bullets and the zombies do damage and make the player able to actually get the pickups.

Giving the player a crosshair

Adding a crosshair is easy and only requires one new concept. Add the following highlighted code, and then we can run through it:

// 100 bullets should do

Bullet bullets[100];

int currentBullet = 0;

int bulletsSpare = 24;

int bulletsInClip = 6;

int clipSize = 6;

float fireRate = 1;

// When was the fire button last pressed?

Time lastPressed;

// Hide the mouse pointer and replace it with crosshair

window.setMouseCursorVisible(true);

Sprite spriteCrosshair;

Texture textureCrosshair = TextureHolder::GetTexture("graphics/crosshair.png");

spriteCrosshair.setTexture(textureCrosshair);

spriteCrosshair.setOrigin(25, 25);

// The main game loop

while (window.isOpen())

First, we call the setMouseCursorVisible function on our window object. We then load a Texture and declare a Sprite instance and initialize it in the usual way. Furthermore, we set the sprite's origin to its center to make it convenient and simpler to make the bullets fly to the middle, as you would expect to happen.

Now, we need to update the crosshair each frame with the world coordinates of the mouse. Add the following highlighted line of code, which uses the mouseWorldPosition vector to set the crosshair's position each frame:

/*

 ****************

 UPDATE THE FRAME

 ****************

 */

if (state == State::PLAYING)

{

    // Update the delta time

    Time dt = clock.restart();

    // Update the total game time

    gameTimeTotal += dt;

    // Make a decimal fraction of 1 from the delta time

    float dtAsSeconds = dt.asSeconds();

    // Where is the mouse pointer

    mouseScreenPosition = Mouse::getPosition();

    // Convert mouse position to world coordinates of mainView

    mouseWorldPosition = window.mapPixelToCoords(

        Mouse::getPosition(), mainView);

    // Set the crosshair to the mouse world location

    spriteCrosshair.setPosition(mouseWorldPosition);

    // Update the player

    player.update(dtAsSeconds, Mouse::getPosition());

Next, as you have probably come to expect, we can draw the crosshair each frame. Add the following highlighted line of code in the position shown. This line of code needs no explanation, but its position after all the other game objects is important, so it is drawn on top:

/*

 **************

 Draw the scene

 **************

 */

if (state == State::PLAYING)

{

    window.clear();

    // set the mainView to be displayed in the window

    // And draw everything related to it

    window.setView(mainView);

    // Draw the background

    window.draw(background, &textureBackground);

    // Draw the zombies

    for (int i = 0; i < numZombies; i++)

    {

        window.draw(zombies[i].getSprite());

    }

    for (int i = 0; i < 100; i++)

    {

        if (bullets[i].isInFlight())

        {

            window.draw(bullets[i].getShape());

        }

    }

    // Draw the player

    window.draw(player.getSprite());

    //Draw the crosshair

    window.draw(spriteCrosshair);

}

Now, you can run the game and will see a cool crosshair instead of a mouse cursor:

Notice how the bullet fires neatly through the center of the crosshair. The way the shooting mechanism works is analogous to allowing the player to choose to shoot from the hip or aim down the sights. If the player keeps the crosshair close to the center, they can fire and turn rapidly, yet must carefully judge the position of distant zombies.

Alternatively, the player can hover their crosshair directly over the head of a distant zombie and score a precise hit; however, they then have much further to move the crosshair back if a zombie attacks from another direction.

An interesting improvement to the game would be to add a small random amount of inaccuracy to each shot. This inaccuracy could perhaps be mitigated with an upgrade between waves.

Coding a class for pickups

In this section, we will code a Pickup class that has a Sprite member, as well as other member data and functions. We will add pickups to our game in just a few steps:

  1. First, we will code the Pickup.h file. This will reveal all the details of the member data and the prototypes for the functions.
  2. Then, we will code the Pickup.cpp file which, of course, will contain the definitions for all the functions of the Pickup class. As we step through this, I will explain exactly how an object of the Pickup type will work and be controlled.
  3. Finally, we will use the Pickup class in the main function to spawn them, update them, and draw them.

Let's get started with step 1.

Coding the Pickup header file

To make the new header file, right-click Header Files in the Solution Explorer and select Add | New Item.... In the Add New Item window, highlight (by left-clicking) Header File (.h), and then, in the Name field, type Pickup.h.

Add and study the following code to the Pickup.h file and then we can go through it:

#pragma once

#include <SFML/Graphics.hpp>

using namespace sf;

class Pickup

{

private:

    //Start value for health pickups

    const int HEALTH_START_VALUE = 50;

    const int AMMO_START_VALUE = 12;

    const int START_WAIT_TIME = 10;

    const int START_SECONDS_TO_LIVE = 5;

    

    // The sprite that represents this pickup

    Sprite m_Sprite;

    // The arena it exists in

    IntRect m_Arena;

    // How much is this pickup worth?

    int m_Value;

    

    // What type of pickup is this?

    // 1 = health, 2 = ammo

    int m_Type;

    // Handle spawning and disappearing

    bool m_Spawned;

    float m_SecondsSinceSpawn;

    float m_SecondsSinceDeSpawn;

    float m_SecondsToLive;

    float m_SecondsToWait;    

// Public prototypes go here

};

The previous code declares all the private variables of the Pickup class. Although the names should be quite intuitive, it might not be obvious why many of them are needed at all. Let's go through them, starting from the top:

  • const int HEALTH_START_VALUE = 50: This constant variable is used to set the starting value of all health pickups. The value will be used to initialize the m_Value variable, which will need to be manipulated throughout the course of a game.
  • const int AMMO_START_VALUE = 12: This constant variable is used to set the starting value of all ammo pickups. The value will be used to initialize the m_Value variable, which will need to be manipulated throughout the course of a game.
  • const int START_WAIT_TIME = 10: This variable determines how long a pickup will wait before it respawns after disappearing. It will be used to initialize the m_SecondsToWait variable, which can be manipulated throughout the game.
  • const int START_SECONDS_TO_LIVE = 5: This variable determines how long a pickup will last between spawning and being de-spawned. Like the previous three constants, it has a non-constant associated with it that can be manipulated throughout the course of the game. The non-constant it's used to initialize is m_SecondsToLive.
  • Sprite m_Sprite: This is the sprite to visually represent the object.
  • IntRect m_Arena: This will hold the size of the current arena to help the pickup to spawn in a sensible position.
  • int m_Value: How much health or ammo is this pickup worth? This value is used when the player levels up the value of the health or ammo pickup.
  • int m_Type: This will be either 1 or 2 for health or ammo, respectively. We could have used an enumeration class, but that seemed like overkill for just two options.
  • bool m_Spawned: Is the pickup currently spawned?
  • float m_SecondsSinceSpawn: How long is it since the pickup was spawned?
  • float m_SecondsSinceDeSpawn: How long is it since the pickup was de-spawned (disappeared)?
  • float m_SecondsToLive: How long should this pickup stay spawned before de-spawning?
  • float m_SecondsToWait: How long should this pickup stay de-spawned before respawning?

    Tip

    Note that most of the complexity of this class is due to the variable spawn time and its upgradeable nature. If the pickups just respawned when collected and had a fixed value, this would be a very simple class. We need our pickups to be upgradeable so that the player is forced to develop a strategy to progress through the waves.

Next, add the following public function prototypes to the Pickup.h file. Be sure to familiarize yourself with the new code so that we can go through it:

// Public prototypes go here

public:

    Pickup::Pickup(int type);

    // Prepare a new pickup

    void setArena(IntRect arena);

    void spawn();

    // Check the position of a pickup

    FloatRect getPosition();

    // Get the sprite for drawing

    Sprite getSprite();

    // Let the pickup update itself each frame

    void update(float elapsedTime);

    // Is this pickup currently spawned?

    bool isSpawned();

    // Get the goodness from the pickup

    int gotIt();

    // Upgrade the value of each pickup

    void upgrade();

};

Let's talk briefly about each of the function definitions.

  • The first function is the constructor and is named after the class. Note that it takes a single int parameter. This will be used to initialize the type of pickup it will be (health or ammo).
  • The setArena function receives an IntRect. This function will be called for each Pickup instance at the start of each wave. The Pickup objects will then "know" the areas into which they can spawn.
  • The spawn function will, of course, handle spawning the pickup.
  • The getPosition function, just like in the Player, Zombie, and Bullet classes, will return a FloatRect instance that represents the current location of the object in the game world.
  • The getSprite function returns a Sprite object that allows the pickup to be drawn once each frame.
  • The update function receives the time the previous frame took. It uses this value to update its private variables and make decisions about when to spawn and de-spawn.
  • The isSpawned function returns a Boolean that will let the calling code know whether or not the pickup is currently spawned.
  • The gotIt function will be called when a collision is detected with the player. The code of the Pickup class can then prepare itself for respawning at the appropriate time. Note that it returns an int value so that the calling code knows how much the pickup is "worth" in either health or ammo.
  • The upgrade function will be called when the player chooses to level up the properties of a pickup during the leveling up phase of the game.

Now that we have gone through the member variables and function prototypes, it should be quite easy to follow along as we code the function definitions.

Coding the Pickup class function definitions

Now, we can create a new .cpp file that will contain the function definitions. Right-click Source Files in the Solution Explorer and select Add | New Item.... In the Add New Item window, highlight (by left-clicking) C++ File (.cpp), and then, in the Name field, type Pickup.cpp. Finally, click the Add button. We are now ready to code the class.

Add the following code to the Pickup.cpp file. Be sure to review the code so that we can discuss it:

#include "Pickup.h"

#include "TextureHolder.h"

Pickup::Pickup(int type)

{

    // Store the type of this pickup

    m_Type = type;

    // Associate the texture with the sprite

    if (m_Type == 1)

    {

        m_Sprite = Sprite(TextureHolder::GetTexture(

            "graphics/health_pickup.png"));

        // How much is pickup worth

        m_Value = HEALTH_START_VALUE;

    }

    else

    {

        m_Sprite = Sprite(TextureHolder::GetTexture(

            "graphics/ammo_pickup.png"));

        // How much is pickup worth

        m_Value = AMMO_START_VALUE;

    }

    m_Sprite.setOrigin(25, 25);

    m_SecondsToLive = START_SECONDS_TO_LIVE;

    m_SecondsToWait = START_WAIT_TIME;

}

In the previous code, we added the familiar include directives. Then, we added the Pickup constructor. We know it is the constructor because it has the same name as the class.

The constructor receives an int called type and the first thing the code does is assign the value that's received from type to m_Type. After this, there is an if else block that checks whether m_Type is equal to 1. If it is, m_Sprite is associated with the health pickup texture and m_Value is set to HEALTH_START_VALUE.

If m_Type is not equal to 1, the else block associates the ammo pickup texture with m_Sprite and assigns the value of AMMO_START_VALUE to m_Value.

After the if else block, the code sets the origin of m_Sprite to the center using the setOrigin function and assigns START_SECONDS_TO_LIVE and START_WAIT_TIME to m_SecondsToLive and m_SecondsToWait, respectively.

The constructor has successfully prepared a Pickup object that is ready for use.

Now, we will add the setArena function. Examine the code as you add it:

void Pickup::setArena(IntRect arena)

{

    // Copy the details of the arena to the pickup's m_Arena

    m_Arena.left = arena.left + 50;

    m_Arena.width = arena.width - 50;

    m_Arena.top = arena.top + 50;

    m_Arena.height = arena.height - 50;

    spawn();

}

The setArena function that we just coded simply copies the values from the passed in arena object but varies the values by + 50 on the left and top and - 50 on the right and bottom. The Pickup object is now aware of the area in which it can spawn. The setArena function then calls its own spawn function to make the final preparations for being drawn and updated each frame.

The spawn function is next. Add the following code after the setArena function:

void Pickup::spawn()

{

    // Spawn at a random location

    srand((int)time(0) / m_Type);

    int x = (rand() % m_Arena.width);

    srand((int)time(0) * m_Type);

    int y = (rand() % m_Arena.height);

    m_SecondsSinceSpawn = 0;

    m_Spawned = true;

    m_Sprite.setPosition(x, y);

}

The spawn function does everything necessary to prepare the pickup. First, it seeds the random number generator and gets a random number for both the horizontal and vertical position of the object. Notice that it uses the m_Arena.width and m_Arena.height variables as the ranges for the possible horizontal and vertical positions.

The m_SecondsSinceSpawn variable is set to zero so that the length of time that's allowed before it is de-spawned is reset. The m_Spawned variable is set to true so that, when we call isSpawned, from main, we will get a positive response. Finally, m_Sprite is moved into position with setPosition, ready for being drawn to the screen.

In the following block of code, we have three simple getter functions. The getPosition function returns a FloatRect of the current position of m_Sprite, getSprite returns a copy of m_Sprite itself, and isSpawned returns true or false, depending on whether the object is currently spawned.

Add and examine the code we have just discussed:

FloatRect Pickup::getPosition()

{

    return m_Sprite.getGlobalBounds();

}

Sprite Pickup::getSprite()

{

    return m_Sprite;

}

bool Pickup::isSpawned()

{

    return m_Spawned;

}

Next, we will code the gotIt function. This function will be called from main when the player touches/collides (gets) with the pickup. Add the gotIt function after the isSpawned function:

int Pickup::gotIt()

{

    m_Spawned = false;

    m_SecondsSinceDeSpawn = 0;

    return m_Value;

}

The gotIt function sets m_Spawned to false so that we know not to draw and check for collisions anymore. m_SecondsSinceDespawn is set to zero so that the countdown to spawning begins again from the start. m_Value is then returned to the calling code so that the calling code can handle adding extra ammunition or health, as appropriate.

Following this, we need to code the update function, which ties together many of the variables and functions we have seen so far. Add and familiarize yourself with the update function, and then we can talk about it:

void Pickup::update(float elapsedTime)

{

    if (m_Spawned)

    {

        m_SecondsSinceSpawn += elapsedTime;

    }

    else

    {

        m_SecondsSinceDeSpawn += elapsedTime;

    }

    // Do we need to hide a pickup?

    if (m_SecondsSinceSpawn > m_SecondsToLive && m_Spawned)

    {

        // Remove the pickup and put it somewhere else

        m_Spawned = false;

        m_SecondsSinceDeSpawn = 0;

    }

    // Do we need to spawn a pickup

    if (m_SecondsSinceDeSpawn > m_SecondsToWait && !m_Spawned)

    {

        // spawn the pickup and reset the timer

        spawn();

    }

}

The update function is divided into four blocks that are considered for execution each frame:

  1. An if block that executes if m_Spawned is true: if (m_Spawned). This block of code adds the time this frame to m_SecondsSinceSpawned, which keeps track of how long the pickup has been spawned.
  2. A corresponding else block that executes if m_Spawned is false. This block adds the time this frame took to m_SecondsSinceDeSpawn, which keeps track of how long the pickup has waited since it was last de-spawned (hidden).
  3. Another if block that executes when the pickup has been spawned for longer than it should have been: if (m_SecondsSinceSpawn > m_SecondsToLive && m_Spawned). This block sets m_Spawned to false and resets m_SecondsSinceDeSpawn to zero. Now, block 2 will execute until it is time to spawn it again.
  4. A final if block that executes when the time to wait since de-spawning has exceeded the necessary wait time, and the pickup is not currently spawned: if (m_SecondsSinceDeSpawn > m_SecondsToWait && !m_Spawned). When this block is executed, it is time to spawn the pick up again, and the spawn function is called.

These four tests are what control the hiding and showing of a pickup.

Finally, add the definition for the upgrade function:

void Pickup::upgrade()

{

    if (m_Type == 1)

    {

        m_Value += (HEALTH_START_VALUE * .5);

    }

    else

    {

        m_Value += (AMMO_START_VALUE * .5);

    }

    // Make them more frequent and last longer

    m_SecondsToLive += (START_SECONDS_TO_LIVE / 10);

    m_SecondsToWait -= (START_WAIT_TIME / 10);

}

The upgrade function tests for the type of pickup, either health or ammo, and then adds 50% of the (appropriate) starting value on to m_Value. The next two lines after the if else blocks increase the amount of time the pickup will remain spawned and decreases the amount of time the player must wait between spawns.

This function is called when the player chooses to level up the pickups during the LEVELING_UP state.

Our Pickup class is ready for use.

Using the Pickup class

After all that hard work implementing the Pickup class, we can now go ahead and write code in the game engine to put some pickups into the game.

The first thing we will do is add an include directive to the ZombieArena.cpp file:

#include <SFML/Graphics.hpp>

#include "ZombieArena.h"

#include "Player.h"

#include "TextureHolder.h"

#include "Bullet.h"

#include "Pickup.h"

using namespace sf;

In this following code, we are adding two Pickup instances: one called healthPickup and another called ammoPickup. We pass the values 1 and 2, respectively, into the constructor so that they are initialized to the correct type of pickup. Add the following highlighted code, which we have just discussed:

// Hide the mouse pointer and replace it with crosshair

window.setMouseCursorVisible(true);

Sprite spriteCrosshair;

Texture textureCrosshair = TextureHolder::GetTexture(

         "graphics/crosshair.png");

spriteCrosshair.setTexture(textureCrosshair);

spriteCrosshair.setOrigin(25, 25);

// Create a couple of pickups

Pickup healthPickup(1);

Pickup ammoPickup(2);

// The main game loop

while (window.isOpen())

In the LEVELING_UP state of the keyboard handling, add the following highlighted lines within the nested PLAYING code block:

if (state == State::PLAYING)

{

    // Prepare the level

    // We will modify the next two lines later

    arena.width = 500;

    arena.height = 500;

    arena.left = 0;

    arena.top = 0;

    // Pass the vertex array by reference

    // to the createBackground function

    int tileSize = createBackground(background, arena);

    // Spawn the player in the middle of the arena

    player.spawn(arena, resolution, tileSize);

    // Configure the pick-ups

    healthPickup.setArena(arena);

    ammoPickup.setArena(arena);

    // Create a horde of zombies

    numZombies = 10;

    // Delete the previously allocated memory (if it exists)

    delete[] zombies;

    zombies = createHorde(numZombies, arena);

    numZombiesAlive = numZombies;

    // Reset the clock so there isn't a frame jump

    clock.restart();

}

The preceding code simply passes arena into the setArena function of each pickup. The pickups now know where they can spawn. This code executes for each new wave, so, as the arena's size grows, the Pickup objects will get updated.

The following code simply calls the update function for each Pickup object on each frame:

// Loop through each Zombie and update them

    for (int i = 0; i < numZombies; i++)

    {

        if (zombies[i].isAlive())

        {

            zombies[i].update(dt.asSeconds(), playerPosition);

        }

    }

    // Update any bullets that are in-flight

    for (int i = 0; i < 100; i++)

    {

        if (bullets[i].isInFlight())

        {

            bullets[i].update(dtAsSeconds);

        }

    }

    // Update the pickups

    healthPickup.update(dtAsSeconds);

    ammoPickup.update(dtAsSeconds);

}// End updating the scene

The following code in the draw part of the game loop checks whether the pickup is currently spawned and if it is, draws it. Let's add it:

    // Draw the player

    window.draw(player.getSprite());

    // Draw the pick-ups, if currently spawned

    if (ammoPickup.isSpawned())

    {

        window.draw(ammoPickup.getSprite());

    }

    

    if (healthPickup.isSpawned())

    {

        window.draw(healthPickup.getSprite());

    }

    //Draw the crosshair

    window.draw(spriteCrosshair);

}

Now, you can run the game and see the pickups spawn and de-spawn. You can't, however, actually pick them up yet:

Now that we have all the objects in our game, it is a good time to make them interact (collide) with each other.

Detecting collisions

We just need to know when certain objects from our game touch certain other objects. We can then respond to that event in an appropriate manner. In our classes, we have already added functions that will be called when our objects collide. They are as follows:

  • The Player class has a hit function. We will call it when a zombie collides with the player.
  • The Zombie class has a hit function. We will call it when a bullet collides with a zombie.
  • The Pickup class has a gotIt function. We will call it when the player collides with a pickup.

If necessary, look back to refresh your memory regarding how each of those functions works. All we need to do now is detect the collisions and call the appropriate functions.

We will use rectangle intersection to detect collisions. This type of collision detection is straightforward (especially with SFML). We will use the same technique that we used in the Pong game. The following image shows how a rectangle can reasonably accurately represent the zombies and the player:

We will deal with this in three sections of code that will all follow on from one another. They will all go at the end of the update part of our game engine.

We need to know the answers to the following three questions for each frame:

  1. Has a Zombie been shot?
  2. Has the player been touched by a Zombie?
  3. Has the player touched a pickup?

First, let's add a couple more variables for score and hiscore. We can then change them when a zombie is killed. Add the following code:

// Create a couple of pickups

Pickup healthPickup(1);

Pickup ammoPickup(2);

// About the game

int score = 0;

int hiScore = 0;

// The main game loop

while (window.isOpen())

Now, let's start by detecting whether a zombie is colliding with a bullet.

Has a zombie been shot?

The following code might look complicated but, when we step through it, we will see it is nothing we haven't seen before. Add the following code just after the call to update the pickups each frame. Then, we can go through it:

// Update the pickups

healthPickup.update(dtAsSeconds);

ammoPickup.update(dtAsSeconds);

// Collision detection

// Have any zombies been shot?

for (int i = 0; i < 100; i++)

{

    for (int j = 0; j < numZombies; j++)

    {

        if (bullets[i].isInFlight() &&

            zombies[j].isAlive())

        {

            if (bullets[i].getPosition().intersects

                (zombies[j].getPosition()))

            {

                // Stop the bullet

                bullets[i].stop();

                // Register the hit and see if it was a kill

                if (zombies[j].hit())

                {

                    // Not just a hit but a kill too

                    score += 10;

                    if (score >= hiScore)

                    {

                        hiScore = score;

                    }

                    numZombiesAlive--;

                    // When all the zombies are dead (again)

                    if (numZombiesAlive == 0) {

                        state = State::LEVELING_UP;

                    }

                }    

                

            }

        }

    }

}// End zombie being shot

In the next section, we will see all the zombie and bullet collision detection code again. We will do so a bit at a time so that we can discuss it. First of all, notice the structure of the nested for loops in the preceding code (with some code stripped out), as shown again here:

// Collision detection

// Have any zombies been shot?

for (int i = 0; i < 100; i++)

{

    for (int j = 0; j < numZombies; j++)

    {

        ...

        ...

        ...

    }

}

The code loops through every bullet (0 to 99) for each and every zombie (0 to less than numZombies.).

Within the nested for loops, we do the following.

We check whether the current bullet is in flight and the current zombie is still alive with the following code:

if (bullets[i].isInFlight() && zombies[j].isAlive())

Provided the zombie is alive and the bullet is in flight, we test for a rectangle intersection with the following code:

if (bullets[i].getPosition().intersects(zombies[j].getPosition()))

If the current bullet and zombie have collided, then we take a number of steps, as detailed next.

Stop the bullet with the following code:

// Stop the bullet

bullets[i].stop();

Register a hit with the current zombie by calling its hit function. Note that the hit function returns a Boolean that lets the calling code know whether the zombie is dead yet. This is shown in the following line of code:

// Register the hit and see if it was a kill

if (zombies[j].hit()) {

Inside this if block, which detects when the zombie is dead and hasn't just wounded us, do the following:

  • Add ten to score.
  • Change hiScore if the score the player has achieved has exceeded (beaten) score.
  • Reduce numZombiesAlive by one.
  • Check whether all the zombies are dead with (numZombiesAlive == 0) and if so, change state to LEVELING_UP.

Here is the block of code inside if(zombies[j].hit()) that we have just discussed:

// Not just a hit but a kill too

score += 10;

if (score >= hiScore)

{

    hiScore = score;

}

numZombiesAlive--;

// When all the zombies are dead (again)

if (numZombiesAlive == 0)

{

    state = State::LEVELING_UP;

}

That's the zombies and the bullets taken care of. You can now run the game and see the blood. Of course, you won't see the score until we implement the HUD in the next chapter.

Has the player been touched by a zombie?

This code is much shorter and simpler than the zombie and bullet collision detection code. Add the following highlighted code just after the previous code we wrote:

}// End zombie being shot

// Have any zombies touched the player            

for (int i = 0; i < numZombies; i++)

{

    if (player.getPosition().intersects

        (zombies[i].getPosition()) && zombies[i].isAlive())

    {

        if (player.hit(gameTimeTotal))

        {

            // More here later

        }

        if (player.getHealth() <= 0)

        {

            state = State::GAME_OVER;            

        }

    }

}// End player touched

Here, we detect whether a zombie has collided with the player by using a for loop to go through all the zombies. For each zombie that is alive, the code uses the intersects function to test for a collision with the player. When a collision has occurred, we call player.hit. Then, we check whether the player is dead by calling player.getHealth. If the player's health is equal to or less than zero, then we change state to GAME_OVER.

You can run the game and collisions will be detected. However, as there is no HUD or sound effects yet, it is not clear that this is happening. In addition, we need to do some more work resetting the game when the player had died, and a new game is starting. So, although the game runs, the results are not especially satisfying right now. We will improve this over the next two chapters.

Has the player touched a pickup?

The collision detection code between the player and each of the two pickups is shown here. Add the following highlighted code just after the previous code that we added:

    }// End player touched

    // Has the player touched health pickup

    if (player.getPosition().intersects

        (healthPickup.getPosition()) && healthPickup.isSpawned())

    {

        player.increaseHealthLevel(healthPickup.gotIt());

        

    }

    // Has the player touched ammo pickup

    if (player.getPosition().intersects

        (ammoPickup.getPosition()) && ammoPickup.isSpawned())

    {

        bulletsSpare += ammoPickup.gotIt();

        

    }

}// End updating the scene

The preceding code uses two simple if statements to see whether either healthPickup or ammoPickup have been touched by the player.

If a health pickup has been collected, then the player.increaseHealthLevel function uses the value returned from the healthPickup.gotIt function to increase the player's health.

If an ammo pickup has been collected, then bulletsSpare is increased by the value that's returned from ammoPickup.gotIt.

Important note

You can now run the game, kill zombies, and collect pickups! Note that, when your health equals zero, the game will enter the GAME_OVER state and pause. To restart it, you will need to press Enter, followed by a number between 1 and 6. When we implement the HUD, the home screen, and the leveling up screen, these steps will be intuitive and straightforward for the player. We will do so in the next chapter.

Summary

This was a busy chapter, but we achieved a lot. Not only did we add bullets and pickups to the game through two new classes, but we also made all the objects interact as they should by detecting when they collide with each other.

Despite these achievements, we need to do more work to set up each new game and to give the player feedback through a HUD. In the next chapter, we will build the HUD.

FAQ

Here are some questions that might be on your mind:

Q) Are there any better ways of doing collision detection?

A) Yes. There are lots more ways to do collision detection, including but not limited to the following.

  • You can divide objects up into multiple rectangles that fit the shape of the sprite better. It is perfectly manageable for C++ to check on thousands of rectangles each frame. This is especially the case when you use techniques such as neighbor checking to reduce the number of tests that are necessary each frame.
  • For circular objects, you can use the radius overlap method.
  • For irregular polygons, you can use the passing number algorithm.

You can review all of these techniques, if you wish, by taking a look at the following links:

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.145.21.205