Chapter 8

Input Systems

This chapter takes an in-depth look at a wide variety of input devices for games, including the keyboard, mouse, and controller. It explores how to integrate these devices into a cohesive system that all actors and components in the game can interact with for their input needs.

Input Devices

Without input, games would be a static form of entertainment, much like film or television. The fact that a game responds to the keyboard, mouse, controller, or another input device is what enables interactivity. You query these input devices for their current state during the “process input” phase of the game loop, and this affects the game world during the “update game world” phase of the game loop.

Some input devices yield only Boolean values. For example, for the keyboard you can check the state of each key, and this state is true or false, depending on whether the key is down or up. There’s no way for us to discern whether a key is “half pressed” because the input device simply doesn’t detect this.

Other input devices give a range of values. For example, most joysticks yield a range of values in two axes that you can use to determine how far the user has moved the joystick in a specific direction.

Many of the devices used in games are composite, meaning they combine multiple types of inputs into one. For example, a typical controller might have two joysticks and triggers that yield a range of values, as well as other buttons that yield only Boolean values. Similarly, the movement of the mouse or scroll wheel might be some range, but the mouse buttons may be Boolean.

Polling

Earlier in this book, you used the SDL_GetKeyboardState function to get the Boolean state of every key on the keyboard. With the additions in Chapter 3, “Vectors and Basic Physics,” you then passed this keyboard state to every actor’s ProcessInput function, which in turn passes it to every component’s ProcessInput function. Then, in these functions you can query the state of a specific key to decide whether to perform an action, such as moving the player character forward when pressing the W key. Because you’re checking the value of a specific key on every frame, this approach is considered polling the state of the key.

Input systems designed around polling are conceptually simple to understand, and for this reason many game developers prefer to use a polling approach. It works especially well for things like character movement because you need to know the state of some input device on every frame and update the character movement based on that. And, in fact, you will stick to this basic polling approach for most of the input needs in the code for this book.

Positive and Negative Edges

Consider a game where pressing the spacebar causes a character to jump. On every frame, you check the state of the spacebar. Suppose the spacebar is up for the first three frames, and then the player presses the spacebar prior to frame 4. The player continues to hold the spacebar down until prior to frame 6 and then releases it. You can draw this as a graph, as in Figure 8.1, where the x-axis corresponds to the time at each frame and the y-axis corresponds to the binary value for that frame. On frame 4, the spacebar changes from 0 to 1, and on frame 6, the spacebar changes back from 1 to 0. The frame where the input changes from 0 to 1 is a positive edge (or rising edge), and the frame where the input changes from 1 to 0 is a negative edge (or falling edge).

Graph shows the spacebar polled over nine frames.

Figure 8.1 Graph of the spacebar polled over nine frames

Now consider what would happen if the process input for the character simply said the following (in pseudocode):

if (spacebar == 1)
   character.jump()

For the sample input in Figure 8.1, this code would call the character.jump() function twice: once on frame 4 and once on frame 5. And if the player held the button for 10 frames instead of 2, then you’d call character.jump() 10 times. Clearly, you don’t want the character to jump every frame when the spacebar value is 1. Instead, you should only call character.jump() on the frame where the spacebar has a positive edge. For the input graph in Figure 8.1, this is on frame 4. This way, for every press of the spacebar, regardless of how long the player holds the spacebar, the character jumps only once. In this case, you want pseudocode like this:

if (spacebar has positive edge)
   character.jump()

The “has positive edge” term in the pseudocode means that on the last frame the key was 0, and on this frame the key is 1. But with the current method of using SDL_GeyKeyboardState to get the state of the keyboard on the current frame, it might not be apparent how to implement this. If you add a variable called spacebarLast that you initialize to 0, you can use this variable to track the value in the last frame. Then you initiate the jump only if the value in the last frame is 0 and the value in this frame is 1:

if (spacebar == 1 and spacebarLast == 0)
   character.jump()

spacebarLast = spacebar

Consider what happens in the case of the example in Figure 8.1. On frame 3, you set spacebarLast to the current value of spacebar, or 0. Then, on frame 4, spacebar is 1 while spacebarLast is 0, so you trigger character.jump(). After this, spacebarLast becomes the current value of spacebar, or 1. Then on frame 5, both spacebar and spacebarLast are 1, so the character doesn’t jump.

You could use this pattern throughout the code. However, it would be nice to have a system that tracks values of keys on the previous frame automatically. That way, you could easily ask the system whether a key has a positive edge or negative edge, which might reduce the burden for other programmers on the team.

If you generalize the approach of storing the value of the input last frame and comparing it to the value this frame, there are four possible results, as shown in Table 8.1. If both values are 0,  the button state is None. Similarly, if both values are 1, this means the player holds the key for consecutive frames, or the button state is Held. Finally, if the values are different, it’s either a positive edge or negative edge, which you denote with the button states Pressed and Released, respectively.

Table 8.1 Four Possible Input States, Given the Value in the Last Frame and in the Current Frame

Last Frame

Current Frame

Button State

0

0

None

0

1

Pressed

1

0

Released

1

1

Held

Consider how you might use this for a game where the player can hold a key to charge up an attack. On the frame on which you detect the Pressed state of the key, you begin charging the attack. Then as long as the key’s state on subsequent frames remains Held, you continue to charge the attack. Finally, when the key’s state becomes Released, it means the player let go of the key, and you can now execute the attack with the appropriate charge level.

