Chapter 17: Sound Spatialization and the HUD

In this chapter, we will be adding all the sound effects and the HUD. We have done this in two of the previous projects, but we will do things a bit differently this time. We will explore the concept of sound spatialization and how SFML makes this otherwise complicated concept nice and easy. In addition, we will build a HUD class to encapsulate our code that draws information to the screen.

We will complete these tasks in the following order.

  • What is spatialization?
  • How SFML handles spatialization
  • Building a SoundManager class
  • Deploying emitters
  • Using the SoundManager class
  • Building a HUD class
  • Using the HUD class

What is spatialization?

Spatialization is the act of making something relative to the space it is a part of, or within. In our daily lives, everything in the natural world, by default, is spatialized. If a motorbike whizzes past from left to right, we will hear the sound grow from faint to loud from one side to the other. As it passes by, it will become more prominent in the other ear, before fading into the distance once more. If we were to wake up one morning and the world was no longer spatialized, it would be exceptionally weird.

If we can make our video games a little bit more like the real world, our players can become more immersed. Our zombie game would have been a lot more fun if the player could have heard them faintly in the distance and their inhuman wailing grew louder as they drew closer, from one direction or another.

It is probably obvious that the mathematics of spatialization will be complex. How do we calculate how loud a given sound will be in a specific speaker based on the distance and direction from the player (the hearer of the sound) to the object that is making the sound (the emitter)?

Fortunately, SFML does all the complicated processes for us. All we need to do is get familiar with a few technical terms and then we can start using SFML to spatialize our sound effects.

Emitters, attenuation, and listeners

We will need to be aware of a few pieces of information in order to give SFML what it needs to do its work. We will need to be aware of where the sound is coming from in our game world. This source of the sound is called an emitter. In a game, the emitter could be a zombie, a vehicle, or in the case of our current project, a fire tile. We have already been keeping track of the position of the objects in our game, so giving SFML the emitter's location will be quite straightforward.

The next factor we need to be aware of is attenuation. Attenuation is the rate at which a wave deteriorates. You could simplify that statement and make it specific to sound and say that attenuation is how quickly the sound reduces in volume. It isn't technically accurate, but it is a good enough description for the purposes of this chapter and our game.

The final factor that we need to consider is the listener. When SFML spatializes the sound, where is it spatializing it relative to; where are the "ears" of the game.? In most games, the logical thing to do is use the player character. In our game, we will use Thomas (our player character).

Handling spatialization using SFML

SFML has several functions that allow us to handle emitters, attenuation, and listeners. Let's take a look at them hypothetically and then we will write some code to add spatialized sound to our project for real.

We can set up a sound effect ready to be played, as we have done so often already, like this:

// Declare SoundBuffer in the usual way

SoundBuffer zombieBuffer;

// Declare a Sound object as-per-usual

Sound zombieSound;

// Load the sound from a file like we have done so often

zombieBuffer.loadFromFile("sound/zombie_growl.wav");

// Associate the Sound object with the Buffer

zombieSound.setBuffer(zombieBuffer);

We can set the position of the emitter using the setPosition function shown in the following code:

// Set the horizontal and vertical positions of the emitter

// In this case the emitter is a zombie

// In the Zombie Arena project we could have used

// getPosition().x and getPosition().y

// These values are arbitrary

float x = 500;

float y = 500;

zombieSound.setPosition(x, y, 0.0f);

As suggested in the comments of the previous code, how exactly we can obtain the coordinates of the emitter will probably be dependent on the type of game. As shown in the previous code, this would be quite simple in the Zombie Arena project. We will have a few challenges to overcome when we set the position in this project.

We can set the attenuation level as follows:

zombieSound.setAttenuation(15);

The actual attenuation level can be a little ambiguous. The effect that we want the player to get might be different from the accurate scientific formula that is used to reduce the volume over distance based on attenuation. Getting the right attenuation level is usually achieved by experimenting. The higher the level of attenuation, the quicker the sound level reduces to silence.

