Chapter 8: SFML Views – Starting the Zombie Shooter Game

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:

  • Planning and starting the Zombie Arena game
  • Coding the Player class
  • Learning about the SFML View class
  • Building the Zombie Arena game engine
  • Putting the Player class to work

Planning and starting the Zombie Arena game

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:

  • A Heads Up Display (HUD) that shows details such as the score, high score, and bullets in clip, the number of bullets left, player health, and the number of zombies left to kill.
  • The player will shoot zombies while frantically running away from them.
  • Move around a scrolling world using the WASD keyboard keys while aiming the gun using the mouse.
  • In-between each level, the player will choose a "level-up" that will affect the way the game needs to be played for the player to win.
  • The player will need to collect "pick-ups" to restore health and ammunition.
  • Each wave brings more zombies and a bigger arena to make it more challenging.

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:

  1. The score and hi-score. These, along with the other parts of the HUD, will be drawn in a separate layer, known as a view, and represented by an instance of the View class. The hi-score will be saved and loaded to a file.
  2. A texture that will build a wall around the arena. This texture is contained in a single graphic called a sprite sheet, along with the other background textures (points 3, 5, and 6).
  3. The first of two mud textures from the sprite sheet.
  4. This is an "ammo pick-up." When the player gets this, they will be given more ammunition. There is a "health pick-up" as well, from which the player will receive more health. These pick-ups can be chosen by the player to be upgraded in-between waves of zombies.
  5. A grass texture, also from the sprite sheet.
  6. The second mud texture from the sprite sheet.
  7. A blood splat where there used to be a zombie.
  8. The bottom part of the HUD. From left to right, there is an icon to represent ammo, the number of bullets in the clip, the number of spare bullets, a health bar, the current wave of zombies, and the number of zombies remaining for the current wave.
  9. The player's character.
  10. A crosshair, which the player aims with the mouse.
  11. A slow-moving, but strong, "bloater" zombie.
  12. A slightly faster-moving, but weaker, "crawler" zombie. There is also a "chaser zombie" that is very fast and weak. Unfortunately, I couldn't manage to get one in the screenshot before they were all killed.

So, we have a lot to do and new C++ skills to learn. Let's start by creating a new project.

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:

  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 on the Next button. You will then see the Configure your new project window.
  3. In the Configure your new project window, type Zombie Arena 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 the preceding steps, click on 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 | Zombie Arena properties…. At this stage, you should have the Zombie Arena Property Pages window open.
  8. In the Zombie Arena Property Pages window, take the following steps. Select All Configurations from the Configuration: dropdown menu.
  9. Now, select C/C++ and then General from the left-hand menu.
  10. Next, 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, will be D:SFMLinclude. Vary your path if you installed SFML on a different drive.
  11. Click on Apply to save your configurations so far.
  12. Now, still in the same window, perform these next 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, will be D:SFMLlib. Change your path if you installed SFML to a different drive.
  14. Click on Apply to save your configurations so far.
  15. Next, still in the same window, perform these steps. Switch the Configuration: dropdown menu 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 on it in 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 as not to overwrite any of the text that is already there.
  18. Click on OK.
  19. Click on Apply and then OK.

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:

  1. My main project directory is D:VS Projectsombie Arena. This folder was created by Visual Studio in the previous steps. If you put your Projects folder somewhere else, then perform this step in your directory. The files we need to copy into the project folder are in your 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. Next, we will explore and add the project assets.

The project assets

The assets in this project are more numerous and more diverse than the previous games. The assets include the following:

  • A font for the text on the screen
  • Sound effects for different actions such as shooting, reloading, or getting hit by a zombie
  • Graphics for the character, zombies, and a sprite sheet for the various background textures

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.

Exploring the assets

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:

  • hit.wav: A sound that plays when a zombie comes into contact with the player.
  • pickup.wav: A sound that plays when the player collides or steps on (collects) a health boost (pick-up).
  • powerup.wav: A sound for when the player chooses an attribute to increase their strength (power-up) in-between each wave of zombies.
  • reload.wav: A satisfying click to let the player know they have loaded a fresh clip of ammunition.
  • reload_failed.wav: A less satisfying sound that indicates failing to load new bullets.
  • shoot.wav: A shooting sound.
  • splat.wav: A sound like a zombie being hit by a bullet.

Once you have decided which assets you will use, it is time to add them to the project.