But for actions such as just moving forward if W is 1, you’d rather just use the old approach, where you check the value of the input on that frame. In this chapter’s input system, you will give the option of either querying this basic value or querying for the different input states.

Events

Recall from Chapter 1, “Game Programming Overview,” that SDL generates different events that the program can optionally respond to. Currently, you respond to the SDL_Quit event, which occurs when the player tries to close the window. Game::ProcessInput checks every frame if there are events in the queue and can selectively choose to respond to them.

SDL also generates events for input devices. For example, every time the player presses a key on the keyboard, SDL generates an SDL_KEYDOWN event (corresponding to the Pressed button state). Conversely, every time the player releases a key, it generates an SDL_KEYUP event (corresponding to the Released state). If you only care about positive and negative edges, then this is a very quick way to set up code to respond to these actions.

However, for the case of pressing W to move forward, this means you need extra code to track whether W is held because you only get the negative and positive edges from the SDL events. Although you can certainly design an input system entirely based around events, this chapter uses SDL events only when required (such as for mouse wheel scrolling).

There is one subtle relationship between SDL events and the various polling functions. The keyboard state you get from SDL_GetKeyboardState updates only after calling SDL_PollEvents in the message pump loop. This means you can delineate when the state data changes between frames because you know where the code calls SDL_PollEvents. This comes in handy when implementing an input system that saves the data for the previous frame.

Basic InputSystem Architecture

Before diving into each of the different input devices, let’s consider a structure for an input system. Currently you let actors and components know about the current keyboard state via ProcessInput. However, this mechanism means that ProcessInput currently cannot access the mouse or controller without directly calling SDL functions. While this works for a simple game (and it’s the approach largely used outside this chapter), it’s better if the programmers writing the code for actors and components do not need much specific knowledge of SDL functions. Furthermore, some SDL input functions return the difference in state between calls of the function. If you call those functions more than once during one frame, you’ll get values of zero after the first call.

To solve this problem, you can have the InputSystem class populate data in a helper class called InputState. You can then pass this InputState by const reference to actors/components via their ProcessInput function. You can also add several helper functions to InputState to make it easy to query whatever state the actor/component cares about.

Listing 8.1 shows the initial declaration of the relevant pieces. First, declare a ButtonState enum to correspond to the four different states outlined in Table 8.1. Next, declare an InputState struct (which currently has no members). Finally, you declare InputSystem,  which contains Initialize/Shutdown functions (much like Game). It also has a PrepareForUpdate function that is called before SDL_PollEvents, and then an Update function that is called after polling events. The GetState function returns a const reference to the InputState it holds as member data.

Listing 8.1 Basic InputSystem Declarations


enum ButtonState
{
   ENone,
   EPressed,
   EReleased,
   EHeld
};

// Wrapper that contains current state of input
struct InputState
{
   KeyboardState Keyboard;
};

class InputSystem
{
public:
   bool Initialize();
   void Shutdown();

   // Called right before SDL_PollEvents loop
   void PrepareForUpdate();
   // Called right after SDL_PollEvents loop
   void Update();

   const InputState& GetState() const { return mState; }
private:
   InputState mState;
};


To integrate this code into the game, you add an InputSystem pointer to the member data of Game called mInputSystem. Game::Initialize allocates and initializes InputSystem and Game::Shutdown shuts down and deletes it.

Next, you change the declaration of ProcessInput in both Actor and Component to the following:

void ProcessInput(const InputState& state);

Recall that in Actor, ProcessInput is not overridable because it calls ProcessInput on all the attached components. However, actors also have an overridable ActorInput function for any input specific to that actor. So, you similarly change the declaration of ActorInput to take in a constant InputState reference.

Finally, the implementation of Game::ProcessInput changes to the following outline of steps:

void Game::ProcessInput()
{
   mInputSystem->PrepareForUpdate();

   // SDL_PollEvent loop...

   mInputSystem->Update();
   const InputState& state = mInputSystem->GetState();

   // Process any keys here as desired...

   // Send state to all actor's ProcessInput...
}

With the InputSystem in place, you now have the basics needed to add support for several input devices. For each of these devices, you need to add a new class to encapsulate the state and add an instance of this class to the InputState struct.

Keyboard Input

Recall that  the SDL_GetKeyboardState function returns a pointer to the keyboard state. Notably, the return value of SDL_GetKeyboardState does not change throughout the lifetime of the application, as it points to internal SDL data. Therefore, to track the current state of the keyboard, you merely need a single pointer that you initialize once. However, because SDL overwrites the current keyboard state when you call SDL_PollEvents, you need a separate array to save the previous frame state.

This leads naturally to the member data in the declaration of KeyboardState, shown in Listing 8.2. You have a pointer that points to the current state and an array for the previous state. The size of the array corresponds to the size of the buffer that SDL uses for keyboard scan codes. For the member functions of KeyboardState, you provide both a method to get the basic current value of a key (GetKeyValue) and one that returns one of the four button states (GetKeyState). Finally, you make InputSystem a friend of KeyboardState. This makes it easy for InputSystem to directly manipulate the member data of KeyboardState.

Listing 8.2 KeyboardState Declaration


class KeyboardState
{
public:
   // Friend so InputSystem can easily update it
   friend class InputSystem;

   // Get just the boolean true/false value of key
   bool GetKeyValue(SDL_Scancod keyCode) const;

   // Get a state based on current and previous frame
   ButtonState GetKeyState(SDL_Scancode keyCode) const;
private:
   // Current state
   const Uint8* mCurrState;
   // State previous frame
   Uint8 mPrevState[SDL_NUM_SCANCODES];
};