Also, we might want to set a zone around the emitter where the volume is not attenuated at all. We might do this if the feature isn't appropriate beyond a certain range or if we have many sound sources and don't won't to "overdo" the feature. To do so, we can use the setMinimumDistance function as shown here:

zombieSound.setMinDistance(150);

With the previous line of code, attenuation would not  be calculated until the listener is 150 pixels/units away from the emitter.

Some other useful functions from the SFML library include the setLoop function. This function will tell SFML to keep playing the sound over and over when true is passed in as a parameter, like in the following code:

zombieSound.setLoop(true);

The sound would continue to play until we end it with the following code:

zombieSound.stop();

From time to time, we would want to know the status of a sound (playing or stopped). We can achieve this with the getStatus function, as demonstrated in the following code:

if (zombieSound.getStatus() == Sound::Status::Stopped)

{

    // The sound is NOT playing

    // Take whatever action here

}

if (zombieSound.getStatus() == Sound::Status::Playing)

{

    // The sound IS playing

    // Take whatever action here

}

There is just one more aspect of using sound spatialization with SFML that we need to cover. The listener. Where is the listener? We can set the position of the listener with the following code:

// Where is the listener?

// How we get the values of x and y varies depending upon the game

// In the Zombie Arena game or the Thomas Was Late game

// We can use getPosition()

Listener::setPosition(m_Thomas.getPosition().x,

    m_Thomas.getPosition().y, 0.0f);

The preceding code will make all the sounds play relative to that location. This is just what we need for the distant roar of a fire tile or incoming zombie, but for regular sound effects like jumping, this is a problem. We could start handling an emitter for the location of the player, but SFML makes things simple for us. Whenever we want to play a "normal" sound, we simply call setRelativeToListener, as shown in the following code, and then play the sound in the exact same way we have done so far. Here is how we might play a "normal" unspatialized jump sound effect:

jumpSound.setRelativeToListener(true);

jumpSound.play();

All we need to do is call Listener::setPosition again before we play any spatialized sounds.

We now have a wide repertoire of SFML sound functions, and we are ready to make some spatialized noise for real.

Building the SoundManager class

You might recall from the previous project that all the sound code took up quite a few lines of code. Now, consider that, with spatialization, it's going to get longer still. To keep our code manageable, we will code a class to manage all our sound effects being played. In addition, to help us with spatialization, we will add a function to the Engine class as well, but we will discuss that when we come to it, later in this chapter.

Coding SoundManager.h

Let's get started by coding and examining the header file.

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 SoundManager.h. Finally, click the Add button. We are now ready to code the header file for the SoundManager class.

Add and examine the following code:

#pragma once

#include <SFML/Audio.hpp>

using namespace sf;

class SoundManager

{

    private:

        // The buffers

        SoundBuffer m_FireBuffer;

        SoundBuffer m_FallInFireBuffer;

        SoundBuffer m_FallInWaterBuffer;

        SoundBuffer m_JumpBuffer;

        SoundBuffer m_ReachGoalBuffer;

        // The Sounds

        Sound m_Fire1Sound;

        Sound m_Fire2Sound;

        Sound m_Fire3Sound;

        Sound m_FallInFireSound;

        Sound m_FallInWaterSound;

        Sound m_JumpSound;

        Sound m_ReachGoalSound;

        // Which sound should we use next, fire 1, 2 or 3

        int m_NextSound = 1;

    public:

        SoundManager();

        void playFire(Vector2f emitterLocation,

            Vector2f listenerLocation);

        void playFallInFire();

        void playFallInWater();

        void playJump();

        void playReachGoal();

};

There is nothing tricky in the code we just added. There are five SoundBuffer objects and eight Sound objects. Three of the Sound objects will play the same SoundBuffer. This explains the reason for the different number of Sound/SoundBuffer objects. We do this so that we can have multiple roaring sound effects playing, with different spatialized parameters, simultaneously.

Note the m_NextSound variable, which will help us keep track of which of these simultaneous sounds we should use next.

