In this chapter, we will take a first look at the penultimate project of this book. The project we will be building will use advanced features such as directional sound, which has the effect of appearing to play relative to the position of the player. It will also have split-screen cooperative gameplay. In addition, this project will introduce the concept of Shaders, which are programs written in another language that run directly on the graphics card. By the end of Chapter 18, Particle Systems and Shaders, you will have a fully functioning, multiplayer platform game built in the style of the hit classic Thomas Was Alone.
This chapter's focus will be getting the project started and exploring how the code will be structured to make better use of OOP. Here are the details of the topics that will be covered in this chapter:
Tip
At this point, if you haven't already, I suggest that you go and watch a video of Thomas Was Alone at http://store.steampowered.com/app/220780/.
Notice the simple but aesthetically excellent graphics. The video also shows a variety of gameplay challenges such as using the character's different attributes (height, jump, power, and so on). To keep our game simple without losing the challenge, we will have fewer puzzle features than Thomas Was Alone but will have the additional challenge of creating the need for two players to play cooperatively. Just to make sure the game is not too easy, we will also make the players have to rush to beat the clock, which is why the name of our game is Thomas Was Late.
Our game will not be nearly as advanced as the masterpiece that we are attempting to emulate, but it will have a good selection of exciting game-play features, such as the following:
Take a look at the following annotated screenshot of the game to see some of the features in action and the components/assets that make up the game:
Let's look at each of these features and describe a few more:
All of these features warrant a few more screenshots so that we can keep the finished product in mind as we write the C++ code.
The following screenshot shows Thomas and Bob arriving at a fire pit that Bob has no chance of jumping over without help:
The following screenshot shows Bob and Thomas collaborating to clear a precarious jump:
The following screenshot shows how we can design puzzles where a "leap of faith" is required in order to reach the goal:
The following screenshot demonstrates how we can design oppressive cave systems of almost any size. We can also devise levels where Bob and Thomas are forced to split up and go different routes:
Creating the Thomas Was Late project will follow the same procedure that we used in the previous three projects. Since creating a project is a slightly fiddly process, I will detail all the steps again here. For even more detail and images, refer to setting up the Timber!!! project in Chapter 1, C++, SFML, Visual Studio, and Starting the First Game:
That's the project properties configured and ready to go. Now, 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.
The assets in this project are even more numerous and diverse than the Zombie Arena game. As usual, the assets include a font for the writing on the screen, sound effects for different actions such as jumping, reaching the goal, or the distant roar of fire, and, of course, graphics for Thomas and Bob as well as a sprite sheet for all the background tiles.
All of the assets that are required for this game are included in the download bundle. They can be found in the Chapter 14/graphics and Chapter 14/sound folders.
In addition to the graphics, sounds, and fonts that we have come to expect, this game has two new asset types. They are level design files and GLSL shader programs. Let's find out about each of them.
Levels are all created in a text file. By using the numbers 0 through 3, we can build level designs to challenge players. All the level designs are in the levels folder in the same directory as the other assets. Feel free to take a peek at one now, but we will look at them in detail in Chapter 18, Particle Systems and Shaders.
In addition to these level design assets, we have a special type of graphical asset called shaders.
Shaders are programs written in GLSL (Graphics Library Shading Language). Don't worry about having to learn another language as we don't need to get too in-depth to take advantage of shaders. Shaders are special as they are entire programs, separate from our C++ code, that are executed by the GPU each and every frame. In fact, some of these shader programs are run every frame, for every pixel! We will find out more about these details in Chapter 18, Particle Systems and Shaders. If you can't wait that long, take a look at the files in the Chapter 14/shaders folder of the download bundle.
The graphical assets make up the parts of the scene of our game. If you take a look at the graphical assets, it should be clear where in our game they will be used:
If the tiles on the tiles_sheet graphic look a little different to the screenshots of the game, this is because they are partly transparent and the background showing through changes them a little. If the background graphic looks totally different to the actual background in the game screenshots, that is because the shader programs we will write will manipulate each and every pixel, each and every frame, to create a kind of "molten" effect.
The sound files are all in .wav format. These files contain the sound effects that we will play at certain events throughout the game. They are as follows:
The sound effects are very straightforward, and you can easily create your own. If you intend to replace the fire1.wav file, be sure to save your sounds in mono (not stereo) format. The reasons for this will be explained in Chapter 17, Sound Spatialization and HUD.
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 in this book's download bundle.
Where you are using your own, simply replace the appropriate sound or graphic file with your own, using exactly the same filename. Let's get started:
Now that we have a new project, along with all the assets we will need for the entire project, we can talk about how we will structure the game engine code.
One of the problems that has been getting worse from project to project, despite taking measures to reduce the problem, is how long and unwieldy the code gets. Object-oriented programming (OOP) allows us to break our projects up into logical and manageable chunks, known as classes.
We will make a big improvement to the manageability of the code in this project with the introduction of an Engine class. Among other functions, the Engine class will have three private functions. These are input, update, and draw. These should sound very familiar. Each of these functions will hold a chunk of the code that was previously in the main function. Each of these functions will be in a code file of its own, that is, Input.cpp, Update.cpp, and Draw.cpp, respectively.
There will also be one public function in the Engine class, which can be called with an instance of Engine. This function is run and will be responsible for calling input, update, and draw once for each frame of the game:
Furthermore, because we have abstracted the major parts of the game engine to the Engine class, we can also move many of the variables from main and make them members of Engine. All we need to do to get our game engine fired up is create an instance of Engine and call its run function. Here is a sneak preview of the super-simple main function:
int main()
{
// Declare an instance of Engine
Engine engine;
// Start the engine
engine.run();
// Quit in the usual way when the engine is stopped
return 0;
}
Tip
Don't add the previous code just yet.
To make our code even more manageable and readable, we will also abstract responsibility for big tasks such as loading a level and collision detection to separate functions (in separate code files). These two functions are loadLevel and detectCollisions. We will also code other functions to handle some of the new features of the Thomas Was Late project. We will cover them in detail, as and when they occur.
To further take advantage of OOP, we will delegate responsibility for areas of the game entirely to new classes. You probably remember that the sound and HUD code was quite lengthy in previous projects. We will build a SoundManager and HUD class to handle these aspects in a cleaner manner. Exactly how they work will be explored in depth when we implement them.
The game levels themselves are also much more in-depth than previous games, so we will also code a LevelManager class.
As you would expect, the playable characters will be made with classes as well. For this project, however, we will learn some more C++ and implement a PlayableCharacter class with all the common functionality of Thomas and Bob. Then, the Thomas and Bob classes will inherit this common functionality as well as implementing their own unique functions and abilities. This technique, perhaps unsurprisingly, is called inheritance. I will go into more detail about inheritance in the next chapter: Chapter 15, Advanced OOP – Inheritance and Polymorphism.
We will also implement several other classes to perform specific responsibilities. For example, we will make some neat explosions using particle systems. You might be able to guess that, to do this, we will code a Particle class and a ParticleSystem class. All these classes will have instances that are members of the Engine class. Doing things this way will make all the features of the game accessible from the game engine but encapsulate the details into the appropriate classes.
Tip
Note that despite these new techniques to separate out the different aspects of our code, by the end of this project, we will still have some slightly unwieldy classes. The final project of this book, while a much simpler shooter game, will explore one more way of organizing our code to make it manageable.
The last thing to mention before we move on to seeing the actual code that will make the Engine class is that we will reuse, without any changes whatsoever, the TextureHolder class that we discussed and coded for the Zombie Arena game.
As we suggested in the previous section, we will code a class called Engine that will control and bind the different parts of the Thomas Was Late game.
The first thing we will do is make the TextureHolder class from the previous project available in this one.
The TextureHolder class that we discussed and coded for the Zombie Arena game will also be useful in this project. While it is possible to add the files (TextureHolder.h and TextureHolder.cpp) directly from the previous project, without recoding them or recreating the files, I don't want to assume that you haven't jumped straight to this project. What follows is very brief instructions, along with the complete code listing we need, to create the TextureHolder class. If you want the class or the code explained, please see Chapter 10, Pointers, the Standard Template Library, and Texture Management.
Tip
If you did complete the previous project and you do want to add the class from the Zombie Arena project, simply do the following. In the Solution Explorer window, right-click Header Files and select Add | Existing Item.... Browse to TextureHolder.h from the previous project and select it. In the Solution Explorer window, right-click Source Files and select Add | Existing Item.... Browse to TextureHolder.cpp from the previous project and select it. You can now use the TextureHolder class in this project. Note that the files are shared between projects and any changes will take effect in both projects.
To create the TextureHolder class from scratch, 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 TextureHolder.h. Finally, click the Add button.
Add the following code to TextureHolder.h:
#pragma once
#ifndef TEXTURE_HOLDER_H
#define TEXTURE_HOLDER_H
#include <SFML/Graphics.hpp>
#include <map>
class TextureHolder
{
private:
// A map container from the STL,
// that holds related pairs of String and Texture
std::map<std::string, sf::Texture> m_Textures;
// A pointer of the same type as the class itself
// the one and only instance
static TextureHolder* m_s_Instance;
public:
TextureHolder();
static sf::Texture& GetTexture(std::string const& filename);
};
#endif
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 TextureHolder.cpp. Finally, click the Add button.
Add the following code to TextureHolder.cpp:
#include "TextureHolder.h"
#include <assert.h>
using namespace sf;
using namespace std;
TextureHolder* TextureHolder::m_s_Instance = nullptr;
TextureHolder::TextureHolder()
{
assert(m_s_Instance == nullptr);
m_s_Instance = this;
}
sf::Texture& TextureHolder::GetTexture(std::string const& filename)
{
// Get a reference to m_Textures using m_S_Instance
auto& m = m_s_Instance->m_Textures;
// auto is the equivalent of map<string, Texture>
// Create an iterator to hold a key-value-pair (kvp)
// and search for the required kvp
// using the passed in file name
auto keyValuePair = m.find(filename);
// auto is equivalent of map<string, Texture>::iterator
// Did we find a match?
if (keyValuePair != m.end())
{
// Yes
// Return the texture,
// the second part of the kvp, the texture
return keyValuePair->second;
}
else
{
// File name not found
// Create a new key value pair using the filename
auto& texture = m[filename];
// Load the texture from file in the usual way
texture.loadFromFile(filename);
// Return the texture to the calling code
return texture;
}
}
We can now get on with our new Engine class.
As usual, we will start with the header file, which holds the function declarations and member variables. Note that we will revisit this file throughout the project to add more functions and member variables. At this stage, we will add just the code that is necessary.
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 Engine.h. Finally, click the Add button. We are now ready to code the header file for the Engine class.
Add the following member variables, as well as the function declarations. Many of them we have seen before in the other projects and some of them were discussed in the Structuring the Thomas Was Late Code section. Take note of the function and variable names, as well as whether they are private or public. Add the following code to the Engine.h file and then we will talk about it:
#pragma once
#include <SFML/Graphics.hpp>
#include "TextureHolder.h"
using namespace sf;
class Engine
{
private:
// The texture holder
TextureHolder th;
const int TILE_SIZE = 50;
const int VERTS_IN_QUAD = 4;
// The force pushing the characters down
const int GRAVITY = 300;
// A regular RenderWindow
RenderWindow m_Window;
// The main Views
View m_MainView;
View m_LeftView;
View m_RightView;
// Three views for the background
View m_BGMainView;
View m_BGLeftView;
View m_BGRightView;
View m_HudView;
// Declare a sprite and a Texture
// for the background
Sprite m_BackgroundSprite;
Texture m_BackgroundTexture;
// Is the game currently playing?
bool m_Playing = false;
// Is character 1 or 2 the current focus?
bool m_Character1 = true;
// Start in full screen (not split) mode
bool m_SplitScreen = false;
// Time left in the current level (seconds)
float m_TimeRemaining = 10;
Time m_GameTimeTotal;
// Is it time for a new/first level?
bool m_NewLevelRequired = true;
// Private functions for internal use only
void input();
void update(float dtAsSeconds);
void draw();
public:
// The Engine constructor
Engine();
// Run will call all the private functions
void run();
};
Here is a complete run down of all the private variables and functions. Where it is appropriate, I will spend a little longer on the explanation:
Now, let's run through all the public functions:
Next, we will see the definitions of all these functions and some of the variables in action.
In all our previous classes, we have put all the function definitions into the .cpp file prefixed with the class name. As our aim for this project is to make the code more manageable, we are doing things a little differently.
In the Engine.cpp file, we will place the constructor (Engine) and the public run function. The rest of the functions will be going in their own .cpp file with a name that makes it clear which function goes where. This will not be a problem for the compiler if we add the appropriate include directive (#include "Engine.h") at the top of all the files that contain function definitions from the Engine class.
Let's get started by coding Engine and running it in Engine.cpp. 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 Engine.cpp. Finally, click the Add button. We are now ready to code the .cpp file for the Engine class.
The code for this function will go in the Engine.cpp file we have recently created.
Add the following code and then we can discuss it:
#include "Engine.h"
Engine::Engine()
{
// Get the screen resolution
// and create an SFML window and View
Vector2f resolution;
resolution.x = VideoMode::getDesktopMode().width;
resolution.y = VideoMode::getDesktopMode().height;
m_Window.create(VideoMode(resolution.x, resolution.y),
"Thomas was late",
Style::Fullscreen);
// Initialize the full screen view
m_MainView.setSize(resolution);
m_HudView.reset(
FloatRect(0, 0, resolution.x, resolution.y));
// Initialize the split-screen Views
m_LeftView.setViewport(
FloatRect(0.001f, 0.001f, 0.498f, 0.998f));
m_RightView.setViewport(
FloatRect(0.5f, 0.001f, 0.499f, 0.998f));
m_BGLeftView.setViewport(
FloatRect(0.001f, 0.001f, 0.498f, 0.998f));
m_BGRightView.setViewport(
FloatRect(0.5f, 0.001f, 0.499f, 0.998f));
m_BackgroundTexture = TextureHolder::GetTexture(
"graphics/background.png");
// Associate the sprite with the texture
m_BackgroundSprite.setTexture(m_BackgroundTexture);
}
We have seen much of this code before. For example, there are the usual lines of code to get the screen resolution, as well as to create a RenderWindow. At the end of the previous code, we use the now-familiar code to load a texture and assign it to a Sprite. In this case, we are loading the background.png texture and assigning it to m_BackgroundSprite.
It is the code in between the four calls to the setViewport function that needs some explanation. The setViewport function assigns a portion of the screen to an SFML View object. It doesn't work with pixel coordinates, however. It works using a ratio. Here, "1" is the entire screen (width or height). The first two values in each call to setViewport are the starting position (horizontally then vertically), while the last two are the ending position.
Notice that m_LeftView and m_BGLeftView are placed in exactly the same place, that is, starting on virtually the far-left (0.001) of the screen and ending two-thousandths from the center (0.498).
m_RightView and m_BGRightView are also in exactly the same position as each other, starting just right of the previous two View objects (0.5) and extending to almost the far-right-hand side (0.998).
Furthermore, all the views leave a tiny slither of a gap at the top and bottom of the screen. When we draw these View objects on the screen, on top of a white background, it will have the effect of splitting the screen with a thin white line between the two sides of the screen, as well as a thin white border around the edges.
I have tried to represent this effect in the following diagram:
The best way to understand it is to finish this chapter, run the code, and see it in action.
The code for this function will go in the Engine.cpp file we have recently created.
Add the following code immediately after the previous constructor code:
void Engine::run()
{
// Timing
Clock clock;
while (m_Window.isOpen())
{
Time dt = clock.restart();
// Update the total game time
m_GameTimeTotal += dt;
// Make a decimal fraction from the delta time
float dtAsSeconds = dt.asSeconds();
// Call each part of the game loop in turn
input();
update(dtAsSeconds);
draw();
}
}
The run function is the center of our engine; it initiates all the other parts. First, we declare a Clock object. Next, we have the familiar while(window.isOpen()) loop, which creates the game loop. Inside this while loop, we do the following:
All of this should look very familiar. What's new is that it is wrapped in the run function.
As we explained previously, the code for the input function will go in its own file because it is more extensive than the constructor or the run function. We will use #include "Engine.h" and prefix the function signature with Engine:: to make sure the compiler is aware of our intentions.
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 Input.cpp. Finally, click the Add button. We are now ready to code the input function.
Add the following code:
void Engine::input()
{
Event event;
while (m_Window.pollEvent(event))
{
if (event.type == Event::KeyPressed)
{
// Handle the player quitting
if (Keyboard::isKeyPressed(Keyboard::Escape))
{
m_Window.close();
}
// Handle the player starting the game
if (Keyboard::isKeyPressed(Keyboard::Return))
{
m_Playing = true;
}
// Switch between Thomas and Bob
if (Keyboard::isKeyPressed(Keyboard::Q))
{
m_Character1 = !m_Character1;
}
// Switch between full and split-screen
if (Keyboard::isKeyPressed(Keyboard::E))
{
m_SplitScreen = !m_SplitScreen;
}
}
}
}
Like the previous projects, we check the RenderWindow event queue each frame. Also like we've already done before, we detect specific keyboard keys using if (Keyboard::isKeyPressed.... The most relevant information in the code we just added is what the keys do:
Most of this keyboard functionality will be fully working by the end of this chapter. We are getting close to being able to run our game engine. Next, let's code the update function.
As we explained previously, the code for this function will go in its own file because it is more extensive than the constructor or the run function. We will use #include "Engine.h" and prefix the function signature with Engine:: to make sure the compiler is aware of our intentions.
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 Update.cpp. Finally, click the Add button. We are now ready to write some code for the update function.
Add the following code to the Update.cpp file to implement the update function:
#include "Engine.h"
#include <SFML/Graphics.hpp>
#include <sstream>
using namespace sf;
void Engine::update(float dtAsSeconds)
{
if (m_Playing)
{
// Count down the time the player has left
m_TimeRemaining -= dtAsSeconds;
// Have Thomas and Bob run out of time?
if (m_TimeRemaining <= 0)
{
m_NewLevelRequired = true;
}
}// End if playing
}
First, notice that the update function receives the time the previous frame took as a parameter. This, of course, will be essential for the update function to fulfill its role.
The previous code doesn't achieve anything visible at this stage. It does put in the structure that we will require for future chapters. It subtracts the time the previous frame took from m_TimeRemaining and checks whether time has run out. If it has, it sets m_NewLevelRequired to true. All this code is wrapped in an if statement that only executes when m_Playing is true. The reason for this is that, like the previous projects, we don't want time advancing and objects updating when the game has not started.
We will build on this code as the project continues.
As we explained previously, the code for this function will go in its own file because it is more extensive than the constructor or the run function. We will use #include "Engine.h" and prefix the function signature with Engine:: to make sure the compiler is aware of our intentions.
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 Draw.cpp. Finally click the Add button. We are now ready to add some code to the draw function.
Add the following code to the Draw.cpp file to implement the draw function:
#include "Engine.h"
void Engine::draw()
{
// Rub out the last frame
m_Window.clear(Color::White);
if (!m_SplitScreen)
{
// Switch to background view
m_Window.setView(m_BGMainView);
// Draw the background
m_Window.draw(m_BackgroundSprite);
// Switch to m_MainView
m_Window.setView(m_MainView);
}
else
{
// Split-screen view is active
// First draw Thomas' side of the screen
// Switch to background view
m_Window.setView(m_BGLeftView);
// Draw the background
m_Window.draw(m_BackgroundSprite);
// Switch to m_LeftView
m_Window.setView(m_LeftView);
// Now draw Bob's side of the screen
// Switch to background view
m_Window.setView(m_BGRightView);
// Draw the background
m_Window.draw(m_BackgroundSprite);
// Switch to m_RightView
m_Window.setView(m_RightView);
}
// Draw the HUD
// Switch to m_HudView
m_Window.setView(m_HudView);
// Show everything we have just drawn
m_Window.display();
}
In the previous code, there is nothing we haven't seen before. The code starts, as usual, by clearing the screen. In this project, we clear the screen with White. What's new is the way the different drawing options are separated by a condition that checks whether the screen is currently split or not:
if (!m_SplitScreen)
{
}
else
{
}
If the screen is not split, we draw the background sprite in the background View (m_BGView) and then switch to the main full screen View (m_MainView). Note that, currently, we don't do any drawing in m_MainView.
If, on the other hand, the screen is split, the code in the else block is executed and we draw m_BGLeftView with the background sprite on the left of the screen, followed by switching to m_LeftView.
Then, still in the else block, we draw m_BGRightView with the background sprite on the right of the screen, followed by switching to m_RightView.
Outside of the if else structure we just described, we switch to the m_HUDView. At this stage, we are not actually drawing anything in m_HUDView.
Like the other two (input, update) of the three most significant functions, we will go back to the draw function often. We will add new elements for our game that need to be drawn. You will notice that, each time we do, we will add code into each of the main, left-hand, and right-hand sections.
Let's quickly recap on the Engine class and then we can fire it up.
What we have done is abstract all the code that used to be in the main function into the input, update, and draw functions. The continuous looping of these functions, as well as the timing, is handled by the run function.
Consider leaving the Input.cpp, Update.cpp, and Draw.cpp tabs open in Visual Studio, perhaps organized in order, as shown in the following screenshot:
We will revisit each of these functions throughout the course of the project and add more code. Now that we have the basic structure and functionality of the Engine class, we can create an instance of it in the main function and see it in action.
Let's rename the TFL.cpp file that was autogenerated when the project was created to Main.cpp. Right-click the TFL file in the Solution Explorer and select Rename. Change the name to Main.cpp. This will be the file that contains our main function and the code that instantiates the Engine class.
Add the following code to Main.cpp:
#include "Engine.h"
int main()
{
// Declare an instance of Engine
Engine engine;
// Start the engine VRRrrrrmmm
engine.run();
// Quit in the usual way when the engine is stopped
return 0;
}
All we do is add an include directive for the Engine class, declare an instance of Engine, and then call its run function. Everything will be handled by the Engine class until the player quits and the execution returns to main and the return 0 statement.
That was easy. Now, we can run the game and see the empty background, either in full screen or split-screen, which will eventually contain all the action.
Here is the game so far in full screen mode, showing just the background:
Now, tap the E key. You will be able to see the screen neatly partitioned into two halves, ready for split-screen coop gameplay:
In this chapter, we introduced the Thomas Was Late game and laid the foundations of understanding as well as the code structure for the rest of the project. It is certainly true that there are a lot of files in the Solution Explorer but, provided we understand the purpose of each, we will find the implementation of the rest of the project quite easy.
In the next chapter, we will learn about two more fundamental C++ topics, inheritance and polymorphism. We will also begin to put them to use by building three classes to represent two playable characters.
Here is a question that might be on your mind:
Q) I don't fully understand the structure of the code files. What should I do?
A) It is true that abstraction can make the structure of our code less clear, but the actual code itself becomes so much easier. Instead of cramming everything into the main function like we did in the previous projects, we will split the code up into Input.cpp, Update.cpp, and Draw.cpp. Furthermore, we will use more classes to group together related code as we proceed. Study the Structuring the Thomas Was Late code section again, especially the diagrams.
18.188.131.255