C++ Game Engine Development Part 5 – Real Time Input

This is part of an ongoing series where we write a complete 2D game engine in C++ and SFML. A new tutorial is released every Monday. You can find the complete list of tutorials here and download the source code from the projects GitHub page.

In this tutorial we will begin to write our Input manager. We will look at recording keyboard input (controller support will be added in a later tutorial) and how we can differentiate between a key that has just been pressed or released.

Create  a new class called Input

Input.hpp
#ifndef Input_hpp
#define Input_hpp

#include <SFML/Graphics.hpp>

class Input
{
public:
    bool IsKeyPressed(Key keycode); // Returns true if the key is pressed.
    bool IsKeyDown(Key keycode); // Returns true if the key was just pressed.
    bool IsKeyUp(Key keycode); // Returns true if the key was just released.
};

#endif /* Input_hpp */

This summarises what we want from our input system. It wont build right now, as it does not know what a ‘Key’ is. So lets create that and also add an Update method.

Input.hpp
class Input
{
public:
    enum class Key
    {
        None = 0,
        Left = 1,
        Right = 2,
        Up = 3,
        Down = 4,
        Esc = 5
    };

    void Update();
…
};

The Key enum will hold every key we are interested in (currently thats not many but it will be expanded in future). We have specified a value for each entry as we will use these values as bit positions using the Bitmask class that we created in the previous tutorial.

The update method will be responsible for polling the keys and determining which keys have been pressed that frame, to do this it needs a way of storing the status of the keys; which is where our Bitmask class comes in.

Input.hpp
#include "Bitmask.hpp"

class Input
{
…
    
private:
    Bitmask thisFrameKeys;
    Bitmask lastFrameKeys;
};

We create two Bitmask variables: one to store the keys status this frame, and another to store the previous frames key status. We store two frames worth of data so that we can determine if a key was just released or pressed by checking its current status against last frames status (i’ll go into more detail on this shortly). We’ll update the bitmasks in the Update method:

Input.cpp
void Input::Update()
{
    lastFrameKeys.SetMask(thisFrameKeys); // 1

    thisFrameKeys.SetBit((int)Key::Left,
                         (sf::Keyboard::isKeyPressed(sf::Keyboard::Left)) || 
						 (sf::Keyboard::isKeyPressed(sf::Keyboard::A))); // 2
    
    thisFrameKeys.SetBit((int)Key::Right,
                         (sf::Keyboard::isKeyPressed(sf::Keyboard::Right)) || 
						 (sf::Keyboard::isKeyPressed(sf::Keyboard::D)));
    
    thisFrameKeys.SetBit((int)Key::Up,
                         (sf::Keyboard::isKeyPressed(sf::Keyboard::Up)) || 
						 (sf::Keyboard::isKeyPressed(sf::Keyboard::W)));
  
    thisFrameKeys.SetBit((int)Key::Down,
                         (sf::Keyboard::isKeyPressed(sf::Keyboard::Down)) || 
						 (sf::Keyboard::isKeyPressed(sf::Keyboard::S)));
  
    thisFrameKeys.SetBit((int)Key::Esc), 
		sf::Keyboard::isKeyPressed(sf::Keyboard::Escape);
}

1. First we set lastFramesKeys bits to what is currently stored in thisFramesKeys bits. This stores the previous frames keys status as we are about to change the contents of thisFramesKeys.

2. This will set or clear a bit depending on if the left key is being pressed. It calls the overloaded function ‘SetBit’ in our Bitmask class. This functions takes two parameters, the position of the bit and a boolean flag used to determine whether to set a bit to 1 or 0 (true or false). For the position parameter we use the keys enum int value (set when we created the enum); and for the boolean flag we call an SFML provided input method ‘isKeyPressed’; this will return true if the specified keys are pressed and therefore set our bit to 1 (key pressed) or 0 (key not pressed).

This is performed for each of they keys we are interested in. Here are a few examples of what our bitmask could look like after a typical update:

Left (pos 1) and Down (pos 4) keys pressed: 

0000 0000 0000 0000 0000 0000 0000 1010