There is a constructor, SoundManager, where we will set up all our sound effects, and there are five functions that will play the sound effects. Four of these functions simply play "normal" sound effects and their code will be simpler.

One of the functions, playFire, will handle the spatialized sound effects and will be a bit more in-depth. Notice the parameters of the playFire function. It receives a Vector2f, which is the location of the emitter and a second Vector2f, which is the location of the listener.

Coding the SoundManager.cpp file

Now, we can code the function definitions. The constructor and the playFire functions have a large amount of code, so we will look at them individually. The other functions are short and sweet, so we will handle them all at once.

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 SoundManager.cpp. Finally, click the Add button. We are now ready to code the .cpp file for the SoundManager class.

Coding the constructor

Add the following code for the include directives and the constructor to SoundManager.cpp:

#include "SoundManager.h"

#include <SFML/Audio.hpp>

using namespace sf;

SoundManager::SoundManager()

{

    // Load the sound in to the buffers

    m_FireBuffer.loadFromFile("sound/fire1.wav");

    m_FallInFireBuffer.loadFromFile("sound/fallinfire.wav");

    m_FallInWaterBuffer.loadFromFile("sound/fallinwater.wav");

    m_JumpBuffer.loadFromFile("sound/jump.wav");

    m_ReachGoalBuffer.loadFromFile("sound/reachgoal.wav");

    // Associate the sounds with the buffers

    m_Fire1Sound.setBuffer(m_FireBuffer);

    m_Fire2Sound.setBuffer(m_FireBuffer);

    m_Fire3Sound.setBuffer(m_FireBuffer);

    m_FallInFireSound.setBuffer(m_FallInFireBuffer);

    m_FallInWaterSound.setBuffer(m_FallInWaterBuffer);

    m_JumpSound.setBuffer(m_JumpBuffer);

    m_ReachGoalSound.setBuffer(m_ReachGoalBuffer);

            

    // When the player is 50 pixels away sound is full volume

    float minDistance = 150;

    // The sound reduces steadily as the player moves further away

    float attenuation = 15;

    // Set all the attenuation levels

    m_Fire1Sound.setAttenuation(attenuation);

    m_Fire2Sound.setAttenuation(attenuation);

    m_Fire3Sound.setAttenuation(attenuation);

    // Set all the minimum distance levels

    m_Fire1Sound.setMinDistance(minDistance);

    m_Fire2Sound.setMinDistance(minDistance);

    m_Fire3Sound.setMinDistance(minDistance);

    // Loop all the fire sounds

    // when they are played

    m_Fire1Sound.setLoop(true);

    m_Fire2Sound.setLoop(true);

    m_Fire3Sound.setLoop(true);

}

In the previous code, we loaded five sound files into the five SoundBuffer objects. Next, we associated the eight Sound objects with one of the SoundBuffer objects. Notice that m_Fire1Sound, m_Fire2Sound, and m_Fire3Sound are all going to be playing from the same SoundBuffer, m_FireBuffer.

Next, we set the attenuation and minimum distance for the three fire sounds.

Tip

The values of 150 and 15, respectively, were arrived at through experimentation. Once the game is running, it is advisable to experiment with these values by changing them around and seeing (or rather, hearing) the difference.

Finally, for the constructor, we use the setLoop function on each of the fire-related Sound objects. Now, when we call play, they will play continuously.

Coding the playFire function

Add the playFire function as follows. Then, we can discuss it:

void SoundManager::playFire(

    Vector2f emitterLocation, Vector2f listenerLocation)