Next, you add a KeyboardState instance called Keyboard to the member data of InputState:

struct InputState
{
   KeyboardState Keyboard;
};

Next, you need to add code to both Initialize and PrepareForUpdate within InputSystem. In Initialize, you need to first set the mCurrState pointer and then also zero out the memory of mPrevState (because before the game starts, the keys have no previous state). You get the current state pointer from SDL_GetKeyboardState, and you can clear the memory with memset:

// (In InputSystem::Initialize...)
// Assign current state pointer
mState.Keyboard.mCurrState = SDL_GetKeyboardState(NULL);
// Clear previous state memory
memset(mState.Keyboard.mPrevState, 0,
   SDL_NUM_SCANCODES);

Then in PrepareForUpdate, you need to copy all the “current” data to the previous buffer. Remember that when you call PrepareForUpdate, the “current” data is stale from the previous frame. This is because you call PrepareForUpdate when you’re on a new frame but haven’t called SDL_PollEvents yet. This is critical because SDL_PollEvents is what updates the internal SDL keyboard state data (which you’re pointing to with mCurrState). So, before SDL overwrites the current state, use memcpy to copy from the current buffer to the previous buffer:

// (In InputSystem::PrepareForUpdate...)
memcpy(mState.Keyboard.mPrevState,
   mState.Keyboard.mCurrState,
   SDL_NUM_SCANCODES);

Next, you need to implement the member functions in KeyboardState. GetKeyValue is straightforward. It simply indexes into the mCurrState buffer and returns true if the value is 1 and false if the value is 0.

The GetKeyState function, shown in Listing 8.3, is slightly more complex. It uses both the current frame’s and previous frame’s key state to determine which of the four button states to return. This simply maps the entries in Table 8.1 into source code.

Listing 8.3 KeyboardState::GetKeyState Implementation


ButtonState KeyboardState::GetKeyState(SDL_Scancode keyCode) const
{
   if (mPrevState[keyCode] == 0)
   {
      if (mCurrState[keyCode] == 0)
      { return ENone; }
      else
      { return EPressed; }
   }
   else // Prev state must be 1
   {
      if (mCurrState[keyCode] == 0)
      { return EReleased; }
      else
      { return EHeld; }
   }
}


With this KeyboardState code, you can still access the value of a key with the GetKeyValue function. For example, the following checks if the current value of the spacebar is true:

if (state.Keyboard.GetKeyValue(SDL_SCANCODE_SPACE))

However, the advantage of the InputState object is that you can also query the button state of a key. For example, the following code in Game::ProcessInput detects if the button state of the Escape key is EReleased, and it exits only at that point:

if (state.Keyboard.GetKeyState(SDL_SCANCODE_ESCAPE)
   == EReleased)
{
   mIsRunning = false;
}

This means that initially pressing Escape does not immediately quit the game, but releasing the key causes the game to quit.

Mouse Input

For mouse input, there are three main types of input to focus on: button input, movement of the mouse, and movement of the scroll wheel. The button input code is like the keyboard code except that the number of buttons is significantly smaller. The movement input is a little more complex because there are two modes of input (absolute and relative). Ultimately, you can still poll the mouse input with a single function call per frame. However, for the scroll wheel, SDL only reports the data via an event, so you must add some code to InputSystem to also process certain SDL events.

By default, SDL shows the system’s mouse cursor (at least on platforms that have a system mouse cursor). However, you can enable or disable the cursor by using SDL_ShowCursor, passing in SDL_TRUE to enable it and SDL_FALSE to disable it. For example, this disables the cursor:

SDL_ShowCursor(SDL_FALSE);

Buttons and Position

For querying both the position of the mouse and the state of its buttons, you use a single call to SDL_GetMouseState. The return value of this function is a bitmask of the button state, and you pass in two integers by address to get the x/y coordinates of the mouse, like this:

int x = 0, y = 0;
Uint32 buttons = SDL_GetMouseState(&x, &y);

note

For the position of the mouse, SDL uses the SDL 2D coordinate system. This means that the top-left corner is (0, 0), positive x is to the right, and positive y is down. However, you can easily convert these coordinates to whichever other system you prefer.

For example, to convert to the simple view-projection coordinate system from Chapter 5, “OpenGL,” you can use the following two lines of code:

x = x - screenWidth/2;
y = screenHeight/2 - y;

Because the return value of SDL_GetMouseState is a bitmask, you need to use a bitwise-AND along with the correct bit value to find out if a specific button is up or down. For example, given the buttons variable populated from SDL_GetMouseState, the following statement is true if the left mouse button is down:

bool leftIsDown = (buttons & SDL_BUTTON(SDL_BUTTON_LEFT)) == 1;

The SDL_BUTTON macro shifts a bit based on the requested button, and the bitwise-AND returns 1 if the button is down and 0 if it’s up. Table 8.2 shows the button constants corresponding to the five different mouse buttons that SDL supports.

Table 8.2 SDL Mouse Button Constants

Button

Constant

Left

SDL_BUTTON_LEFT

Right

SDL_BUTTON_RIGHT

Middle

SDL_BUTTON_MIDDLE

Mouse button 4

SDL_BUTTON_X1

Mouse button 5

SDL_BUTTON_X2

