Engine Improved – Events and Inputs

Let’s improve our Engine with everything we’ve learned so far

Source Code

Scene Events Callbacks

So far we have learned how to use most of the SFML events and created our own callbacks. Let’s add all those callbacks in our Scene class so they can be used right away. The callbacks are declared virtual in order to be overridable in our custom Scenes. Expand the section Handle Event Method below to see how our handleEvent() method now looks like. It’s a lot of callbacks, since this is a tutorial we’ll keep all of them but, in a real application, you may only need few of them. For the next part of the tutorial series, I’ll be mainly using the Joysticks callbacks.

void Scene::handleEvent(const sf::Event& event)
{
    switch(event.type)
    {
        //Window
        case sf::Event::Closed:
            onWindowClosed(); break;
        case sf::Event::Resized:
            onWindowResized(sf::Vector2u(event.size.width, event.size.height)); break;
        case sf::Event::GainedFocus:
            onWindowFocusChanged(true); break;
        case sf::Event::LostFocus:
            onWindowFocusChanged(false); break;
        //Keyboard
        case sf::Event::KeyPressed:
        {
            ModifierKey modifier;
            modifier.alt        = event.key.alt;
            modifier.control    = event.key.control;
            modifier.shift      = event.key.shift;
            modifier.system     = event.key.system;
            onKeyboardButton(event.key.code, true, modifier);
        }break;
        case sf::Event::KeyReleased:
        {
            ModifierKey modifier;
            modifier.alt        = event.key.alt;
            modifier.control    = event.key.control;
            modifier.shift      = event.key.shift;
            modifier.system     = event.key.system;
            onKeyboardButton(event.key.code, false, modifier);
        }break;
        case sf::Event::TextEntered:
        {
            if (event.text.unicode < 128)
            {
                onTextEntered(std::string(1, (static_cast(event.text.unicode))));
            }
        }break;
        //Mouse
        case sf::Event::MouseMoved:
            onMouseMoved(sf::Vector2f(event.mouseMove.x, event.mouseMove.y)); break;
        case sf::Event::MouseButtonPressed:
            onMouseButton(event.mouseButton.button, true, sf::Vector2f(event.mouseButton.x, event.mouseButton.y)); break;
        case sf::Event::MouseButtonReleased:
            onMouseButton(event.mouseButton.button, false, sf::Vector2f(event.mouseButton.x, event.mouseButton.y)); break;
        case sf::Event::MouseWheelScrolled:
            onMouseWheel(event.mouseWheelScroll.wheel, event.mouseWheelScroll.delta,sf::Vector2f(event.mouseWheelScroll.x, event.mouseWheelScroll.y)); break;
        case sf::Event::MouseEntered:
            onMouseWindowSurface(true); break;
        case sf::Event::MouseLeft:
            onMouseWindowSurface(false); break;
        //Joystick
        case sf::Event::JoystickConnected:
            onJoystickConnection(event.joystickConnect.joystickId, true); break;
        case sf::Event::JoystickDisconnected:
            onJoystickConnection(event.joystickConnect.joystickId, false); break;
        case sf::Event::JoystickButtonPressed:
            onJoystickButton(event.joystickButton.joystickId, event.joystickButton.button, true); break;
        case sf::Event::JoystickButtonReleased:
            onJoystickButton(event.joystickButton.joystickId, event.joystickButton.button, false); break;
        case sf::Event::JoystickMoved:
            onJoystickAxis(event.joystickMove.joystickId, event.joystickMove.axis, event.joystickMove.position); break;
    }
}
//events callbacks
    //window
virtual void    onWindowClosed();
virtual void    onWindowResized(const sf::Vector2u& size);
virtual void    onWindowFocusChanged(const bool& gainedFocus);
    //keyboard
virtual void    onKeyboardButton(const sf::Keyboard::Key& key, const bool& isPressed, const ModifierKey& modifier);
virtual void    onTextEntered(const std::string& c);
    //mouse
virtual void    onMouseMoved(const sf::Vector2f& position);
virtual void    onMouseButton(const sf::Mouse::Button& button, const &isPressed, const sf::Vector2f& position);
virtual void    onMouseWheel(const sf::Mouse::Wheel& wheel, const float& delta, const sf::Vector2f& position);
virtual void    onMouseWindowSurface(const bool& mouseEntered);
    //joystick
