Chapter 3: C++ Strings and SFML Time – Player Input and HUD

In this chapter, we will continue with the Timber!! game. We will spend around half of this chapter learning how to manipulate text and display it on the screen, and the other half looking at timing and how a visual time-bar can inform the player about the remaining time and create a sense of urgency in the game.

We will cover the following topics:

  • Pausing and restarting the game
  • C++ Strings
  • SFML text and SFML font classes
  • Adding a HUD to Timber!!!
  • Adding a time-bar to Timber!!!

Pausing and restarting the game

As we work on this game over the next three chapters, the code will obviously get longer and longer. So, now seems like a good time to think ahead and add a little bit more structure to our code. We will add this structure so that we can pause and restart the game.

We will add code so that, when the game is run for the first time, it will be in a paused state. The player will then be able to press the Enter key to start the game. Then, the game will run until either the player gets squished or runs out of time. At this point, the game will pause and wait for the player to press Enter so that they can restart the game.

Let's step through setting this up a bit at a time.

First, declare a new bool variable called paused outside the main game loop and initialize it to true:

// Variables to control time itself

Clock clock;

// Track whether the game is running

bool paused = true;

while (window.isOpen())

{

    /*

    ****************************************

    Handle the players input

    ****************************************

    */

Now, whenever the game is run, we have a paused variable that will be true.

Next, we will add another if statement where the expression will check whether the Enter key is currently being pressed. If it is being pressed, it sets paused to false. Add the following highlighted code just after our other keyboard-handling code:

/*

****************************************

Handle the players input

****************************************

*/

if (Keyboard::isKeyPressed(Keyboard::Escape))

{

    window.close();

}

// Start the game

if (Keyboard::isKeyPressed(Keyboard::Return))

{

    paused = false;

}

/*

****************************************

Update the scene

****************************************

*/

Now, we have a bool called paused that starts off true but changes to false when the player presses the Enter key. At this point, we must make our game loop respond appropriately, based on whatever the current value of paused might be.

This is how we will proceed. We will wrap the entire update part of the code, including the code we wrote in the last chapter for moving the bee and clouds, in an if statement.

In the following code, note that the if block will only execute when paused is not equal to true. Put another way, the game won't move/update when it is paused.

This is exactly what we want. Look carefully at the place where we added the new if statement and its corresponding opening and closing curly braces {...}. If they are put in the wrong place, things will not work as expected.

Add the following highlighted code to wrap the updated part of the code, paying close attention to the context that follows. I have added ... on a few lines to represent hidden code. Obviously, ... is not real code and should not be added to the game. You can identify where to place the new code (highlighted) at the start and the end by the unhighlighted code surrounding it:

/*

****************************************

Update the scene

****************************************

*/

if (!paused)

{

    // Measure time

                ...

        ...

        ...

        

        // Has the cloud reached the right hand edge of the screen?

        if (spriteCloud3.getPosition().x > 1920)

        {

            // Set it up ready to be a whole new cloud next frame

            cloud3Active = false;

        }

    }

} // End if(!paused)

/*

****************************************

Draw the scene

****************************************

*/

Note that, when you place the closing curly brace of the new if block, Visual Studio neatly adjusts all the indenting to keep the code tidy.

Now, you can run the game and everything will be static until you press the Enter key. It is now possible to go about adding features to our game. We just need to remember that, when the player dies or runs out of time, we need to set paused to true.

In the previous chapter, we took our first look at C++ Strings. We need to learn a bit more about them so that we can implement the player's HUD.

C++ Strings

In the previous chapter, we briefly mentioned Strings and we learned that a String can hold alphanumeric data – anything from a single character to a whole book. We didn't look at declaring, initializing, or manipulating Strings, so let's do that now.

Declaring Strings

Declaring a String variable is simple. It is the same process that we used for other variables in the previous chapter: we state the type, followed by the name:

String levelName;

String playerName;

Once we have declared a String, we can assign a value to it.

Assigning a value to a String

To assign a value to a String, just like regular variables, we simply put the name, followed by the assignment operator, and then the value:

levelName = "DastardlyCave";

playerName = "John Carmack";

Note that the values need to be enclosed in quotation marks. Just like regular variables, we can also declare and assign values in a single line:

String score = "Score = 0";

String message = "GAME OVER!!";

In the next section, we will see how we can change the values of our String variables.

Manipulating Strings