You now have enough knowledge to create the initial declaration of MouseState, which is shown in Listing 8.4. You save a 32-bit unsigned integer for both the previous and current buttons’ bitmasks and a Vector2 for the current mouse position. Listing 8.4 omits the implementations of the button functions because they are almost identical to the functions for the keyboard keys. The only difference is that these functions use the bitmask as outlined earlier.

Listing 8.4 Initial MouseState Declaration


class MouseState
{
public:
   friend class InputSystem;

   // For mouse position
   const Vector2& GetPosition() const { return mMousePos; }

   // For buttons
   bool GetButtonValue(int button) const;
   ButtonState GetButtonState(int button) const;
private:
   // Store mouse position
   Vector2 mMousePos;
   // Store button data
   Uint32 mCurrButtons;
   Uint32 mPrevButtons;
};


Next, you add a MouseState instance called Mouse to InputState. Then, in InputSystem,  add the following to PrepareForUpdate, which copies the current button state to the previous state:

mState.Mouse.mPrevButtons = mState.Mouse.mCurrButtons;

In Update, you call SDL_GetMouseState to update all the MouseState members:

int x = 0, y = 0;
mState.Mouse.mCurrButtons = SDL_GetMouseState(&x, &y);
mState.Mouse.mMousePos.x = static_cast<float>(x);
mState.Mouse.mMousePos.y = static_cast<float>(y);

With these changes, you can now access basic mouse information from InputState. For example, to determine if the left mouse button is in state EPressed, you use the following:

if (state.Mouse.GetButtonState(SDL_BUTTON_LEFT) == EPressed)

Relative Motion

SDL supports two different modes for detecting mouse movement. In the default mode, SDL reports the current coordinates of the mouse. However, sometimes you instead want to know the relative change of the mouse between frames. For example, in many first-person games on PC, you can use the mouse to rotate the camera. The speed of the camera’s rotation depends on how fast the player moves the mouse. In this case, exact coordinates of the mouse aren’t useful, but the relative movement between frames is.

You could approximate the relative movement between frames by saving the position of the mouse on the previous frame. However, SDL supports a relative mouse mode that instead reports the relative movement between calls to the SDL_GetRelativeMouseState function. The big advantage of SDL’s relative mouse mode is that it hides the mouse, locks the mouse to the window, and centers the mouse on every frame. This way, it’s not possible for the player to accidentally move the mouse cursor out of the window.

To enable relative mouse mode, call the following:

SDL_SetRelativeMouseMode(SDL_TRUE);

Similarly, to disable relative mouse mode, pass in SDL_FALSE as the parameter.

Once you’ve enabled relative mouse mode, instead of using SDL_GetMouseState, you use SDL_GetRelativeMouseState.

To support this in InputSystem, you first add a function that can enable or disable relative mouse mode:

void InputSystem::SetRelativeMouseMode(bool value)
{
   SDL_bool set = value ? SDL_TRUE : SDL_FALSE;
   SDL_SetRelativeMouseMode(set);

   mState.Mouse.mIsRelative = value;
}

You save the state of the relative mouse mode in a Boolean variable in MouseState that you initialize to false.

Next, change the code in InputSystem::Update so that if you’re in relative mouse mode, you use the correct function to grab the position and buttons of the mouse:

int x = 0, y = 0;
if (mState.Mouse.mIsRelative)
{
   mState.Mouse.mCurrButtons = SDL_GetRelativeMouseState(&x, &y);
}
else
{
   mState.Mouse.mCurrButtons = SDL_GetMouseState(&x, &y);
}
mState.Mouse.mMousePos.x = static_cast<float>(x);
mState.Mouse.mMousePos.y = static_cast<float>(y);

With this code, you can now enable relative mouse mode and access the relative mouse position via MouseState.

Scroll Wheel

For the scroll wheel, SDL does not provide a function to poll the current state of the wheel. Instead, SDL generates the SDL_MOUSEWHEEL event. To support this in the input system, then, you must first add support for passing SDL events to InputSystem. You can do this via a ProcessEvent function, and then you update the event polling loop in Game::ProcessInput to pass the mouse wheel event to the input system:

SDL_Event event;
while (SDL_PollEvent(&event))
{
   switch (event.type)
   {
      case SDL_MOUSEWHEEL:
         mInputSystem->ProcessEvent(event);
         break;
      // Other cases omitted ...
   }
}

Next, in MouseState add the following member variable:

Vector2 mScrollWheel;

You use a Vector2 object because SDL reports scrolling in both the vertical and horizontal directions, as many mouse wheels support scrolling in both directions.

You then need to make changes to InputSystem. First, implement ProcessEvent to read in the scroll wheel’s x/y values from the event.wheel struct, as in Listing 8.5.

Listing 8.5 InputSystem::ProcessEvent Implementation for the Scroll Wheel


void InputSystem::ProcessEvent(SDL_Event& event)
{
   switch (event.type)
   {
   case SDL_MOUSEWHEEL:
      mState.Mouse.mScrollWheel = Vector2(
         static_cast<float>(event.wheel.x),
         static_cast<float>(event.wheel.y));
      break;
   default:
      break;
   }
}


Next, because the mouse wheel event only triggers on frames where the scroll wheel moves, you need to make sure to reset the mScrollWheel variable during PrepareForUpdate:

mState.Mouse.mScrollWheel = Vector2::Zero;

This ensures that if the scroll wheel moves on frame 1 but doesn’t move on frame 2, you don’t erroneously report the same scroll value on frame 2.

With this code, you can now access the scroll wheel state every frame with the following:

Vector2 scroll = state.Mouse.GetScrollWheel();

Controller Input

For numerous reasons, detecting controller input in SDL is more complex than for the keyboard and mouse. First, a controller has a much greater variety of sensors than a keyboard or mouse. For example, a standard Microsoft Xbox controller has two analog joysticks, a directional pad, four standard face buttons, three special face buttons, two bumper buttons, and two triggers—which is a lot of different sensors to get data from.

Furthermore, while PC/Mac users have only a single keyboard or mouse, it’s possible to have multiple controllers connected. Finally, controllers support hot swapping, which means it’s possible to plug and unplug controllers while a program is running. Combined, these elements add complexity to handling controller input.

note

Depending on the controller and your platform, you may need to first install a driver for your controller in order for SDL to detect it.

Before you can use a controller, you must first initialize the SDL subsystem that handles controllers. To enable it, simply add the SDL_INIT_GAMECONTROLLER flag to the SDL_Init call in Game::Initialize:

SDL_Init(SDL_INIT_VIDEO | SDL_INIT_AUDIO | SDL_INIT_GAMECONTROLLER);

Enabling a Single Controller

For now, assume that you’re using only a single controller and that this controller is plugged in when the game starts. To initialize the controller, you need to use the SDL_GameControllerOpen function. This function returns a pointer to an SDL_Controller struct upon successful initialization or nullptr if it fails. You can then use the SDL_Controller* variable to query the state of the controller.

For this single controller, you first add an SDL_Controller* pointer called mController to the InputState member data. Then, add the following call to open controller 0:

mController = SDL_GameControllerOpen(0);

To disable a controller, you can call SDL_GameControllerClose, which takes the SDL_GameController pointer as its parameter.

tip

By default, SDL supports a handful of common controllers, such as the Microsoft Xbox controller. You can find controller mappings that specify the button layouts of many other controllers. The SDL_GameControllerAddMappingsFromFile function can load controller mappings from a supplied file. A community-maintained mapping file is available on GitHub at https://github.com/gabomdq/SDL_GameControllerDB.

Because you do not want to assume that the player has a controller, you must be vigilant to null check mController wherever you might want to access it in code.

Buttons

Game controllers in SDL support many different buttons. SDL uses a naming convention that mirrors the button names of a Microsoft Xbox controller. For example, the names of the face buttons are A, B, X, and Y. Table 8.3 lists the different button constants defined by SDL, where * is a wildcard that denotes multiple possible values.

Table 8.3 SDL Controller Button Constants

Button

Constant

A, B, X, or Y

SDL_CONTROLLER_BUTTON_* (replace * with A, B, X, or Y)

Back

SDL_CONTROLLER_BACK

Start

SDL_CONTROLLER_START

Pressing left/right stick

SDL_CONTROLLER_BUTTON_*STICK (replace * with LEFT or RIGHT)

Left/right shoulder

SDL_CONTROLLER_BUTTON_*SHOULDER (replace * with LEFT or RIGHT)

Directional pad

SDL_CONTROLLER_BUTTON_DPAD_* (replace * with UP, DOWN, LEFT, or RIGHT)

Note that the left and right stick buttons are for when the user physically presses in the left/right stick. Some games use pressing in the right stick for sprinting, for example.

SDL does not have a mechanism to query the state of all controller buttons simultaneously. Instead, you must individually query each button via the SDL_GameControllerGetButton function.

However, you can take advantage of the fact that the enum for the controller button names defines an SDL_CONTROLLER_BUTTON_MAX member that is the number of buttons the controller has. Thus, the first pass of the ControllerState class, shown in Listing 8.6, contains arrays for both the current and previous button states. The code also has a Boolean so the game code can determine whether there’s a controller connected. Finally, the class has declarations for the now-standard button value/state functions.

Listing 8.6 Initial ControllerState Declaration


class ControllerState
{
public:
   friend class InputSystem;

   // For buttons
   bool GetButtonValue(SDL_GameControllerButton button) const;
   ButtonState GetButtonState(SDL_GameControllerButton button)
      const;

   bool GetIsConnected() const { return mIsConnected; }
private:
   // Current/previous buttons
   Uint8 mCurrButtons[SDL_CONTROLLER_BUTTON_MAX];
   Uint8 mPrevButtons[SDL_CONTROLLER_BUTTON_MAX];
   // Is this controlled connected?
   bool mIsConnected;
};


Then add an instance of ControllerState to InputState:

ControllerState Controller;

Next, back in InputSystem::Initialize, after you try to open controller 0, set the mIsConnected variable based on whether the mController pointer is non-null. You also clear out the memory for both mCurrButtons and mPrevButtons:

mState.Controller.mIsConnected = (mController != nullptr);
memset(mState.Controller.mCurrButtons, 0,
   SDL_CONTROLLER_BUTTON_MAX);
memset(mState.Controller.mPrevButtons, 0,
   SDL_CONTROLLER_BUTTON_MAX);

As with the keyboard, the code in PrepareForUpdate then copies the button states from current to previous:

memcpy(mState.Controller.mPrevButtons,
   mState.Controller.mCurrButtons,
   SDL_CONTROLLER_BUTTON_MAX);

Finally, in Update, loop over the mCurrButtons array and set the value of each element to the result of the SDL_GameControllerGetButton call that queries the state of that button:

for (int i = 0; i < SDL_CONTROLLER_BUTTON_MAX; i++)
{
   mState.Controller.mCurrButtons[i] =
      SDL_GameControllerGetButton(mController,
         SDL_GameControllerButton(i));
}