virtual void    onJoystickConnection(const unsigned int& joystickId, const bool& connected);
virtual void    onJoystickButton(const unsigned int& joystickId, const unsigned int& button, const bool& isPressed);
virtual void    onJoystickAxis(const unsigned int& joystickId, const sf::Joystick::Axis& axis, const float& position);

Gamepad Axes Buttons

Our Gamepad class can handle joystick buttons and axes, which is good, but most axes can also be used as buttons. If we consider the Dpad for example, we can press Left, Right, Up and Down. The same thing with the two analogs, we can press the Left Analog Left, Right, Up and Down.

SFML does not provide a way to Press and Release Axes like buttons, so let’s create our own system. First, inside the enum class JSButton we add 12 new buttons. 4 buttons for the Dpad and 4 buttons for each Analog. Those buttons are virtual, while they do not exist, we’ll be able to use them as any other buttons.

enum class JSButton
{
    //.. normal buttons here
    //DPad
    ,DPadLeft
    ,DPadRight
    ,DPadUp
    ,DPadDown
    //LeftAnalog
    ,LeftAnalogLeft
    ,LeftAnalogRight
    ,LeftAnalogUp
    ,LeftAnalogDown
    //RightAnalog
    ,RightAnalogLeft
    ,RightAnalogRight
    ,RightAnalogUp
    ,RightAnalogDown
};

Press and Release Conditions

Now we have to decide what we consider a Press or a Release. For the DPad it’s pretty simple, the value of the position is always 0, 100 or -100. So we’ll use the following conditions

  • Axis = DPadX and position = -100 means JSButton::DpadLeft has been pressed
  • Axis = DPadX and position = 100 means JSButton::DpadRight has been pressed
  • Axis = DPadY and position = 100 means JSButton::DpadUp has been pressed
  • Axis = DPadY and position = -100 means JSButton::DpadDown has been pressed
After a press, the position of the DPad axes returns to zero (0), at this moment we consider that a release happened.
 
For the two Analogs, it’s almost the same. The Analogs are very sensitive and the value of the position is not always well defined. So in this case, we’ll use a threshold. When the position crosses the threshold we consider it a Press and when the position return below the threshold we consider it a Release. For example, for the Left Analog, we’ll have the following conditions
 
  • Axis = LeftAnalogX and position < -threshold means JSButton::LeftAnalogLeft as been pressed
  • Axis = LeftAnalogX and position >  threshold means JSButton::LeftAnalogRight as been pressed
  • Axis = LeftAnalogY and position < -threshold means JSButton::LeftAnalogUp as been pressed
  • Axis = LeftAnalogY and position >  threshold means JSButton::LeftAnalogDown as been pressed
The threshold used is 90, fill free to experiment with any value.

Implementation - Joysticks Events

In the case of Events, we can now check if an Axis button has been pressed or released using the two new methods below. The first method only check if a button has been pressed and returns it. If no button has been pressed it will return a JSButton::NONE. The second method lets you pass a boolean isPressed by reference in order to get the status of the button.

  • JSButton getButton(const sf::Joystick::Axis& axisId, const float& position)
  • JSButton getButton(const sf::Joystick::Axis& axisId, const float& position, bool& isPressed)
The code below shows how to use those two new methods
void onJoystickAxis(const unsigned int& joystickId, const sf::Joystick::Axis& axis, const float& position)
{
    if(gamepad.getId() == joystickId)
    {
        //First mehod : Only check if a button is pressed
        JSButton button = gamepad.getButton(axis, position); //return JSButton::NONE if no press is detected
        if(button == JButton::DPadLeft)
        {
            //do something
        }

        //Second method : Check for Press and Release
        bool isPressed;
        JSButton button = gamepad.getButton(axis, position, isPressed);
        if(button == JButton::DPadLeft)
        {
            if(isPressed)
            {
                //do something
            }
            else
            {
                //do something
            }
        }
    }
}

Implementation - Joysticks Global Inputs

