Home > DirectX > XInput Tutorial Part 2: Mapping gamepad buttons and analog movement to Windows keyboard events

XInput Tutorial Part 2: Mapping gamepad buttons and analog movement to Windows keyboard events

August 30, 2013

In part 1 of this mini-series we looked at the basics of the XInput API and how to read the state of all the analog and digital buttons on a gamepad, then wrapped it up into a simple Gamepad class. In this article, we shall look at how to translate button pushes and analog stick/trigger movements into Windows keyboard events for those applications which use the Windows messages WM_KEYDOWN and WM_KEYUP to handle user keyboard input.

The problem

XInput requires you to poll the controller each frame to get changes in state. If you currently use a function like GetAsyncKeyState() to check for keyboard key presses in your game, this is fine, but if you are using the Windows keyboard messages WM_KEYDOWN and WM_KEYUP, this will not fit in with your current model, and the game will require a bit of re-engineering to handle polling as well. Ideally, we would like to avoid this and just funnel controller movements and button presses through WM_KEYDOWN and WM_KEYUP as if they were normal keyboard key presses.

To do this, we will expand upon our basic Gamepad class to include keyboard mapping and event dispatch functions.

NOTE: If you can’t be bothered with the low-level details and just want to shoehorn gamepad support into a game really quickly, my Simple2D library (version 1.11 and above) includes gamepad support – see the Tetris gamepad support article for a quick example on how to use the library to add gamepad support in just a few minutes!

Create a keyboard mapping table

We’ll use std::map to store a list of controller buttons (using the XINPUT_GAMEPAD_* values we talked about in part 1) and the corresponding keys we want to generate events for. These will be virtual key codes as used by Windows, which are manufacturer-independent codes for each key on a standard keyboard (as opposed to scan codes which are device-dependent).

Add  <map> at the top of your class source file and add the following code into the class definition:

private:
// Mapping of controller buttons to keys
std::map<WORD, int> keyMap;

...
public:
// Add a key translation mapping from XINPUT_GAMEPAD_* to a virtual key code (VK_*)
void AddKeyMapping(WORD, int);

// Remove a key translation mapping from XINPUT_GAMEPAD_* button
void RemoveKeyMappingByButton(WORD);

// Remove a key translation mapping from a virtual key code (VK_*)
void RemoveKeyMapping(int);

// Remove all key translation mappings
void ClearMappings();

Then add in the code for the newly declared functions:

void Gamepad::AddKeyMapping(WORD button, int key)
{
	keyMap.erase(button);
	keyMap.insert(std::map<WORD, int>::value_type(button, key));
}

void Gamepad::RemoveKeyMapping(int key)
{
	for (auto it = keyMap.begin(); it != keyMap.end(); ++it)
		if (it->second == key)
		{
			keyMap.erase(it);
			break;
		}
}

void Gamepad::RemoveKeyMappingByButton(WORD button)
{
	keyMap.erase(button);
}

void Gamepad::ClearMappings()
{
	keyMap.clear();
}

Using this code, we can now create mappings in our application like this:

Gamepad gamepad;
...

// Add classic 'continue/back' mappings to A and B
gamepad.AddKeyMapping(XINPUT_GAMEPAD_A, VK_RETURN);
gamepad.AddKeyMapping(XINPUT_GAMEPAD_B, VK_ESCAPE);

This example maps the A button on the controller to the Enter key and the B button to Escape. Visit the Virtual Key Codes page on MSDN for the complete list of codes.

Selecting a target window to receive keyboard events

The next step is to store the window handle (HWND) of the window to receive our keyboard events. Add the following code to the class definition:

private:
// The window to send keyboard events to (or NULL for none)
HWND targetWindow;
...

public:
// Set the target window to receive key events and enable them
void SetWindow(HWND);

and in the class source code:

void Gamepad::SetWindow(HWND hwnd)
{
	targetWindow = hwnd;
}

You can now set the window to dispatch events to by calling SetWindow(window handle). To disable keyboard event dispatch, use SetWindow(NULL).

Make a list of all the controller buttons

As we will need to iterate over all the possible buttons to see if they are pressed, we need to make a list of them. Change the constructors in your class definition to set up a list of buttons:

// Enable gamepad support
Gamepad() : deadzoneX(0.05f), deadzoneY(0.02f), targetWindow(NULL) { setButtons(); }

// Enable gamepad support supplying default X and Y-axis deadzones
Gamepad(float dzX, float dzY) : deadzoneX(dzX), deadzoneY(dzY), targetWindow(NULL) { setButtons(); }

and add the following code to do the set up:

#include <boost/assign.hpp>
...

public:
// A map of XINPUT_GAMEPAD_* button IDs to string button names
std::map<WORD, string> Buttons;
...

private:
// Configure the controller button names. Internal use only.
void setButtons()
{
	std::map<WORD, string> bn = map_list_of
		(XINPUT_GAMEPAD_A, "A")
		(XINPUT_GAMEPAD_B, "B")
		(XINPUT_GAMEPAD_X, "X")
		(XINPUT_GAMEPAD_Y, "Y")
		(XINPUT_GAMEPAD_DPAD_LEFT, "Left")
		(XINPUT_GAMEPAD_DPAD_RIGHT, "Right")
		(XINPUT_GAMEPAD_DPAD_UP, "Up")
		(XINPUT_GAMEPAD_DPAD_DOWN, "Down")
		(XINPUT_GAMEPAD_LEFT_SHOULDER, "LB")
		(XINPUT_GAMEPAD_RIGHT_SHOULDER, "RB")
		(XINPUT_GAMEPAD_BACK, "Back")
		(XINPUT_GAMEPAD_START, "Start")
		(XINPUT_GAMEPAD_LEFT_THUMB, "LS")
		(XINPUT_GAMEPAD_RIGHT_THUMB, "RS");

	Buttons.insert(bn.begin(), bn.end());
}

