Chapter 14: Abstraction and Code Management – Making Better Use of OOP

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:

  • Introducing the final project, Thomas Was Late, including the gameplay features and project assets
  • A detailed discussion of how we will improve the structure of the code compared to previous projects
  • Coding the Thomas Was Late game engine
  • Implementing split-screen functionality

The Thomas Was Late game

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.

Features of 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:

  • A clock that counts down from a time appropriate to the challenge of the level.
  • Fire pits that emit a roar relative to the position of the player and respawn the player at the start if they fall in. Water pits have the same effect but without the directional sound effects.
  • Cooperative gameplay. Both players will have to get their characters to the goal within the allotted time. They will need to work together frequently so that the shorter, lower-jumping Bob will need to stand on his friend's (Thomas') head.
  • The player will have the option of switching between full and split-screen so they can attempt to control both characters themselves.
  • Each level will be designed in, and loaded from, a text file. This will make it easy to design varied and numerous levels.

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:

  • The preceding screenshot shows a simple HUD that details the level number and the number of seconds remaining until the player(s) fail and must restart the level.
  • You can also clearly see the split-screen coop in action. Remember that this is optional. A single player can take on the game, fullscreen, while switching the camera focus between Thomas and Bob.
  • It is not very clear in the preceding screenshot (especially in print), but when a character dies, they will explode in a starburst/firework-like particle effect.
  • The water and fire tiles can be strategically placed to make the level fun, as well as forcing cooperation between the characters. More on this will be covered in Chapter 16, Building Playable Levels and Collision Detection.
  • Next, notice Thomas and Bob. They are not only different in height but also have significantly varied jumping abilities. This means that Bob is dependent upon Thomas for big jumps, and levels can be designed to force Thomas to take a specific route to avoid him "banging his head".
  • In addition, the fire tiles will emit a roaring sound. These will be relative to the position of Thomas. Not only will they be directional and come from either the left or right speaker, they will also get louder and quieter as Thomas moves closer to or further away from the source.
  • Finally, in the preceding annotated screenshot, you can see the background. Why not compare how that looks to the background.png file (shown later in this chapter)? You will see it is quite different. We will use OpenGL shader effects in Chapter 18, Particle Systems and Shaders, to achieve the moving, almost bubbling, effect in the background.

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 project

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:

  1. Start Visual Studio and click on the Create New Project button. If you have another project open, you can select File | New project.
  2. In the window shown next, choose Console app and click the Next button. You will then see the Configure your new project window.
  3. In the Configure your new project window, type TWL in the Project name field.
  4. In the Location field, browse to the VS Projects folder.
  5. Check the option to Place solution and project in the same directory.
  6. When you have completed these steps, click Create.
  7. We will now configure the project to use the SFML files that we put in the SFML folder. From the main menu, select Project | TWL properties…. At this stage, you should have the TWL Property Pages window open.
  8. In the TWL Property Pages window, take the following steps. Select All Configurations from the Configuration: dropdown.
  9. Now, select C/C++ and then General from the left-hand menu.
  10. Now, locate the Additional Include Directories edit box and type the drive letter where your SFML folder is located, followed by SFMLinclude. The full path to type, if you located your SFML folder on your D drive, is D:SFMLinclude. Vary your path if you installed SFML on a different drive.
  11. Click Apply to save your configurations so far.
  12. Now, still in the same window, perform the following steps. From the left-hand menu, select Linker and then General.
  13. Now, find the Additional Library Directories edit box and type the drive letter where your SFML folder is, followed by SFMLlib. So, the full path to type if you located your SFML folder on your D drive is D:SFMLlib. Vary your path if you installed SFML to a different drive.
  14. Click Apply to save your configurations so far.
  15. Next, still in the same window, perform these steps. Switch the Configuration: dropdown to Debug as we will be running and testing Pong in debug mode.
  16. Select Linker and then Input.
  17. Find the Additional Dependencies edit box and click into it at the far-left-hand side. Now, copy and paste/type the following: sfml-graphics-d.lib;sfml-window-d.lib;sfml-system-d.lib;sfml-network-d.lib;sfml-audio-d.lib;. Be extra careful to place the cursor exactly at the start of the edit box's current content so that you don't overwrite any of the text that is already there.
  18. Click OK.
  19. Click Apply and then OK.

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:

  1. My main project directory is D:VS ProjectsTWL. This folder was created by Visual Studio in the previous steps. If you put your Projects folder somewhere else, then perform this step there instead. The files we need to copy into the project folder are located in our SFMLin folder. Open a window for each of the two locations and highlight all the .dll files.
  2. Now, copy and paste the highlighted files into the project.

The project is now set up and ready to go.

The project's assets

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.

Game level designs

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.

GLSL 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 close up

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 assets close up

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:

  • fallinfire.wav: A sound that will be played when the player's head goes into fire and the player has no chance of escape.
  • fallinwater.wav: Water has the same end effect as fire: death. This sound effect notifies the player that they need to start from the beginning of the level.
  • fire1.wav: This sound effect is recorded in mono. It will be played at different volumes, based on the player's distance from fire tiles and from different speakers based on whether the player is to the left or the right of the fire tile. Clearly, we will need to learn a few more tricks to implement this functionality.
  • jump.wav: A pleasing (slightly predictable) whooping sound for when the player jumps.
  • reachgoal.wav: A pleasing victory sound for when the player(s) get both characters (Thomas and Bob) to the goal tile.

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.

