Chapter 9. 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 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:

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

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. We will add bullets to our game in a few steps:

  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 it, I will explain exactly how an object of type Bullet 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 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.

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

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

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