{

    // Where is the listener? Thomas.

    Listener::setPosition(listenerLocation.x,

        listenerLocation.y, 0.0f);

    switch(m_NextSound)

    {

    case 1:

        // Locate/move the source of the sound

        m_Fire1Sound.setPosition(emitterLocation.x,

            emitterLocation.y, 0.0f);

        if (m_Fire1Sound.getStatus() == Sound::Status::Stopped)

        {

            // Play the sound, if its not already

            m_Fire1Sound.play();

        }

        break;

    case 2:

        // Do the same as previous for the second sound

        m_Fire2Sound.setPosition(emitterLocation.x,

            emitterLocation.y, 0.0f);

        if (m_Fire2Sound.getStatus() == Sound::Status::Stopped)

        {

            m_Fire2Sound.play();

        }

        break;

    case 3:

        // Do the same as previous for the third sound

        m_Fire3Sound.setPosition(emitterLocation.x,

            emitterLocation.y, 0.0f);

        if (m_Fire3Sound.getStatus() == Sound::Status::Stopped)

        {

            m_Fire3Sound.play();

        }

        break;

    }

    // Increment to the next fire sound

    m_NextSound++;

    // Go back to 1 when the third sound has been started

    if (m_NextSound > 3)

    {

        m_NextSound = 1;

    }

}

The first thing we do is call Listener::setPosition and set the location of the listener based on the Vector2f that is passed in as a parameter.

Next, the code enters a switch block that tests the value of m_NextSound. Each of the case statements does the exact same thing but to either m_Fire1Sound, m_Fire2Sound, or m_Fire3Sound.

In each of the case blocks, we set the position of the emitter using the passed in parameter with the setPosition function. The next part of the code in each case block checks whether the sound is currently stopped, and, if it is, plays the sound. Soon, we will see how we arrive at the positions for the emitter and listener that are passed into this function.

The final part of the playFire function increments m_NextSound and ensures that it can only be equal to 1, 2, or 3, as required by the switch block.

Coding the rest of the SoundManager functions

Add these four simple functions:

void SoundManager::playFallInFire()

{

    m_FallInFireSound.setRelativeToListener(true);

    m_FallInFireSound.play();

}

void SoundManager::playFallInWater()

{

    m_FallInWaterSound.setRelativeToListener(true);

    m_FallInWaterSound.play();

}

void SoundManager::playJump()

{

    m_JumpSound.setRelativeToListener(true);

    m_JumpSound.play();

}

void SoundManager::playReachGoal()

{

    m_ReachGoalSound.setRelativeToListener(true);

    m_ReachGoalSound.play();

}

The playFallInFire, playFallInWater, and playReachGoal functions do just two things. First, they each call setRelativeToListener so that the sound effect is not spatialized, making the sound effect "normal", not directional, and then they call play on the appropriate Sound object.

That concludes the SoundManager class. Now, we can use it in the Engine class.

Adding SoundManager to the game engine

Open the Engine.h file and add an instance of the new SoundManager class, as shown in the following highlighted code:

#pragma once

#include <SFML/Graphics.hpp>

#include "TextureHolder.h"

#include "Thomas.h"

#include "Bob.h"

#include "LevelManager.h"

#include "SoundManager.h"

using namespace sf;

class Engine

{

private:

    // The texture holder

    TextureHolder th;

    // Thomas and his friend, Bob

    Thomas m_Thomas;

    Bob m_Bob;

    // A class to manage all the levels

    LevelManager m_LM;

    // Create a SoundManager

    SoundManager m_SM;

    const int TILE_SIZE = 50;

    const int VERTS_IN_QUAD = 4;

At this point, we could use m_SM to call the various play... functions. Unfortunately, there is still a bit more work to be done in order to manage the locations of the emitters (fire tiles).

Populating the sound emitters

Open the Engine.h file and add a new prototype for a populateEmitters function and a new STL vector of Vector2f objects:

    ...

    ...

    ...

    // Run will call all the private functions

    bool detectCollisions(PlayableCharacter& character);

    // Make a vector of the best places to emit sounds from

    void populateEmitters(vector <Vector2f>& vSoundEmitters,

        int** arrayLevel);

    // A vector of Vector2f for the fire emitter locations

    vector <Vector2f> m_FireEmitters;

    

public:

    ...

    ...

