Joystick Events

Learn how to handle Joystick events with our own callbacks

Source Code

SFML Joystick Events

SfML supports up to 8 Joysticks. Interacting with any of your joysticks will generate five (5) types of events

  • sf::Event::JoystickConnected : Emitted when a joystick get connected (for example by plugging its USB cable)
  • sf::Event::JoystickDisconnected : Emitted when a joystick get disconnected (for example by unplugging its USB cable)
  • sf::Event::JoystickButtonPressed : Emitted when a button is pressed
  • sf::Event::JoystickButtonReleased : Emitted when a button is released
  • sf::Event::JoystickMoved : Emitted when one of your joysticks Axes is moved. Axes can be : Analog 1 and 2, DPad and Shoulder Buttons (L2, R2)
The code below shows how to handle those five (5) types of events. Each one of these events will query the Joystick Id, a number between 0 and 7. The events ButtonPressed and ButtonReleased indicate the button concerned with a number between 0 and 31. SFML supports up to 32 Buttons per Joystick. The event JoystickMoved indicates the axis (X, Y, Z, R, U, V, POV X, and POV Y) and how much is the movement (position from -100 to 100). For example, if you press the Left Analog all the way left, the axis will be X with a position of -100 and all the way right will be the same axis X but with a position of 100.
void handleEvent(const sf::Event& event)
{
    if (event.type == sf::Event::JoystickConnected)
    {
        unsigned int joystickId = event.joystickConnect.joystickId; //Ids are numbered from 0 to 7
    }
    else if (event.type == sf::Event::JoystickDisconnected)
    {
        unsigned int joystickId = event.joystickConnect.joystickId; //Ids are numbered from 0 to 7
    }
    if (event.type == sf::Event::JoystickButtonPressed)
    {
        unsigned int joystickId = event.joystickButton.joystickId; //Ids are numbered from 0 to 7
        unsigned int button     = event.joystickButton.button; //Button are numbered from 0 to 31
    }
    if (event.type == sf::Event::JoystickButtonReleased)
    {
        unsigned int joystickId = event.joystickButton.joystickId; //Ids are numbered from 0 to 7
        unsigned int button     = event.joystickButton.button; //Button are numbered from 0 to 31
    }
    if (event.type == sf::Event::JoystickMoved)
    {
        unsigned int joystickId = event.joystickMove.joystickId; //Ids are numbered from 0 to 7
        sf::Joystick::Axis axis = event.joystickMove.axis; //X, Y, Z, R, U, V, POV X and POV Y
        float position          = event.joystickMove.position; //Position is between -100 and 100
        //Axes depend on your GamePad and OS System, but here is an example
        //Axis POV X and POV y  = DPad (Directional Pad)    ,first axis is horizontal (left-right), second is vertical (up-down)
        //Axis X and Y          = First Analog              ,first axis is horizontal (left-right), second is vertical (up-down)
        //Axis Z and R          = Second Analog             ,first axis is horizontal (left-right), second is vertical (up-down)
        //Axis U and V          = Shoulder (L2, R2)         ,first axis is L2, second is R2
    }
}

Joystick Events Callbacks

Using the Joysticks Events as provided by SFML is really complicated and cumbersome. Buttons are called by numbers and Axes do not really mean anything to us. If we were using a PS4 controller, for example, it would great to know that we pressed the Square button or move the left Analog Up. To make things easier let’s create our own Joystick Events callbacks but also our own Gamepad System. For the callbacks, we’ll use the following three (3) callbacks

  • onJoystickConnection(const unsigned int& joystickId, const bool& connected)
  • onJoystickButton(const unsigned int& joystickId, const unsigned int& button, const bool& isPressed)
  • onJoystickAxis(const unsigned int& joystickId, const sf::Joystick::Axis& axis, const float& position)
The Gamepad System we’re going to create here will depend on two things
  • Which controller are we using : I will use the PS4 Dualshock Controller
  • How does our OS System recognizes our Controller Buttons and Axis : I will use Window 10