We already have the method isButtonPressed to check the global state of our gamepad buttons. Let’s modify this method so it can also handle our new virtual buttons. All real buttons can be found inside our object ButtonReverseMapping. When the method isButtonPressed is called, we check if the button is real or not, if it is, we use the SFML Joystick class, but if the button is not real (meaning it’s an axis button) we use our own method isAxisButtonPressed().

And now we can do isButtonPressed(JSButton::DpadUp) or isButtonPressed(JSButton::LeftAnalogUp), pretty cool !?

if(gamepad.isButtonPressed(JSButton::LeftAnalogUp))
{
    //do something
}

You can see the code of the methods isButtonPressed() and isAxisButtonPressed() below

bool Gamepad::isButtonPressed(const JSButton& button)
{
    if(m_ButtonMappingReverse.find(button) != m_ButtonMappingReverse.end())
    {
        return sf::Joystick::isButtonPressed(getId(), m_ButtonMappingReverse.at(button));
    }
    else
    {
        return isAxisButtonPressed(button);
    }
}
bool Gamepad::isAxisButtonPressed(const JSButton& button)
{
    float threshold = AXIS_BUTTON_THRESHOLD;
    switch(button)
    {
        case JSButton::DPadLeft : return (getAxisPosition(JSAxis::DPadX) == -100.f);
        case JSButton::DPadRight : return (getAxisPosition(JSAxis::DPadX) == 100.f);
        case JSButton::DPadUp : return (getAxisPosition(JSAxis::DPadY) == -100.f);
        case JSButton::DPadDown : return (getAxisPosition(JSAxis::DPadY) == 100.f);
        case JSButton::LeftAnalogLeft : return (getAxisPosition(JSAxis::LeftAnalogX) < -threshold);
        case JSButton::LeftAnalogRight : return (getAxisPosition(JSAxis::LeftAnalogX) >  threshold);
        case JSButton::LeftAnalogUp : return (getAxisPosition(JSAxis::LeftAnalogY) < -threshold);
        case JSButton::LeftAnalogDown : return (getAxisPosition(JSAxis::LeftAnalogY) >  threshold);
        case JSButton::RightAnalogLeft : return (getAxisPosition(JSAxis::RightAnalogX) < -threshold);
        case JSButton::RightAnalogRight : return (getAxisPosition(JSAxis::RightAnalogX) >  threshold);
        case JSButton::RightAnalogUp : return (getAxisPosition(JSAxis::RightAnalogY) < -threshold);
        case JSButton::RightAnalogDown : return (getAxisPosition(JSAxis::RightAnalogY) >  threshold);
        default: return false;
    }
}

Compute Game Frame Rate

In our Scene class, we add two new methods to retrieve the number of Frame per second and the duration of one Frame in seconds

  • float getFrameRate() const :  returns number of frame per second
  • float getFrameTime() const : returns duration of one frame in second
The Frame Rate is computing inside the Engine class, then the information is provided to the Scene class. The code below shows how the two values are computed. The idea is pretty simple :
 
  • We count the number of frames at each game loop with the variable m_FrameCount
  • After one (1) second has passed the value of m_FrameCount is equivalent to the frame rate.
  • We divide the total time passed (approximately one second) by the number of frames (m_FrameCount) in order to get the duration of one frame.
  • Finally, we share the information with the Scene class
void Engine::computeFrameRate(const sf::Time& timeStep)
{
    //Accumulate data for on 1 second
    m_ElapsedTime       += timeStep;
    m_FrameCount        += 1;
    //Then compute the frame rate after one (1) second
    if(m_ElapsedTime >= sf::seconds(1.0f))
    {
        m_FramePerSecond    = m_FrameCount;
        m_TimePerFrame      = m_ElapsedTime.asSeconds() / m_FrameCount;
        m_ElapsedTime      -= sf::seconds(1.0f);
        m_FrameCount        = 0;
        //provide new data to the Scene class
        m_Scene->m_FrameRate = m_FramePerSecond;
        m_Scene->m_FrameTime = m_TimePerFrame;
    }
}

Other Improvements

Besides the big improvement made above, here are some little improvements to our Engine
 
  • Simplify the Engine by providing a Default Scene when no Scene is explicitly set
  • Add the file KeyboardUtil that contains our modifier keys functions
  • Adjust some attributes names