..
#gamedev

Architecture For Game Controller

We want to process the inputs that occur on a gamepad to change the behavior of graphic and/or sound variables.

In the example, we want to move the screen and modify the sound tone.

The first important structure is the state of each digital button on a control such as X, Y, A, B, RB, LB, etc.

Below are the structs that we will use to have controls in the game.

struct ButtonState
{
    bool endedDown; // gamepad button is pressed
    int transitionCount; // how many transitions happens in one frame (up-to-down, down-to-up)
};

To know if there was a button state transition, we will need to logically obtain 2 gameinputs. One for the new state and one for the old state (previous frame).

This way, whenever the frame ends, we will do a swap to identify whether the previous button was pressed and is now released and vice versa.

The controller

Below is the struct with the buttons, each button is a ButtonState.

Our controller is made up of 12 buttons based on an Xbox controller. The control will have digital buttons + 2 sticks for the X and Y axis, as well as information on whether it accepts the analogue part and whether it is connected.

struct GameController
{
    bool isAnalog;
    bool isConnected;

    float leftTrigger;
    float rightTrigger;

    float stickAverageX;
    float stickAverageY;

    // TODO: Add Right sticky

    union
    {
        ButtonState buttons[12];
        struct 
        {
            ButtonState btnUp;
            ButtonState btnRight;
            ButtonState btnDown;
            ButtonState btnLeft;

            ButtonState dpadUp;
            ButtonState dpadRight;
            ButtonState dpadDown;
            ButtonState dpadLeft;

            ButtonState leftShoulder;
            ButtonState rightShoulder;

            ButtonState start;
            ButtonState back;
        };
    };
};

Unions in C

Within our control, we define a union. A union is nothing more than informing that the variables inside are pointing to the same memory address. That is, a union:

union
{
    int x;
    float y;
}

This means that we have both x and y pointing to the same address, only in this case one is a float-point and the other is not.

The same happens in our control example. Both the array buttons and the anonymous struct GameState are pointing to the same address.

We do this so that when we need a loop to access all the buttons, we will have an array ready for it. While when we need to individually access each button, we will also be able to do so.

Another point is that we are using an anonymous union. This means that we can access btnUp any other struct value as if it were a property GameController, let's see:

my_controller.buttons;
my_controller.btnUp;

The Input

Now, we will have a last struct that will have an array of controls for us to use in the game and to swap the states in each frame.

struct GameInput {
    GameController controllers[4];
};

In the future, we'll use five controllers, which means the first controller is the keyboard.

Processing Digital Buttons

To process each of the buttons coming from the GamePad we must:

  1. Get the current state of the button (pressed) from XInput
  2. Swap the old control state with the new control state and mark whether there was a transition.
static void
win32ProcessGamepad(WORD buttonState, ButtonState *newState, ButtonState *oldState,
                    DWORD buttonFlag)
{
    bool endedDown = (buttonState & buttonFlag) == buttonFlag;
    newState->endedDown = endedDown;
    newState->transitionCount = oldState->endedDown != endedDown;
}

Now let's allocate 2 game inputs for the state transition. Each input allows for 4 controls.

Outside main loop:

GameInput inputs[2] = {};
GameInput *newInput = &inputs[0];
GameInput *oldInput = &inputs[1];

Inside main loop:

u32 maxController = XUSER_MAX_COUNT;
if (maxController > arrayCount(newInput->controllers)) {
    maxController = arrayCount(newInput->controllers);
}

Retrieve the max controllers available and set up as maximum controller if needed.

The next code is a macro that compute the size of array.

#define arrayCount(array) sizeof(array) / sizeof((array)[0])

Now, inside the for loop:

for(DWORD i = 0; i < maxController; ++i) {
    GameController *newController = &newInput->controllers[i];
    GameController *oldController = &oldInput->controllers[i];

    // store the gamepad button into controller struct.
    win32ProcessGamepad(gamepad.wButtons,
                    &newController->btnDown,
                    &oldController->btnDown, XINPUT_GAMEPAD_A);

    // same with sticky
    newController->stickAverageX =
        win32ProcessGameStick(gamepad.sThumbLX);
}

The next block of code has a Deadzone situation for gamepad. This is required to "calibrate" the user input.

inline static
float win32ProcessGameStick(short value)
{
    float result = 0;
    if (value < -XINPUT_GAMEPAD_LEFT_THUMB_DEADZONE) {
        result = (float) ((value + XINPUT_GAMEPAD_LEFT_THUMB_DEADZONE)
                         / (32768.0f - XINPUT_GAMEPAD_LEFT_THUMB_DEADZONE));
    } else if (value > XINPUT_GAMEPAD_LEFT_THUMB_DEADZONE) {
        result = (float) ((value - XINPUT_GAMEPAD_LEFT_THUMB_DEADZONE)
                          / 32767.0f - XINPUT_GAMEPAD_LEFT_THUMB_DEADZONE);
    }
    return result;
}

Now, at the end of main loop, let's to swap the 2 game input. With this approarch, we can persist the previous state and identify if the user keep the button hold or not.

// end of main loop
GameInput *temp = newInput;
newInput = oldInput;
oldInput = temp;

Send this information to the game update at non-specific platform layer.

gameUpdateAndRender(newInput, &gameBuffer, &soundBuffer);

And handle the buttons or stickies.

static void
gameUpdateAndRender(GameInput *input, GameBuffer *buffer, SoundBuffer *soundBuffer)
{
    GameController *controller0 = &input->controllers[0];

    static int toneHz = 256;
    static int xo = 0;
    static int yo = 0;

    if (controller0->isAnalog) {
        // analog
    } else {
        // digital
    }

    if (controller0->btnDown.endedDown) {
        yo += 1; // pressed a button
    }

    renderWeirdGradient(buffer, xo, yo);
    gameUpdateSound(soundBuffer, toneHz);
}