    ...

The populateEmitters function takes a vector of Vector2f objects as a parameter, as well as a pointer to pointer to int (a two-dimensional array). The vector will hold the location of each emitter in a level. The array is the two-dimensional array that holds the layout of a level.

Coding the populateEmitters function

The job of the populateEmitters function is to scan through all the elements of arrayLevel and decide where to put the emitters. It will store its results in m_FireEmitters.

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 PopulateEmitters.cpp. Finally, click the Add button. Now, we can code the new function, populateEmitters.

Add the code in its entirety. Be sure to study the code as you do, so that we can discuss it:

#include "Engine.h"

using namespace sf;

using namespace std;

void Engine::populateEmitters(

    vector <Vector2f>& vSoundEmitters,

   int** arrayLevel)

{

    // Make sure the vector is empty

    vSoundEmitters.empty();

    // Keep track of the previous emitter

    // so we don't make too many

    FloatRect previousEmitter;

    // Search for fire in the level

    for (int x = 0; x < (int)m_LM.getLevelSize().x; x++)

    {

        for (int y = 0; y < (int)m_LM.getLevelSize().y; y++)

        {

            if (arrayLevel[y][x] == 2)// fire is present

            {

                // Skip over any fire tiles too

                // near a previous emitter

                if (!FloatRect(x * TILE_SIZE,

                    y * TILE_SIZE,

                    TILE_SIZE,

                    TILE_SIZE).intersects(previousEmitter))

                {

                    // Add the coordinates of this water block

                    vSoundEmitters.push_back(

                        Vector2f(x * TILE_SIZE, y * TILE_SIZE));

                    // Make a rectangle 6 blocks x 6 blocks,

                    // so we don't make any more emitters

                    // too close to this one

                    previousEmitter.left = x * TILE_SIZE;

                    previousEmitter.top = y * TILE_SIZE;

                    previousEmitter.width = TILE_SIZE * 6;

                    previousEmitter.height = TILE_SIZE * 6;

                }

            }

        }

    }

    return;

}

Some of the code might appear complex at first glance. Understanding the technique we are using to choose where an emitter will be makes this simpler. In our levels, there are large blocks of fire tiles. For example, in one of the levels, there are more than 30 fire tiles together in a group. The code makes sure that there is only one emitter within a given rectangle. This rectangle is stored in previousEmitter and is 300 pixels by 300 pixels (TILE_SIZE * 6).

The code sets up a nested for loop that loops through arrayLevel, looking for fire tiles. When it finds one, it makes sure that it does not intersect with previousEmitter. Only then does it use the pushBack function to add another emitter to vSoundEmitters. After doing so, it also updates previousEmitter to avoid getting large clusters of sound emitters.

Let's make some noise.

Playing sounds

Open the LoadLevel.cpp file and add the call to the new populateEmitters function, as highlighted in the following code:

void Engine::loadLevel()

{

    m_Playing = false;

    // Delete the previously allocated memory

    for (int i = 0; i < m_LM.getLevelSize().y; ++i)

    {

        delete[] m_ArrayLevel[i];

    }

    delete[] m_ArrayLevel;

    // Load the next 2d array with the map for the level

    // And repopulate the vertex array as well

    m_ArrayLevel = m_LM.nextLevel(m_VALevel);

    // Prepare the sound emitters

    populateEmitters(m_FireEmitters, m_ArrayLevel);

    // How long is this new time limit

    m_TimeRemaining = m_LM.getTimeLimit();

    // Spawn Thomas and Bob

    m_Thomas.spawn(m_LM.getStartPosition(), GRAVITY);

    m_Bob.spawn(m_LM.getStartPosition(), GRAVITY);

    // Make sure this code isn't run again

    m_NewLevelRequired = false;

}

The first sound to add is the jump sound. We remember that the keyboard handling code is in the pure virtual functions within both the Bob and Thomas classes and that the handleInput function returns true when a jump has been successfully initiated.

Open the Input.cpp file and add the following highlighted lines of code to play a jump sound when Thomas or Bob successfully begins a jump:

// Handle input specific to Thomas

if (m_Thomas.handleInput())

{

    // Play a jump sound

    m_SM.playJump();

}

// Handle input specific to Bob

if (m_Bob.handleInput())

{

    // Play a jump sound

    m_SM.playJump();

}

Open the Update.cpp file and add the following highlighted line of code to play a success sound when Thomas and Bob have simultaneously reached the goal for the current level:

// Detect collisions and see if characters have reached the goal tile

// The second part of the if condition is only executed

// when Thomas is touching the home tile

if (detectCollisions(m_Thomas) && detectCollisions(m_Bob))

{

    // New level required

    m_NewLevelRequired = true;

    // Play the reach goal sound

    m_SM.playReachGoal();

}

else

{

    // Run Bobs collision detection

    detectCollisions(m_Bob);

}

Also, within the Update.cpp file, we will add code to loop through the m_FireEmitters vector and decide when we need to call the playFire function of the SoundManager class.

Look closely at the small amount of context around the new highlighted code. It is essential to add this code in exactly the right place:

}// End if playing

