SDL2 controller implementation

This tutorial will show how to implement a controller in your application using SDL2. The controller input has two categories, the BUTTONS and the AXIS. The buttons have an state of 0 is the button is not pressed and 1 if the button is pressed. For the axis we can get a value from -32768 to 32767 for the directional axis and from 0 to 32767 for the triggers.

SDL2 gives us the possibility to add mappings manually using SDL_GameControllerAddMapping to map any controller, but we are going to use the easy way. The function SDL_GameControllerAddMappingsFromFile allow us to load the mappings of different controllers at once. But where do we find the mappings? Thanks to Gabriel Jacobo and the community that maintains it we can use an update version of a database (gamecontrollerdb.txt) that contains the mappings for Windows, OS X and Linux.

Like in the other tutorials I always try to abstract the different systems trying to avoid the use of the SDL events to control the input. In this we are going to use only two events, to detect when a controller connects to the game (SDL_CONTROLLERDEVICEADDED) and when disconnects (SDL_CONTROLLERDEVICEREMOVED).

In this case we are going to define code to work with only one controller. We are going to use the SDL_GameControllerButton enum and the SDL_GameControllerAxis enum to allocate the memory during the object creation avoiding to allocate memory dynamically. Lets define the header:

#ifndef __INPUTCONTROLLER_H__
#define __INPUTCONTROLLER_H__
#pragma once
///////////////////////////////

class InputController
{
public:
	///--- Constructor
	InputController();

	///--- Object control functions
	bool Initialize(void);
	void Update(void);
	void Release(void);

	///--- To receive the connect and disconnect event
	void ReceiveEvent(const SDL_Event& oEvent);

	///--- Functions to get the state of the buttons
	bool IsControllerButtonTriggered(const SDL_GameControllerButton iButton) const;
	bool IsControllerButtonPressed(const SDL_GameControllerButton iButton) const;
	bool IsControllerButtonReleased(const SDL_GameControllerButton iButton) const;

	///--- Function to get the current value of the axis
	float GetAxisValue(const SDL_GameControllerAxis iAxis) const;
private:
	///--- Controller information
	SDL_GameController* m_pGameController;
	int m_iWichController;
	
	///--- Information about the state of the controller
	Uint8 m_uButtonStates[SDL_CONTROLLER_BUTTON_MAX];
	Uint8 m_uButtonStatesPrev[SDL_CONTROLLER_BUTTON_MAX];
	float m_fAxisValues[SDL_CONTROLLER_AXIS_MAX];
};

///////////////////////////////
#endif   //__INPUTCONTROLLER_H__

As always in the constructor we are going to initialize the variables to avoid any problem:

InputController::InputController()
{
	///--- Set the controller to NULL
	m_pGameController=NULL;
	m_iWichController=-1;

	///--- Set the buttons and axis to 0
	memset(m_uButtonStates, 0, sizeof(Uint8)*SDL_CONTROLLER_BUTTON_MAX);
	memset(m_uButtonStatesPrev, 0, sizeof(Uint8)*SDL_CONTROLLER_BUTTON_MAX);
	memset(m_fAxisValues, 0, sizeof(float)*SDL_CONTROLLER_AXIS_MAX);
}

Now that we have the memory set to 0 we have to initialize the object to load the controllers and deactivate the events since we are going to get the information from the controller itself:

bool InputController::Initialize(void)
{
	///--- Initialize InputController
	SDL_Init(SDL_INIT_GAMECONTROLLER);

	///--- Load the gamecontrollerdb.txt and check if there was any problem
	int iNumOfControllers=SDL_GameControllerAddMappingsFromFile("gamecontrollerdb.txt");
	if(iNumOfControllers==-1) {
		SDL_LogWarn(SDL_LOG_CATEGORY_INPUT, "Error loading database [%s]", SDL_GetError());
		return false;
	}

	///--- Ignore the controller events
	SDL_GameControllerEventState(SDL_IGNORE);
	return true;
}

Now there is all ready to start getting the controller information. This time we have to call the function SDL_GameControllerUpdate to tell SDL to update the controller information since the events are deactivated. The update function must be called all the frames to maintain the information of the previous and current frame updated:

void InputController::Update(void)
{
	///--- If there is no controllers attached exit
	if(m_pGameController==NULL) {
		return;
	}

	///--- Copy the buttons state to the previous frame info
	memcpy(&m_uButtonStatesPrev, &m_uButtonStates, sizeof(Uint8)*SDL_CONTROLLER_BUTTON_MAX);

	///--- Update the controller SDL info
	SDL_GameControllerUpdate();
	
	///--- Obtain the current button values
	for(int b=0; b<SDL_CONTROLLER_BUTTON_MAX; ++b) {
		m_uButtonStates[b]=SDL_GameControllerGetButton(m_pGameController, (SDL_GameControllerButton)b);
	}
	///--- Obtain the current axis value
	for(int a=0; a<SDL_CONTROLLER_AXIS_MAX; ++a) {
		m_fAxisValues[a]=SDL_GameControllerGetAxis(m_pGameController, (SDL_GameControllerAxis)a);
	}
}

As you can see we have a check at the beginning of the update function so if there is no controller attached we do not update anything. So we have to detect when a controller is added and removed from the system, for this we have to receive the events explained at the beginning of this tutorial:

void InputController::ReceiveEvent(const SDL_Event& oEvent)
{
	switch(oEvent.type) {
		///--- Controller added event
		case SDL_CONTROLLERDEVICEADDED:
		{
			///--- If there is no controller attached
			if(m_pGameController!=NULL) {
				///--- Open the controller
				m_iWichController=oEvent.cdevice.which;
				m_pGameController=SDL_GameControllerOpen(m_iWichController);
				///--- Set the memory to 0 to avoid problems with previous added controllers
				memset(m_uButtonStates, 0, sizeof(Uint8)*SDL_CONTROLLER_BUTTON_MAX);
				memset(m_uButtonStatesPrev, 0, sizeof(Uint8)*SDL_CONTROLLER_BUTTON_MAX);
				memset(m_fAxisValues, 0, sizeof(float)*SDL_CONTROLLER_AXIS_MAX);
			}
			break;
		}
		///--- Controller removed event
		case SDL_CONTROLLERDEVICEREMOVED:
		{
			///--- Check if is the same controller
			if(m_iWichController=oEvent.cdevice.which) {
				m_iWichController=-1;
				m_pGameController=NULL;
			}
			break;
		}
	}
}

Now we have all ready to start getting the information. For the triggered function we have to check if the button was 0 during the previous frame and 1 during the current one:

bool InputController::IsControllerButtonTriggered(const SDL_GameControllerButton iButton) const
{
	return (m_uButtonStates[iButton]==1&&m_uButtonStatesPrev[iButton]==0);
}

For the pressed state we have only to check the current button state:

bool InputController::IsControllerButtonPressed(const SDL_GameControllerButton iButton) const
{
	return (m_uButtonStates[iButton]==1);
}

And for the release just do the opposite than the triggered

bool InputController::IsControllerButtonReleased(const SDL_GameControllerButton iButton) const
{
	return (m_uButtonStates[iButton]==0&&m_uButtonStatesPrev[iButton]==1);
}

The only function left is the axis value, that is only a return of the selected axis:

float InputController::GetAxisValue(const SDL_GameControllerAxis iAxis) const
{
	return m_fAxisValues[iAxis];
}

The only thing left to be careful with are the called dead zones. The directional axis when idle not always return a value of 0. That imprecision may cause problems if the game uses the values to move at different velocities. There isn’t an standard for the values of this zone but if we take a look ad the XInput Microsoft tutorial there is a section about the dead zones that define some values we can use.

#define XINPUT_GAMEPAD_LEFT_THUMB_DEADZONE  7849
#define XINPUT_GAMEPAD_RIGHT_THUMB_DEADZONE 8689
#define XINPUT_GAMEPAD_TRIGGER_THRESHOLD    30

Feel free to use this code without any restriction and share this page if you think this may be useful for other developers.

Leave a Reply

Your email address will not be published. Required fields are marked *

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