Home > Game Development Walkthroughs > Tetris: Adding gamepad support

Tetris: Adding gamepad support


In this article, we will look at how to add gamepad support to a game using our Tetris clone as an example.

Simple2D 1.11 includes gamepad support using XInput – the new replacement for DirectInput in DirectX 11 – and makes it ridiculously easy to add support to existing games as we shall see here. If you would rather code everything yourself and want the nitty gritty, check out my 2-part mini-series XInput Tutorial: Adding gamepad support to your Windows game for the low-level details.

Download (SimpleTetris 1.5): Source code | Executable

Dependencies:

Setting up the gamepad

In the linker inputs, add a reference (if you don’t already have one) to xinput9_1_0.lib.

In the SimpleTetris application class definition, you just need to add one line of code:

// Gamepad
Gamepad gamepad;

The existing Tetris game uses Windows keyboard events (WM_KEYDOWN and WM_KEYUP) to respond to player input, so to avoid having to add gamepad-specific code to our keyboard input handlers, we will map the digital buttons and analog sticks on the controller to keyboard key presses.

In the main class constructor, we enable dispatch of controller button presses as Windows keyboard events and set the repeat interval of all the buttons, analog sticks and triggers to 100ms:

// Enable gamepad support
gamepad.EnableKeyEvents();
gamepad.SetRepeatIntervalMsAll(100);

The EnableKeyEvents() function turns on keyboard event dispatch and targets the Simple2D application’s main window. If you want to target a different window, use SetWindow(HWND).

At the very end of our per-frame UpdateObjects() function (right after the audio engine is updated), add in one line of code to fetch the game controller state:

// Update gamepad state
gamepad.Refresh();

This automatically deals with controller connections and disconnections and tracks the controller state internally in the Gamepad class. If no controller is connected, no keyboard events will be dispatched and the USB ports will be polled periodically to find any connected controllers. As soon as a controller is connected, dispatch of keyboard events will be resumed. No error is reported if the controller is disconnected but if you need to check the connection state, bool Gamepad::CheckConnection() will return true if at least one controller is connected.

The controller used is the first on found on the (typically) four available ports.

Configuring the controller-to-keyboard mappings

When using XInput directly, we have to poll the controller each frame and then check each button we are interested in. Simple2D’s Gamepad class handles this automatically for us when we call Refresh() each frame, and dispatches any keyboard mappings we have set up to the Windows message queue. So all we have to do to make the game work with a gamepad is to set up the keyboard mappings.

The game can be in various states – main menu, credits screen, main game screen, paused etc. and the gamepad buttons will perform different actions depending on which game state we are currently in. Our Tetris implementation includes a SetGameState() function which is called whenever the game state changes. This is the perfect place to re-map the controller buttons. Let’s tackle the intro sequence and credits sequence states first, where we just want pressing A on the controller to skip the sequence:

switch (gameState) {

case IntroSequence:
    // Start the intro sequence
	generateIntroSequence();

	// Allow skipping of intro sequence with game controller by pressing A
	gamepad.ClearMappings();
	gamepad.SetRepeatIntervalMs(XINPUT_GAMEPAD_A, 0);
	gamepad.AddKeyMapping(XINPUT_GAMEPAD_A, VK_SPACE);
	break;

case CreditsSequence:
    // Stop the main menu music and start the credits sequence
	menuMusic.Stop();
	SetScene(credits);

	// Allow returning to main menu with game controller by pressing A
	gamepad.ClearMappings();
	gamepad.SetRepeatIntervalMs(XINPUT_GAMEPAD_A, 0);
	gamepad.AddKeyMapping(XINPUT_GAMEPAD_A, VK_SPACE);
	break;

The A button is mapped to the space bar, and the repeat rate is set to zero to ensure that only one WM_KEYDOWN event is sent. If we don’t do this, the event will be sent continuously until we release the button, which for example if we go to the main menu can cause the game to start immediately, so we want to avoid that.

When in the game over state, we want to do the same thing – just allow the A button to continue on to high score name entry or the main menu:

case GameOver:
    // Fade out the in-game music and play the game over sound effect
	gameMusic[trkIndex].Fade(300);
	gameOverEffect.Play();

	// Allow returning to main menu with game controller by pressing A
	gamepad.ClearMappings();
	gamepad.AddKeyMapping(XINPUT_GAMEPAD_A, VK_SPACE);
	break;

When entering a name on the high score tables, we don’t want the controller to do anything:

case EnterName:
    // ...code elided...

    // Gamepad
    gamepad.ClearMappings();

By clearing any previously set mappings, no buttons on the controller will respond to being pressed.

In-game keyboard mappings

The bulk of the work is in the main game playing state. This state is entered either from the main menu or when the game has been paused and is subsequently unpaused.

The code looks like this:

case Playing:
    // ...code elided...

	// Gamepad support
	gamepad.ClearMappings();
	gamepad.AddKeyMapping(XINPUT_GAMEPAD_DPAD_LEFT, (*gameKeys)[TetrisControls::Left]);
	gamepad.AddKeyMapping(XINPUT_GAMEPAD_DPAD_RIGHT, (*gameKeys)[TetrisControls::Right]);
	gamepad.AddKeyMapping(XINPUT_GAMEPAD_DPAD_DOWN, (*gameKeys)[TetrisControls::Down]);
	gamepad.AddKeyMapping(XINPUT_GAMEPAD_A, (*gameKeys)[TetrisControls::Rotate]);
	gamepad.AddKeyMapping(XINPUT_GAMEPAD_LEFT_SHOULDER, (*gameKeys)[TetrisControls::Shift]);
	gamepad.AddKeyMapping(XINPUT_GAMEPAD_RIGHT_SHOULDER, (*gameKeys)[TetrisControls::Shift]);
	gamepad.AddKeyMapping(XINPUT_GAMEPAD_X, (*gameKeys)[TetrisControls::FastDrop]);
	gamepad.AddKeyMapping(XINPUT_GAMEPAD_START, (*gameKeys)[TetrisControls::Pause]);
	gamepad.SetRepeatIntervalMsAll(100);
	gamepad.SetRepeatIntervalMs(XINPUT_GAMEPAD_A, 0);
	gamepad.SetRepeatIntervalMs(XINPUT_GAMEPAD_X, 0);

	gamepad.AddAnalogKeyMapping(Gamepad::AnalogButtons::LeftStickLeft, 0.1f, (*gameKeys)[TetrisControls::Left]);
	gamepad.AddAnalogKeyMapping(Gamepad::AnalogButtons::LeftStickRight, 0.1f, (*gameKeys)[TetrisControls::Right]);
	gamepad.AddAnalogKeyMapping(Gamepad::AnalogButtons::LeftStickDown, 1.f, (*gameKeys)[TetrisControls::Down]);

	break;

The object (*gameKeys) is a mapping of game control enum values to the keys the user has defined for controlling the game. Since the player can re-define the controls from the main menu, it is important to re-configure the controller mappings every time a new game starts, otherwise they could be mapped to the wrong keys.

Let’s take a closer look at what is happening:

  • all existing mappings are cleared
  • the directional pad left, right and down arrows are mapped to the corresponding movement keys
  • the A button is mapped to the rotate key
  • both of the shoulder buttons (LB and RB) are mapped to the shift piece key
  • the X button is mapped to the fast drop key
  • the Start button is mapped to the pause key (pressing Start again will unpause the game)
  • the repeat interval for all the buttons is set to 100ms (see below)
  • the repeat interval for the A and X buttons are set to zero (see below)
  • the left analog stick X-axis is mapped to the left and right movement keys with a deadzone of 10%
  • the left analog stick Y-axis (down direction only) is mapped to the down movement key with a deadzone of 100%

Setting the repeat rate to 100ms means that if the user holds down one of the mapped buttons above, a WM_KEYDOWN message will be re-sent every 100ms. This allows the user to hold down an arrow and have the piece move continuously, one grid square per 100ms, without having to re-tap the button for each movement and without the piece zooming around wildly because of no repeat rate being set and a continuous stream of WM_KEYDOWN messages being sent. If you think about how keyboard input usually works, this is the behaviour we want: when you hold down a key on the keyboard, it only repeats every so often, not continuously. So we emulate this behaviour for the controller.

By setting the repeat rate of A and X to zero, we prevent any repeats, meaning that only one WM_KEYDOWN message is sent no matter how long the user holds down the button. Since these buttons are mapped to rotate and fast drop, this prevents rapid spinning of a piece or fast dropping of multiple pieces causing the game to end prematurely. When the user presses fast drop, we only want the current piece to be dropped!

On the left analog stick we have set deadzones. By setting the X-axis deadzone to 10%, we only register movement of the stick if it has been pushed at least 10% away from its center resting position. This is to prevent minute movements of the stick caused by air or the user placing their thumb on it from causing unwanted moevments. The deadzone for the Y-axis is 100%, meaning that the user has to press the stick all the way down to cause a downward movement. This prevents accidental downward movement from occurring when the user is moving the stick left and right; users tend to move the stick slightly on the Y-axis as well when they do this, so we have to take this into account by setting a large deadzone on the axis.

Allowing the user to start the game from the main menu

The previous version of Tetris forces the user to click the Play button on the main menu with the mouse before the game will start. We can easily circumvent this problem by adding a simple piece of code to allow them to press the space bar to start the game, then mapping the A button on the controller to it. In the OnKeyDown() handler, we add some code at the very beginning for this:

bool SimpleTetris::OnKeyDown(int key, int rc, bool prev)
{
	// Space pressed in main menu starts new game
	if (gameState == Menu && key == ' ' && !prev)
	{
		newGame();
		return true;
	}

    // ...
}

and over in SetGameState():

case Menu:
	// Stop intro/game/credits sequence
	SetScene(mainMenuScene);

    // Start the menu music if applicable
	if (previousState != MenuOptions && previousState != EnterName)
		menuMusic.Start();

	// Allow starting game with game controller by pressing A
	gamepad.ClearMappings();
	gamepad.SetRepeatIntervalMs(XINPUT_GAMEPAD_A, 0);
	gamepad.AddKeyMapping(XINPUT_GAMEPAD_A, VK_SPACE);
	break;

And that is quite literally all there is to it. As you can see, adding gamepad support is really trivial using the Gamepad class. Many gamers prefer to use gamepads so the small time investment is well worth the effort.

Side note: Analog movement and acceleration

Notice that we haven’t covered using analog sticks for actual analog movement here: we have converted any movement beyond the deadzone into ‘pressed’ and any movement below it into ‘not pressed’. However, applying analog movement is also extremely easy. Gamepad provides float public fields leftStickX, leftStickY, rightStickX, rightStickY, leftTrigger and rightTrigger which contain the percentages that these items are moved (from -1 to +1 where -1 is the far left or far down and +1 is far right or far up; the triggers return values from 0-1 indicating how far they are depressed). If you have a game which uses acceleration on the player, and pressing a keyboard movement key provides your standard desired amount of per-frame acceleration, you can simply multiply this value by one of the public fields to reduce the acceleration depending on how far the stick or trigger has been pressed. You can configure the stick X and Y axis deadzones by calling SetDeadzone(float x, float y).

That was simplez!

I hope you found the tutorial useful. Don’t forget to leave your comments below! Until next time!

  1. Cortez James
    September 7, 2021 at 14:10

    I’d like to see a part 3 of the xinput tutorial, for function keys. So that when you hold a specific button it changes the key mappings to another set, but only while the specific button is held, on release the key mappings return to the original set of key mappings.

  2. Frederick Lombardi
    December 4, 2017 at 10:49

    This is great! Thank you very much! I was wondering if you could also post a standalone editable X_Input game pad to keyboard keys tutorial. Seems doable from gamepad.cpp, so that we could use it for existing dos and retro games that are pre compiled. Something like a simple window that would allow keyboard key bindings to controller buttons, so that we could bind those keys to the game bindings and play! Thanks again! You rox my sox!

  3. September 8, 2014 at 15:41

    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

    • Dan Martinez
      April 22, 2015 at 17:46

      I realize I’m late on this reply, but to answer your eloquently stated question, Boost is a very well-known and widely used library set. A lot of people who work on Boost are on the C++ standards (ISO) group, and parts of Boost have even made it into the official C++ standard itself.
      See also:
      http://www.boost.org
      http://www.grammarbook.com/punctuation/capital.asp

  1. May 13, 2018 at 14:29
  2. August 30, 2013 at 16:49
  3. August 30, 2013 at 16:49

Share your thoughts! Note: to post source code, enclose it in [code lang=...] [/code] tags. Valid values for 'lang' are cpp, csharp, xml, javascript, php etc. To post compiler errors or other text that is best read monospaced, use 'text' as the value for lang.

This site uses Akismet to reduce spam. Learn how your comment data is processed.