Build a Game Engine

Gamepad Input

Prerequisites

Article

Our final input device of this section will be from a game pad. Whether on a PC, a handheld or a game console, supporting a game pad opens up a wide range of additional devices.

Gamepad input has some interesting properties... for example, a stick or joystick on a gamepad is almost never zero. It is almost always transmitting some sort of state. In really bad cases, this contributes to "drift" - turning or movement when the player isn't touching the joystick. To account for this, we'll follow in the footsteps of other game engines and define a "dead zone" of less than 0.3, where we won't count that as player input. That may seem high, but we want player movement to be intentional and it still leaves us plenty of room for walk/jog/run or whatever animation transitions we want to make.

In the same vein, joystick input changes can be very tiny. We don't really want to fire an event every time the state changes by 0.01. Once we get to our networked input and replication, that would put the server in overdrive. To counter this effect, we'll also define a "step zone" of 0.1 - that is, we don't really care about changes that are less than 0.1.

Here are our defines:

#define GAMEPAD_DEAD_ZONE   0.3
#define GAMEPAD_STEP_ZONE   0.1

We'll also need to map our buttons and axes. Unlike our mouse, where we treated the scroll wheel as "just another button", we will define the axes separately since they are the primary method of player input (whereas a scroll wheel may trigger a "zoom" or move the local camera and is unlikely to be a server transmitted event):

enum GamepadButtons {
    BUTTON_A = 0,
    BUTTON_B,
    BUTTON_X,
    BUTTON_Y,
    BUTTON_LEFT_BUMPER,
    BUTTON_RIGHT_BUMPER,
    BUTTON_LEFT_TRIGGER,
    BUTTON_RIGHT_TRIGGER,
    BUTTON_BACK,
    BUTTON_START,
    BUTTON_LEFT_STICK,
    BUTTON_RIGHT_STICK,
    BUTTON_DPAD_UP,
    BUTTON_DPAD_RIGHT,
    BUTTON_DPAD_DOWN,
    BUTTON_DPAD_LEFT,
    BUTTON_GAMEPAD_LAST,
    BUTTON_CROSS = BUTTON_A,
    BUTTON_CIRCLE = BUTTON_B,
    BUTTON_SQUARE = BUTTON_X,
    BUTTON_TRIANGLE = BUTTON_Y
};

enum GamepadAxes {
    AXIS_LEFT_X = BUTTON_GAMEPAD_LAST,
    AXIS_LEFT_Y,
    AXIS_RIGHT_X,
    AXIS_RIGHT_Y,
    AXIS_LEFT_TRIGGER,
    AXIS_RIGHT_TRIGGER,
    AXIS_LAST = AXIS_RIGHT_TRIGGER
};

You can see all of our fan favorite buttons here: A, B, X, Y, triggers, and "bumpers", as well as the PlayStation mappings. You'll also notice the sticks are buttons - on PlayStation and Xbox controllers, you can click the sticks like a mouse button, in addition to pushing them around. Finally, we have our axes for each joystick. In our axes enum, we're continuing on the number from our buttons enum; we'll store all of their state together and have some differences in our code that is joystick or axis specific.

Let's take a look at our Gamepad class:

class UGamepad : public UInputDevice {
public:
    UGamepad();
    ~UGamepad() = default;

    /**
     * Init and update functions (if needed by device)
     */
    void Initialize();

    /**
     * Update state
     */
    void Update();

    /**
     * Get the state of a button
     */
    float GetButtonState(int button);

    /**
     * Process a button action
     */
    void SetButtonState(int button, float state);

    /**
     * Get device type
     */
    int Type() { return UInputDeviceType::GAMEPAD; }

public:
    // Controller number
    int controller;

protected:
    // Button mappings
    float m_buttonStates[AXIS_LAST];
};

You'll see a few difference here from our keyboard and mouse classes. While we implement the usual functions (Initialize, GetButtonState, SetButtonState, and Type), we are also using the Update function (called once per frame) and have a controller number in our variables. This will allow for local multiplayer on the same machine with multiple gamepads (think Mario Kart). When an action event is triggered from a Gamepad device, the programmer will be able to use the controller property to decide which player should process the action.

Our Initialize function is similar to our Keyboard class and sets up the mappings from GLFW, as well as setting our controller number to zero (the first one in a zero based array):

std::map<int, int> gamepadMappings;

