In this project, we will be making even more use of OOP and to a powerful effect. We will also be exploring the SFML View class. This versatile class will allow us to easily divide our game up into layers for different aspects of the game. In the Zombie Shooter project, we will have a layer for the HUD and a layer for the main game. This is necessary because as the game world expands each time the player clears a wave of zombies and, eventually, the game world will be bigger than the screen and will need to scroll. The use of the View class will prevent the text of the HUD from scrolling with the background. In the next project, we will take things even further and create a co-op split screen game with the SFML View class doing most of the hard work.
This is what we will do in this chapter:
At this point, if you haven't already, I suggest you go and watch a video of Over 9000 Zombies (http://store.steampowered.com/app/273500/) and Crimson Land (http://store.steampowered.com/app/262830/). Our game will obviously not be as in-depth or advanced as either of these examples, but we will also have the same basic set of features and game mechanics, such as the following:
There will be three types of zombies to splatter. They will have different attributes, such as appearance, health, and speed. We will call them chasers, bloaters, and crawlers. Take a look at the following annotated screenshot of the game to see some of the features in action and the components and assets that make up the game:
Here is some more information about each of the numbered points:
So, we have a lot to do and new C++ skills to learn. Let's start by creating a new project.
As creating a project is a relatively involved process, I will detail all the steps again. For even more detail and images, please refer to the Setting up the Timber project section in Chapter 1, C++, SFML, Visual Studio, and Starting the First Game.
As setting up a project is a fiddly process, we will go through it step by step, like we did for the Timber project. I won't show you the same images as I did for the Timber project, but the process is the same, so flip back to Chapter 1, C++, SFML, Visual Studio, and Starting the First Game if you want a reminder of the locations of the various project properties. Let's look at the following steps:
Now, you have configured the project properties and you are nearly ready to go. Next, we need to copy the SFML .dll files into the main project directory by following these steps:
The project is now set up and ready to go. Next, we will explore and add the project assets.
The assets in this project are more numerous and more diverse than the previous games. The assets include the following:
All the graphics and sound effects that are required for the game are included in the download bundle. They can be found in the Chapter 8/graphics and Chapter 8/sound folders, respectively.
The font that is required has not been supplied. This is done to avoid any possible ambiguity regarding the license. This will not cause a problem because the links for downloading the fonts and how and where to choose the font will be provided.
The graphical assets make up the parts of the scene of our Zombie Arena game. Look at the following graphical assets; it should be clear to you where the assets in the game will be used:
What might be less obvious, however, is the background_sheet.png file, which contains four different images. This is the sprite sheet we mentioned previously. We will see how we can save memory and increase the speed of our game using the sprite sheet in Chapter 9, C++ References, Sprite Sheets, and Vertex Arrays.
The sound files are all in .wav format. These are files that contain the sound effects that will be played when certain events are triggered. They are as follows:
Once you have decided which assets you will use, it is time to add them to the project.
The following instructions will assume you are using all the assets that were supplied is the book's download bundle. Where you are using your own assets, simply replace the appropriate sound or graphic file with your own, using the same filename. Let's take a look at the steps:
Now, it's time to consider how OOP will help us with this project and then we can start writing the code for Zombie Arena.
The initial problem we are faced with is the complexity of the current project. Let's consider that there is just a single zombie; here is what we need to make it function in the game:
This suggests perhaps a dozen variables for just one zombie, and entire arrays of each of these variables will be required for managing a zombie horde. But what about all the bullets from the machine gun, the pick-ups, and the different level-ups? The simple Timber!!! and Pong games also started to get a bit unmanageable, and it is easy to speculate that this more complicated shooter will be many times worse!
Fortunately, we will put all the OOP skills we learned in the previous two chapters into action, as well as learn some new C++ techniques.
We will start our coding for this project with a class to represent the player.
Let's think about what our Player class will need to do and what we require for it. The class will need to know how fast it can move, where in the game world it currently is, and how much health it has. As the Player class, in the player's eyes, is represented as a 2D graphical character, the class will need both a Sprite object and a Texture object.
Furthermore, although the reasons might not be obvious at this point, our Player class will also benefit from knowing a few details about the overall environment the game is running in. These details are screen resolution, the size of the tiles that make up an arena, and the overall size of the current arena.
As the Player class will be taking full responsibility for updating itself in each frame (like the bat and ball did), it will need to know the player's intentions at any given moment. For example, is the player currently holding down a keyboard direction key? Or is the player currently holding down multiple keyboard direction keys? Boolean variables are used to determine the status of the W, A, S, and D keys and will be essential.
It is clear that we are going to need quite a selection of variables in our new class. Having learned all we have about OOP, we will, of course, be making all of these variables private. This means that we must provide access, where appropriate, from the main function.
We will use a whole bunch of getter functions as well as some functions to set up our object. These functions are quite numerous. There are 21 functions in this class. At first, this might seem a little daunting, but we will go through them all and see that most of them simply set or get one of the private variables.
There are just a few in-depth functions: update, which will be called once each frame from the main function, and spawn, which will handle initializing some of the private variables each time the player is spawned. As we will see, however, there is nothing complicated and they will all be described in detail.
The best way to proceed is to code the header file. This will give us the opportunity to see all the private variables and examine all the function signatures.
Tip
Pay close attention to the return values and argument types, as this will make understanding the code in the function definitions much easier.
Start by right-clicking on Header Files in Solution Explorer and select Add | New Item…. In the Add New Item window, highlight (by left-clicking on it) Header File (.h) and then, in the Name field, type Player.h. Finally, click on the Add button. We are now ready to code the header file for our first class.
Start coding the Player class by adding the declaration, including the opening and closing curly braces, followed by a semicolon:
#pragma once
#include <SFML/Graphics.hpp>
using namespace sf;
class Player
{
};
Now, let's add all our private member variables in the file. Based on what we have already discussed, see whether you can work out what each of them will do. We will go through them individually in a moment:
class Player
{
private:
const float START_SPEED = 200;
const float START_HEALTH = 100;
// Where is the player
Vector2f m_Position;
// Of course, we will need a sprite
Sprite m_Sprite;
// And a texture
// !!Watch this space – Interesting changes here soon!!
Texture m_Texture;
// What is the screen resolution
Vector2f m_Resolution;
// What size is the current arena
IntRect m_Arena;
// How big is each tile of the arena
int m_TileSize;
// Which direction(s) is the player currently moving in
bool m_UpPressed;
bool m_DownPressed;
bool m_LeftPressed;
bool m_RightPressed;
// How much health has the player got?
int m_Health;
// What is the maximum health the player can have
int m_MaxHealth;
// When was the player last hit
Time m_LastHit;
// Speed in pixels per second
float m_Speed;
// All our public functions will come next
};
The previous code declares all our member variables. Some are regular variables, while some of them are objects. Notice that they are all under the private: section of the class and, therefore, are not directly accessible from outside the class.
Also, notice that we are using the naming convention of prefixing m_ to all the names of the non-constant variables. The m_ prefix will remind us, while coding the function definitions, that they are member variables, are distinct from the local variables we will create in some of the functions, and are also distinct from the function parameters.
All the variables that are used are straightforward, such as m_Position, m_Texture, and m_Sprite, which are for the current location, texture, and sprite of the player, respectively. In addition to this, each variable (or group of variables) is commented to make its usage plain.
However, why exactly they are needed, and the context they will be used in, might not be so obvious. For example, m_LastHit, which is an object of the Time type, is for recording the time that the player last received a hit from a zombie. It is not obvious why we might need this information, but we will go over this soon.
As we piece the rest of the game together, the context for each of the variables will become clearer. The important thing, for now, is to familiarize yourself with the names and data types to make following along with the rest of the project trouble-free.
Tip
You don't need to memorize the variable names and types as we will discuss all the code when they are used. You do, however, need to take your time to look over them and get more familiar with them. Furthermore, as we proceed, it might be worth referring to this header file if anything seems unclear.
Now, we can add a complete long list of functions. Add the following highlighted code and see whether you can work out what it all does. Pay close attention to the return types, parameters, and the name of each function. This is key to understanding the code we will write throughout the rest of this project. What do they tell us about each function? Add the following highlighted code and then we will examine it:
// All our public functions will come next
public:
Player();
void spawn(IntRect arena, Vector2f resolution, int tileSize);
// Call this at the end of every game
void resetPlayerStats();
// Handle the player getting hit by a zombie
bool hit(Time timeHit);
// How long ago was the player last hit
Time getLastHitTime();
// Where is the player
FloatRect getPosition();
// Where is the center of the player
Vector2f getCenter();
// What angle is the player facing
float getRotation();
// Send a copy of the sprite to the main function
Sprite getSprite();
// The next four functions move the player
void moveLeft();
void moveRight();
void moveUp();
void moveDown();
// Stop the player moving in a specific direction
void stopLeft();
void stopRight();
void stopUp();
void stopDown();
// We will call this function once every frame
void update(float elapsedTime, Vector2i mousePosition);
// Give the player a speed boost
void upgradeSpeed();
// Give the player some health
void upgradeHealth();
// Increase the maximum amount of health the player can have
void increaseHealthLevel(int amount);
// How much health has the player currently got?
int getHealth();
};
Firstly, note that all the functions are public. This means we can call all of these functions using an instance of the class from the main function with code like this:
player.getSprite();
Assuming player is a fully set up instance of the Player class, the previous code will return a copy of m_Sprite. Putting this code into a real context, we could, in the main function, write code like this:
window.draw(player.getSprite());
The previous code would draw the player graphic in its correct location, just as if the sprite was declared in the main function itself. This is what we did with the Bat class in the Pong project.
Before we move on to implement (that is, write the definitions) of these functions in a corresponding .cpp file, let's take a closer look at each of them in turn:
Important note
Note that these are integer screen coordinates and are distinct from the floating-point world coordinates.
Like the variables, it should now be plain what each of the functions is for. Also the why and the precise context of using some of these functions will only reveal themselves as we progress with the project.
Tip
You don't need to memorize the function names, return types, or parameters as we will discuss the code when they are used. You do, however, need to take your time to look over them, along with the previous explanations, and get more familiar with them. Furthermore, as we proceed, it might be worth referring to this header file if anything seems unclear.
Now, we can move on to the meat of our functions: the definitions.
Finally, we can begin writing the code that does the work of our class.
Right-click on Source Files in Solution Explorer and select Add | New Item.... In the Add New Item window, highlight (by left-clicking on) C++ File (.cpp) and then, in the Name field, type Player.cpp. Finally, click on the Add button.
Tip
From now on, I will simply ask you to create a new class or header file. So, commit the preceding step to memory or refer back here if you need a reminder.
We are now ready to code the .cpp file for our first class in this project.
Here are the necessary include directives, followed by the definition of the constructor. Remember, the constructor will be called when we first instantiate an object of the Player type. Add the following code to the Player.cpp file and then we can take a closer look at it:
#include "player.h"
Player::Player()
{
m_Speed = START_SPEED;
m_Health = START_HEALTH;
m_MaxHealth = START_HEALTH;
// Associate a texture with the sprite
// !!Watch this space!!
m_Texture.loadFromFile("graphics/player.png");
m_Sprite.setTexture(m_Texture);
// Set the origin of the sprite to the center,
// for smooth rotation
m_Sprite.setOrigin(25, 25);
}
In the constructor function, which, of course, has the same name as the class and no return type, we write code that begins to set up the Player object, ready for use.
To be clear; this code will run when we write the following code from the main function:
Player player;
Don't add the previous line of code just yet.
All we do in the constructor is initialize m_Speed, m_Health, and m_MaxHealth from their related constants. Then, we load the player graphic into m_Texture, associate m_Texture with m_Sprite, and set the origin of m_Sprite to the center, (25, 25).
Tip
Note the cryptic comment, // !!Watch this space!!, indicating that we will return to the loading of our texture and some important issues regarding it. We will eventually change how we deal with this texture once we have discovered a problem and learned a bit more C++. We will do so in Chapter 10, Pointers, the Standard Template Library, and Texture Management.
Next, we will code the spawn function. We will only ever create one instance of the Player class. We will, however, need to spawn it into the current level for each wave. This is what the spawn function will handle for us. Add the following code to the Player.cpp file and be sure to examine the details and read the comments:
void Player::spawn(IntRect arena,
Vector2f resolution,
int tileSize)
{
// Place the player in the middle of the arena
m_Position.x = arena.width / 2;
m_Position.y = arena.height / 2;
// Copy the details of the arena
// to the player's m_Arena
m_Arena.left = arena.left;
m_Arena.width = arena.width;
m_Arena.top = arena.top;
m_Arena.height = arena.height;
// Remember how big the tiles are in this arena
m_TileSize = tileSize;
// Store the resolution for future use
m_Resolution.x = resolution.x;
m_Resolution.y = resolution.y;
}
The preceding code starts off by initializing the m_Position.x and m_Position.y values to half the height and width of the passed in arena. This has the effect of moving the player to the center of the level, regardless of its size.
Next, we copy all the coordinates and dimensions of the passed in arena to the member object of the same type, m_Arena. The details of the size and coordinates of the current arena are used so frequently that it makes sense to do this. We can now use m_Arena for tasks such as making sure the player can't walk through walls. In addition to this, we copy the passed in tileSize instance to the member variable, m_TileSize, for the same purpose. We will see m_Arena and m_TileSize in action in the update function.
The final two lines from the preceding code copy the screen resolution from the Vector2f, resolution, which is a parameter of spawn, into m_Resolution, which is a member variable of Player. We now have access to these values inside the Player class.
Now, add the very straightforward code of the resetPlayerStats function:
void Player::resetPlayerStats()
{
m_Speed = START_SPEED;
m_Health = START_HEALTH;
m_MaxHealth = START_HEALTH;
}
When the player dies, we will use this to reset any upgrades they might have used.
We will not write the code that calls the resetPlayerStats function until nearly completing the project, but it is there ready for when we need it.
In the next part of the code, we will add two more functions. They will handle what happens when the player is hit by a zombie. We will be able to call player.hit() and pass in the current game time. We will also be able to query the last time that the player was hit by calling player.getLastHitTime(). Exactly how these functions are useful will become apparent when we have some zombies.
Add the two new definitions to the Player.cpp file and then examine the C++ code a little more closely:
Time Player::getLastHitTime()
{
return m_LastHit;
}
bool Player::hit(Time timeHit)
{
if (timeHit.asMilliseconds()
- m_LastHit.asMilliseconds() > 200)
{
m_LastHit = timeHit;
m_Health -= 10;
return true;
}
else
{
return false;
}
}
The code for getLastHitTime() is very straightforward; it will return whatever value is stored in m_LastHit.
The hit function is a bit more in-depth and nuanced. First, the if statement checks to see whether the time that's passed in as a parameter is 200 milliseconds further ahead than the time stored in m_LastHit. If it is, m_LastHit is updated with the time passed in and m_Health has 10 deducted from its current value. The last line of code in this if statement is return true. Notice that the else clause simply returns false to the calling code.
The overall effect of this function is that health points will only be deducted from the player up to five times per second. Remember that our game loop might be running at thousands of iterations per second. In this scenario, without the restriction this function provides, a zombie would only need to be in contact with the player for one second and tens of thousands of health points would be deducted. The hit function controls and restricts this phenomenon. It also lets the calling code know whether a new hit has been registered (or not) by returning true or false.
This code implies that we will detect collisions between a zombie and the player in the main function. We will then call player.hit() to determine whether to deduct any health points.
Next, for the Player class, we will implement a bunch of getter functions. They allow us to keep the data neatly encapsulated in the Player class, at the same time as making their values available to the main function.
Add the following code, right after the previous block:
FloatRect Player::getPosition()
{
return m_Sprite.getGlobalBounds();
}
Vector2f Player::getCenter()
{
return m_Position;
}
float Player::getRotation()
{
return m_Sprite.getRotation();
}
Sprite Player::getSprite()
{
return m_Sprite;
}
int Player::getHealth()
{
return m_Health;
}
The previous code is very straightforward. Each one of the previous five functions returns the value of one of our member variables. Look carefully at each of them and familiarize yourself with which function returns which value.
The next eight short functions enable the keyboard controls (which we will use from the main function) so that we can change the data contained in our object of the Player type. Add the following code to the Player.cpp file and then we will summarize how it all works:
void Player::moveLeft()
{
m_LeftPressed = true;
}
void Player::moveRight()
{
m_RightPressed = true;
}
void Player::moveUp()
{
m_UpPressed = true;
}
void Player::moveDown()
{
m_DownPressed = true;
}
void Player::stopLeft()
{
m_LeftPressed = false;
}
void Player::stopRight()
{
m_RightPressed = false;
}
void Player::stopUp()
{
m_UpPressed = false;
}
void Player::stopDown()
{
m_DownPressed = false;
}
The previous code has four functions (moveLeft, moveRight, moveUp, and moveDown), which set the related Boolean variables (m_LeftPressed, m_RightPressed, m_UpPressed, and m_DownPressed) to true. The other four functions (stopLeft, stopRight, stopUp, and stopDown) do the opposite and set the same Boolean variables to false. The instance of the Player class can now be kept informed of which of the WASD keys were pressed and which were not.
The following function is the one that does all the hard work. The update function will be called once in every single frame of our game loop. Add the following code, and then we will examine it in detail. If we followed along with the previous eight functions and we remember how we animated the clouds and bees for the Timber!!! project and the bat and ball for Pong, we will probably understand most of the following code:
void Player::update(float elapsedTime, Vector2i mousePosition)
{
if (m_UpPressed)
{
m_Position.y -= m_Speed * elapsedTime;
}
if (m_DownPressed)
{
m_Position.y += m_Speed * elapsedTime;
}
if (m_RightPressed)
{
m_Position.x += m_Speed * elapsedTime;
}
if (m_LeftPressed)
{
m_Position.x -= m_Speed * elapsedTime;
}
m_Sprite.setPosition(m_Position);
// Keep the player in the arena
if (m_Position.x > m_Arena.width - m_TileSize)
{
m_Position.x = m_Arena.width - m_TileSize;
}
if (m_Position.x < m_Arena.left + m_TileSize)
{
m_Position.x = m_Arena.left + m_TileSize;
}
if (m_Position.y > m_Arena.height - m_TileSize)
{
m_Position.y = m_Arena.height - m_TileSize;
}
if (m_Position.y < m_Arena.top + m_TileSize)
{
m_Position.y = m_Arena.top + m_TileSize;
}
// Calculate the angle the player is facing
float angle = (atan2(mousePosition.y - m_Resolution.y / 2,
mousePosition.x - m_Resolution.x / 2)
* 180) / 3.141;
m_Sprite.setRotation(angle);
}
The first portion of the previous code moves the player sprite. The four if statements check which of the movement-related Boolean variables (m_LeftPressed, m_RightPressed, m_UpPressed, or m_DownPressed) are true and changes m_Position.x and m_Position.y accordingly. The same formula, from the previous two projects, to calculate the amount to move is also used:
position (+ or -) speed * elapsed time.
After these four if statements, m_Sprite.setPosition is called and m_Position is passed in. The sprite has now been adjusted by exactly the right amount for that one frame.
The next four if statements check whether m_Position.x or m_Position.y is beyond any of the edges of the current arena. Remember that the confines of the current arena were stored in m_Arena, in the spawn function. Let's look at the first one of these four if statements in order to understand them all:
if (m_Position.x > m_Arena.width - m_TileSize)
{
m_Position.x = m_Arena.width - m_TileSize;
}
The previous code tests to see whether m_position.x is greater than m_Arena.width, minus the size of a tile (m_TileSize). As we will see when we create the background graphics, this calculation will detect the player straying into the wall.
When the if statement is true, the m_Arena.width - m_TileSize calculation is used to initialize m_Position.x. This means that the center of the player graphic will never be able to stray past the left-hand edge of the right-hand wall.
The next three if statements, which follow the one we have just discussed, do the same thing but for the other three walls.
The last two lines in the preceding code calculate and set the angle that the player sprite is rotated to (that is, facing). This line of code might look a little complex, but it is simply using the position of the crosshair (mousePosition.x and mousePosition.y) and the center of the screen (m_Resolution.x and m_Resolution.y) in a tried-and-tested trigonometric function.
How atan uses these coordinates along with Pi (3.141) is quite complicated, and that is why it is wrapped up in a handy function for us.
Important note
If you want to explore trigonometric functions in more detail, you can do so here: http://www.cplusplus.com/reference/cmath/.
The last three functions we will add for the Player class make the player 20% faster, increase the player's health by 20%, and increase the player's health by the amount passed in, respectively.
Add the following code at the end of the Player.cpp file, and then we will take a closer look at it:
void Player::upgradeSpeed()
{
// 20% speed upgrade
m_Speed += (START_SPEED * .2);
}
void Player::upgradeHealth()
{
// 20% max health upgrade
m_MaxHealth += (START_HEALTH * .2);
}
void Player::increaseHealthLevel(int amount)
{
m_Health += amount;
// But not beyond the maximum
if (m_Health > m_MaxHealth)
{
m_Health = m_MaxHealth;
}
}
In the preceding code, the upgradeSpeed() and upgradeHealth() functions increase the value stored in m_Speed and m_MaxHealth, respectively. These values are increased by 20% by multiplying the starting values by .2 and adding them to the current values. These functions will be called from the main function when the player is choosing what attributes of their character they wish to improve (that is, level up) between levels.
The increaseHealthLevel() function takes an int value from main in the amount parameter. This int value will be provided by a class called Pickup, which we will write in Chapter 11, Collision Detection, Pickups, and Bullets. The m_Health member variable is increased by the passed-in value. However, there is a catch for the player. The if statement checks whether m_Health has exceeded m_MaxHealth and, if it has, sets it to m_MaxHealth. This means the player cannot simply gain infinite health from pick-ups. Instead, they must carefully balance the upgrades they choose between levels.
Of course, our Player class can't do anything until we instantiate it and put it to work in our game loop. Before we do that, let's look at the concept of a game camera.
In my opinion, the SFML View class is one of the neatest classes. After finishing this book, when we make games without using a media/gaming library, we will really notice the absence of View.
The View class allows us to consider our game as taking place in its own world, with its own properties. What do I mean? Well, when we create a game, we are usually trying to create a virtual world. That virtual world rarely, if ever, is measured in pixels, and rarely, if ever, will that world be the same number of pixels as the player's monitor. We need a way to abstract the virtual world we are building so that it can be of whatever size or shape we like.
Another way to think of SFML View is as a camera through which the player views a part of our virtual world. Most games will have more than one camera/view of the world.
For example, consider a split screen game where two players can be in different parts of the world at the same time.
Or, consider a game where there is a small area of the screen that represents the entire game world, but at a very high level/zoomed out, like a mini map.
Even if our games are much simpler than the previous two examples and don't need split screens or mini maps, we will likely want to create a world that is bigger than the screen it is being played on. This is, of course, the case with Zombie Arena.
Additionally, if we are constantly moving the game camera around to show different parts of the virtual world (usually to track the player), what happens to the HUD? If we draw the score and other onscreen HUD information and then we scroll the world around to follow the player, the score would move relative to that camera.
The SFML View class easily enables all these of features and solves this problem with very straightforward code. The trick is to create an instance of View for every camera – perhaps a View instance for the mini map, a View instance for the scrolling game world, and then a View instance for the HUD.
The instances of View can be moved around, sized, and positioned as required. So, the main View instance following the game can track the player, the mini-map view can remain in a fixed, zoomed-out small corner of the screen, and the HUD can overlay the entire screen and never move, despite the fact the main View instance could go wherever the player goes.
Let's look at some code using a few instances of View.
Tip
This code is being used to introduce the View class. Don't add this code to the Zombie Arena project.
Create and initialize a few instances of View:
// Create a view to fill a 1920 x 1080 monitor
View mainView(sf::FloatRect(0, 0, 1920, 1080));
// Create a view for the HUD
View hudView(sf::FloatRect(0, 0, 1920, 1080));
The previous code creates two View objects that fill a 1920 x 1080 monitor. Now, we can do some magic with mainView while leaving hudView completely alone:
// In the update part of the game
// There are lots of things you can do with a View
// Make the view centre around the player
mainView.setCenter(player.getCenter());
// Rotate the view 45 degrees
mainView.rotate(45)
// Note that hudView is totally unaffected by the previous code
When we manipulate the properties of a View instance, we do so like this. When we draw sprites, text, or other objects to a view, we must specifically set the view as the current view for the window:
// Set the current view
window.setView(mainView);
Now, we can draw everything we want into that view:
// Do all the drawing for this view
window.draw(playerSprite);
window.draw(otherGameObject);
// etc
The player might be at any coordinate whatsoever; it doesn't matter because mainView is centered around the graphic.
Now, we can draw the HUD into hudView. Note that just like we draw individual elements (background, game objects, text, and so on) in layers from back to front, we also draw views from back to front as well. Hence, a HUD is drawn after the main game scene:
// Switch to the hudView
window.setView(hudView);
// Do all the drawing for the HUD
window.draw(scoreText);
window.draw(healthBar);
// etc
Finally, we can draw/show the window and all its views for the current frame in the usual way:
window.display();
Tip
If you want to take your understanding of SFML View further than is necessary for this project, including how to achieve split screens and mini maps, then the best guide on the web is on the official SFML website: https://www.sfml-dev.org/tutorials/2.5/graphics-view.php.
Now that we have learned about View, we can start coding the Zombie Arena main function and use our first View instance for real. In Chapter 12, Layering Views and Implementing the HUD, we will introduce a second instance of View for the HUD and layer it over the top of the main View instance.
In this game, we will need a slightly upgraded game engine in main. We will have an enumeration called state, which will track what the current state of the game is. Then, throughout main, we can wrap parts of our code so that different things happen in different states.
When we created the project, Visual Studio created a file for us called ZombieArena.cpp. This will be the file that contains our main function and the code that instantiates and controls all our classes.
We begin with the now-familiar main function and some include directives. Note the addition of an include directive for the Player class.
Add the following code to the ZombieArena.cpp file:
#include <SFML/Graphics.hpp>
#include "Player.h"
using namespace sf;
int main()
{
return 0;
}
The previous code has nothing new in it except that the #include "Player.h" line means we can now use the Player class within our code.
Let's flesh out some more of our game engine. The following code does quite a lot. Be sure to read the comments when you add the code to get an idea of what is going on. We will then go through it in more detail.
Add the following highlighted code at the start of the main function:
int main()
{
// The game will always be in one of four states
enum class State { PAUSED, LEVELING_UP,
GAME_OVER, PLAYING };
// Start with the GAME_OVER state
State state = State::GAME_OVER;
// Get the screen resolution and
// create an SFML window
Vector2f resolution;
resolution.x =
VideoMode::getDesktopMode().width;
resolution.y =
VideoMode::getDesktopMode().height;
RenderWindow window(
VideoMode(resolution.x, resolution.y),
"Zombie Arena", Style::Fullscreen);
// Create a an SFML View for the main action
View mainView(sf::FloatRect(0, 0,
resolution.x, resolution.y));
// Here is our clock for timing everything
Clock clock;
// How long has the PLAYING state been active
Time gameTimeTotal;
// Where is the mouse in
// relation to world coordinates
Vector2f mouseWorldPosition;
// Where is the mouse in
// relation to screen coordinates
Vector2i mouseScreenPosition;
// Create an instance of the Player class
Player player;
// The boundaries of the arena
IntRect arena;
// The main game loop
while (window.isOpen())
{
}
return 0;
}
Let's run through each section of all the code that we entered. Just inside the main function, we have the following code:
// The game will always be in one of four states
enum class State { PAUSED, LEVELING_UP, GAME_OVER, PLAYING };
// Start with the GAME_OVER state
State state = State::GAME_OVER;
The previous code creates a new enumeration class called State. Then, the code creates an instance of the State class called state. The state enumeration can now be one of four values, as defined in the declaration. Those values are PAUSED, LEVELING_UP, GAME_OVER, and PLAYING. These four values will be just what we need for keeping track and responding to the different states that the game can be in at any given time. Note that it is not possible for state to hold more than one value at a time.
Immediately after, we added the following code:
// Get the screen resolution and create an SFML window
Vector2f resolution;
resolution.x = VideoMode::getDesktopMode().width;
resolution.y = VideoMode::getDesktopMode().height;
RenderWindow window(VideoMode(resolution.x, resolution.y),
"Zombie Arena", Style::Fullscreen);
The previous code declares a Vector2f instance called resolution. We initialize the two member variables of resolution (x and y) by calling the VideoMode::getDesktopMode function for both width and height. The resolution object now holds the resolution of the monitor on which the game is running. The final line of code creates a new RenderWindow instance called window using the appropriate resolution.
The following code creates an SFML View object. The view is positioned (initially) at the exact coordinates of the pixels of the monitor. If we were to use this View to do some drawing in this current position, it would be the same as drawing to a window without a view. However, we will eventually start to move this view to focus on the parts of our game world that the player needs to see. Then, when we start to use a second View instance, which remains fixed (for the HUD), we will see how this View instance can track the action while the other remains static to display the HUD:
// Create a an SFML View for the main action
View mainView(sf::FloatRect(0, 0, resolution.x, resolution.y));
Next, we created a Clock instance to do our timing and a Time object called gameTimeTotal that will keep a running total of the game time that has elapsed. As the project progresses, we will also introduce more variables and objects to handle timing:
// Here is our clock for timing everything
Clock clock;
// How long has the PLAYING state been active
Time gameTimeTotal;
The following code declares two vectors: one holding two float variables, called mouseWorldPosition, and one holding two integers, called mouseScreenPosition. The mouse pointer is something of an anomaly because it exists in two different coordinate spaces. We could think of these as parallel universes if we like. Firstly, as the player moves around the world, we will need to keep track of where the crosshair is in that world. These will be floating-point coordinates and will be stored in mouseWorldCoordinates. Of course, the actual pixel coordinates of the monitor itself never change. They will always be 0,0 to horizontal resolution -1, vertical resolution -1. We will track the mouse pointer position that is relative to this coordinate space using the integers stored in mouseScreenPosition:
// Where is the mouse in relation to world coordinates
Vector2f mouseWorldPosition;
// Where is the mouse in relation to screen coordinates
Vector2i mouseScreenPosition;
Finally, we get to use our Player class. This line of code will cause the constructor function (Player::Player) to execute. Refer to Player.cpp if you want to refresh your memory about this function:
// Create an instance of the Player class
Player player;
This IntRect object will hold starting horizontal and vertical coordinates, as well as a width and a height. Once initialized, we will be able to access the size and location details of the current arena with code such as arena.left, arena.top, arena.width, and arena.height:
// The boundaries of the arena
IntRect arena;
The last part of the code that we added previously is, of course, our game loop:
// The main game loop
while (window.isOpen())
{
}
We have probably noticed that the code is getting quite long. We'll talk about this inconvenience in the following section.
One of the advantages of abstraction using classes and functions is that the length (number of lines) of our code files can be reduced. Even though we will be using more than a dozen code files for this project, the length of the code in ZombieArena.cpp will still get a little unwieldy toward the end. In the final project, Space Invaders++, we will look at even more ways to abstract and manage our code.
For now, use this tip to keep things manageable. Notice that on the left-hand side of the code editor in Visual Studio, there are several + and - signs, one of which is shown in this diagram:
There will be one sign for each block (if, while, for, and so on) of the code. You can expand and collapse these blocks by clicking on the + and - signs. I recommend keeping all the code not currently under discussion collapsed. This will make things much clearer.
Furthermore, we can create our own collapsible blocks. I suggest making a collapsible block out of all the code before the start of the main game loop. To do so, highlight the code and then right-click and choose Outlining | Hide Selection, as shown in the following screenshot:
Now, you can click the - and + signs to expand and collapse the block. Each time we add code before the main game loop (and that will be quite often), you can expand the code, add the new lines, and then collapse it again. The following screenshot shows what the code looks like when it is collapsed:
This is much more manageable than it was before. Now, we can make a start with the main game loop.
As you can see, the last part of the preceding code is the game loop (while (window.isOpen()){}). We will turn our attention to this now. Specifically, we will be coding the input handling section of the game loop.
The code that we will be adding is quite long. There is nothing complicated about it, though, and we will examine it all in a moment.
Add the following highlighted code to the game loop:
// The main game loop
while (window.isOpen())
{
/*
************
Handle input
************
*/
// Handle events by polling
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)
{
}
}
}// End event polling
}// End game loop
In the preceding code, we instantiate an object of the Event type. We will use event, like we did in the previous projects, to poll for system events. To do so, we wrap the rest of the code from the previous block in a while loop with the window.pollEvent(event) condition. This will keep looping each frame until there are no more events to process.
Inside this while loop, we handle the events we are interested in. First, we test for Event::KeyPressed events. If the Return key is pressed while the game is in the PLAYING state, then we switch state to PAUSED.
If the Return key is pressed while the game is in the PAUSED state, then we switch state to PLAYING and restart the clock object. The reason we restart clock after switching from PAUSED to PLAYING is because, while the game is paused, the elapsed time still accumulates. If we didn't restart the clock, all our objects would update their locations as if the frame had just taken a very long time. This will become more apparent as we flesh out the rest of the code in this file.
We then have an else if block to test whether the Return key was pressed while the game was in the GAME_OVER state. If it was, then state is changed to LEVELING_UP.
Important note
Note that the GAME_OVER state is the state where the home screen is displayed. So, the GAME_OVER state is the state after the player has just died and when the player first runs the game. The first thing that the player gets to do each game is pick an attribute to improve (that is, level up).
In the previous code, there is a final if condition to test whether the state is equal to PLAYING. This if block is empty and we will add code to it throughout the project.
Tip
We will add code to lots of different parts of this file throughout the project. Therefore, it is worthwhile taking the time to understand the different states our game can be in and where we handle them. It will also be very beneficial to collapse and expand the different if, else, and while blocks as and when appropriate.
Spend some time thoroughly familiarizing yourself with the while, if, and else if blocks we have just coded. We will be referring to them regularly.
Next, immediately after the previous code and still inside the game loop, which is still dealing with handling input, add the following highlighted code. Note the existing code (not highlighted) that shows exactly where the new (highlighted) code goes:
}// End event polling
// Handle the player quitting
if (Keyboard::isKeyPressed(Keyboard::Escape))
{
window.close();
}
// Handle WASD while playing
if (state == State::PLAYING)
{
// Handle the pressing and releasing of the WASD keys
if (Keyboard::isKeyPressed(Keyboard::W))
{
player.moveUp();
}
else
{
player.stopUp();
}
if (Keyboard::isKeyPressed(Keyboard::S))
{
player.moveDown();
}
else
{
player.stopDown();
}
if (Keyboard::isKeyPressed(Keyboard::A))
{
player.moveLeft();
}
else
{
player.stopLeft();
}
if (Keyboard::isKeyPressed(Keyboard::D))
{
player.moveRight();
}
else
{
player.stopRight();
}
}// End WASD while playing
}// End game loop
In the preceding code, we first test to see whether the player has pressed the Escape key. If it is pressed, the game window will be closed.
Next, within one big if(state == State::PLAYING) block, we check each of the WASD keys in turn. If a key is pressed, we call the appropriate player.move... function. If it is not, we call the related player.stop... function.
This code ensures that, in each frame, the player object will be updated with the WASD keys that are pressed and those that are not. The player.move... and player.stop... functions store the information in the member Boolean variables (m_LeftPressed, m_RightPressed, m_UpPressed, and m_DownPressed). The Player class then responds to the value of these Booleans, in each frame, in the player.update function, which we will call in the update section of the game loop.
Now, we can handle the keyboard input to allow the player to level up at the start of each game and in-between each wave. Add and study the following highlighted code and then we will discuss it:
}// End WASD while playing
// Handle the LEVELING up state
if (state == State::LEVELING_UP)
{
// Handle the player LEVELING up
if (event.key.code == Keyboard::Num1)
{
state = State::PLAYING;
}
if (event.key.code == Keyboard::Num2)
{
state = State::PLAYING;
}
if (event.key.code == Keyboard::Num3)
{
state = State::PLAYING;
}
if (event.key.code == Keyboard::Num4)
{
state = State::PLAYING;
}
if (event.key.code == Keyboard::Num5)
{
state = State::PLAYING;
}
if (event.key.code == Keyboard::Num6)
{
state = State::PLAYING;
}
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;
// We will modify this line of code later
int tileSize = 50;
// Spawn the player in the middle of the arena
player.spawn(arena, resolution, tileSize);
// Reset the clock so there isn't a frame jump
clock.restart();
}
}// End LEVELING up
}// End game loop
In the preceding code, which is all wrapped in a test to see whether the current value of state is equal to LEVELING_UP, we handle the keyboard keys 1, 2, 3, 4, 5, and 6. In the if block for each, we simply set state to State::PLAYING. We will add some code to deal with each level up option later in Chapter 13, Sound Effects, File I/O, and Finishing the Game.
This code does the following things:
Now, we have an actual spawned player object that is aware of its environment and can respond to key presses. We can now update the scene on each pass through the loop.
Be sure to neatly collapse the code from the input handling part of the game loop since we are done with that for now. The following code is in the updating part of the game loop. Add and study the following highlighted code and then we can discuss it:
}// End LEVELING up
/*
****************
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);
// Update the player
player.update(dtAsSeconds, Mouse::getPosition());
// Make a note of the players new position
Vector2f playerPosition(player.getCenter());
// Make the view centre around the player
mainView.setCenter(player.getCenter());
}// End updating the scene
}// End game loop
First, note that the previous code is wrapped in a test to make sure the game is in the PLAYING state. We don't want this code to run if the game has been paused, it has ended, or if the player is choosing what to level up.
First, we restart the clock and store the time that the previous frame took in the dt variable:
// Update the delta time
Time dt = clock.restart();
Next, we add the time that the previous frame took to the accumulated time the game has been running for, as held by gameTimeTotal:
// Update the total game time
gameTimeTotal += dt;
Now, we initialize a float variable called dtAsSeconds with the value returned by the dt.AsSeconds function. For most frames, this will be a fraction of one. This is perfect for passing into the player.update function to be used to calculate how much to move the player sprite.
Now, we can initialize mouseScreenPosition using the MOUSE::getPosition function.
Important note
You might be wondering about the slightly unusual syntax for getting the position of the mouse. This is called a static function. If we define a function in a class with the static keyword, we can call that function using the class name and without an instance of the class. C++ OOP has lots of quirks and rules like this. We will see more as we progress.
We then initialize mouseWorldPosition using the SFML mapPixelToCoords function on window. We discussed this function when talking about the View class earlier in this chapter.
At this point, we are now able to call player.update and pass in dtAsSeconds and the position of the mouse, as is required.
We store the player's new center in a Vector2f instance called playerPosition. At the moment, this is unused, but we will have a use for this later in the project.
We can then center the view around the center of the player's up-to-date position with mainView.setCenter(player.getCenter()).
We are now able to draw the player to the screen. Add the following highlighted code, which splits the draw section of the main game loop into different states:
}// End updating the scene
/*
**************
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 player
window.draw(player.getSprite());
}
if (state == State::LEVELING_UP)
{
}
if (state == State::PAUSED)
{
}
if (state == State::GAME_OVER)
{
}
window.display();
}// End game loop
return 0;
}
Within the if(state == State::PLAYING) section of the previous code, we clear the screen, set the view of the window to mainView, and then draw the player sprite with window.draw(player.getSprite()).
After all the different states have been handled, the code shows the scene in the usual manner with window.display();.
You can run the game and see our player character spin around in response to moving the mouse.
Tip
When you run the game, you need to press Enter to start the game, and then select a number from 1 to 6 to simulate choosing an upgrade option. Then, the game will start.
You can also move the player around within the (empty) 500 x 500 pixel arena. You can see our lonely player in the center of the screen, as shown here:
You can't, however, get any sense of movement because we haven't implemented the background. We will do so in the next chapter.
Phew! That was a long one. We have done a lot in this chapter: we built our first class for the Zombie Arena project, Player, and put it to use in the game loop. We also learned about and used an instance of the View class, although we haven't explored the benefits this gives us just yet.
In the next chapter, we will build our arena background by exploring what sprite sheets are. We will also learn about C++ references, which allow us to manipulate variables, even when they are out of scope (that is, in another function).
Q) I noticed we have coded quite a few functions of the Player class that we don't use. Why is this?
A) Rather than keep coming back to the Player class, we have added all the code that we will need throughout the project. By the end of Chapter 13, Sound Effects, File I/O, and Finishing the Game, we will have made full use of all of these functions.
3.133.124.145