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:
Let's start with 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:
Let's get started with step 1.
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.
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.
We will make the bullets usable by following these six steps:
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.
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.
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:
Now, let's shoot 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.
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.
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.
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.
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:
Let's get started with step 1.
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:
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.
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.
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:
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.
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.
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:
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:
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.
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:
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.
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.
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.
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.
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 review all of these techniques, if you wish, by taking a look at the following links:
3.21.100.62