So depending on the controller you will use and how your OS recognizes your controller inputs, you will have to adjust the code I will present below.

The PS4 Controller

I will be using the PS4 Dualshock controller (Image below). I am using Window 10 and the controller works simply by connecting it through USB cable or Bluetooth. No installation of any kind is required, just connect and use. The controller has 14 buttons (0 to 13) and 8 Axes. The Dpad (Left, Right, Up, Down) is an axis, not four (4) buttons and the buttons L2 and R2 are both buttons and Axes.

  • Buttons : Square, Cross, Circle, Triangle, Shoulder Buttons (L1, L2, R1, R2), Analog Buttons (L3, R3), Share, Options, PS Button (I will call this one Start Button), and the Touchpad button
  • Axes : Dpad (PovX, PovY), Left Analog (X, Y), Right Analog (Z, R), Shoulder Buttons L2 and R2 (U, V)

The Gampad Utility File

In the source code, you can see that our Engine has a new file named GamepadUtil : Simple Engine – GampadUtil File.

Inside this file, we do two important things

  • Create a list of our Buttons and Axes
  • Create a mapping between our controller buttons/ases named and the numbers used by SFML
The buttons list is stored as an Enum Class called JSButton (for Joystick Button), and the Axes list as an Enum Class called JSAxis (for Joystick Axis). The buttons and Axes are named for the PS4 controller. If you are using a different controller you can change the names accordingly. For example, the Xbox controller uses the name (A, B, X, and Y).
enum class JSButton
{
    //Action
    ,Square
    ,Cross
    ,Circle
    ,Triangle
    //.. continued
};
enum class JSAxis
{
     DPadX
    ,DPadY
    ,LeftAnalogX
    ,LeftAnalogY
    ,RightAnalogX
    ,RightAnalogY
    ,ShoulderX
    ,ShoulderY
};

Now that we have the list of Buttons and Axes with the appropriate names, it’s time to create a Mapping between those names and the SFML Buttons numbers and Axes names. We can use a simple std::map for that. The code below shows two std::map renamed JSButtonMapping and JSAxisMapping.

typedef std::map<unsigned int, JSButton>        JSButtonMapping;
typedef std::map<sf::Joystick::Axis, JSAxis>    JSAxisMapping;

Below you can see our Mappings called JSPs4ButtonMapping and JSPs4AxisMapping. This mapping may be different for you. You have to try each button and axis one by one and find out on your own the appropriate Mapping.

const JSButtonMapping JSPs4ButtonMapping
{{0,    JSButton::Square},
 {1,    JSButton::Cross},
 {2,    JSButton::Circle},
 {3,    JSButton::Triangle},
 {4,    JSButton::L1},
 {5,    JSButton::R1},
 {6,    JSButton::L2},
 {7,    JSButton::R2},
 {8,    JSButton::Share},
 {9,    JSButton::Option},
 {10,   JSButton::L3},
 {11,   JSButton::R3},
 {12,   JSButton::Start},
 {13,   JSButton::Touchpad}};
const JSAxisMapping JSPs4AxisMapping
{{sf::Joystick::X,      JSAxis::LeftAnalogX},
 {sf::Joystick::Y,      JSAxis::LeftAnalogY},
 {sf::Joystick::Z,      JSAxis::RightAnalogX},
 {sf::Joystick::R,      JSAxis::RightAnalogY},
 {sf::Joystick::PovX,   JSAxis::DPadX},
 {sf::Joystick::PovY,   JSAxis::DPadY},
 {sf::Joystick::U,      JSAxis::ShoulderX},
 {sf::Joystick::V,      JSAxis::ShoulderY}};

Finally the GamepadUtil file contains the function JString (for Joystick String) that will help us print the Buttons and Axis names when it comes to debug our code.

std::string JString(const JSButton& button)
std::string JString(const JSAxis& axis)

The Gampad Class