// Check if a fire sound needs to be played

vector<Vector2f>::iterator it;

// Iterate through the vector of Vector2f objects

for (it = m_FireEmitters.begin(); it != m_FireEmitters.end(); it++)

{

    // Where is this emitter?

    // Store the location in pos

    float posX = (*it).x;

    float posY = (*it).y;

    // is the emitter near the player?

    // Make a 500 pixel rectangle around the emitter

    FloatRect localRect(posX - 250, posY - 250, 500, 500);

    // Is the player inside localRect?

    if (m_Thomas.getPosition().intersects(localRect))

    {

        // Play the sound and pass in the location as well

        m_SM.playFire(Vector2f(posX, posY), m_Thomas.getCenter());

    }

}

    

// Set the appropriate view around the appropriate character

The preceding code is a bit like collision detection for sound. Whenever Thomas strays within a 500 by 500-pixel rectangle surrounding a fire emitter, the playFire function is called, passing in the coordinates of the emitter and of Thomas. The playFire function does the rest of the work and plays a spatialized, looping sound effect.

Open the DetectCollisions.cpp file, find the appropriate place, and add the following highlighted code. The two highlighted lines of code trigger the sound effect when either character falls into a water or fire tile:

// Has character been burnt or drowned?

// Use head as this allows him to sink a bit

if (m_ArrayLevel[y][x] == 2 || m_ArrayLevel[y][x] == 3)

{

    if (character.getHead().intersects(block))

    {

        character.spawn(m_LM.getStartPosition(), GRAVITY);

        // Which sound should be played?

        if (m_ArrayLevel[y][x] == 2)// Fire, ouch!

        {

            // Play a sound

            m_SM.playFallInFire();

        }

        else // Water

        {

            // Play a sound

            m_SM.playFallInWater();

        }

    }

}

Playing the game will now allow you to hear all the sounds, including cool spatialization, when you're near a fire tile.

Implementing the HUD class

The HUD is super-simple and not really anything different compared to the Zombie Arena project. What we will do that is different is wrap all the code up in a new HUD class. If we declare all the Font, Text, and other variables as members of this new class, we can then initialize them in the constructor and provide getter functions to all their values. This will keep the Engine class clear from loads of declarations and initializations.

Coding HUD.h

First, we will code the HUD.h file with all the member variables and function declarations. 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 HUD.h. Finally, click the Add button. We are now ready to code the header file for the HUD class.

Add the following code to HUD.h:

#pragma once

#include <SFML/Graphics.hpp>

using namespace sf;

class Hud

{

private:

    Font m_Font;

    Text m_StartText;

    Text m_TimeText;

    Text m_LevelText;

public:

    Hud();

    Text getMessage();

    Text getLevel();

    Text getTime();

    void setLevel(String text);

    void setTime(String text);

};

In the preceding code, we added one Font instance and three Text instances. The Text objects will be used to show a message prompting the user to start, the time remaining, and the current level number.

The public functions are more interesting. First, there is the constructor where most of the code will go. The constructor will initialize the Font and Text objects, as well as position them on the screen relative to the current screen resolution.

The three getter functions, getMessage, getLevel, and getTime, will return a Text object to the calling code so that it can draw them to the screen.