This code uses Boost.Assign to make initializing the map easier. If you don’t have access to the Boost library, you will have to use a series of Buttons.insert() calls instead.

The code creates a mapping of XInput button values to names in a public field called Buttons. This is handy for debugging or showing the controls as you can just fetch a button name from eg. Buttons[XINPUT_GAMEPAD_LEFT_SHOULDER] and so on.

Storing the previous state of the controller

We need to store the previous state of the controller to determine if there have been any changes. Add a private field to the class definition:

// The previous state of the controller
XINPUT_STATE previous;

and add code inside Refresh() to store the previous state immediately before XInputGetState is called to fetch the new state:

// Store previous state
previous = state;

Performing the translation

Now we are ready to do the heavy lifting, which takes place in our Refresh() function; if you recall, this is to be called once per frame to update the controller’s state. After all the calculations for the analog sticks and triggers in Refresh(), add the following code:

// Dispatch keyboard events if desired
if (targetWindow != NULL)
{
	for (auto button : Buttons)
	{
		// If button is pushed
		if ((state.Gamepad.wButtons & button.first) != 0)
		{
			// Get key mapping or use XINPUT_GAMEPAD_* value if no mapping exists
			WORD mapping = (keyMap.find(button.first) != keyMap.end()?
				keyMap.find(button.first)->second : button.first);

			// Send keyboard event
			SendMessage(targetWindow, WM_KEYDOWN, mapping,
			((previous.Gamepad.wButtons & button.first) == 0? 0 << 30 : 1 << 30));
		}

		// Checking for button release events, will cause the state
		// packet number to be incremented
		if (previous.dwPacketNumber < state.dwPacketNumber)
  		{
  			// If the button was pressed but is no longer pressed
  			if ((state.Gamepad.wButtons & button.first) == 0
  				&& (previous.Gamepad.wButtons & button.first) != 0)
  			{
  				// Get key mapping if one exists
  				WORD mapping = (keyMap.find(button.first) != keyMap.end()?
  					keyMap.find(button.first)->second : button.first);

				// Send keyboard event
				SendMessage(targetWindow, WM_KEYUP, mapping, 0);
			}
		}
	}
}

Here we iterate over each button and check its state. If it is currently pressed down, we find the corresponding key from the keyboard mapping table (or just use the raw XInput value as the virtual key code if none is found) and send a WM_KEYDOWN event to the target window. Bit 30 of LPARAM (the last parameter) in WM_KEYDOWN should reflect whether the key was already held down before the message was sent, or whether this is a new key press, so we set the bit accordingly. If the key is not currently down but was the last time we checked, we once again find the mapping and send a WM_KEYUP message. Note that to avoid unnecessary checks, we first look at the packet number of the controller state. This number only increases when the controller’s state changes, so if it is the same as on the last frame, nothing has changed so it is not possible that a button has been released.

Notice that WM_KEYDOWN is sent for every frame where the button is held down (the first instance has bit 30 of LPARAM set to 0 and the rest have it set to 1, so the receiving window can ignore duplicate messages if desired), but only one WM_KEYUP message is sent when the key is released. This is the same as how normal keyboard events work, with one important difference that must be addressed (see below after the section on analog movements).

Translating analog stick and trigger movements into keyboard events

Dealing with analog movements is a bit more tricky because we have to only register them as keyboard events when a certain threshold of movement is exceeded (the deadzone, or another value of your choice). Also, there are no built-in values for the analog sticks and triggers so we have to create some ourselves in order to be able to make a mapping table. We can do this with a simple enum in the class definition:

// Names for each axis of movement for each analog item
enum AnalogButtons {
	LeftStickLeft,
	LeftStickRight,
	LeftStickUp,
	LeftStickDown,
	RightStickLeft,
	RightStickRight,
	RightStickUp,
	RightStickDown,
	LeftTrigger,
	RightTrigger,
	EndOfButtons
};

// Analog keyboard mappings have a percentage threshold and a target keyboard key
struct AnalogMapping {
	float threshold;
	int key;
};

We also create a struct called AnalogMapping to hold both the virtual key code and the movement threshold required to trigger it. We now make a series of functions similar to those for digital buttons to create the mapping table. In the class definition:

private:
// Mapping of analog controller items to keys
std::map<AnalogButtons, AnalogMapping> analogMap;
...
// Internal use only
void sendKeysOnThreshold(AnalogButtons, float, float, float, int);
...

public:
// Add a key translation mapping from an analog item moved more than the specified threshold to a virtual key code (VK_*)
void AddAnalogKeyMapping(AnalogButtons, float, int);

// Remove a key translation mapping from an analog item
void RemoveAnalogKeyMapping(AnalogButtons);

and in the class source code:

void Gamepad::AddAnalogKeyMapping(AnalogButtons button, float threshold, int key)
{
	AnalogMapping a = { threshold, key };

	analogMap.erase(button);
	analogMap.insert(std::make_pair(button, a));
}

void Gamepad::RemoveAnalogKeyMapping(AnalogButtons button)
{
	analogMap.erase(button);
}

void Gamepad::ClearMappings()
{
	keyMap.clear();
	analogMap.clear();
}

A couple of notes. First we have updated ClearMappings() to clear both digital and analog mapping tables. Secondly, we have defined a function called sendKeysOnThreshold(). Since processing the analog keys is a bit more complicated than the digital buttons and we need to supply different enum values for each analog item (which aren’t stored in an array), we will abstract the key message dispatch code to this function. Third, we allow an extra parameter to AddAnalogKeyMapping to specify the movement threshold that must be exceeded for the movement to qualify as a key press.