In addition to the GamepadUtil file, our Engine now has a new Gamepad Class : Simple Engine – Gamepad Class. This class will represent our Game Controller. The code below shows how to create a new Gamepad. The new Gamepad is given an ID (ONE to EIGHT), a Button Mapping, an Axis Mapping, a connection status. Finally, we adjust the sensitivity of the axes for all controllers.

//Create a Gamepad
ng::Gamepad gamepadOne;                                 //New gamepad with no id (id = ng::Gamepad::NONE)
gamepadOne.setId(ng::Gamepad::ONE)                      //SFML supports 8 controllers, so we have eight IDs ONE to EIGHT
gamepadOne.setButtonMapping(ng::JSPs4ButtonMapping);    //Set our PS4 Buttons mapping
gamepadOne.setAxisMapping(ng::JSPs4AxisMapping)          //Set our PS4 Axes mapping
gamepadOne.setConnected(sf::Joystick::isConnected(mGamepadOne.getId())); //Update the connection status of the Gamepad using SFML.
//A little setting
//The Axes (Analog for example) can be very sensitive and generate many unwanted events, so we adjust the sensitivity.
//This applies to all eight (8) controllers.
getRenderWindow().setJoystickThreshold(ng::Gamepad::AXIS_SENSIVITY);

The real utility of this new class is its ability to convert SFML buttons and axes IDs into our JSButton and JSAxis. This is done using the two methods below. The methods use the Button and Axis mappings provided in order to make the conversion.

JSButton    getButton(const unsigned int& buttonId);
JSAxis      getAxis(const sf::Joystick::Axis& axisId);

The Callbacks

We’re finally at the end. Now that we have our Gamepad Class with the correct mapping, we can use our new Callbacks. As you see, it’s now easy to know which Button has been pressed or which Axis has been moved.

void onJoystickConnection(const unsigned int& joystickId, const bool& connected)
{
    if(mGamepadOne.getId() == joystickId)
    {
        mGamepadOne.setConnected(connected);
        std::string info = connected ? "gamepad connection" : "gamepad disconnection";
        nero_log(info);
        //do something
            //inform the player that the controller has just disconnected
    }
}
void onJoystickButton(const unsigned int& joystickId, const unsigned int& button, const bool& isPressed)
{
    if(mGamepadOne.getId() == joystickId)
    {
        ng::JSButton jsButton = mGamepadOne.getButton(button);
        std::string info = ng::JString(jsButton) + " " + (isPressed ? "Pressed" : "Released");
        nero_log(info);
        if(jsButton == ng::JSButton::Square)
        {
            //do something
                //shoot a bullet
                //through a punch
        }
    }
}
void onJoystickAxis(const unsigned int& joystickId, const sf::Joystick::Axis& axis, const float& position)
{
    if(mGamepadOne.getId() == joystickId)
    {
        ng::JSAxis jsAxis = mGamepadOne.getAxis(axis);
        nero_log(ng::JString(jsAxis) + " " + _se(position));
        if(jsAxis == ng::JSAxis::LeftAnalogX)
        {
            if(position < 0)
            {
                //move the player right
            }
            else if(position > 0)
            {
                //move player left 
            }         
        }
    }
}

The code in our handleEvent method now looks like this

void handleEvent(const sf::Event& event)
{
    if (event.type == sf::Event::JoystickConnected)
    {
        //callback
        onJoystickConnection(event.joystickConnect.joystickId, true);
    }
    else if (event.type == sf::Event::JoystickDisconnected)
    {
        //callback
        onJoystickConnection(event.joystickConnect.joystickId, false);
    }
    if (event.type == sf::Event::JoystickButtonPressed)
    {
        //callback
        onJoystickButton(event.joystickButton.joystickId, event.joystickButton.button, true);
    }
    if (event.type == sf::Event::JoystickButtonReleased)
    {
        //callback
        onJoystickButton(event.joystickButton.joystickId, event.joystickButton.button, false);
    }
    if (event.type == sf::Event::JoystickMoved)
    {
        //callback
        onJoystickAxis(event.joystickMove.joystickId, event.joystickMove.axis, event.joystickMove.position);
    }
}