void UGamepad::Initialize() {
    gamepadMappings[GLFW_GAMEPAD_BUTTON_A] = BUTTON_A;
    gamepadMappings[GLFW_GAMEPAD_BUTTON_B] = BUTTON_B;
    gamepadMappings[GLFW_GAMEPAD_BUTTON_X] = BUTTON_X;
    gamepadMappings[GLFW_GAMEPAD_BUTTON_Y] = BUTTON_Y;
    gamepadMappings[GLFW_GAMEPAD_BUTTON_LEFT_BUMPER] = BUTTON_LEFT_BUMPER;
    gamepadMappings[GLFW_GAMEPAD_BUTTON_RIGHT_BUMPER] = BUTTON_RIGHT_BUMPER;
    gamepadMappings[GLFW_GAMEPAD_BUTTON_BACK] = BUTTON_BACK;
    gamepadMappings[GLFW_GAMEPAD_BUTTON_START] = BUTTON_START;
    gamepadMappings[GLFW_GAMEPAD_BUTTON_LEFT_THUMB] = BUTTON_LEFT_STICK;
    gamepadMappings[GLFW_GAMEPAD_BUTTON_RIGHT_THUMB] = BUTTON_RIGHT_STICK;
    gamepadMappings[GLFW_GAMEPAD_BUTTON_DPAD_UP] = BUTTON_DPAD_UP;
    gamepadMappings[GLFW_GAMEPAD_BUTTON_DPAD_RIGHT] = BUTTON_DPAD_RIGHT;
    gamepadMappings[GLFW_GAMEPAD_BUTTON_DPAD_DOWN] = BUTTON_DPAD_DOWN;
    gamepadMappings[GLFW_GAMEPAD_BUTTON_DPAD_LEFT] = BUTTON_DPAD_LEFT;

    // Start with controller zero
    controller = 0;
}

Our Update function is a little more interesting:

void UGamepad::Update() {
    GLFWgamepadstate state;
    UInputSystem* ioSystem = GetUSystem<UInputSystem>();

    if (glfwGetGamepadState(GLFW_JOYSTICK_1 + controller, &state)) {
        // Collect and set button state
        for (int i = 0; i < GLFW_GAMEPAD_BUTTON_LAST; i++) {
            float currentState = this->GetButtonState(gamepadMappings[i]);
            this->SetButtonState(gamepadMappings[i], state.buttons[i]);
            if (currentState != state.buttons[i]) {
                // Potentially trigger an action
                ioSystem->TriggerAction(this, gamepadMappings[i], state.axes[i]);
            }
        }

        // Collect and set axis states
        for (int i = 0; i < GLFW_GAMEPAD_AXIS_LAST; i++) {
            // Check current state
            float updatedState = 0.0f;
            float currentState = this->GetButtonState(i + BUTTON_GAMEPAD_LAST);

            // If we're over the dead zone, check for a state change
            if (state.axes[i] > GAMEPAD_DEAD_ZONE) {
                updatedState = state.axes[i];
            }

            // Finally, measure in discrete steps
            if(std::abs(updatedState - currentState) > GAMEPAD_STEP_ZONE) {
                // Set button state
                this->SetButtonState(i + BUTTON_GAMEPAD_LAST, updatedState);

                // Potentially trigger an action
                ioSystem->TriggerAction(this, i + BUTTON_GAMEPAD_LAST, updatedState);
            }
        }
    }
}

Here, we use our service locator function GetUSystem from our ECS Design article to get our InputSystem. Next, we check the gamepad state for the gamepad / joystick (GLFW treats gamepads as specialized joysticks).

Unlike our mouse and keyboard implementations, which used callbacks, because our joystick and gamepad input changes can be so tiny, there are no equivalent callback functions in GLFW for gamepads (as of the time of writing this). Instead, we iterate over each button and set the state, as well as checking for a change in state and triggering an action event if necessary.

Next we do the same for axes. For each axis, we start with an assumed state of 0.0, then we load current button state. If the new state of the button is over the dead zone, we set it to state.axes[i]; if it is not greater than our dead zone, it stays at 0.0. Next, we check whether the different between current and updated state is greater than our "step zone" (eg. it's changed by more than 0.1). If so, we set our button state and trigger a potential action associated with this gamepad and axis.

Finally, we have our GetButtonState and SetButtonState functions:

float UGamepad::GetButtonState(int button) {
    UASSERT(0 <= button && button < AXIS_LAST, "Button outside of range.");
    return(m_buttonStates[button]);
}

void UGamepad::SetButtonState(int button, float state) {
    UASSERT(0 <= button && button < AXIS_LAST, "Button outside of range.");
    m_buttonStates[button] = state;
}

Both of these simply do a range change and then get or set the button state.

Conclusion

Our engine now supports keyboard, mouse, and game pad input. We can map buttons and axes to action events (eg. "jump" or "move forward") and get and set them as actions, regardless of which buttons the user has mapped. We also have the ability to support multiple controllers for local multiplayer game player.

We haven't covered actual joysticks, but if you've building a flight simulator, this article should help to get you started. I'll leave that implementation as an exercise for the reader.