Once again we need to store the previous state of the analog items so we add more variables and code to do this. In the class definition:

private:
// The previous state of the analog sticks and triggers
float prevLeftStickX;
float prevLeftStickY;
float prevRightStickX;
float prevRightStickY;
float prevLeftTrigger;
float prevRightTrigger;

and in Refresh(), right after we store the previous state of the digital buttons (XINPUT_STATE):

prevLeftStickX = leftStickX;
prevLeftStickY = leftStickY;
prevRightStickX = rightStickX;
prevRightStickY = rightStickY;
prevLeftTrigger = leftTrigger;
prevRightTrigger = rightTrigger;

After the digital button key events have been dispatched in Refresh(), we add a call to sendKeysOnThreshold() for each item that has a mapping:

// Do keyboard event dispatch processing for analog items
// (unmapped items won't have events generated for them)
for (auto item : analogMap)
{
	WORD mapping = item.second.key;

	switch (item.first) {
	case AnalogButtons::LeftStickLeft:
		sendKeysOnThreshold(AnalogButtons::LeftStickLeft, leftStickX, prevLeftStickX, -item.second.threshold, mapping);
		break;

	case AnalogButtons::LeftStickRight:
		sendKeysOnThreshold(AnalogButtons::LeftStickRight, leftStickX, prevLeftStickX, item.second.threshold, mapping);
		break;

	case AnalogButtons::LeftStickUp:
		sendKeysOnThreshold(AnalogButtons::LeftStickUp, leftStickY, prevLeftStickY, item.second.threshold, mapping);
		break;

	case AnalogButtons::LeftStickDown:
		sendKeysOnThreshold(AnalogButtons::LeftStickDown, leftStickY, prevLeftStickY, -item.second.threshold, mapping);
		break;

	case AnalogButtons::RightStickLeft:
		sendKeysOnThreshold(AnalogButtons::RightStickLeft, rightStickX, prevRightStickX, -item.second.threshold, mapping);
		break;

	case AnalogButtons::RightStickRight:
		sendKeysOnThreshold(AnalogButtons::RightStickRight, rightStickX, prevRightStickX, item.second.threshold, mapping);
		break;

	case AnalogButtons::RightStickUp:
		sendKeysOnThreshold(AnalogButtons::RightStickUp, rightStickY, prevRightStickY, item.second.threshold, mapping);
		break;

	case AnalogButtons::RightStickDown:
		sendKeysOnThreshold(AnalogButtons::RightStickDown, rightStickY, prevRightStickY, -item.second.threshold, mapping);
		break;

	case AnalogButtons::LeftTrigger:
		sendKeysOnThreshold(AnalogButtons::LeftTrigger, leftTrigger, prevLeftTrigger, item.second.threshold, mapping);
		break;

	case AnalogButtons::RightTrigger:
		sendKeysOnThreshold(AnalogButtons::RightTrigger, rightTrigger, prevRightTrigger, item.second.threshold, mapping);
		break;
	}
}

We pass five arguments to this function:

  • the enum value of the analog item being processed
  • the current movement state of the item
  • the previous movement state of the item
  • the movement threshold of the item (note that some are changed to negative so that they match the axis to be tested)
  • the virtual key code to use if a keyboard message is to be dispatched

We need the current and previous states to test whether the analog stick or trigger was previously below the threshold and is now above it, or vice versa.

The dirty work is of course done in sendKeysOnThreshold(), which looks like this:

// Processing of analog item key event dispatch
void Gamepad::sendKeysOnThreshold(AnalogButtons button, float now, float before, float threshold, int key)
{
	// Check whether the item is and was passed the threshold or not
	bool isPressed = (now >= threshold && threshold > 0) || (now <= threshold && threshold < 0);
  	bool wasPressed = (before >= threshold && threshold > 0) || (before <= threshold && threshold < 0);

	// If currently pressed
	if (isPressed)
		SendMessage(targetWindow, WM_KEYDOWN, key, (wasPressed? 1 << 30 : 0 << 30));

	// Same logic as digital buttons
	if (previous.dwPacketNumber < state.dwPacketNumber)
		if (!isPressed && wasPressed)
			SendMessage(targetWindow, WM_KEYUP, key, 0);
}

The code should be fairly self-explanatory as it uses the same logic as for the digital buttons.

Now we are finally ready to test it! Analog mappings can now be added to your application like this:

// Add classic 'WASD' movement key mappings to left analog stick (threshold = 100%)
gamepad.AddAnalogKeyMapping(Gamepad::AnalogButtons::LeftStickLeft, 1, 'A');
gamepad.AddAnalogKeyMapping(Gamepad::AnalogButtons::LeftStickRight, 1, 'D');
gamepad.AddAnalogKeyMapping(Gamepad::AnalogButtons::LeftStickUp, 1, 'W');
gamepad.AddAnalogKeyMapping(Gamepad::AnalogButtons::LeftStickDown, 1, 'S');

Here we map all four movement directions on the left analog stick to send A, D, W or S depending on the stick’s direction. These keys are commonly used for player movement in keyboard-based video games. You may want to add classic camera movement by mapping the right stick to the arrow keys like this, for example:

// Add arrow key mappings to right analog stick (threshold = 100%)
gamepad.AddAnalogKeyMapping(Gamepad::AnalogButtons::RightStickLeft, 1, VK_LEFT);
gamepad.AddAnalogKeyMapping(Gamepad::AnalogButtons::RightStickRight, 1, VK_RIGHT);
gamepad.AddAnalogKeyMapping(Gamepad::AnalogButtons::RightStickUp, 1, VK_UP);
gamepad.AddAnalogKeyMapping(Gamepad::AnalogButtons::RightStickDown, 1, VK_DOWN);

