Beginning C++ Game Programming
上QQ阅读APP看书,第一时间看更新

Handling the player's input

A few different things depend on the movement of the player, as follows:

  • When to show the axe
  • When to begin animating the log
  • When to move all of the branches down

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:

  1. Wait for the player to use the left or right arrow keys to chop a log.
  2. When the player chops, disable key press detection.
  3. Wait for the player to remove their finger from a key.
  4. Reenable chop detection.
  5. Repeat from step 1.

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.

Handling setting up a new game

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.

Detecting the player chopping

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:

  • First, we detect whether the player has chopped on the right-hand side of the tree. If they have, then we set playerSide to side::RIGHT. We will respond to the value of playerSide later in the code. Then, we add one to the score with score ++.
  • The next line of code is slightly mysterious, but all that is happening is we are adding to the amount of time remaining. We are rewarding the player for taking action. The problem for the player, however, is that the higher the score gets, the less additional time is added on. You can play with this formula to make the game easier or harder.
  • Then, the axe is moved into its right-hand-side position with spriteAxe.setPosition and the player sprite is moved into its right-hand-position as well.
  • Next, we call updateBranches to move all the branches down one place and spawn a new random branch (or space) at the top of the tree.
  • Then, spriteLog is moved into its starting position, camouflaged against the tree, and its speedX variable is set to a negative number so that it whizzes off to the left. Also, logActive is set to true so that the log moving code that we will write soon animates the log each frame.
  • Finally, acceptInput is set to false. At this point, no more chops can be made by the player. We have solved the problem of the presses being detected too frequently, and we will see how we can reenable chopping soon.

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.

Detecting a key being 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.

Animating the chopped logs and the axe

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.