With this code, you can then query the state of a specific game controller button, using a pattern like the keyboard and mouse buttons. For example, this code checks if the A button on the controller has a positive edge this frame:

if (state.Controller.GetButtonState(SDL_CONTROLLER_BUTTON_A) == EPressed)

Analog Sticks and Triggers

SDL supports a total of six axes. Each analog stick has two axes: one in the x direction and one in the y direction. Furthermore, each of the triggers has a single axis. Table 8.4 shows the list of axes. (Once again, * denotes a wildcard.)

Table 8.4 SDL Controller Axis Constants

Button

Constant

Left analog stick

SDL_CONTROLLER_AXIS_LEFT* (replace * with X or Y)

Right analog stick

SDL_CONTROLLER_AXIS_RIGHT* (replace * with X or Y)

Left/right triggers

SDL_CONTROLLER_AXIS_TRIGGER* (replace * with LEFT or RIGHT)

For triggers, the value ranges from 0 to 32,767, with 0 meaning there is no pressure on the trigger. For the analog stick axes, the value ranges from -32,768 to 32,767, with 0 representing centered. A positive y-axis value corresponds to down on the analog stick, and a positive x-axis value corresponds to right.

However, an issue with continuous input such as these axes is that the ranges specified by the API are theoretical. Each individual device has its own imprecisions. You can observe this behavior by releasing one of the analog sticks, which returns the stick to its center. You might reasonably expect that because the stick is at rest, the values reported for the stick’s x- and y-axes are zero. However, in practice the values will be around zero but rarely precisely zero. Conversely, if the player slams the stick all the way to the right, the value reported by the stick’s x-axis will be near the maximum value but rarely precisely the maximum value.

This is problematic for games for two reasons. First, it may cause phantom inputs, where the player isn’t touching an input axis but the game reports that something is happening. For example, suppose the player completely puts the controller down on a table. The player should rightfully expect that his or her character in game will not move around. However, if the issue isn’t handled, the game will detect some value of input to the axis and move the character.

Furthermore, many games have the character move based on how far the analog stick is moved in one direction—so that slightly moving the stick might cause the character to slowly walk, whereas moving the stick all the way in a direction might cause the character to sprint. However, if you only make the player sprint when the axis reports the maximum value, the player will never sprint.

To solve this issue, code that processes the input from an axis should filter the value. Specifically, you want to interpret values close to zero as zero and values close to the minimum or maximum as the minimum or maximum. Furthermore, it’s convenient for users of the input system if you convert the integral ranges into a normalized floating-point range. For the axes that yield both positive and negative values, this means a range between −1.0 and 1.0.

Figure 8.2 shows an example of such a filter for a single axis. The numbers above the line are the integral values before filtering, and the numbers below the line are the floating-point values after filtering. The area near zero that you interpreted as 0.0 is called a dead zone.

Figure shows a sample filter for an axis with the input values above and the output values below.

Figure 8.2 A sample filter for an axis, with the input values above and the output values below

Listing 8.7 shows the implementation the InputSystem::Filter1D function, which the input system uses to filter one-dimensional axes such as the triggers. First, you declare two constants for the dead zone and maximum value. Note that deadZone here is 250—which is less than in Figure 8.2 because this value works better for the triggers (but you could make the constants parameters or user configurable, if desired).

Next, the code takes the absolute value of the input by using a ternary operator. If this value is less than the dead zone constant, you simply return 0.0f. Otherwise, you convert the input to a fractional value representing where it lands in between the dead zone and the maximum value. For example, an input halfway between deadZone and maxValue is 0.5f.

Then you ensure that the sign of this fractional value matches the sign of the original input. Finally, you clamp the value to the range of -1.0 to 1.0 to account for the cases where the input is greater than the maximum value constant. The implementation Math::Clamp is in the custom Math.h header file.

Listing 8.7 Filter1D Implementation


float InputSystem::Filter1D(int input)
{
   // A value < dead zone is interpreted as 0%
   const int deadZone = 250;
   // A value > max value is interpreted as 100%
   const int maxValue = 30000;

   float retVal = 0.0f;

   // Take absolute value of input
   int absValue = input > 0 ? input : -input;
   // Ignore input within dead zone
   if (absValue > deadZone)
   {
      // Compute fractional value between dead zone and max value
      retVal = static_cast<float>(absValue - deadZone) /
         (maxValue - deadZone);

      // Make sure sign matches original value
      retVal = input > 0 ? retVal : -1.0f * retVal;
  
      // Clamp between -1.0f and 1.0f
      retVal = Math::Clamp(retVal, -1.0f, 1.0f);
   }

   return retVal;
}


Using the Filter1D function, an input value of 5000 returns 0.0f, and a value of -19000 returns -0.5f.

The Filter1D function works well when you only need a single axis, such as for one of the triggers. However, because the analog sticks really are two different axes in concert, it’s usually preferable to instead filter them in two dimensions, as discussed in the next section.

For now, you can add two floats to ControllerState for the left and right triggers:

float mLeftTrigger;
float mRightTrigger;

Next, in InputSystem::Update use the SDL_GameControllerGetAxis function to read in the values of both triggers and call the Filter1D function on this value to convert it to a range of 0.0 to 1.0 (because triggers cannot be negative). For example, the following sets the mLeftTrigger member:

mState.Controller.mLeftTrigger =
   Filter1D(SDL_GameControllerGetAxis(mController,
      SDL_CONTROLLER_AXIS_TRIGGERLEFT));

You then add GetLeftTrigger() and GetRightTrigger() functions to access these. For example, the following code gets the value of the left trigger:

float left = state.Controller.GetLeftTrigger();

Filtering Analog Sticks in Two Dimensions

A common control scheme for an analog stick is that the orientation of the stick corresponds to the direction in which the player’s character moves. For example, pressing the stick up and to the left would cause the character onscreen to also move in that direction. To implement this, you should interpret the x- and y-axes together.

Although it is tempting to apply the Filter1D function to the x- and y-axes independently, doing so can cause an interesting issue. If the player moves the stick all the way up, interpreting it as a normalized vector yields <0.0, 1.0>. On the other hand, if the player moves the stick all the way up and to the right, the normalized vector is <1.0, 1.0>. The length of these two vectors is different, which is a problem if you use the length to dictate the speed at which the character moves: The character could move faster diagonally than straight in one direction!

Although you could just normalize vectors with a length greater than one, interpreting each axis independently still ultimately means you’re interpreting the dead zone and maximum values as a square. A better approach is to interpret them as concentric circles, as shown in Figure 8.3. The square border represents the raw input values, the inner circle represents the dead zone, and the outer circle represents the maximum values.

Figure shows a circle labeled Dead Zone enclosed within a circle that is enclosed within a square.

Figure 8.3 Filtering in two dimensions

Listing 8.8 gives the code for Filter2D, which takes in both the x- and y-axes for the analog stick and filters in two dimensions. You first create a 2D vector and then determine the length of that vector. Lengths less than the dead zone result in Vector2::Zero. For lengths greater than the dead zone, you determine the fractional value between the dead zone and max and set the length of the vector to this fractional value.

Listing 8.8 InputSystem::Filter2D Implementation


Vector2 InputSystem::Filter2D(int inputX, int inputY)
{
   const float deadZone = 8000.0f;
   const float maxValue = 30000.0f;

   // Make into 2D vector
   Vector2 dir;
   dir.x = static_cast<float>(inputX);
   dir.y = static_cast<float>(inputY);

   float length = dir.Length();

   // If length < deadZone, should be no input
   if (length < deadZone)
   {
      dir = Vector2::Zero;
   }
   else
   {
      // Calculate fractional value between
      // dead zone and max value circles
      float f = (length - deadZone) / (maxValue - deadZone);
      // Clamp f between 0.0f and 1.0f
      f = Math::Clamp(f, 0.0f, 1.0f);
      // Normalize the vector, and then scale it to the
      // fractional value
      dir *= f / length;
   }

   return dir;
}


Next, add two Vector2s to ControllerState for the left and right sticks, respectively. You can then add code in InputSystem::Update to grab the values of the two axes for each stick and then run Filter2D to get the final analog stick value. For example, the following code filters the left stick and saves the result in the controller state:

x = SDL_GameControllerGetAxis(mController,
   SDL_CONTROLLER_AXIS_LEFTX);
y = -SDL_GameControllerGetAxis(mController,
   SDL_CONTROLLER_AXIS_LEFTY);
mState.Controller.mLeftStick = Filter2D(x, y);

Note that this code negates the y-axis value. This is because SDL reports the y-axis in the SDL coordinate system where +y is down. Thus, to get the expected values in the game’s coordinate system, you must negate the value.

You can then access the value of the left stick via InputState with code like this:

Vector2 leftStick = state.Controller.GetLeftStick();

Supporting Multiple Controllers

Supporting multiple local controllers is more complex than supporting one. This section briefly touches on the different pieces of code needed to support it, though it does not fully implement this code. First, to initialize all connected controllers at startup, you need to rewrite the controller detection code to loop over all joysticks and see which ones are controllers. You can then open each one individually, with code roughly like this:

for (int i = 0; i < SDL_NumJoysticks(); ++i)
{
   // Is this joystick a controller?
   if (SDL_IsGameController(i))
   {
      // Open this controller for use
      SDL_GameController* controller = SDL_GameControllerOpen(i);
      // Add to vector of SDL_GameController* pointers
   }
}

Next, you change InputState to contain several ControllerStates instead of just one. You also update all the functions in InputSystem to support each of these different controllers.