Note that in all cases here, the threshold is set to 1 – the maximum – meaning that the stick must be pushed all the way across to trigger a keyboard event. If you want more sensitive control, you can lower the thresholds.

As you can see, this code essentially translates analog movements into digital key up/down states: the analog item is either above or below the threshold, changing its behaviour from an analog device to a digital on/off switch.

Controlling the frequency of WM_KEYDOWN messages when a button or analog stick/trigger is held down

This is all very nice, however there is a problem. If you try the code above, you’ll see that holding down a button sends a huge stream of WM_KEYDOWN messages (one per frame per button). We do generally want this, but we need to throttle the frequency of the messages.

Why? Consider a game like Tetris. You hold down the left and right arrows on the keyboard to move a piece, and it moves once grid square every 150ms or so (for example). This happens because the keyboard settings on your PC define a repeat rate for keys. If you open a text editor and hold down a key, the pressed character appears once, and if you continue to hold it down, it repeats at an even frequency. This same behaviour applies to normally generated WM_KEYDOWN events, causing the periodic delay in the movement of the Tetris piece, for example. However, with our code, there is no repeat rate delay, so if you try to plumb the existing code into a game like Tetris, the pieces will whizz about extremely fast and you will have no precise control over their placement.

There are two kinds of repeat delay on a normal keyboard setup: the delay between the time the key is first pressed and the first time it repeats (the initial repeat delay), and the delay between each subsequent repeat. If you check out Control Panel, these are called ‘Repeat delay’ and ‘Repeat rate’ respectively.

Charmingly, implementing this for our gamepad requires a veritable mountain of code (relatively speaking). We are going to ignore the initial repeat delay as it is not needed for most games, and just implement a fixed repeat rate. We need to:

  • store when each digital button was last pressed or repeated
  • store when each analog item last crossed the threshold or repeated
  • store the desired repeat rate for each digital button and each analog item
  • add functions to allow the repeat rates to be configured
  • when deciding whether to dispatch a keyboard event, compare the current time to the last press time of each button or analog item, and only send the event if the difference exceeds the repeat rate, or if the key has just been pressed
  • update the last press time of each button and analog item
  • when a button is released, erase the last press time of the button

We will add some flexibility here: each button and analog item can have its own repeat rate, and if the repeat rate is set to 0, no repeats will occur. This is great for games like Tetris: moving left and right can repeat every so many milliseconds, but if you hold down fast drop to drop a piece instantly, we can configure the button to not repeat so that subsequent pieces won’t be accidentally dropped by the player.

Let’s not beat around the bush then. We’ve got work to do!

In the class definition:

private:
// Repeat rate of generated key events from controller buttons
std::map<WORD, unsigned int> repeatMs;

// Repeat rate of generated key events from analog controller items
std::map<AnalogButtons, unsigned int> analogRepeatMs;

// The GetTickCount() of when each button was last pressed
std::map<WORD, DWORD> lastPress;

// The GetTickCount() of when each analog item was pressed (passed the threshold)
std::map<AnalogButtons, DWORD> analogLastPress;
...

public:
// Set the global keyboard repeat interval for all buttons and analog items on the controller - overwrites any previous settings
void SetRepeatIntervalMsAll(unsigned int);

// Set the keyboard repeat interval for the specified XINPUT_GAMEPAD_* button in milliseconds
void SetRepeatIntervalMs(WORD, unsigned int);

// Set the keyboard repeat interval for the specified analog item in milliseconds
void SetAnalogRepeatIntervalMs(AnalogButtons, unsigned int);

This defines maps of controller buttons to desired repeat rates and functions to allow us to change them. We implement these in the class source code as usual:

void Gamepad::SetRepeatIntervalMsAll(unsigned int ms)
{
	repeatMs.clear();

	for (auto button : Buttons)
		repeatMs.insert(std::map<WORD, unsigned int>::value_type(button.first, ms));

	analogRepeatMs.clear();

	for (int i = 0; i < AnalogButtons::EndOfButtons; i++)
		analogRepeatMs.insert(std::map<AnalogButtons, unsigned int>::value_type((AnalogButtons) i, ms));
}

void Gamepad::SetRepeatIntervalMs(WORD button, unsigned int ms)
{
	repeatMs.erase(button);
	repeatMs.insert(std::map<WORD, unsigned int>::value_type(button, ms));
}

void Gamepad::SetAnalogRepeatIntervalMs(AnalogButtons button, unsigned int ms)
{
	analogRepeatMs.erase(button);
	analogRepeatMs.insert(std::map<AnalogButtons, unsigned int>::value_type(button, ms));
}

This should be fairly self-explanatory. The tricky part of course comes in our new implementation of keyboard event dispatch in Refresh(). Let’s look at the changes to digital button presses first:

// If button is pushed
if ((state.Gamepad.wButtons & button.first) != 0)
{
	// Get key mapping or use XINPUT_GAMEPAD_* value if no mapping exists
	WORD mapping = (keyMap.find(button.first) != keyMap.end()?
		keyMap.find(button.first)->second : button.first);

	// Get current time and last WM_KEYDOWN message for repeat interval check
	DWORD now = GetTickCount();
	DWORD last = (lastPress.find(button.first) != lastPress.end()?
		lastPress.find(button.first)->second : 0);

	// Find desired repeat interval for this button
	unsigned int ms = repeatMs.find(button.first)->second;

	// If first press, or repeat interval passed (and repeat interval != 0)
	if ((now - last >= ms && ms > 0)
		|| last == 0
		|| (ms == 0 && (previous.Gamepad.wButtons & button.first) == 0))
	{
		// Update last press time
		lastPress.erase(button.first);
		lastPress.insert(std::map<WORD, DWORD>::value_type(button.first, now));

		// Send keyboard event
		SendMessage(targetWindow, WM_KEYDOWN, mapping,
		((previous.Gamepad.wButtons & button.first) == 0? 0 << 30 : 1 << 30));
	}
}