We can use the #include <sstream> directive to give us some extra manipulation options for our Strings. The sstream class allows us to "add" some Strings together. When we add Strings together, this is known as concatenation:

String part1 = "Hello ";

String part2 = "World";

sstream ss;

ss<< part1 << part2;

// ss now holds "Hello World"

In addition to this, by using sstream objects a String variable can even be concatenated with a variable of a different type. The following code starts to reveal how Strings might be useful to us:

String scoreText = "Score = ";

int score = 0;

// Later in the code

score ++;

sstream ss;

ss<<scoreText<< score;

// ss now holds "Score = 1"

In the preceding code, ss is used to join the content of scoreText with the value from score. Note that although score holds an int value, the final value held by ss is still a String that holds an equivalent value; in this case, "1".

Tip

The << operator is one of the bitwise operators. C++, however, allows you to write your own classes and override what a specific operator does within the context of your class. The sstream class has done this to make the << operator work the way it does. The complexity is hidden in the class. We can use its functionality without worrying about how it works. If you are feeling adventurous, you can read about operator overloading at http://www.tutorialspoint.com/cplusplus/cpp_overloading.htm. You don't need any more information in order to continue with the project.

Now that we know the basics of C++ Strings and how we can use sstream, we will look at how we can use some SFML classes to display them on the screen.

SFML's Text and Font classes

Let's talk about the Text and Font classes using some hypothetical code before we go ahead and add the code to our game.

The first step in being able to draw text on the screen is to have a font. In Chapter 1, C++, SFML, Visual Studio, and Starting the First Game, we added a font file to the project folder. Now, we can load the font into an SFML Font object, so that it's ready to use.

The code to do so looks like the following:

Font font;

font.loadFromFile("myfont.ttf");

In the preceding code, we first declare the Font object and then load an actual font file. Note that myfont.ttf is a hypothetical font and that we could use any font in the project folder.

Once we have loaded a font, we need an SFML Text object:

Text myText;

Now, we can configure our Text object. This includes the size, the color, the position on-screen, the String that holds the message, and of course the act of associating it with our font object:

// Assign the actual message

myText.setString("Press Enter to start!");

// assign a size

myText.setCharacterSize(75);

// Choose a color

myText.setFillColor(Color::White);

// Set the font to our Text object

myText.setFont(font);

Now that we can create and manipulate String values as well as assign, declare, and initialize SFML Text objects, we can move on to the next section, where we will add a HUD to Timber!!!

Implementing the HUD

Now, we know enough about Strings, SFML Text, and SFML Font to go about implementing the HUD. HUD stands for Heads Up Display. It can be as simple as the score and text messages on the screen or it can include more complex elements such as a time-bar, mini-map, or compass that represents the direction that the player character is facing.

To get started with the HUD, we need to add another #include directive to the top of the code file to add access to the sstream class. As we already know, the sstream class adds some really useful functionality for combining Strings and other variable types into a String.

Add the following line of highlighted code:

#include <sstream>

#include <SFML/Graphics.hpp>

using namespace sf;

int main()