To support hot swapping (adding/removing controllers while the game is running), SDL generates two different events for adding and removing controllers: SDL_CONTROLLERDEVICEADDED and SDL_CONTROLLERDEVICEREMOVED. Consult the SDL documentation for further information about these events (see https://wiki.libsdl.org/SDL_ControllerDeviceEvent).

Input Mappings

The way you currently use the data from InputState, the code assumes that specific input devices and keys map directly to actions. For example, if you want the player character to jump on the positive edge of a spacebar, you add code like this to ProcessInput:

bool shouldJump = state.Keyboard.GetKeyState(SDL_SCANCODE_SPACE)
                  == Pressed;

Although this works, ideally you’d like to instead define an abstract “Jump” action. Then, you want some mechanism that allows the game code to specify that “Jump” corresponds to the spacebar key. To support this, you want a map between these abstract actions and the {device, button} pair corresponding to this abstract action. (You will actually work on implementing this in Exercise 8.2.)

You could further enhance this system by allowing for multiple bindings to the same abstract action. This means you could bind both the spacebar and the A button on the controller to “Jump.”

Another advantage of defining such abstract actions is that doing so makes it easier for AI-controlled characters to perform the same action. Rather than needing some separate code path for the AI, you could update the AI character such that it generates a “Jump” action when the AI wants to jump.

Another improvement to this system allows for the definition of a movement along an axis, such as a “ForwardAxis” action that corresponds to the W and S keys or one of the controller axes. You can then use this action to specify movement of characters in the game.

Finally, with these types of mappings, you can add a mechanism to load mappings from a file. This makes it easy for designers or users to configure the mappings without modifying the code.

Game Project

This chapter’s game project adds a full implementation of the InputSystem from this chapter to the game project from Chapter 5. This includes all the code for the keyboard, mouse, and controller. Recall that the Chapter 5 project uses 2D movement (so position is a Vector2). The code is available in the book’s GitHub repository, in the Chapter08 directory. Open Chapter08-windows.sln on Windows and Chapter08-mac.xcodeproj on Mac.

In this chapter’s project, the game controller moves the spaceship. The left stick affects the direction in which the ship travels, and the right stick rotates the direction the ship faces. The right trigger fires a laser. This is a control scheme popularized by “twin stick shooter” games.

With the input system already returning 2D axes for the left/right stick, implementing the twin stick–style controls does not require too much code. First, in Ship::ActorInput, you add the following lines of code to grab both the left and right sticks and save them in member variables:

if (state.Controller.GetIsConnected())
{
   mVelocityDir = state.Controller.GetLeftStick();
   if (!Math::NearZero(state.Controller.GetRightStick().Length()))
   {
      mRotationDir = state.Controller.GetRightStick();
   }
}

You add the NearZero check for the right stick to make sure that if the player releases the right stick completely, the ship doesn’t snap back to an initial angle of zero.

Next, in Ship::UpdateActor, add the following code to move the actor based on the direction of the velocity, a speed, and delta time:

Vector2 pos = GetPosition();
pos += mVelocityDir * mSpeed * deltaTime;
SetPosition(pos);

Note that this code reduces the speed based on how far you move the left stick in a direction because mVelocityDir can have a length less than one in this case.

Finally, you add the following code (also in UpdateActor) to rotate the actor based on the mRotationDir, using the atan2 approach:

float angle = Math::Atan2(mRotationDir.y, mRotationDir.x);
SetRotation(angle);

Again, this code compiles because the Actor class in this chapter’s project harkens back to the 2D actor class that used a single float for the angle, as opposed to the quaternion rotation used in 3D.

Figure 8.4 shows what the game looks like with the ship moving around.

Screenshot of the Game Programming in C++ (Chapter 5) window shows a set of rocks all over the screen with a ship at the bottom left with pinpoints at the top.

Figure 8.4 Ship moving around in the Chapter 8 game project

Summary

Many different input devices are used for games. A device might report either a single Boolean value or a range of inputs. For a key/button that reports a simple on/off state, it’s useful to consider the difference between the value in this frame and the value in the last frame. This way, you can detect the positive or negative edge of the input, corresponding to a “pressed” or “released” state.

SDL provides support for the most common input devices including the keyboard, mouse, and controller. For each of these devices, you add data in an InputState struct that you then pass to each actor’s ProcessInput function. This way, actors can query the input state for not only the current values of inputs but also negative and positive edges.

For devices that give a range of values, such as the triggers or analog sticks, you typically need to filter this data. This is because even when the device is at rest, the device may give spurious signals. The filtering implemented in this chapter ensures that input less than some dead zone is ignored and also ensures that you detect the maximum input even when the input is only “almost” the maximum.

This chapter’s game project takes advantage of the new controller input functionality to add support for twin-stick shooter–style movement.

Additional Reading

Bruce Dawson covers how to record input and then play it back, which is very useful for testing. The Oculus SDK documentation covers how to interface with Oculus VR touch controllers. Finally, Mick West explores how to measure input lag, which is the amount of time it takes a game to detect inputs from controllers. Input lag is generally not the fault of the input code, but West’s material is interesting nonetheless.

Dawson, Bruce. “Game Input Recording and Playback.” Game Programming Gems 2, edited by Mark DeLoura. Cengage Learning, 2001.

Oculus PC SDK. Accessed November 29, 2017. https://developer.oculus.com/documentation/pcsdk/latest/.

West, Mick. “Programming Responsiveness.” Gamasutra. Accessed November 29, 2017. http://www.gamasutra.com/view/feature/1942/programming_responsiveness.php?print=1.

Exercises

In this chapter’s exercises you will improve the input system. In the first exercise you add support for multiple controllers. In the second exercise you add input mappings.

Exercise 8.1

Recall that to support multiple controllers, you need to have multiple ControllerState instances in the InputState struct. Add code to support a maximum of four controllers simultaneously. On initialization, change the code to detect any connected controllers and enable them individually. Then change the Update code so that it updates up to all four controllers instead of just a single one.

Finally, investigate the events that SDL sends when the user connects/disconnects controllers and add support to dynamically add and remove controllers.

Exercise 8.2

Add support for basic input mappings for actions. To do this, create a text file format that maps actions to both a device and a button/key on that device. For example, an entry in this text file to specify that the “Fire” action corresponds to the A button on the controller might look like this:

Fire,Controller,A

Then parse this data in the InputSystem and save it into a map. Next, add a generic GetMappedButtonState function to InputState that takes in the action name and returns the ButtonState from the correct device. The signature of this function is roughly the following:

ButtonState GetMappedButtonState(const std::string& actionName);

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.129.148.210