(new and changed lines are highlighted)

The first step is to get the current time (in milliseconds elapsed since the application started – this is returned by the Windows API function GetTickCount() – remember to <Windows.h> at the start of your source code) and retrieve the previous press time of the button. If there is no previous press time, this is set to zero to force a WM_KEYDOWN message to be sent for the first press.

We then fetch the repeat interval for the button in ms. The complex if statement will cause a message to be dispatched in three cases:

  • the repeat rate is non-zero and the button is being pressed (and was previously not pressed)
  • the repeat rate is zero and the button is being pressed (and was previously not pressed)
  • the repeat rate is non-zero and the repeat rate time has been exceeded

Inside the if statement, the last button press time is updated to the current time so that the wait for the next repeat is reset. The message is then sent as before.

When the button on the controller is released, the last press time is erased by adding one line of code to the WM_KEYUP handler:

// Checking for button release events, will cause the state
// packet number to be incremented
if (previous.dwPacketNumber < state.dwPacketNumber)
{
  	// If the button was pressed but is no longer pressed
  	if ((state.Gamepad.wButtons & button.first) == 0
  		&& (previous.Gamepad.wButtons & button.first) != 0)
  	{
  		// Get key mapping if one exists
  		WORD mapping = (keyMap.find(button.first) != keyMap.end()? keyMap.find(button.first)->second : button.first);

		// Remove last press time
		lastPress.erase(button.first);

		// Send keyboard event
		SendMessage(targetWindow, WM_KEYUP, mapping, 0);
	}
}

We deal with analog movements in exactly the same way inside sendKeysOnThreshold():

// Processing of analog item key event dispatch
void Gamepad::sendKeysOnThreshold(AnalogButtons button, float now, float before, float threshold, int key)
{
	// Check whether the item is and was passed the threshold or not
	bool isPressed = (now >= threshold && threshold > 0) || (now <= threshold && threshold < 0);
  	bool wasPressed = (before >= threshold && threshold > 0) || (before <= threshold && threshold < 0);
  	// If currently pressed
  	if (isPressed)
 	{
  		// Repeat interval calculation
  		DWORD now = GetTickCount();
  		DWORD last = (analogLastPress.find(button) != analogLastPress.end()?
  			analogLastPress.find(button)->second : 0);

		unsigned int ms = analogRepeatMs.find(button)->second;

		// Send message (uses same logic as digital buttons)
		if ((now - last >= ms && ms > 0) || last == 0 || (ms == 0 && !wasPressed))
		{
			analogLastPress.erase(button);
			analogLastPress.insert(std::map<AnalogButtons, DWORD>::value_type(button, now));

			SendMessage(targetWindow, WM_KEYDOWN, key, (wasPressed? 1 << 30 : 0 << 30));
		}
	}

	// Same logic as digital buttons
	if (previous.dwPacketNumber < state.dwPacketNumber)
		if (!isPressed && wasPressed)
		{
			analogLastPress.erase(button);

			SendMessage(targetWindow, WM_KEYUP, key, 0);
		}
}

(new and changed lines are highlighted)

And that’s all there is to it! (haha :P) You can now configure the desired repeat rates per button or analog item in your application code. Here is an example with the following properties:

  • maps the left and right directional pad arrows and the left analog stick to the left and right arrow keys on the keyboard
  • sets the analog stick X-axis trigger threshold to 10% in both the left and right directions
  • maps the A button to the space bar
  • configures all of the buttons and analog sticks on the pad to repeat every 100ms
  • configures the A button not to repeat
gamepad.ClearMappings();

gamepad.AddKeyMapping(XINPUT_GAMEPAD_DPAD_LEFT, VK_LEFT);
gamepad.AddKeyMapping(XINPUT_GAMEPAD_DPAD_RIGHT, VK_RIGHT);

gamepad.AddAnalogKeyMapping(Gamepad::AnalogButtons::LeftStickLeft, 0.1f, VK_LEFT);
gamepad.AddAnalogKeyMapping(Gamepad::AnalogButtons::LeftStickRight, 0.1f, VK_RIGHT);

gamepad.AddKeyMapping(XINPUT_GAMEPAD_A, VK_SPACE);

gamepad.SetRepeatIntervalMsAll(100);
gamepad.SetRepeatIntervalMs(INPUT_GAMEPAD_A, 0);

At this point the advantages of the Gamepad class should be becoming obvious: the above code is literally all you have to add to your game along with a per-frame call to Refresh() to completely support the gamepad, without any other changes to your code.

Complete source code

Here is the complete Gamepad class so you don’t have to piece it all together from the article if you don’t want to:

Gamepad.h
/* XInput support */
/* (c) Katy Coe (sole proprietor) 2013 */
/* www.djkaty.com */

#include <Windows.h>
#include <Xinput.h>
#include <algorithm>
#include <boost/assign.hpp>
#include <string>
#include <map>

using namespace boost::assign;
using std::string;

// The gamepad support class
class Gamepad
{
public:
	// =========================================================================
	// Public usable sub-types
	// =========================================================================

	// Names for each axis of movement for each analog item
	enum AnalogButtons {
		LeftStickLeft,
		LeftStickRight,
		LeftStickUp,
		LeftStickDown,
		RightStickLeft,
		RightStickRight,
		RightStickUp,
		RightStickDown,
		LeftTrigger,
		RightTrigger,
		EndOfButtons
	};

	// Analog keyboard mappings have a percentage threshold and a target keyboard key
	struct AnalogMapping {
		float threshold;
		int key;
	};

private:
	// The port on which the controller is connected (0-3)
	int cId;