The setLevel and setTime functions will be used to update the text shown in m_LevelText and m_TimeText, respectively.

Now, we can code all the definitions for the functions we have just declared.

Coding the HUD.cpp file

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 HUD.cpp. Finally, click the Add button. We are now ready to code the .cpp file for the HUD class.

Add the include directives and the following code. Then, we will discuss it:

#include "Hud.h"

Hud::Hud()

{

    Vector2u resolution;

    resolution.x = VideoMode::getDesktopMode().width;

    resolution.y = VideoMode::getDesktopMode().height;

    // Load the font

    m_Font.loadFromFile("fonts/Roboto-Light.ttf");

    // when Paused

    m_StartText.setFont(m_Font);

    m_StartText.setCharacterSize(100);

    m_StartText.setFillColor(Color::White);

    m_StartText.setString("Press Enter when ready!");

    // Position the text

    FloatRect textRect = m_StartText.getLocalBounds();

    m_StartText.setOrigin(textRect.left +

        textRect.width / 2.0f,

        textRect.top +

        textRect.height / 2.0f);

    m_StartText.setPosition(

        resolution.x / 2.0f, resolution.y / 2.0f);

    // Time

    m_TimeText.setFont(m_Font);

    m_TimeText.setCharacterSize(75);

    m_TimeText.setFillColor(Color::White);

    m_TimeText.setPosition(resolution.x - 150, 0);

    m_TimeText.setString("------");

    // Level

    m_LevelText.setFont(m_Font);

    m_LevelText.setCharacterSize(75);

    m_LevelText.setFillColor(Color::White);

    m_LevelText.setPosition(25, 0);

    m_LevelText.setString("1");

}

First, we store the horizontal and vertical resolution in a Vector2u called resolution. Next, we load the font from the fonts directory that we added back in Chapter 14, Abstraction and Code Management – Making Better Use of OOP.

The next four lines of code set the font, the color, the size, and the text of m_StartText. The block of code after this captures the size of the rectangle that wraps m_StartText and performs a calculation to work out how to position it centrally on the screen. If you want a more thorough explanation of this part of the code, then refer to Chapter 3, C++ Strings and SFML Time – Player Input and HUD.

In the final two blocks of code in the constructor, the font, text size, color, position, and actual text for m_TimeText and m_LevelText are set. In a moment, we will see that these two Text objects will be updatable through two setter functions, whenever it is required.

Add the following getter and setter functions immediately underneath the code we have just added:

Text Hud::getMessage()

{

    return m_StartText;

}

Text Hud::getLevel()

{

    return m_LevelText;

}

Text Hud::getTime()

{

    return m_TimeText;

}

void Hud::setLevel(String text)

{

    m_LevelText.setString(text);

}

void Hud::setTime(String text)

{

    m_TimeText.setString(text);

}

The first three functions in the previous code simply return the appropriate Text object, that is, m_StartText, m_LevelText, or m_TimeText. We will use these functions shortly when we draw the HUD to the screen. The final two functions, setLevel and setTime, use the setString functions to update the appropriate Text object with the value that will be passed in from the update function of the Engine class, every 500 frames.

With all that done, we can put the HUD class to work in our game engine.

Using the HUD class

Open Engine.h, add an include for our new class, declare an instance of the new HUD class, and declare and initialize two new member variables that will keep track of how often we update the HUD. As we learned in the previous projects, we don't need to update the HUD every frame.

 Add the following highlighted code to Engine.h:

#pragma once

#include <SFML/Graphics.hpp>

#include "TextureHolder.h"

#include "Thomas.h"

#include "Bob.h"

#include "LevelManager.h"

#include "SoundManager.h"

#include "HUD.h"

using namespace sf;

class Engine

{

private:

    // The texture holder

    TextureHolder th;

    // Thomas and his friend, Bob

    Thomas m_Thomas;

    Bob m_Bob;

    // A class to manage all the levels

    LevelManager m_LM;

    // Create a SoundManager

    SoundManager m_SM;