Adding the assets to the project

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:

  1. Browse to the D:VS ProjectsTWL folder.
  2. Create five new folders within this folder and name them graphics, sound, fonts, shaders, and levels.
  3. From the download bundle, copy the entire contents of Chapter 14/graphics into the D:VS ProjectsTWLgraphics folder.
  4. From the download bundle, copy the entire contents of Chapter 14/sound into the D:VS ProjectsTWLsound folder.
  5. Now, visit http://www.dafont.com/roboto.font in your web browser and download the Roboto Light font.
  6. Extract the contents of the zipped download and add the Roboto-Light.ttf file to the D:VS ProjectsTWLfonts folder.
  7. From the download bundle, copy the entire contents of Chapter 12/levels into the D:VS ProjectsTWLlevels folder.
  8. From the download bundle, copy the entire contents of Chapter 12/shaders into the D:VS ProjectsTWLshaders folder.

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.

Structuring the Thomas Was Late 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.

Building the game engine

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.

Reusing the TextureHolder class

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.

Coding Engine.h

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:

  • TextureHolder th: The one and only instance of the TextureHolder class.
  • TILE_SIZE: A useful constant to remind us that each tile in the sprite-sheet is 50 pixels wide and 50 pixels high.
  • VERTS_IN_QUAD: A useful constant to make our manipulation of a VertexArray less error-prone. There are, in fact, four vertices in a quad. Now, we can't forget it.
  • GRAVITY: A constant int value representing the number of pixels by which the game characters will be pushed downward each second. This is quite a fun value to play with once the game is done. We initialize it to 300 here as this works well for our initial level designs.
  • m_Window: The usual RenderWindow object that we have had in all our projects.
  • The SFML View objects, m_MainView, m_LeftView, m_RightView, m_BGMainView: m_BGLeftView, m_BGRightView, and m_HudView: The first three View objects are for the full screen view and the left and right and split-screen views of the game. We also have a separate SFML View object for each of those three, which will draw the background behind. The last View object, m_HudView, will be drawn on top of the appropriate combination of the other six views to display the score, the remaining time, and any messages to the players. Having seven different View objects might imply complexity, but when you see how we deal with them as the chapter progresses, you will see they are quite straightforward. We will have the whole split-screen/full screen conundrum sorted out by the end of this chapter.
  • Sprite m_BackgroundSprite and Texture m_BackgroundTexture: Somewhat predictably, this combination of SFML Sprite and Texture will be for showing and holding the background graphic from the graphics assets folder.
  • m_Playing: This Boolean will keep the game engine informed about whether the level has started yet (by pressing the Enter key). The player does not have the option to pause the game once they have started it.
  • m_Character1: When the screen is full screen, should it center on Thomas (m_Character1 = true) or Bob (m_Character1 = false)? Initially, it is initialized to true, to center on Thomas.
  • m_SplitScreen: This variable is used to determine whether the game currently being played is in split-screen mode or not. We will use this variable to decide how exactly to use all the View objects we declared a few steps ago.
  • m_TimeRemaining variable: This float variable holds how much time (in seconds) is remaining to get to the goal of the current level. In the previous code, it is set to 10 for the purposes of testing, until we get to set a specific time for each level.
  • m_GameTimeTotal variable: This variable is an SFML Time object. It keeps track of how long the game has been played for.
  • m_NewLevelRequired Boolean variable: This variable keeps an eye on whether the player has just completed or failed a level. We can then use it to trigger loading the next level or restarting the current level.
  • The input function: This function will handle all the player's input, which in this game is entirely from the keyboard. At first glance, it would appear that it handles all the keyboard input directly. In this game, however, we will be handling keyboard input that directly affects Thomas or Bob within the Thomas and Bob classes. This function will also handle keyboard input such as quitting, switching to split-screen, and any other keyboard input.
  • The update function: This function will do all the work that we previously did in the update section of the main function. We will also call some other functions from the update function in order to keep the code organized. If you look back at the code, you will see that it receives a float parameter that will hold the fraction of a second that has passed since the previous frame. This, of course, is just what we need to update all our game objects.
  • The draw function: This function will hold all the code that used to go in the drawing section of the main function in previous projects. We will, however, have some drawing code that is not kept in this function when we look at other ways to draw with SFML. We will see this new code when we learn about particle systems in Chapter 18, Particle Systems and Shaders.

Now, let's run through all the public functions:

  • The Engine constructor function: As we have come to expect, this function will be called when we first declare an instance of Engine. It will do all the setup and initialization of the class. We will see exactly what when we code the Engine.cpp file shortly.
  • The run function: This is the only public function that we need to call. It will trigger the execution of input, update, and draw, and will do all the work for us.

Next, we will see the definitions of all these functions and some of the variables in action.

Coding Engine.cpp

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.

Coding the Engine class constructor definition

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.

Coding the run function definition

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:

  1. Restart clock and save the time that the previous loop took in dt.
  2. Keep track of the total time elapsed in m_GameTimeTotal.
  3. Declare and initialize a float to represent the fraction of a second that elapsed during the previous frame.
  4. Call input.
  5. Call update, passing in the elapsed time (dtAsSeconds).
  6. Call draw.

All of this should look very familiar. What's new is that it is wrapped in the run function.

Coding the input function definition

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:

  • As per usual, the Esc key closes the window and the game will quit.
  • The Enter key sets m_Playing to true and eventually this will have the effect of starting the level.
  • The Q key alternates the value of m_Character1 between true and false. This key only has an effect in full screen mode. It will switch between Thomas and Bob being the center of the main View.
  • The E keyboard key switches m_SplitScreen between true and false. This will have the effect of switching between full screen and split-screen views.

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.

Coding the update function definition

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.

Coding the draw function definition

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.

The Engine class so far

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.

Coding the main function

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:

Summary

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.

FAQ

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.

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

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