	// The last retrieved state of the controller
	XINPUT_STATE state;

	// The window to send keyboard events to (or NULL for none)
	HWND targetWindow;

	// The X-axis analog stick deadzone (0-1)
	float deadzoneX;

	// The Y-axis analog stick deadzone (0-1)
	float deadzoneY;

	// Mapping of controller buttons to keys
	std::map<WORD, int> keyMap;

	// Mapping of analog controller items to keys
	std::map<AnalogButtons, AnalogMapping> analogMap;

	// Repeat rate of generated key events from controller buttons
	std::map<WORD, unsigned int> repeatMs;

	// Repeat rate of generated key events from analog controller items
	std::map<AnalogButtons, unsigned int> analogRepeatMs;

	// The GetTickCount() of when each button was last pressed
	std::map<WORD, DWORD> lastPress;

	// The GetTickCount() of when each analog item was pressed (passed the threshold)
	std::map<AnalogButtons, DWORD> analogLastPress;

	// Configure the controller button names. Internal use only.
	void setButtons()
	{
		std::map<WORD, string> bn = map_list_of
			(XINPUT_GAMEPAD_A, "A")
			(XINPUT_GAMEPAD_B, "B")
			(XINPUT_GAMEPAD_X, "X")
			(XINPUT_GAMEPAD_Y, "Y")
			(XINPUT_GAMEPAD_DPAD_LEFT, "Left")
			(XINPUT_GAMEPAD_DPAD_RIGHT, "Right")
			(XINPUT_GAMEPAD_DPAD_UP, "Up")
			(XINPUT_GAMEPAD_DPAD_DOWN, "Down")
			(XINPUT_GAMEPAD_LEFT_SHOULDER, "LB")
			(XINPUT_GAMEPAD_RIGHT_SHOULDER, "RB")
			(XINPUT_GAMEPAD_BACK, "Back")
			(XINPUT_GAMEPAD_START, "Start")
			(XINPUT_GAMEPAD_LEFT_THUMB, "LS")
			(XINPUT_GAMEPAD_RIGHT_THUMB, "RS");

		Buttons.insert(bn.begin(), bn.end());
	}

	// The previous state of the controller
	XINPUT_STATE previous;

	// The previous state of the analog sticks and triggers
	float prevLeftStickX;
	float prevLeftStickY;
	float prevRightStickX;
	float prevRightStickY;
	float prevLeftTrigger;
	float prevRightTrigger;

	// Internal use only
	void sendKeysOnThreshold(AnalogButtons, float, float, float, int);

public:
	// =========================================================================
	// Public methods and properties
	// =========================================================================

	// Enable gamepad support
	Gamepad() : deadzoneX(0.05f), deadzoneY(0.02f), targetWindow(NULL) { setButtons(); SetRepeatIntervalMsAll(0); }

	// Enable gamepad support supplying default X and Y-axis deadzones
	Gamepad(float dzX, float dzY) : deadzoneX(dzX), deadzoneY(dzY), targetWindow(NULL) { setButtons(); SetRepeatIntervalMsAll(0); }

	// A map of XINPUT_GAMEPAD_* button IDs to string button names
	std::map<WORD, string> Buttons;

	// The current state of the analog sticks and triggers (movement amount from 0-1)
	float leftStickX;
	float leftStickY;
	float rightStickX;
	float rightStickY;
	float leftTrigger;
	float rightTrigger;

	// Get the port on which the active controller is plugged in (1-4)
	int  GetPort();

	// Get the current state of the controller (not normally needed)
	XINPUT_GAMEPAD *GetState();

	// Try to establish a connection with the controller (returns true if succeeded)
	bool CheckConnection();

	// Refresh the state of the controller. Call once per frame (calls CheckConnection())
	bool Refresh();

	// Returns true if the specified XINPUT_GAMEPAD_* button is pressed
	bool IsPressed(WORD);

	// Set the X and Y-axis analog stick deadzones
	void SetDeadzone(float, float);

	// Set the target window to receive key events and enable them
	void SetWindow(HWND);

	// Add a key translation mapping from XINPUT_GAMEPAD_* to a virtual key code (VK_*)
	void AddKeyMapping(WORD, int);

	// Remove a key translation mapping from XINPUT_GAMEPAD_* button
	void RemoveKeyMappingByButton(WORD);

	// Remove a key translation mapping from a virtual key code (VK_*)
	void RemoveKeyMapping(int);

	// Add a key translation mapping from an analog item moved more than the specified threshold to a virtual key code (VK_*)
	void AddAnalogKeyMapping(AnalogButtons, float, int);

	// Remove a key translation mapping from an analog item
	void RemoveAnalogKeyMapping(AnalogButtons);

	// Remove all digital and analog key translation mappings
	void ClearMappings();

	// Set the global keyboard repeat interval for all buttons and analog items on the controller - overwrites any previous settings
	void SetRepeatIntervalMsAll(unsigned int);

	// Set the keyboard repeat interval for the specified XINPUT_GAMEPAD_* button in milliseconds
	void SetRepeatIntervalMs(WORD, unsigned int);

	// Set the keyboard repeat interval for the specified analog item in milliseconds
	void SetAnalogRepeatIntervalMs(AnalogButtons, unsigned int);
};
Gamepad.cpp
/* XInput support */
/* (c) Katy Coe (sole proprietor) 2013 */
/* www.djkaty.com */

#include "Gamepad.h"
#include <math.h>

using std::string;
using std::map;
using std::pair;

int Gamepad::GetPort()
{
	return cId + 1;
}

XINPUT_GAMEPAD *Gamepad::GetState()
{
	return &state.Gamepad;
}

