Engine Improved – Events and Inputs
Let’s improve our Engine with everything we’ve learned so far
Source Code
- The code for this tutorial can be found on Github : Learn SFML – Engine Improved v0.2
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
- 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
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)
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
- 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
- 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