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 him. 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 his 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 we will cover the topics:
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. We will add bullets to our game in a few steps:
Bullet.h
file. This will reveal all the details of the member data and the prototypes for the functions.Bullet.cpp
file which, of course, will contain the definitions for all the functions of the Bullet
class. As we step through it, I will explain exactly how an object of type Bullet
will work and be controlled.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.
To make the new header file, right-click on 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 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 bullets 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 enable us to decide whether we need to call its update
function each frame and whether or not 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 that we will perform calculations on. They will be used to decide the horizontal and vertical change in the bullets position in each frame.
Finally, for the previous code, 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, 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, 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 8: Pointers, Standard Template Library, and Texture Management that zombies also had a getPosition
function.
Next we have the getShape
function, which returns an object of type RectangleShape
. 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 the 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 one 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 the include directives and the constructor. We know it is the constructor because the function has the same name as the class:
#include "stdafx.h" #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 have the more substantial shoot
function. Add the following code to the Bullet.cpp
file, 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 parameters startX
and startY
. 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 simple trigonometry to determine the gradient of travel for a bullet. The progression horizontally and vertically of a bullet must vary based on the slope of the line 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 following code first 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 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. We will see this in the update
function where we test to 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 RectangleShape which represents the bullet to its starting location. We use the setPosition
function as we have often done before:
// Position the bullet ready to be drawn m_BulletShape.setPosition(m_Position);
Next we have four straightforward functions. 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
let us know what the current state is.
The getPosition
function returns a FloatRect
and we will see how we use the FloatRect
from each game object to detect collisions, soon.
Finally, for the previous code, getShape
returns a RectangleShape
so 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) required to move the bullet at just the right angle. Then we use the setPosition
function to actually move the RectangleShape
.
The last thing we do in update
is 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 can see how to shoot some in the main
function.
3.145.186.83