bool Gamepad::CheckConnection()
{
	int controllerId = -1;

	// Check each port for a connection until one is found
	for (DWORD i = 0; i < XUSER_MAX_COUNT && controllerId == -1; i++)
	{
		XINPUT_STATE state;
		ZeroMemory(&state, sizeof(XINPUT_STATE));

		if (XInputGetState(i, &state) == ERROR_SUCCESS)
			controllerId = i;
	}

	cId = controllerId;

	return controllerId != -1;
}

// Returns false if the controller has been disconnected
bool Gamepad::Refresh()
{
	// Try to establish a connection with the controller if none was connected last time
	if (cId == -1)
		CheckConnection();

	// If the controller is connected...
	if (cId != -1)
	{
		// Store previous state
		previous = state;

		prevLeftStickX = leftStickX;
		prevLeftStickY = leftStickY;
		prevRightStickX = rightStickX;
		prevRightStickY = rightStickY;
		prevLeftTrigger = leftTrigger;
		prevRightTrigger = rightTrigger;

		// Check state and check for disconnection
		ZeroMemory(&state, sizeof(XINPUT_STATE));
		if (XInputGetState(cId, &state) != ERROR_SUCCESS)
		{
			cId = -1;
			return false;
		}

		// Calculate deadzone-normalized percentages of movement for the
		// analog sticks and triggers

		float normLX = max(-1, (float) state.Gamepad.sThumbLX / 32767);
		float normLY = max(-1, (float) state.Gamepad.sThumbLY / 32767);

		leftStickX = (abs(normLX) < deadzoneX ? 0 : (fabsf(normLX) - deadzoneX) * (normLX / fabsf(normLX)));
		leftStickY = (abs(normLY) < deadzoneY ? 0 : (fabsf(normLY) - deadzoneY) * (normLY / fabsf(normLY)));

 		if (deadzoneX > 0) leftStickX *= 1 / (1 - deadzoneX);
		if (deadzoneY > 0) leftStickY *= 1 / (1 - deadzoneY);

		float normRX = max(-1, (float) state.Gamepad.sThumbRX / 32767);
		float normRY = max(-1, (float) state.Gamepad.sThumbRY / 32767);

		rightStickX = (abs(normRX) < deadzoneX ? 0 : (fabsf(normRX) - deadzoneX) * (normRX / fabsf(normRX)));
		rightStickY = (abs(normRY) < deadzoneY ? 0 : (fabsf(normRY) - deadzoneY) * (normRY / fabsf(normRY)));

 		if (deadzoneX > 0) rightStickX *= 1 / (1 - deadzoneX);
		if (deadzoneY > 0) rightStickY *= 1 / (1 - deadzoneY);

		leftTrigger = (float) state.Gamepad.bLeftTrigger / 255;
		rightTrigger = (float) state.Gamepad.bRightTrigger / 255;

		// Dispatch keyboard events if desired
		if (targetWindow != NULL)
		{
			for (auto button : Buttons)
			{
				// If button is pushed
				if ((state.Gamepad.wButtons & button.first) != 0)
				{
					// Get key mapping or use XINPUT_GAMEPAD_* value if no mapping exists
					WORD mapping = (keyMap.find(button.first) != keyMap.end()?
						keyMap.find(button.first)->second : button.first);

					// Get current time and last WM_KEYDOWN message for repeat interval check
					DWORD now = GetTickCount();
					DWORD last = (lastPress.find(button.first) != lastPress.end()?
						lastPress.find(button.first)->second : 0);

					// Find desired repeat interval for this button
					unsigned int ms = repeatMs.find(button.first)->second;

					// If first press, or repeat interval passed (and repeat interval != 0)
					if ((now - last >= ms && ms > 0)
						|| last == 0
						|| (ms == 0 && (previous.Gamepad.wButtons & button.first) == 0))
					{
						// Update last press time
						lastPress.erase(button.first);
						lastPress.insert(std::map<WORD, DWORD>::value_type(button.first, now));

						// Send keyboard event
						SendMessage(targetWindow, WM_KEYDOWN, mapping,
						((previous.Gamepad.wButtons & button.first) == 0? 0 << 30 : 1 << 30));
					}
				}

				// Checking for button release events, will cause the state
				// packet number to be incremented
				if (previous.dwPacketNumber < state.dwPacketNumber)
 				{
 					// If the button was pressed but is no longer pressed
 					if ((state.Gamepad.wButtons & button.first) == 0
 						&& (previous.Gamepad.wButtons & button.first) != 0)
 					{
 						// Get key mapping if one exists
 						WORD mapping = (keyMap.find(button.first) != keyMap.end()?
 							keyMap.find(button.first)->second : button.first);

						// Remove last press time
						lastPress.erase(button.first);

						// Send keyboard event
						SendMessage(targetWindow, WM_KEYUP, mapping, 0);
					}
				}
			}

			// Do keyboard event dispatch processing for analog items
			// (unmapped items won't have events generated for them)
			for (auto item : analogMap)
			{
				WORD mapping = item.second.key;

				switch (item.first) {
				case AnalogButtons::LeftStickLeft:
					sendKeysOnThreshold(AnalogButtons::LeftStickLeft, leftStickX, prevLeftStickX, -item.second.threshold, mapping);
					break;

				case AnalogButtons::LeftStickRight:
					sendKeysOnThreshold(AnalogButtons::LeftStickRight, leftStickX, prevLeftStickX, item.second.threshold, mapping);
					break;

				case AnalogButtons::LeftStickUp:
					sendKeysOnThreshold(AnalogButtons::LeftStickUp, leftStickY, prevLeftStickY, item.second.threshold, mapping);
					break;

				case AnalogButtons::LeftStickDown:
					sendKeysOnThreshold(AnalogButtons::LeftStickDown, leftStickY, prevLeftStickY, -item.second.threshold, mapping);
					break;

				case AnalogButtons::RightStickLeft:
					sendKeysOnThreshold(AnalogButtons::RightStickLeft, rightStickX, prevRightStickX, -item.second.threshold, mapping);
					break;

				case AnalogButtons::RightStickRight:
					sendKeysOnThreshold(AnalogButtons::RightStickRight, rightStickX, prevRightStickX, item.second.threshold, mapping);
					break;

				case AnalogButtons::RightStickUp:
					sendKeysOnThreshold(AnalogButtons::RightStickUp, rightStickY, prevRightStickY, item.second.threshold, mapping);
					break;

				case AnalogButtons::RightStickDown:
					sendKeysOnThreshold(AnalogButtons::RightStickDown, rightStickY, prevRightStickY, -item.second.threshold, mapping);
					break;

				case AnalogButtons::LeftTrigger:
					sendKeysOnThreshold(AnalogButtons::LeftTrigger, leftTrigger, prevLeftTrigger, item.second.threshold, mapping);
					break;

				case AnalogButtons::RightTrigger:
					sendKeysOnThreshold(AnalogButtons::RightTrigger, rightTrigger, prevRightTrigger, item.second.threshold, mapping);
					break;
				}
			}
		}

		return true;
	}
	return false;
}