Adding the assets 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:

  1. Browse to D:VS ProjectsombieArena.
  2. Create three new folders within this folder and name them graphics, sound, and fonts.
  3. From the download bundle, copy the entire contents of Chapter 8/graphics into the D:VS ProjectsombieArenagraphics folder.
  4. From the download bundle, copy the entire contents of Chapter 6/sound into the D:VS ProjectsombieArenasound folder.
  5. Now, visit http://www.1001freefonts.com/zombie_control.font in your web browser and download the Zombie Control font.
  6. Extract the contents of the zipped download and add the zombiecontrol.ttf file to the D:VS ProjectsombieArenafonts folder.

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.

OOP and the Zombie Arena project

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:

  • Its horizontal and vertical position
  • Its size
  • The direction it is facing
  • A different texture for each zombie type
  • A Sprite
  • A different speed for each zombie type
  • A different health for each zombie type
  • Keeping track of the type of each zombie
  • Collision detection data
  • Its intelligence (to chase the player), which is slightly different for each type of zombie
  • An indication of whether the zombie is alive or dead

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.

Building the player – the first class

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.

Coding the Player class header file

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:

  • void spawn(IntRect arena, Vector2f resolution, int tileSize): This function does what its name suggests. It will prepare the object ready for use, which includes putting it in its starting location (that is, spawning it). Notice that it doesn't return any data, but it does have three arguments. It receives an IntRect instance called arena, which will be the size and location of the current level; a Vector2f instance, which will contain the screen resolution; and an int, which will hold the size of a background tile.
  • void resetPlayerStats: Once we give the player the ability to level up between waves, we will need to be able to take away/reset those abilities at the start of a new game.
  • Time getLastHitTime(): This function does just one thing – it returns the time when the player was last hit by a zombie. We will use this function when detecting collisions, and it will allow us to make sure the player isn't punished too frequently for making contact with a zombie.
  • FloatRect getPosition(): This function returns a FloatRect instance that describes the horizontal and vertical floating-point coordinates of the rectangle, which contains the player graphic. This is also useful for collision detection.
  • Vector2f getCenter(): This is slightly different to getPosition because it is a Vector2f type and contains just the x and y locations of the very center of the player graphic.
  • float getRotation(): The code in the main function will sometimes need to know, in degrees, which way the player is currently facing. 3 o'clock is 0 degrees and increases clockwise.
  • Sprite getSprite(): As we discussed previously, this function returns a copy of the sprite that represents the player.
  • void moveLeft(), ..Right(), ..Up(), ..Down(): These four functions have no return type or parameters. They will be called from the main function and the Player class will then be able to act when one or more of the WASD keys have been pressed.
  • void stopLeft(), ..Right(), ..Up(), ..Down(): These four functions have no return type or parameters. They will be called from the main function, and the Player class will then be able to act when one or more of the WASD keys have been released.
  • void update(float elapsedTime, Vector2i mousePosition): This will be the only long function of the entire class. It will be called once per frame from main. It will do everything necessary to make sure the player object's data is updated so that it's ready for collision detection and drawing. Notice that it returns no data but receives the amount of elapsed time since the last frame, along with a Vector2i instance, which will hold the horizontal and vertical screen location of the mouse pointer/crosshair.

    Important note

    Note that these are integer screen coordinates and are distinct from the floating-point world coordinates.

  • void upgradeSpeed(): A function that can be called from the leveling up screen when the player chooses to make the player faster.
  • void upgradeHealth(): Another function that can be called from the leveling up screen when the player chooses to make the player stronger (that is, have more health).
  • void increaseHealthLevel(int amount): A subtle but important difference regarding the previous function in that this one will increase the amount of health the player has, up to the maximum that's currently set. This function will be used when the player picks up a health pick-up.
  • int getHealth(): With the level of health being as dynamic as it is, we need to be able to determine how much health the player has at any given moment. This function returns an int, which holds that value.

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.

Coding the Player class function 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.

Controlling the game camera with SFML View

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.

Starting the Zombie Arena game engine

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.

Managing the code files

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.

Starting to code 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:

  1. If the state is equal to LEVELING_UP, wait for either the 1, 2, 3, 4, 5, or 6 keys to be pressed.
  2. When pressed, change state to PLAYING.
  3. When the state changes, still within the if (state == State::LEVELING_UP) block, the nested if(state == State::PLAYING) block will run.
  4. Within this block, we set the location and size of arena, set the tileSize to 50, pass all the information to player.spawn, and call clock.restart.

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.

Summary

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

FAQ

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.

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

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