This is the final phase of the first project. By the end of this chapter, you will have your first completed game. Once you have Timber!!! up and running, be sure to read the last section of this chapter as it will suggest ways to make the game better.
In this chapter, we will cover the following topics:
Let's add the code for the player's sprite as well as a few more sprites and textures at the same time. The following, quite large, block of code also adds a gravestone sprite for when the player gets squashed, an axe sprite to chop with, and a log sprite that can whiz away each time the player chops.
Notice that, after the spritePlayer object, we declare a side variable, playerSide, to keep track of where the player is currently standing. Furthermore, we add some extra variables for the spriteLog object, including logSpeedX, logSpeedY, and logActive, to store how fast the log will move and whether it is currently moving. The spriteAxe also has two related float constant variables to remember where the ideal pixel position is on both the left and the right.
Add the following block of code just before the while(window.isOpen()) code, like we have done so often before. Note that all of the code in the following block is new, not just the highlighted code. I haven't provided any extra context for this block of code as the while(window.isOpen()) should be easy to identify. The highlighted code is the code we have just discussed.
Add the entirety of the following code, just before the while(window.isOpen()) line, and make a mental note of the highlighted lines we briefly discussed. It will make the rest of this chapter's code easier to understand:
// Prepare the player
Texture texturePlayer;
texturePlayer.loadFromFile("graphics/player.png");
Sprite spritePlayer;
spritePlayer.setTexture(texturePlayer);
spritePlayer.setPosition(580, 720);
// The player starts on the left
side playerSide = side::LEFT;
// Prepare the gravestone
Texture textureRIP;
textureRIP.loadFromFile("graphics/rip.png");
Sprite spriteRIP;
spriteRIP.setTexture(textureRIP);
spriteRIP.setPosition(600, 860);
// Prepare the axe
Texture textureAxe;
textureAxe.loadFromFile("graphics/axe.png");
Sprite spriteAxe;
spriteAxe.setTexture(textureAxe);
spriteAxe.setPosition(700, 830);
// Line the axe up with the tree
const float AXE_POSITION_LEFT = 700;
const float AXE_POSITION_RIGHT = 1075;
// Prepare the flying log
Texture textureLog;
textureLog.loadFromFile("graphics/log.png");
Sprite spriteLog;
spriteLog.setTexture(textureLog);
spriteLog.setPosition(810, 720);
// Some other useful log related variables
bool logActive = false;
float logSpeedX = 1000;
float logSpeedY = -1500;
In the preceding code, we added quite a few new variables. They are hard to explain in full until we get to where we actually use them, but here is an overview of what they will be used for. There is a variable of the side enumeration type called playerSide that is initialized to left. This will track which side of the tree the player is on.
There are two const float values that determine the horizontal position the axe will be drawn at, depending on whether the player is on the left-or right-hand side of the tree.
There are also three variables to help to keep control of the log as it is chopped and flies off of the tree, bool to determine whether the log is in motion (logActive) and two float values to hold the horizontal and vertical speeds of the log.
Now, we can draw all of our new sprites.
Before we add the code to move the player and use all of our new sprites, let's draw them. We are doing it this way so that as we add code to update/change/move them, we will be able to see what is happening.
Add the following highlighted code to draw the four new sprites:
// Draw the tree
window.draw(spriteTree);
// Draw the player
window.draw(spritePlayer);
// Draw the axe
window.draw(spriteAxe);
// Draw the flying log
window.draw(spriteLog);
// Draw the gravestone
window.draw(spriteRIP);
// Draw the bee
window.draw(spriteBee);
The preceding code passes our four new sprites, one after the other, to the draw function.
Run the game and you will see our new sprites in the scene:
We are really close to a working game now. The next task is to write some code to allow the player to control what happens.
A few different things depend on the movement of the player, as follows:
Therefore, it makes sense to set up keyboard handling for the player who's chopping. Once this is done, we can put all of the features we just mentioned into the same part of the code.
Let's think for a moment about how we detect keyboard presses. Each frame, we test whether a particular keyboard key is currently being held down. If it is, we take action. If the Esc key is being held down, we quit the game, and if the Enter key is being held down, we restart the game. So far, this has been sufficient for our needs.
There is, however, a problem with this approach when we try and handle the chopping of the tree. The problem has always been there; it just didn't matter until now. Depending on how powerful your PC is, the game loop could be executing thousands of times per second. Each and every pass through the game loop that a key is held down, it is detected, and the related code will execute.
So, actually, every time you press Enter to restart the game, you are most likely restarting it well in excess of a hundred times. This is because even the briefest of presses will last a significant fraction of a second. You can verify this by running the game and holding down the Enter key. Note that the time-bar doesn't move. This is because the game is being restarted over and over again, hundreds or even thousands of times a second.
If we don't use a different approach for the player chopping, then just one attempted chop will bring the entire tree down in a mere fraction of a second. We need to be a bit more sophisticated. What we will do is allow the player to chop, and then when they do so, disable the code that detects a key press. We will then detect when the player removes their finger from a key and then reenable the detection of key presses. Here are the steps laid out clearly:
This might sound complicated but, with SFML's help, this will be straightforward. Let's implement this now, one step at a time.
Add the following highlighted line of code, which declares a bool variable called acceptInput, which will be used to determine when to listen for chops and when to ignore them:
float logSpeedX = 1000;
float logSpeedY = -1500;
// Control the player input
bool acceptInput = false;
while (window.isOpen())
{
Now that we have our Boolean set up, we can move on to the next step.
So that we're ready to handle chops, add the following highlighted code to the if block that starts a new game:
/*
****************************************
Handle the players input
****************************************
*/
if (Keyboard::isKeyPressed(Keyboard::Escape))
{
window.close();
}
// Start the game
if (Keyboard::isKeyPressed(Keyboard::Return))
{
paused = false;
// Reset the time and the score
score = 0;
timeRemaining = 6;
// Make all the branches disappear -
// starting in the second position
for (int i = 1; i < NUM_BRANCHES; i++)
{
branchPositions[i] = side::NONE;
}
// Make sure the gravestone is hidden
spriteRIP.setPosition(675, 2000);
// Move the player into position
spritePlayer.setPosition(580, 720);
acceptInput = true;
}
/*
****************************************
Update the scene
****************************************
*/
In the previous code, we are using a for loop to prepare the tree with no branches. This is fair to the player because, if the game started with a branch right above their head, it would be considered unsporting. Then, we simply move the gravestone off of the screen and the player into their starting location on the left. The last thing the preceding code does is set acceptInput to true.
We are now ready to receive chopping key presses.
Now, we can handle the left and right cursor key presses. Add this simple if block, which only executes when acceptInput is true:
// Start the game
if (Keyboard::isKeyPressed(Keyboard::Return))
{
paused = false;
// Reset the time and the score
score = 0;
timeRemaining = 5;
// Make all the branches disappear
for (int i = 1; i < NUM_BRANCHES; i++)
{
branchPositions[i] = side::NONE;
}
// Make sure the gravestone is hidden
spriteRIP.setPosition(675, 2000);
// Move the player into position
spritePlayer.setPosition(675, 660);
acceptInput = true;
}
// Wrap the player controls to
// Make sure we are accepting input
if (acceptInput)
{
// More code here next...
}
/*
****************************************
Update the scene
****************************************
*/
Now, inside the if block that we just coded, add the following highlighted code to handle what happens when the player presses the right cursor key on the keyboard:
// Wrap the player controls to
// Make sure we are accepting input
if (acceptInput)
{
// More code here next...
// First handle pressing the right cursor key
if (Keyboard::isKeyPressed(Keyboard::Right))
{
// Make sure the player is on the right
playerSide = side::RIGHT;
score ++;
// Add to the amount of time remaining
timeRemaining += (2 / score) + .15;
spriteAxe.setPosition(AXE_POSITION_RIGHT,
spriteAxe.getPosition().y);
spritePlayer.setPosition(1200, 720);
// Update the branches
updateBranches(score);
// Set the log flying to the left
spriteLog.setPosition(810, 720);
logSpeedX = -5000;
logActive = true;
acceptInput = false;
}
// Handle the left cursor key
}
Quite a bit is happening in that preceding code, so let's go through it:
Now, still inside the if(acceptInput) block that we just coded, add the following highlighted code to handle what happens when the player presses the left cursor key on the keyboard:
// Handle the left cursor key
if (Keyboard::isKeyPressed(Keyboard::Left))
{
// Make sure the player is on the left
playerSide = side::LEFT;
score++;
// Add to the amount of time remaining
timeRemaining += (2 / score) + .15;
spriteAxe.setPosition(AXE_POSITION_LEFT,
spriteAxe.getPosition().y);
spritePlayer.setPosition(580, 720);
// update the branches
updateBranches(score);
// set the log flying
spriteLog.setPosition(810, 720);
logSpeedX = 5000;
logActive = true;
acceptInput = false;
}
}
The previous code is just the same as the code that handles the right-hand-side chop, except that the sprites are positioned differently and the logSpeedX variable is set to a positive value so that the log whizzes to the right.
Now, we can code what happens when a keyboard key is released.
To make the preceding code work beyond the first chop, we need to detect when the player releases a key and then set acceptInput back to true.
This is slightly different to the key handling we have seen so far. SFML has two different ways of detecting keyboard input from the player. We have already seen the first way when we handled the Enter key, and it is dynamic and instantaneous, which is exactly what we need to respond immediately to a key press.
The following code uses the method of detecting when a key is released. Enter the following highlighted code at the top of the Handle the players input section and then we will go through it:
/*
****************************************
Handle the players input
****************************************
*/
Event event;
while (window.pollEvent(event))
{
if (event.type == Event::KeyReleased && !paused)
{
// Listen for key presses again
acceptInput = true;
// hide the axe
spriteAxe.setPosition(2000,
spriteAxe.getPosition().y);
}
}
if (Keyboard::isKeyPressed(Keyboard::Escape))
{
window.close();
}
In the preceding code, we declare an object of the Event type called event. Then, we call the window.pollEvent function, passing in our new object, event. The pollEvent function puts data into the event object that describes an operating system event. This could be a key press, key release, mouse movement, mouse click, game controller action, or something that happened to the window itself (resized, moved, and so on).
The reason that we wrap our code in a while loop is because there might be many events stored in a queue. The window.pollEvent function will load them, one at a time, into event. With each pass through the loop, we will see whether we are interested in the current event and respond if we are. When window.pollEvent returns false, that means there are no more events in the queue and the while loop will exit.
This if condition (event.type == Event::KeyReleased && !paused) executes when both a key has been released and the game is not paused.
Inside the if block, we set acceptInput back to true and hide the axe sprite off screen.
You can now run the game and gaze in awe at the moving tree, swinging axe, and animated player. It won't, however, squash the player, and the log doesn't move yet when chopped.
Let's move on to making the log move.
When the player chops, logActive is set to true, so we can wrap some code in a block that only executes when logActive is true. Furthermore, each chop sets logSpeedX to either a positive or negative number, so the log is ready to start flying away from the tree in the correct direction.
Add the following highlighted code right after where we update the branch sprites:
// update the branch sprites
for (int i = 0; i < NUM_BRANCHES; i++)
{
float height = i * 150;
if (branchPositions[i] == side::LEFT)
{
// Move the sprite to the left side
branches[i].setPosition(610, height);
// Flip the sprite round the other way
branches[i].setRotation(180);
}
else if (branchPositions[i] == side::RIGHT)
{
// Move the sprite to the right side
branches[i].setPosition(1330, height);
// Flip the sprite round the other way
branches[i].setRotation(0);
}
else
{
// Hide the branch
branches[i].setPosition(3000, height);
}
}
// Handle a flying log
if (logActive)
{
spriteLog.setPosition(
spriteLog.getPosition().x +
(logSpeedX * dt.asSeconds()),
spriteLog.getPosition().y +
(logSpeedY * dt.asSeconds()));
// Has the log reached the right hand edge?
if (spriteLog.getPosition().x < -100 ||
spriteLog.getPosition().x > 2000)
{
// Set it up ready to be a whole new log next frame
logActive = false;
spriteLog.setPosition(810, 720);
}
}
} // End if(!paused)
/*
****************************************
Draw the scene
****************************************
*/
The code sets the position of the sprite by getting its current horizontal and vertical location with getPosition and then adding to it using logSpeedX and logSpeedY, respectively, multiplied by dt.asSeconds.
After the log sprite has been moved each frame, the code uses an if block to see whether the sprite has disappeared out of view on either the left or the right. If it has, the log is moved back to its starting point, ready for the next chop.
If you run the game now, you will be able to see the log flying off to the appropriate side of the screen:
Now, let's move on to a more sensitive subject.
Every game must end badly with either the player running out of time (which we have already handled) or getting squashed by a branch.
Detecting the player getting squashed is really simple. All we want to know is: does the last branch in the branchPositions array equal playerSide? If it does, the player is dead.
Add the following highlighted code that detects and executes when the player is squashed by a branch. We will talk about it later:
// Handle a flying log
if (logActive)
{
spriteLog.setPosition(
spriteLog.getPosition().x +
(logSpeedX * dt.asSeconds()),
spriteLog.getPosition().y +
(logSpeedY * dt.asSeconds()));
// Has the log reached the right-hand edge?
if (spriteLog.getPosition().x < -100 ||
spriteLog.getPosition().x > 2000)
{
// Set it up ready to be a whole new cloud next frame
logActive = false;
spriteLog.setPosition(800, 600);
}
}
// has the player been squished by a branch?
if (branchPositions[5] == playerSide)
{
// death
paused = true;
acceptInput = false;
// Draw the gravestone
spriteRIP.setPosition(525, 760);
// hide the player
spritePlayer.setPosition(2000, 660);
// Change the text of the message
messageText.setString("SQUISHED!!");
// Center it on the screen
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);
}
} // End if(!paused)
/*
****************************************
Draw the scene
****************************************
*/
The first thing the preceding code does, after the player's demise, is set paused to true. Now, the loop will complete this frame and won't run the update part of the loop again until a new game is started by the player.
Then, we move the gravestone into position, near where the player was standing, and hide the player sprite off screen.
We set the String of messageText to "Squished!!" and then use the usual technique to center it on the screen.
You can now run the game and play it for real. The following screenshot shows the player's final score and their gravestone, as well as the SQUISHED message:
There is just one more problem to deal with. Is it just me, or is it a little bit quiet?
In this section, we will add three sounds. Each sound will be played on a particular game event, that is, a simple thud sound whenever the player chops, a gloomy losing sound when the player runs out of time, and a retro crushing sound when the player is squashed to death.
SFML plays sound effects using two different classes. The first class is the SoundBuffer class. This is the class that holds the actual audio data from the sound file. It is SoundBuffer that is responsible for loading the .wav files into the PC's RAM in a format that can be played without any further decoding work.
When we write code for the sound effects in a minute, we will see that, once we have a SoundBuffer object with our sound stored in it, we will then create another object of the Sound type. We can then associate this Sound object with a SoundBuffer object. Then, at the appropriate moment in our code, we will be able to call the play function of the appropriate Sound object.
As we will see very soon, the C++ code to load and play sounds is really simple. What we need to consider, however, is when we call the play function, where in our code will we put the function calls to play? Let's see:
Now, we can write our sound code.
First, we will add another #include directive to make the SFML sound-related classes available. Add the following highlighted code:
#include <sstream>
#include <SFML/Graphics.hpp>
#include <SFML/Audio.hpp>
using namespace sf;
Now, we will declare three different SoundBuffer objects, load three different sound files into them, and associate three different objects of the Sound type with the related objects of the SoundBuffer type. Add the following highlighted code:
// Control the player input
bool acceptInput = false;
// Prepare the sounds
// The player chopping sound
SoundBuffer chopBuffer;
chopBuffer.loadFromFile("sound/chop.wav");
Sound chop;
chop.setBuffer(chopBuffer);
// The player has met his end under a branch
SoundBuffer deathBuffer;
deathBuffer.loadFromFile("sound/death.wav");
Sound death;
death.setBuffer(deathBuffer);
// Out of time
SoundBuffer ootBuffer;
ootBuffer.loadFromFile("sound/out_of_time.wav");
Sound outOfTime;
outOfTime.setBuffer(ootBuffer);
while (window.isOpen())
{
Now, we can play our first sound effect. Add the following single line of code to the if block, which detects that the player has pressed the right cursor key:
// Wrap the player controls to
// Make sure we are accepting input
if (acceptInput)
{
// More code here next...
// First handle pressing the right cursor key
if (Keyboard::isKeyPressed(Keyboard::Right))
{
// Make sure the player is on the right
playerSide = side::RIGHT;
score++;
timeRemaining += (2 / score) + .15;
spriteAxe.setPosition(AXE_POSITION_RIGHT,
spriteAxe.getPosition().y);
spritePlayer.setPosition(1120, 660);
// update the branches
updateBranches(score);
// set the log flying to the left
spriteLog.setPosition(800, 600);
logSpeedX = -5000;
logActive = true;
acceptInput = false;
// Play a chop sound
chop.play();
}
Tip
Add exactly the same code at the end of the next block of code that starts with if (Keyboard::isKeyPressed(Keyboard::Left)) to make a chopping sound when the player chops on the left-hand side of the tree.
Find the code that deals with the player running out of time and add the following highlighted code to play the out of time-related sound effect:
if (timeRemaining <= 0.f) {
// 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);
// Play the out of time sound
outOfTime.play();
}
Finally, to play the death sound when the player is squished, add the following highlighted code to the if block, which executes when the bottom branch is on the same side as the player:
// has the player been squished by a branch?
if (branchPositions[5] == playerSide)
{
// death
paused = true;
acceptInput = false;
// Draw the gravestone
spriteRIP.setPosition(675, 660);
// hide the player
spritePlayer.setPosition(2000, 660);
messageText.setString("SQUISHED!!");
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);
// Play the death sound
death.play();
}
That's it! We have finished the first game. Let's discuss some possible enhancements before we move on to the second project.
Take a look at these suggested enhancements for the Timber!!! project. You can see the enhancements in action in the Runnable folder of the download bundle:
Take a look at the game in action with extra trees, clouds, and a transparent background for the text:
To see the code for these enhancements, take a look in the Timber Enhanced Version folder of the download bundle.
In this chapter, we added the finishing touches and graphics to the Timber!!! game. If, prior to this book, you had never coded a single line of C++, then you can give yourself a big pat on the back. In just five modest chapters, you have gone from zero knowledge to a working game.
However, we will not be congratulating ourselves for too long because, in the next chapter, we will move straight on to some slightly more hardcore C++. While the next game, a simple Pong game, in some ways is simpler than Timber!!, learning about writing our own classes will prepare us for building more complicated and fuller-featured games.
Q) I admit that the arrays solution for the clouds was more efficient. But do we really need three separate arrays—one for active, one for speed, and one for the sprite itself?
A) If we look at the properties/variables that various objects have, for example, Sprite objects, we will see they are numerous. Sprites have position, color, size, rotation, and more as well. But it would be just perfect if they had active, speed, and perhaps some more. The problem is that the coders at SFML can't possibly predict all of the ways that we will want to use their Sprite class. Fortunately, we can make our own classes. We could make a class called Cloud that has a Boolean for active and int for speed. We can even give our Cloud class an SFML Sprite object. We could then simplify our cloud code even further. We will look at designing our own classes in the next chapter.
18.118.20.231