// Processing of analog item key event dispatch
void Gamepad::sendKeysOnThreshold(AnalogButtons button, float now, float before, float threshold, int key)
{
	// Check whether the item is and was passed the threshold or not
	bool isPressed = (now >= threshold && threshold > 0) || (now <= threshold && threshold < 0);
 	bool wasPressed = (before >= threshold && threshold > 0) || (before <= threshold && threshold < 0);
 	// If currently pressed
 	if (isPressed)
 	{
 		// Repeat interval calculation
 		DWORD now = GetTickCount();
 		DWORD last = (analogLastPress.find(button) != analogLastPress.end()?
 			analogLastPress.find(button)->second : 0);

		unsigned int ms = analogRepeatMs.find(button)->second;

		// Send message (uses same logic as digital buttons)
		if ((now - last >= ms && ms > 0) || last == 0 || (ms == 0 && !wasPressed))
		{
			analogLastPress.erase(button);
			analogLastPress.insert(std::map<AnalogButtons, DWORD>::value_type(button, now));

			SendMessage(targetWindow, WM_KEYDOWN, key, (wasPressed? 1 << 30 : 0 << 30));
		}
	}

	// Same logic as digital buttons
	if (previous.dwPacketNumber < state.dwPacketNumber)
		if (!isPressed && wasPressed)
		{
			analogLastPress.erase(button);

			SendMessage(targetWindow, WM_KEYUP, key, 0);
		}
}

bool Gamepad::IsPressed(WORD button)
{
	return (state.Gamepad.wButtons & button) != 0;
}

void Gamepad::SetDeadzone(float x, float y)
{
	deadzoneX = x;
	deadzoneY = y;
}

void Gamepad::SetWindow(HWND hwnd)
{
	targetWindow = hwnd;
}

void Gamepad::AddKeyMapping(WORD button, int key)
{
	keyMap.erase(button);
	keyMap.insert(std::map<WORD, int>::value_type(button, key));
}

void Gamepad::RemoveKeyMapping(int key)
{
	for (auto it = keyMap.begin(); it != keyMap.end(); ++it)
		if (it->second == key)
		{
			keyMap.erase(it);
			break;
		}
}

void Gamepad::RemoveKeyMappingByButton(WORD button)
{
	keyMap.erase(button);
}

void Gamepad::AddAnalogKeyMapping(AnalogButtons button, float threshold, int key)
{
	AnalogMapping a = { threshold, key };

	analogMap.erase(button);
	analogMap.insert(std::make_pair(button, a));
}

void Gamepad::RemoveAnalogKeyMapping(AnalogButtons button)
{
	analogMap.erase(button);
}

void Gamepad::ClearMappings()
{
	keyMap.clear();
	analogMap.clear();
}

void Gamepad::SetRepeatIntervalMsAll(unsigned int ms)
{
	repeatMs.clear();

	for (auto button : Buttons)
		repeatMs.insert(std::map<WORD, unsigned int>::value_type(button.first, ms));

	analogRepeatMs.clear();

	for (int i = 0; i < AnalogButtons::EndOfButtons; i++)
		analogRepeatMs.insert(std::map<AnalogButtons, unsigned int>::value_type((AnalogButtons) i, ms));
}

void Gamepad::SetRepeatIntervalMs(WORD button, unsigned int ms)
{
	repeatMs.erase(button);
	repeatMs.insert(std::map<WORD, unsigned int>::value_type(button, ms));
}

void Gamepad::SetAnalogRepeatIntervalMs(AnalogButtons button, unsigned int ms)
{
	analogRepeatMs.erase(button);
	analogRepeatMs.insert(std::map<AnalogButtons, unsigned int>::value_type(button, ms));
}

Wrapping up

I hope you found this tutorial useful! For a real-world example of how to add gamepad support to a game by using the Gamepad class, see the Tetris: Adding gamepad support article which applies the Gamepad class to an existing game.

Good luck with your game programming and don’t forget to leave comments below!

  1. September 8, 2014 at 15:58

    seriously wdf going on here people, why provide a great piece of code but with the exception of it using external code that cannot be imported to vs…
    how the hell is anyone meant to import boost/assign.hpp to vs when its a header file thats invalid, and not provided with this code or even as part of the c++ libraries,
    please explain whats going on here and what is this boost/assign package because its using a / symbol which is invalid as a filename unless its a package of folders

  2. KEoma
    July 13, 2014 at 17:36

    where should i put these files?

  1. August 30, 2013 at 16:01
Comments are closed.