{

Next, we will set up our SFML Text objects: one to hold a message that we will vary to suit the state of the game and one that will hold the score and will need to be regularly updated.

The code declares the Text and Font objects, loads the font, assigns the font to the Text objects, and then adds the String messages, color, and size. This should look familiar from our discussion in the previous section. In addition, we added a new int variable called score that we can manipulate so that it holds the player's score.

Tip

Remember that, if you chose a different font from KOMIKAP_.ttf, back in Chapter 1, C++, SFML, Visual Studio, and Starting the First Game, you will need to change that part of the code to match the .ttf file that you have in the Visual Studio Stuff/Projects/Timber/fonts folder.

By adding the following highlighted code, we will be ready to move on to updating the HUD:

// Track whether the game is running

bool paused = true;

// Draw some text

int score = 0;

Text messageText;

Text scoreText;

// We need to choose a font

Font font;

font.loadFromFile("fonts/KOMIKAP_.ttf");

// Set the font to our message

messageText.setFont(font);

scoreText.setFont(font);

// Assign the actual message

messageText.setString("Press Enter to start!");

scoreText.setString("Score = 0");

// Make it really big

messageText.setCharacterSize(75);

scoreText.setCharacterSize(100);

// Choose a color

messageText.setFillColor(Color::White);

scoreText.setFillColor(Color::White);

while (window.isOpen())

{

    /*

    ****************************************

    Handle the players input

    ****************************************

    */

In the preceding code we have achieved the following:

  • Declared a variable to hold the score
  • Declared some SFML Text and Font objects
  • Initialized the Font object by loading a font from a file
  • Initialized the Text objects using the font and some Strings
  • Set the size and color of the Text objects using the setCharacterSize and setFillColor functions

The following snippet of code might look a little convoluted, even complex. It is, however, straightforward when you break it down a bit. Examine and add the new highlighted code. We will go through it after this:

// Choose a color

messageText.setFillColor(Color::White);

scoreText.setFillColor(Color::White);

// Position the text

FloatRect textRect = messageText.getLocalBounds();

messageText.setOrigin(textRect.left +

    textRect.width / 2.0f,

    textRect.top +

    textRect.height / 2.0f);

messageText.setPosition(1920 / 2.0f, 1080 / 2.0f);

scoreText.setPosition(20, 20);

while (window.isOpen())

{

    /*

    ****************************************

    Handle the players input

    ****************************************

    */

We have two objects of the Text type that we will display on the screen. We want to position scoreText to the top left with a little bit of padding. This is not a challenge; we simply use scoreText.setPosition(20, 20), which positions it at the top left with 20 pixels of horizontal and vertical padding.

Positioning messageText, however, is not so easy. We want to position it in the exact midpoint of the screen. Initially, this might not seem like a problem, but then we have to remember that the origin of everything we draw is at the top left-hand corner. So, if we simply divide the screen width and height by two and use the results in mesageText.setPosition..., then the top left of the text will be in the center of the screen and it will spread out untidily to the right.

The following is the code under discussion again for convenience:

// Position the text

FloatRect textRect = messageText.getLocalBounds();

messageText.setOrigin(textRect.left +

    textRect.width / 2.0f,

    textRect.top +

    textRect.height / 2.0f);

What the code does is set the center of messageText to the center of the screen. The rather complex-looking bit of code that we are reviewing repositions the origin of messageText to the center of itself.

In the preceding code, we first declare a new object of the FloatRect type called textRect. A FloatRect object, as its name suggests, holds a rectangle with floating-point coordinates.

The code then uses the mesageText.getLocalBounds function to initialize textRect with the coordinates of the rectangle that wraps messageText.

The next line of code, which is spread over four lines as it is quite long, uses the messageText.setOrigin function to change the origin (the point that is used to draw at) to the center of textRect. Of course, textRect holds a rectangle that matches the coordinates that wrap messageText. Then, this following line of code executes:

messageText.setPosition(1920 / 2.0f, 1080 / 2.0f);

Now, messageText will be neatly positioned in the exact center of the screen. We will use this code each time we change the text of messageText because changing the message changes the size of messageText, so its origin will need recalculating.

Next, we declare an object of the stringstream type called ss. Note that we use the full name, including the namespace, that is, std::stringstream. We could avoid this syntax by adding using namespace std to the top of our code file. We aren't going to here, though, because we use it infrequently. Take a look at the following code and add it to the game; then, we can go through it in more detail. Since we only want this code to execute when the game is not paused, be sure to add it with the other code, inside the if(!paused) block, as follows:

else

    {

        spriteCloud3.setPosition(

            spriteCloud3.getPosition().x +

            (cloud3Speed * dt.asSeconds()),

            spriteCloud3.getPosition().y);

        // Has the cloud reached the right hand edge of the screen?

        if (spriteCloud3.getPosition().x > 1920)

        {

            // Set it up ready to be a whole new cloud next frame

            cloud3Active = false;

        }

    }

    // Update the score text

    std::stringstream ss;

    ss<< "Score = " << score;

    scoreText.setString(ss.str());

}// End if(!paused)

/*

****************************************

Draw the scene

****************************************

*/

We use ss and the special functionality provided by the << operator, which concatenates variables into a stringstream. Here, ss << "Score = " << score has the effect of creating a String with "Score = ". Whatever the value of score is, is concatenated together. For example, when the game first starts, score is equal to zero, so ss will hold the "Score = 0" value. If score ever changes, ss will adapt each frame.

The following line of code simply sets the String contained in ss to scoreText:

scoreText.setString(ss.str());

It is now ready to be drawn to the screen.

This following code draws both Text objects (scoreText and messageText), but the code that draws messageText is wrapped in an if statement. This if statement causes messageText to only be drawn when the game is paused.

Add the following highlighted code:

// Now draw the insect

window.draw(spriteBee);

// Draw the score

window.draw(scoreText);

if (paused)

{

    // Draw our message

    window.draw(messageText);

}

// Show everything we just drew

window.display();

We can now run the game and see our HUD being drawn on the screen. You will see the Score = 0 and PRESS ENTER TO START messages. The latter will disappear when you press Enter:

If you want to see the score updating, add a temporary line of code, score ++;, anywhere in the while(window.isOpen) loop. If you add this temporary line, you will see the score go up fast, very fast!

If you added the temporary code, that is, score ++;, be sure to delete it before continuing.

Adding a time-bar

Since time is a crucial mechanism in the game, it is necessary to keep the player aware of it. They need to know if their allotted six seconds are about to run out. It will give them a sense of urgency as the end of the game draws near and a sense of accomplishment if they perform well enough to maintain or increase their remaining time.

Drawing the number of seconds remaining on the screen is not easy to read (when concentrating on the branches), nor is it a particularly interesting way to achieve the objective.

What we need is a time-bar. Our time-bar will be a simple red rectangle that's prominently displayed on the screen. It will start off nice and wide, but rapidly shrink as time runs out. When the player's remaining time reaches zero, the time-bar will be gone completely.

At the same time as adding the time-bar, we will add the necessary code to keep track of the player's remaining time, and respond when it runs out. Let's go through this step by step.

Find the Clock clock; declaration from earlier and add the highlighted code just after it, as follows:

// Variables to control time itself

Clock clock;

// Time bar

RectangleShape timeBar;

float timeBarStartWidth = 400;

float timeBarHeight = 80;

timeBar.setSize(Vector2f(timeBarStartWidth, timeBarHeight));

timeBar.setFillColor(Color::Red);

timeBar.setPosition((1920 / 2) - timeBarStartWidth / 2, 980);

Time gameTimeTotal;

float timeRemaining = 6.0f;

float timeBarWidthPerSecond = timeBarStartWidth / timeRemaining;

// Track whether the game is running

bool paused = true;

First, we declare an object of the RectangleShape type and call it timeBar. RectagleShape is an SFML class that is perfect for drawing simple rectangles.

Next, we will add a few float variables, timeBarStartWidth and timeBarHeight. We initialize them to 400 and 80, respectively. These variables will help us keep track of the size we need to draw timeBar at each frame.

Next, we set the size of timeBar using the timeBar.setSize function. We don't just pass in our two new float variables. First, we create a new object of the Vector2f type. What is different here, however, is that we don't give the new object a name. Instead, we simply initialize it with our two float variables and pass it straight in to the setSize function.

Tip

Vector2f is a class that holds two float variables. It also has some other functionality that will be introduced throughout this book.

After that, we color timeBar red by using the setFillColor function.

The last thing we do to timeBar in the previous code is set its position. The vertical coordinate is completely straightforward but the way we set the horizontal coordinate is slightly convoluted. Here is the calculation again:

(1920 / 2) - timeBarStartWidth / 2

First, the code divides 1920 by 2. Then, it divides timeBarStartWidth by 2. Finally, it subtracts the latter from the former.

The result makes timeBar sit neatly and centrally, in a horizontal fashion, on the screen.

The final three lines of code that we are talking about declare a new Time object called gameTimeTotal, a new float called timeRemaining that is initialized to 6, and a curious-sounding float named timeBarWidthPerSecond, which we will discuss next.

The timeBarWidthPerSecond variable is initialized with timeBarStartWidth divided by timeRemaining. The result is exactly the amount of pixels that timeBar needs to shrink by each second of the game. This will be useful when we resize timeBar in each frame.

Obviously, we need to reset the time remaining each time the player starts a new game. The logical place to do this is when the Enter key is pressed. We can also set score back to zero at the same time. Let's do that now by adding the following highlighted code

// Start the game

if (Keyboard::isKeyPressed(Keyboard::Return))

{

    paused = false;

    // Reset the time and the score

    score = 0;

    timeRemaining = 6;

}

Now, we must reduce each frame by the amount of time remaining and resize timeBar accordingly. Add the following highlighted code to the update section, as shown here:

/*

****************************************

Update the scene

****************************************

*/

if (!paused)

{

    // Measure time

    Time dt = clock.restart();

    // Subtract from the amount of time remaining

    timeRemaining -= dt.asSeconds();

    // size up the time bar

    timeBar.setSize(Vector2f(timeBarWidthPerSecond *

        timeRemaining, timeBarHeight));

    // Set up the bee

    if (!beeActive)

    {

        // How fast is the bee

        srand((int)time(0) * 10);

        beeSpeed = (rand() % 200) + 200;

        // How high is the bee

        srand((int)time(0) * 10);

        float height = (rand() % 1350) + 500;

        spriteBee.setPosition(2000, height);

        beeActive = true;

    }

    else

        // Move the bee

First, we subtracted the amount of time the player has left by however long the previous frame took to execute with the following code:

timeRemaining -= dt.asSeconds();

Then, we adjusted the size of timeBar with the following code:

timeBar.setSize(Vector2f(timeBarWidthPerSecond *

        timeRemaining, timeBarHeight));

The x value of Vector2F is initialized with timebarWidthPerSecond when multiplied by timeRemaining. This produces exactly the right width, relative to how long the player has left. The height remains the same and timeBarHeight is used without any manipulation.

And of course, we must detect when the time has run out. For now, we will simply detect that time has run out, pause the game, and change the text of messageText. Later, we will do more work here. Add the following highlighted code right after the previous code we added. We will look at it in more detail later:

// Measure time

Time dt = clock.restart();

// Subtract from the amount of time remaining

timeRemaining -= dt.asSeconds();

// resize up the time bar

timeBar.setSize(Vector2f(timeBarWidthPerSecond *

    timeRemaining, timeBarHeight));

if (timeRemaining<= 0.0f) {

    

    // Pause the game

    paused = true;

    // Change the message shown to the player

    messageText.setString("Out of time!!");

    //Reposition the text based on its new size

    FloatRect textRect = messageText.getLocalBounds();

    messageText.setOrigin(textRect.left +

        textRect.width / 2.0f,

        textRect.top +

        textRect.height / 2.0f);

    messageText.setPosition(1920 / 2.0f, 1080 / 2.0f);

}

// Set up the bee

if (!beeActive)

{

    // How fast is the bee

    srand((int)time(0) * 10);

    beeSpeed = (rand() % 200) + 200;

    // How high is the bee

    srand((int)time(0) * 10);

    float height = (rand() % 1350) + 500;

    spriteBee.setPosition(2000, height);

    beeActive = true;

}

else

    // Move the bee

Let's step through the previous code:

  1. First, we test whether time has run out with if(timeRemaining<= 0.0f).
  2. Then, we set paused to true, so this will be the last time the update part of our code is executed (until the player presses Enter again).
  3. Then, we change the message of messageText, calculate its new center to be set as its origin, and position it in the center of the screen.

Finally, for this part of the code, we need to draw timeBar. There is nothing new in this code that we haven't seen many times before. Just note that we draw timeBar after the tree so that it is not partially obscured. Add the following highlighted code to draw the time-bar:

// Draw the score

window.draw(scoreText);

// Draw the timebar

window.draw(timeBar);

if (paused)

{

    // Draw our message

    window.draw(messageText);

}

// Show everything we just drew

window.display();

Now, you can run the game, press Enter to start it, and watch the time-bar smoothly disappear down to nothing:

The game then pauses and the OUT OF TIME!! message will appear in the center of the screen:

You can, of course, press Enter to start the game again and watch it run from the beginning.

Summary

In this chapter, we learned about Strings, SFML Text, and SFML Font. Between them, they allowed us to draw text to the screen, which provided the player with a HUD. We also used sstream, which allows us to concatenate Strings and other variables to display the score.

We also explored the SFML RectangleShape class, which does exactly what its name suggests. We used an object of the RectangleShape type and some carefully planned variables to draw a time-bar that visually displays to the player how much time they have left. Once we implement chopping and moving branches that can squash the player, the time-bar will provide visual feedback that will create tension and urgency.

In the next chapter, we are going to learn about a whole range of new C++ features, including loops, arrays, switching, enumerations, and functions. This will allow us to move tree branches, keep track of their locations, and squash the player.

FAQ

Q) I can foresee that positioning sprites by their top-left corner could sometimes be inconvenient. Is there an alternative?

A) Fortunately, you can choose what point of a sprite is used as the positioning/origin pixel, just like we did with messageText, using the setOrigin function.

Q) The code is getting rather long and I am struggling to keep track of where everything is. How can we fix this?

A) Yes, I agree. In the next chapter, we will look at the first of a few ways we can organize our code and make it more readable. We will look at this when we learn about writing C++ functions. In addition, we will learn about a new way we can handle multiple objects/variables of the same type (like the clouds) when we learn about C++ arrays.

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

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