Chapter 13: Sound Effects, File I/O, and Finishing the Game

We are nearly there. This short chapter will demonstrate how we can easily manipulate files stored on the hard drive using the C++ standard library, and we will also add sound effects. Of course, we know how to add sound effects, but we will discuss exactly where in the code the calls to the play function will go. We will also tie up a few loose ends to make the game complete.

In this chapter, we will cover the following topics:

  • Saving and loading the hi-score using file input and file output
  • Adding sound effects
  • Allowing the player to level up
  • Creating multiple never-ending waves

Saving and loading the high score

File i/o or input/output is a fairly technical subject. Fortunately for us, as it is such a common requirement in programming, there is a library that handles all this complexity for us. Like concatenating strings for our HUD, it is the C++ Standard Library that provides the necessary functionality through fstream.

First, we include fstream in the same way we included sstream:

#include <sstream>

#include <fstream>

#include <SFML/Graphics.hpp>

#include "ZombieArena.h"

#include "Player.h"

#include "TextureHolder.h"

#include "Bullet.h"

#include "Pickup.h"

using namespace sf;

Now, add a new folder in the ZombieArena folder called gamedata. Next, right-click in this folder and create a new file called scores.txt. It is in this file that we will save the player's high score. You can easily open the file and add a score to it. If you do, make sure it is quite a low score so that we can easily test whether beating that score results in the new score being added. Be sure to close the file once you are done with it or the game will not be able to access it.

In the following code, we will create an ifstream object called inputFile and send the folder and file we just created as a parameter to its constructor.

if(inputFile.is_open()) checks that the file exists and is ready to read from. We then put the contents of the file into hiScore and close the file. Add the following highlighted code:

// Score

Text scoreText;

scoreText.setFont(font);

scoreText.setCharacterSize(55);

scoreText.setColor(Color::White);

scoreText.setPosition(20, 0);

// Load the high score from a text file

std::ifstream inputFile("gamedata/scores.txt");

if (inputFile.is_open())

{

    // >> Reads the data

    inputFile >> hiScore;

    inputFile.close();

}

// Hi Score

Text hiScoreText;

hiScoreText.setFont(font);

hiScoreText.setCharacterSize(55);

hiScoreText.setColor(Color::White);

hiScoreText.setPosition(1400, 0);

std::stringstream s;

s << "Hi Score:" << hiScore;

hiScoreText.setString(s.str());

Now, we can handle saving a potentially new high score. Within the block that handles the player's health being less than or equal to zero, we need to create an ofstream object called outputFile, write the value of hiScore to the text file, and then close the file, like so:

// Have any zombies touched the player            

for (int i = 0; i < numZombies; i++)

{

    if (player.getPosition().intersects

        (zombies[i].getPosition()) && zombies[i].isAlive())

    {

        if (player.hit(gameTimeTotal))

        {

            // More here later

        }

        if (player.getHealth() <= 0)

        {

            state = State::GAME_OVER;

            std::ofstream outputFile("gamedata/scores.txt");

            // << writes the data

            outputFile << hiScore;

            outputFile.close();

            

        }

    }

}// End player touched

You can play the game and your hi-score will be saved. Quit the game and notice that your hi-score is still there if you play it again.

Let's make some noise.

Preparing sound effects

In this section, we will create all the SoundBuffer and Sound objects that we need to add a range of sound effects to the game.

Start by adding the required SFML #include statements:

#include <sstream>

#include <fstream>

#include <SFML/Graphics.hpp>

#include <SFML/Audio.hpp>

#include "ZombieArena.h"

#include "Player.h"

#include "TextureHolder.h"

#include "Bullet.h"

#include "Pickup.h"

Now, go ahead and add the seven SoundBuffer and Sound objects that load and prepare the seven sound files that we prepared in Chapter 8, SFML Views – Starting the Zombie Shooter Game:

// When did we last update the HUD?

int framesSinceLastHUDUpdate = 0;

// What time was the last update

Time timeSinceLastUpdate;

// How often (in frames) should we update the HUD

int fpsMeasurementFrameInterval = 1000;

// Prepare the hit sound

SoundBuffer hitBuffer;

hitBuffer.loadFromFile("sound/hit.wav");

Sound hit;

hit.setBuffer(hitBuffer);

// Prepare the splat sound

SoundBuffer splatBuffer;

splatBuffer.loadFromFile("sound/splat.wav");

Sound splat;

splat.setBuffer(splatBuffer);

// Prepare the shoot sound

SoundBuffer shootBuffer;

shootBuffer.loadFromFile("sound/shoot.wav");

Sound shoot;

shoot.setBuffer(shootBuffer);

// Prepare the reload sound

SoundBuffer reloadBuffer;

reloadBuffer.loadFromFile("sound/reload.wav");

Sound reload;

reload.setBuffer(reloadBuffer);

// Prepare the failed sound

SoundBuffer reloadFailedBuffer;

reloadFailedBuffer.loadFromFile("sound/reload_failed.wav");

Sound reloadFailed;

reloadFailed.setBuffer(reloadFailedBuffer);

// Prepare the powerup sound

SoundBuffer powerupBuffer;