Up (pos 3) and Down (pos 4) keys pressed:     

0000 0000 0000 0000 0000 0000 0000 1100

All keys pressed: 

0000 0000 0000 0000 0000 0000 0011 1110

As you can see from the above examples, for each of our bitmasks (current and previous frames), we are only using 5 out of a possible 32 bits. In future this will not be too much of an issue as we expand the number of keys we are interested in, but it is definitely something to be aware of. We may want to create a reduced version of our bitmask or we may even find that we want to respond to more than 31 (32 minus the key::None option) different keys and create a larger bitmask. Also eventually, rather than always checking the status of all keys, we will want to add the ability to only check the keys we are currently interested in.

Now that we store the status of our keys, lets implement the methods for querying that data.

Input.cpp
// Return true if the specified key is currently being pressed.
bool Input::IsKeyPressed(Key keycode) 
{
    return thisFrameKeys.GetBit((int)keycode);
}

// Returns true if the key was just pressed 
// (i.e. registered as pressed this frame but not the previous).
bool Input::IsKeyDown(Key keycode)
{
    bool lastFrame = lastFrameKeys.GetBit((int)keycode);
    bool thisFrame = thisFrameKeys.GetBit((int)keycode);
    
    return thisFrame && !lastFrame;
}

// Returns true if the key was just released (i.e. registered as 
// pressed last frame but not the current frame).
bool Input::IsKeyUp(Key keycode)
{
    bool lastFrame = lastFrameKeys.GetBit((int)keycode);
    bool thisFrame = thisFrameKeys.GetBit((int)keycode);
    
    return !thisFrame && lastFrame;
}

IsKeyPressed will recognise a pressed key as long as its held down, whereas the other two methods only work for one frame; so if the keys status is not queried that frame the state change will be missed.

Now that we have implemented the basics of our Input class; lets integrate the system in our engine.

We need to ensure our Input class is updated, we’ll do this by adding a new ‘CaptureInput’ method to our Game class.

Game.hpp
#include "Input.hpp"

class Game
{
public:
…
    void CaptureInput();
   
private:
…    
    Input input;
};

Game.cpp
void Game::CaptureInput()
{
    input.Update();
}

We need to call this in our main game loop:

Game.cpp
while (game.IsRunning())
{
	game.CaptureInput(); // We want to capture input at the start of a frame.
    game.Update();
	game.LateUpdate();
	game.Draw();
	game.CalculateDeltaTime();
}

Now we are registering keyboard input we can move our viking. Lets change our Game’s Update method to do just that.

Game.cpp
void Game::Update()
{
    window.Update();

    const sf::Vector2f& spritePos = vikingSprite.getPosition();
    const int moveSpeed = 100;
    
    int xMove = 0;
    if(input.IsKeyPressed(Input::Key::Left)) // 1
    {
        xMove = -moveSpeed; // 2
    }
    else if(input.IsKeyPressed(Input::Key::Right))
    {
        xMove = moveSpeed;
    }
    
    int yMove = 0;
    if(input.IsKeyPressed(Input::Key::Up))
    {
        yMove = -moveSpeed;
    }
    else if(input.IsKeyPressed(Input::Key::Down))
    {
        yMove = moveSpeed;
    }

    float xFrameMove = xMove * deltaTime; // 2
    float yFrameMove = yMove * deltaTime;
    
    vikingSprite.setPosition(spritePos.x + xFrameMove, spritePos.y + yFrameMove);
}

1. This is where we call our new input class. We call the IsKeyPressed method as we want to move the sprite as long as the specified key is pressed.

2. We need to scale our movement by deltaTime, for more information see my previous tutorial on frame-independent movement.

Now when you run the game, you will be able to move the sprite around the screen using either the arrow keys or WASD. You can also also change the input method calls to IsKeyDown (the character will only move once when a key is pressed) or IsKeyUp (the character will only move once when the key is released) to test that functionality.

As always, if you have any suggestions or are having problems with the code, then let me know in the comments and I’ll look into it. Thank you for reading 🙂