    // The Hud

    Hud m_Hud;

    int m_FramesSinceLastHUDUpdate = 0;

    int m_TargetFramesPerHUDUpdate = 500;

    const int TILE_SIZE = 50;

Next, we need to add some code to the update function of the Engine class. Open Update.cpp and add the following highlighted code to update the HUD once every 500 frames:

    // Set the appropriate view around the appropriate character

    if (m_SplitScreen)

    {

        m_LeftView.setCenter(m_Thomas.getCenter());

        m_RightView.setCenter(m_Bob.getCenter());

    }

    else

    {

        // Centre full screen around appropriate character

        if (m_Character1)

        {

            m_MainView.setCenter(m_Thomas.getCenter());

        }

        else

        {

            m_MainView.setCenter(m_Bob.getCenter());

        }

    }

    // Time to update the HUD?

    // Increment the number of frames since

   // the last HUD calculation

    m_FramesSinceLastHUDUpdate++;

    // Update the HUD every m_TargetFramesPerHUDUpdate frames

    if (m_FramesSinceLastHUDUpdate > m_TargetFramesPerHUDUpdate)

    {

        // Update game HUD text

        stringstream ssTime;

        stringstream ssLevel;

        // Update the time text

        ssTime << (int)m_TimeRemaining;

        m_Hud.setTime(ssTime.str());

        // Update the level text

        ssLevel << "Level:" << m_LM.getCurrentLevel();

        m_Hud.setLevel(ssLevel.str());

        m_FramesSinceLastHUDUpdate = 0;

    }

}// End of update function

In the preceding code, m_FramesSinceLastUpdate is incremented each frame. When m_FramesSinceLastUpdate exceeds m_TargetFramesPerHUDUpdate, then execution enters the if block. Inside the if block, we use stringstream objects to update our Text, like we did in the previous projects. In this project, we are using the HUD class, so we call the setTime and setLevel functions by passing in the current values that the Text objects need to be set to.

The final step in the if block is to set m_FramesSinceLastUpdate back to zero so it can start counting toward the next update.

Finally, open the Draw.cpp file and add the following highlighted code to draw the HUD each frame:

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

        // Draw the Level

        m_Window.draw(m_VALevel, &m_TextureTiles);

            

        // Draw thomas

        m_Window.draw(m_Bob.getSprite());

        // Draw thomas

        m_Window.draw(m_Thomas.getSprite());

        

        // 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 Level

        m_Window.draw(m_VALevel, &m_TextureTiles);

        // Draw thomas

        m_Window.draw(m_Thomas.getSprite());

        // Draw bob

        m_Window.draw(m_Bob.getSprite());

                

    }

    // Draw the HUD

    // Switch to m_HudView

    m_Window.setView(m_HudView);

    m_Window.draw(m_Hud.getLevel());

    m_Window.draw(m_Hud.getTime());

    if (!m_Playing)

    {

        m_Window.draw(m_Hud.getMessage());

    }

    // Show everything we have just drawn

    m_Window.display();

}// End of draw

The preceding code draws the HUD by using the getter functions from the HUD class. Notice that the call to draw the message that prompts the player to start is only used when the game is not currently playing (!m_Playing).

Run the game and play a few levels to see the time tick down and the levels tick up. When you get back to level 1 again, notice that you have 10% less time than before.

Summary

In this chapter, we have explored sound spatialization. Our "Thomas Was Late" game is not only fully playable now, but we have added directional sound effects and a simple but informative HUD. We can also add new levels with ease. At this point, we could call it a day.

It would be nice to add a bit more sparkle. In the next chapter, we will look into two gaming concepts. First, we will look at particle systems, which are how we can handle things such as explosions or other special effects. To achieve this, we will need to learn a bit more C++. Due to this, the topic of multiple inheritance will be introduced.

After that, we will add the final flourish to the game when we learn about OpenGL and the programmable graphics pipeline. We will then be able to dip our toes into the GLSL language, which allows us to write code that executes directly on the GPU so that we can create some special effects.

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

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