powerupBuffer.loadFromFile("sound/powerup.wav");

Sound powerup;

powerup.setBuffer(powerupBuffer);

// Prepare the pickup sound

SoundBuffer pickupBuffer;

pickupBuffer.loadFromFile("sound/pickup.wav");

Sound pickup;

pickup.setBuffer(pickupBuffer);

// The main game loop

while (window.isOpen())

Now, the seven sound effects are ready to play. We just need to work out where in our code each of the calls to the play function will go.

Leveling up

The following code we will add allows the player to level up between waves. It is because of the work we have already done that this is straightforward to achieve.

Add the following highlighted code to the LEVELING_UP state where we handle player input:

// Handle the LEVELING up state

if (state == State::LEVELING_UP)

{

    // Handle the player LEVELING up

    if (event.key.code == Keyboard::Num1)

    {

        // Increase fire rate

        fireRate++;

        state = State::PLAYING;

    }

    if (event.key.code == Keyboard::Num2)

    {

        // Increase clip size

        clipSize += clipSize;

        state = State::PLAYING;

    }

    if (event.key.code == Keyboard::Num3)

    {

        // Increase health

        player.upgradeHealth();

        state = State::PLAYING;

    }

    if (event.key.code == Keyboard::Num4)

    {

        // Increase speed

        player.upgradeSpeed();

        state = State::PLAYING;

    }

    if (event.key.code == Keyboard::Num5)

    {

        // Upgrade pickup

        healthPickup.upgrade();

        state = State::PLAYING;

    }

    if (event.key.code == Keyboard::Num6)

    {

        // Upgrade pickup

        ammoPickup.upgrade();

        state = State::PLAYING;

    }

    if (state == State::PLAYING)

    {

The player can now level up each time a wave of zombies is cleared. We can't, however, increase the number of zombies or the size of the level just yet.

In the next part of the LEVELING_UP state, right after the code we have just added, amend the code that runs when the state changes from LEVELING_UP to PLAYING.

Here is the code in full. I have highlighted the lines that are either new or have been slightly amended.

Add or amend the following highlighted code:

    if (event.key.code == Keyboard::Num6)

    {

        ammoPickup.upgrade();

        state = State::PLAYING;

    }

    if (state == State::PLAYING)

    {

        // Increase the wave number

        wave++;

        // Prepare the level

        // We will modify the next two lines later

        arena.width = 500 * wave;

        arena.height = 500 * wave;

        arena.left = 0;

        arena.top = 0;

        // Pass the vertex array by reference

        // to the createBackground function

        int tileSize = createBackground(background, arena);

        // Spawn the player in the middle of the arena

        player.spawn(arena, resolution, tileSize);

        // Configure the pick-ups

        healthPickup.setArena(arena);

        ammoPickup.setArena(arena);

        // Create a horde of zombies

        numZombies = 5 * wave;

        // Delete the previously allocated memory (if it exists)

        delete[] zombies;

        zombies = createHorde(numZombies, arena);

        numZombiesAlive = numZombies;

        // Play the powerup sound

        powerup.play();

        // Reset the clock so there isn't a frame jump

        clock.restart();

    }

}// End LEVELING up

The previous code starts by incrementing the wave variable. Then, the code is amended to make the number of zombies and size of the arena relative to the new value of wave. Finally, we add the call to powerup.play() to play the leveling up sound effect.

Restarting the game

We already determine the size of the arena and the number of zombies by the value of the wave variable. We must also reset the ammo and gun-related variables, as well as setting wave and score to zero at the start of each new game. Find the following code in the event-handling section of the game loop and add the following highlighted code:

// Start a new game while in GAME_OVER state

else if (event.key.code == Keyboard::Return &&

    state == State::GAME_OVER)

{

    state = State::LEVELING_UP;

    wave = 0;

    score = 0;

    // Prepare the gun and ammo for next game

    currentBullet = 0;

    bulletsSpare = 24;

    bulletsInClip = 6;

    clipSize = 6;

    fireRate = 1;

    // Reset the player's stats

    player.resetPlayerStats();

}

Now, we can play the game, the player can get even more powerful, and the zombies will get ever more numerous within an arena of increasing size—until they die. Then, the game starts all over again.

Playing the rest of the sounds

Now, we will add the rest of the calls to the play function. We will deal with each of them individually, as locating exactly where they go is key to playing them at the right moment.

Adding sound effects while the player is reloading

Add the following highlighted code in three places to play the appropriate reload or reloadFailed sound when the player presses the R key to attempt to reload their gun:

if (state == State::PLAYING)

{

    // Reloading

    if (event.key.code == Keyboard::R)

    {

        if (bulletsSpare >= clipSize)

        {

            // Plenty of bullets. Reload.

            bulletsInClip = clipSize;

            bulletsSpare -= clipSize;        

            reload.play();

        }

        else if (bulletsSpare > 0)

        {

            // Only few bullets left

            bulletsInClip = bulletsSpare;

            bulletsSpare = 0;                

            reload.play();

        }

        else

        {

            // More here soon?!

            reloadFailed.play();

        }

    }

}

The player will now get an audible response when they reload or attempt to reload. Let's move on to playing a shooting sound.

Making a shooting sound

Add the following highlighted call to shoot.play() near the end of the code that handles the player clicking the left mouse button:

// Fire a bullet

if (sf::Mouse::isButtonPressed(sf::Mouse::Left))

{

    if (gameTimeTotal.asMilliseconds()

        - lastPressed.asMilliseconds()

        > 1000 / fireRate && bulletsInClip > 0)

    {

        // Pass the centre of the player and crosshair

        // to the shoot function

        bullets[currentBullet].shoot(

            player.getCenter().x, player.getCenter().y,

            mouseWorldPosition.x, mouseWorldPosition.y);

        currentBullet++;

        if (currentBullet > 99)

        {

            currentBullet = 0;

        }

        lastPressed = gameTimeTotal;

        shoot.play();

        bulletsInClip--;

    }

}// End fire a bullet

The game will now play a satisfying shooting sound. Next, we will play a sound when the player is hit by a zombie.

Playing a sound when the player is hit

In this following code, we wrap the call to hit.play in a test to see if the player.hit function returns true. Remember that the player.hit function tests to see if a hit has been recorded in the previous 100 milliseconds. This will have the effect of playing a fast-repeating thud sound, but not so fast that the sound blurs into one noise.

Add the call to hit.play, as highlighted in the following code:

// Have any zombies touched the player            

for (int i = 0; i < numZombies; i++)

{

    if (player.getPosition().intersects

        (zombies[i].getPosition()) && zombies[i].isAlive())

    {

        if (player.hit(gameTimeTotal))

        {

            // More here later

            hit.play();

        }

        if (player.getHealth() <= 0)

        {

            state = State::GAME_OVER;

            std::ofstream OutputFile("gamedata/scores.txt");

            OutputFile << hiScore;

            OutputFile.close();

            

        }

    }

}// End player touched

The player will hear an ominous thudding sound when a zombie touches them, and this sound will repeat around five times per second if the zombie continues touching them. The logic for this is contained in the hit function of the Player class.

Playing a sound when getting a pickup

When the player picks up a health pickup, we will play the regular pickup sound. However, when the player gets an ammo pickup, we will play the reload sound effect.

Add the two calls to play sounds within the appropriate collision detection code:

// Has the player touched health pickup

if (player.getPosition().intersects

    (healthPickup.getPosition()) && healthPickup.isSpawned())

{

    player.increaseHealthLevel(healthPickup.gotIt());

    // Play a sound

    pickup.play();

    

}

// Has the player touched ammo pickup

if (player.getPosition().intersects

    (ammoPickup.getPosition()) && ammoPickup.isSpawned())

{

    bulletsSpare += ammoPickup.gotIt();

    // Play a sound

    reload.play();

    

}

Making a splat sound when a zombie is shot

Add a call to splat.play at the end of the section of code that detects a bullet colliding with a zombie:

// Have any zombies been shot?

for (int i = 0; i < 100; i++)

{

    for (int j = 0; j < numZombies; j++)

    {

        if (bullets[i].isInFlight() &&

            zombies[j].isAlive())

        {

            if (bullets[i].getPosition().intersects

                (zombies[j].getPosition()))

            {

                // Stop the bullet

                bullets[i].stop();

                // Register the hit and see if it was a kill

                if (zombies[j].hit()) {

                    // Not just a hit but a kill too

                    score += 10;

                    if (score >= hiScore)

                    {

                        hiScore = score;

                    }

                    numZombiesAlive--;

                    // When all the zombies are dead (again)

                    if (numZombiesAlive == 0) {

                        state = State::LEVELING_UP;

                    }

                }    

                // Make a splat sound

                splat.play();

                

            }

        }

    }

}// End zombie being shot

You can now play the completed game and watch the number of zombies and the arena increase each wave. Choose your level-ups carefully:

Congratulations!

Summary

We've finished the Zombie Arena game. It has been quite a journey. We have learned a whole bunch of C++ fundamentals, such as references, pointers, OOP, and classes. In addition, we have used SFML to manage cameras (views), vertex arrays, and collision detection. We learned how to use sprite sheets to reduce the number of calls to window.draw and speed up the frame rate. Using C++ pointers, the STL, and a little bit of OOP, we built a singleton class to manage our textures. In the next project, we will extend this idea to manage all of our game's assets.

Coming up in the penultimate project of this book, we will discover particle effects, directional sound, and split-screen co-op gaming. In C++, we will encounter inheritance, polymorphism, and a few more new concepts as well.

FAQ

Here are some questions that might be on your mind:

Q) Despite using classes, I am finding that the code is getting very long and unmanageable again.

A) One of the biggest issues is the structure of our code. As we learn more C++, we will also learn ways to make the code more manageable and generally less lengthy. We will do so in the next project and the final project too. By the end of this book, you will know about a number of strategies that you can use to manage your code.

Q) The sound effects seem a bit flat and unrealistic. How can they be improved?

A) One way to significantly improve the feeling the player gets from sound is to make the sound directional, as well as changing the volume based on the distance of the sound source to the player character. We will use SFML's advanced sound features in the next project.

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

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