Selected Article

XInput Tutorial

Posted on: 2024-12-28, Updated on: 2024-12-28 03:55:42 | Read time: 13.9 minutes

Topic(s): programming & gamedev

The XInput API is used for gathering input information from Xbox 360 and Xbox One controllers on Windows and Xbox operating systems. The API is relatively simple in that it only relies on a few functions and structures.

XBox Controller

Setup

I'll be making a regular console application just to show that things work. To use XInput we need to link against a version. Available versions range from v1.3, v9.1.0, and v1.4 (in that order). All of which have their advantages and disadvantages that can be seen here on MSDN: XInput Versions. I will be using v1.4 which is the newest one available on Windows 10. Go to Project->Properties->Linker->Input->Additional Dependencies and add xinput.lib. You may also wish to use Visual C/C++'s #pragma comment(lib, "xinput.lib") as a easier way to link the library with the preprocessor.

The Class

Go to Project->Add Class and name the class Gamepad in the class Wizard. Now we can start coding in the Gamepad.h header file

#pragma once

#define WIN32_LEAN_AND_MEAN
#define NOMINMAX
#include <Windows.h>

#pragma comment(lib, "xinput.lib")
#include <XInput.h>
#include <queue>
#include <string>
#include <optional>

We add our include directives at the top of the header file. We define NOMINMAX before Windows because we will use the std::min and std::max functions later in our member function definitions of the Gamepad class. This removes the MIN and MAX macros normally in the Windows header that could potentially interfere with the standard library min and max functions. The WIN32_LEAN_AND_MEAN macro excludes some of the less important parts of the Win32 API. And last but not least, we include the XInput header.

We can then begin the declaration for the Gamepad class:

class Gamepad
{
	// Soon to be filled in...
};

Nested Types

The Gamepad class will declare several nested types: Button, ButtonEvent, Axis, and Deadzone to make interacting with the class easier for the user.

Button

	// Inside of Gamepad class declaration
public:
	enum class Button : unsigned int
	{
		DPAD_UP = XINPUT_GAMEPAD_DPAD_UP,
		DPAD_DOWN = XINPUT_GAMEPAD_DPAD_DOWN,
		DPAD_LEFT = XINPUT_GAMEPAD_DPAD_LEFT,
		DPAD_RIGHT = XINPUT_GAMEPAD_DPAD_RIGHT,
		START = XINPUT_GAMEPAD_START,
		BACK = XINPUT_GAMEPAD_BACK,
		LEFT_THUMB = XINPUT_GAMEPAD_LEFT_THUMB,
		RIGHT_THUMB = XINPUT_GAMEPAD_RIGHT_THUMB,
		LEFT_SHOULDER = XINPUT_GAMEPAD_LEFT_SHOULDER,
		RIGHT_SHOULDER = XINPUT_GAMEPAD_RIGHT_SHOULDER,
		A = XINPUT_GAMEPAD_A,
		B = XINPUT_GAMEPAD_B,
		X = XINPUT_GAMEPAD_X,
		Y = XINPUT_GAMEPAD_Y,
	};

The Button scoped enum provides aliases for the XINPUT_GAMEPAD_ button constants that are easier to write.

ButtonEvent

	// Button declaration above...	
	class ButtonEvent
	{
	public:
		enum class Type
		{
			PRESS,
			RELEASE,
		};
	private:
		Type type;
		Button button;
	public:
		ButtonEvent( Type type, Button button ) noexcept
			: 
			type( type ),
			button ( button )
		{}
		bool IsPress() const noexcept
		{
			return type == Type::PRESS;
		}
		bool IsRelease() const noexcept
		{
			return type == Type::RELEASE;
		}
		Button GetButton() const noexcept
		{
			return button;
		}
	};

The ButtonEvent nested class encapsulates a Button value with the scoped enum ButtonEvent::Type that indicates whether a button was pressed or released.

Axis & Deadzone

	// ButtonEvent declaration above...
	struct Axis
	{
		float x, y;
	};
	using Deadzone = Axis;

The Axis struct simply contains two members, x and y, for representing an axis with two directions such as the controller's thumbsticks. Deadzone is an alias for indicating the values are intended to applied as an axis deadzone. Deadzones represent a small area in which the left and right thumbsticks will not read values starting in the center. This is because not all joysticks are perfect and might read in very small values when not used causing the camera in a first person game, for example, to be very jittery.

Constructors/Special Member Functions

public:
	// id is a number of 1-4 but is represented by 0-3 in the API
	explicit Gamepad( unsigned int id, const Deadzone& deadzone = {
		XINPUT_GAMEPAD_LEFT_THUMB_DEADZONE,
		XINPUT_GAMEPAD_RIGHT_THUMB_DEADZONE
	} );
	Gamepad( const Gamepad& ) = delete;
	Gamepad& operator=( const Gamepad& ) = delete;
	Gamepad( Gamepad&& ) noexcept = delete;
	Gamepad& operator=( Gamepad&& ) noexcept = delete;

The sole constructor takes in an id parameter that will be used to initialize controllerID. It also takes an optional deadzone parameter which is default initialized to the XINPUT_GAMEPAD_THUMB_DEADZONE constants. The copy and move constructors/assignment operators are marked as deleted to prevent copy or move construction.

Public Member Functions

	UINT GetControllerID() const noexcept;
	XINPUT_GAMEPAD* GetGamepad() noexcept;
	const XINPUT_GAMEPAD* GetGamepad() const noexcept;
	bool IsConnected();
	bool Update();
	void Vibrate( unsigned short leftSpeed, unsigned short rightSpeed );
	void Vibrate( unsigned short bothSpeed );
	void ResetVibration();
	bool IsButtonPressed( Button button ) const;
	std::optional<ButtonEvent> ReadButtonBuffer() noexcept;
	Axis& LeftStick() noexcept;
	const Axis& LeftStick() const noexcept;
	Axis& RightStick() noexcept;
	const Axis& RightStick() const noexcept;
	float LeftTrigger() const noexcept;
	float RightTrigger() const noexcept;
	bool GetAudioDeviceIDs(
		const std::wstring& pRenderDeviceId, unsigned int* pRenderCount,
		const std::wstring& pCaptureDeviceId, unsigned int* pCaptureCount
	) const;
	XINPUT_CAPABILITIES* GetCapabilities( unsigned long flags = 0u );
	const XINPUT_CAPABILITIES* GetCapabilities(unsigned long flags = 0u) const;
	void SetDeadzone( const Deadzone &deadzone ) noexcept;
	Deadzone& GetDeadzone() noexcept;
	const Deadzone& GetDeadzone() const noexcept;
	bool ButtonIsEmpty() const noexcept;
	void Flush() noexcept;
	// XInputGetBatteryInformation is not supported for 9.1.0
	// XINPUT_BATTERY_INFORMATION* GetBatteryInfo();
	// void Enable(bool value) const; no supported for 9.1.0

These functions will be explained in the definitions to keep things clear. Functions regarding XINPUT_BATTERY_INFORMATION are of course commented out because they do not work in XInput v1.4.

Private Class Members

private: // axis & trigger values
	Axis leftStick;
	Axis rightStick;
	float leftTrigger, rightTrigger;
private:
	void OnButtonPressed( Button button ) noexcept;
	void OnButtonReleased( Button button ) noexcept;
	static float ApplyDeadzone( float value, float maxValue, float deadzone );
	template <typename buf>
	static void TrimBuffer( std::queue<buf>& buffer ) noexcept
	{
		while ( buffer.size() > bufferSize )
			buffer.pop(); // [[yesdiscard]] xD
	}
private:
	static constexpr unsigned int bufferSize = 16u;
	static constexpr float maxAxisValue = 1.0f;
	static constexpr float triggerThreshold = XINPUT_GAMEPAD_TRIGGER_THRESHOLD / 255.0f;

	unsigned int controllerID;
	Deadzone deadzone;

	mutable XINPUT_STATE state;
	mutable XINPUT_VIBRATION vibration;
	mutable XINPUT_BATTERY_INFORMATION battery;
	mutable XINPUT_CAPABILITIES capabilities;

	std::queue<ButtonEvent> buttonbuffer;

Now we will declare the private class members. The leftStick and rightStick members contain the x and y values of the left and right stacks in the range of -1.0 to 1.0. The leftTrigger and rightTrigger members contain the values of the left and right trigger axes in the range of 0.0 to 1.0.

The member functions void OnButtonPressed( Button ) noexcept and void OnButtonReleased( Button ) noexcept process buttons as they are pressed and released. The static member function void TrimBuffer( std::queue<buf>& ) noexcept is used to limit the buttonBuffer to bufferSize elements. The static member function float ApplyDeadzone( float, float, float ) takes in the value of the axis, the max value of that axis, and the deadzone (in the range -1.0 to 1.0) to be applied.

The maxAxisValue constant will be used as a constant for the normalized(a value from -1.0f to +1.0f) max value of our controller's left and right joysticks. The controllerID represents the selected controller that we will perform operations on. Windows allows for up to 4 controllers to be connected at once. In our class the range of the controllerID will be 1-4 but XInput functions expect a number ranging from 0-3. So we will simply subtract one from the controllerID when we use it as a parameter. The deadzone member contains the deadzone that will be applied to both the left and right thumbsticks. The XINPUT_STATE structure contains state of all of the controllers buttons in a XINPUT_GAMEPAD member, as seen here:

typedef struct _XINPUT_STATE {
  DWORD          dwPacketNumber;
  XINPUT_GAMEPAD Gamepad;
} XINPUT_STATE, *PXINPUT_STATE;

The XINPUT_VIBRATION structure consists of the speeds for the left and right force-feedback motors housed in the controller:

typedef struct _XINPUT_VIBRATION {
  WORD wLeftMotorSpeed;
  WORD wRightMotorSpeed;
} XINPUT_VIBRATION, *PXINPUT_VIBRATION;

The XINPUT_BATTERY_INFORMATION structure is commented out because it is not available in XInput v1.4.

The buttonbuffer queue is used to maintain a sequence of the most recent ButtonEvent's. The queue's capacity is capped by the bufferSize constant.

The Class Definition

Now we can move to the Gamepad.cpp source file.

#include "Gamepad.h"
#include <algorithm>
#include <climits>
#include <cmath>

Let's start by adding our include directories. We will of course include our Gamepad.h header file and algorithm which includes std::min and std::max which we will use later for working with deadzones. climits has constants for the maximum and minimum numbers that a variable type can hold which will be normalizing the axis values. cmath is used for some operations regarding the gamepad's axis values.

static float Normalize( float value, float min, float max );

Gamepad::Gamepad( unsigned int id, const Deadzone& deadzone )
	: controllerID( id ), deadzone( deadzone )
{
	ZeroMemory( &state, sizeof(XINPUT_STATE) );
	ZeroMemory( &vibration, sizeof(XINPUT_VIBRATION) );
	ZeroMemory( &battery, sizeof(XINPUT_BATTERY_INFORMATION) );
	ZeroMemory( &capabilities, sizeof(XINPUT_CAPABILITIES) );
}

We'll start with a forward declaration of a non-member function Normalize, which I will explain later. Then we define the Gamepad constructor which uses the initializer list to initialize controllerID and the deadzone. The deadzones are initialized with the XInput constants:

// In the XInput header:
#define XINPUT_GAMEPAD_LEFT_THUMB_DEADZONE 7849
#define XINPUT_GAMEPAD_RIGHT_THUMB_DEADZONE 8689

These are likely meant to be SHORT values so we will have to normalize these later on. In the constructor body we call ZeroMemory on both the XINPUT_STATE and XINPUT_VIBRATION member structures to give them default values.

UINT Gamepad::GetControllerID() const noexcept
{
	return this->controllerID - 1;
}
XINPUT_GAMEPAD* Gamepad::GetGamepad() noexcept
{
	return &this->state.Gamepad;
}
const XINPUT_GAMEPAD *Gamepad::GetGamepad() const noexcept
{
	return &this->state.Gamepad;
}
/*XINPUT_BATTERY_INFORMATION* Gamepad::GetBatteryInfo()
{
	XInputGetBatteryInformation(controllerID, XINPUT_DEVTYPE_GAMEPAD, &battery);
	return &battery;
}*/
bool Gamepad::IsConnected()
{
	return ( XInputGetState( this->controllerID - 1, &state ) == ERROR_SUCCESS )
		? true : false;
}

The GetControllerID member function returns the controller ID number. The GetGamepad member function and its const overload return a pointer to the XINPUT_GAMEPAD type. The GetBatteryInfo member function implementation is commented out since the API version used in this tutorial doesn't support retrieving battery information. The IsConnected member function uses the XInputGetState function to update the state structure. The first parameter takes in the ID of the controller ranging from 0-3. Since our controllerID expects class users to enter a value from 1 to 4 we will subtract one from controllerID. The second parameter takes in a pointer to our XINPUT_STATE member so it can be updated. XInputGetState returns a DWORD that either equates to the ERROR_SUCCES or ERROR_DEVICE_NOT_CONNECTED constants. If the call equates to ERROR_SUCCESS the function returns true, otherwise it returns false.

bool Gamepad::Update()
{
	if (!IsConnected())
		return false; // don't update the controller if it's not connected

	if (state.Gamepad.wButtons != 0u) 
	{
		OnButtonPressed( static_cast<Gamepad::Button>(state.Gamepad.wButtons) );
	}
	else
	{
		if (!ButtonIsEmpty())
			OnButtonReleased( buttonbuffer.front().getButton() );
	}

	static constexpr std::numeric_limits<short> limitShrt{};
	static constexpr auto limitMin = limitShrt.min();
	static constexpr auto limitMax = limitShrt.max();
	// Normalize input from the left and right stick's x and y axes.
	const float normLX = Normalize( static_cast<float>(state.Gamepad.sThumbLX),
								   limitMin, limitMax);
	const float normLY = Normalize( static_cast<float>(state.Gamepad.sThumbLY),
								   limitMin, limitMax);

	const float normRX = Normalize( static_cast<float>(state.Gamepad.sThumbRX),
								   limitMin, limitMax);
	const float normRY = Normalize( static_cast<float>(state.Gamepad.sThumbRY),
								   limitMin, limitMax);
	
	// Apply the deadzones to the normalized form of each stick axes.
	leftStick.x = ApplyDeadzone(  normLX, maxAxisValue, Normalize(deadzone.x, limitMin, limitMax) );
	leftStick.y = ApplyDeadzone(  normLY, maxAxisValue, Normalize(deadzone.y, limitMin, limitMax) );
	rightStick.x = ApplyDeadzone( normRX, maxAxisValue, Normalize(deadzone.x, limitMin, limitMax) );
	rightStick.y = ApplyDeadzone( normRY, maxAxisValue, Normalize(deadzone.y, limitMin, limitMax) );
	// Normalize trigger input of BYTE/char limit.
	static constexpr std::numeric_limits<unsigned char> limitChr{};
	leftTrigger = (
		static_cast<float>(state.Gamepad.bLeftTrigger)
		/ static_cast<float>(limitChr.max())
	);
	rightTrigger = (
		static_cast<float>(state.Gamepad.bRightTrigger)
		/ static_cast<float>(limitChr.max())
	);
	return true;
}

In the Update function we first check if the controller is connected, if not it Update returns false and the rest of the function body is not performed. The IsConnected member function also takes care of updating the XINPUT_STATE member. This ensures that the rest of the update will have access to updated controller information. If the controller is connected, the Update member function continues by updating button state, normalizing thumbstick axes, and applying deadzones.

/*void Gamepad::Enable(bool value) const
{
	// XInputEnable is meant for use when an application loses/gains focus
	// for example the WM_ACTIVATEAPP message.
	// you may pass FALSE when you wish for vibrations to stop and vice versa
	XInputEnable(static_cast<BOOL>(value));
}*/

The Enable member function is also commented out like GetBatteryInfo due to the API version.

void Gamepad::Vibrate( unsigned short leftSpeed, unsigned short rightSpeed )
{
	vibration.wLeftMotorSpeed = leftSpeed;
	vibration.wRightMotorSpeed = rightSpeed;
	XInputSetState( GetControllerID(), &vibration );
}
void Gamepad::Vibrate( unsigned short lrSpeed )
{
	vibration.wLeftMotorSpeed = lrSpeed;
	vibration.wRightMotorSpeed = lrSpeed / 2u;
	XInputSetState( GetControllerID(), &vibration );
}
void Gamepad::ResetVibration()
{
	vibration.wLeftMotorSpeed = 0u;
	vibration.wRightMotorSpeed = 0u;
	XInputSetState(GetControllerID(), &vibration);
}

The above member functions set the state of the gamepad's vibration motors. Vibrate takes in two different speeds, one for the left and one for the right. The parameter type is unsigned short because the motor speed range is 0-65535. The XINPUT_VIBRATION contains the wLeftMotorSpeed and wRightMotorSpeed members to control the force feedback motors' speeds. The XInputSetState is specific for updating controller vibration effects. It takes the controllerID as the first parameter and a pointer to the vibration member variable. The overload of Vibrate with a single parameter halves the speed for the right motor since it stronger than the left in XBox 360 and XBox One controllers. ResetVibration simply resets both motor speeds to 0 and updates the vibration state.

bool Gamepad::IsButtonPressed( Button button ) const
{
	return ( state.Gamepad.wButtons & static_cast<unsigned short>(button) ) != 0u;
}

The IsButtonPressed member function returns true if the given gamepad button is currently held down.


std::optional<Gamepad::ButtonEvent> Gamepad::ReadButtonBuffer() noexcept
{
	if ( buttonbuffer.size() > 0u )
	{
		ButtonEvent e = buttonbuffer.front();
		buttonbuffer.pop();
		return e;
	}
	return {};
}

The ReadButtonBuffer member function attempts removes a ButtonEvent from the front of the buttonbuffer. As such this member function can be used to retrieve the most recent button presses.

Gamepad::Axis& Gamepad::LeftStick() noexcept
{
	return leftStick;
}
const Gamepad::Axis& Gamepad::LeftStick() const noexcept
{
	return leftStick;
}
Gamepad::Axis& Gamepad::RightStick() noexcept
{
	return rightStick;
}
const Gamepad::Axis& Gamepad::RightStick() const noexcept
{
	return rightStick;
}
float Gamepad::LeftTrigger() const noexcept
{
	return leftTrigger;
}
float Gamepad::RightTrigger() const noexcept
{
	return rightTrigger;
}

The above member functions simply return the axis values for the left thumbstick, right thumbstick, left trigger, and right trigger.

bool Gamepad::GetAudioDeviceIDs( const std::wstring& pRenderDeviceId, unsigned int* pRenderCount,
	const std::wstring& pCaptureDeviceId, unsigned int* pCaptureCount ) const
{ // I'm not quite sure how to use this one but this should wrap it okay
	const auto result = XInputGetAudioDeviceIds( GetControllerID(),
		const_cast<wchar_t*>(pRenderDeviceId.c_str()), pRenderCount, 
		const_cast<wchar_t*>(pCaptureDeviceId.c_str()), pCaptureCount );
	if (result != ERROR_SUCCESS)
		return false;
	return true;
}
XINPUT_CAPABILITIES* Gamepad::GetCapabilities( unsigned long flags )
{ // returns const pointer to XINPUT_CAPABILITIES structure that details  
  // useful charateristics about the controller with the dwUserIndex parameter
  // you may pass XINPUT_FLAG_GAMEPAD as the value of flags to limit use to Xbox 360 controllers
	const auto result = XInputGetCapabilities(GetControllerID(), flags,
											  &this->capabilities);
	return &this->capabilities;
}
const XINPUT_CAPABILITIES* Gamepad::GetCapabilities( unsigned long flags ) const
{
	const auto result = XInputGetCapabilities(GetControllerID(), flags,
											  &this->capabilities);
	return &this->capabilities;
}

The GetCapabilities member function wraps the XInputGetCapabilities function which returns a XINPUT_CAPABILITIES structure that contains information about the gamepad and what features it supports.

void Gamepad::SetDeadzone( const Deadzone& deadzone ) noexcept
{
	this->deadzone = deadzone;
}
Gamepad::Deadzone& Gamepad::GetDeadzone() noexcept
{
	return deadzone;
}
const Gamepad::Deadzone& Gamepad::GetDeadzone() const noexcept
{
	return deadzone;
}

The GetDeadzone and SetDeadzone member functions simply retrieve and set the current deadzone used by the thumbsticks respectively.

void Gamepad::OnButtonPressed( Button button ) noexcept
{
	using ButtonEventType = ButtonEvent::Type;
	buttonbuffer.push( ButtonEvent( ButtonEventType::PRESS,button ) );
	TrimBuffer( buttonbuffer );
}

void Gamepad::OnButtonReleased( Button button ) noexcept
{
	using ButtonEventType = ButtonEvent::Type;
	buttonbuffer.push( ButtonEvent( ButtonEventType::RELEASE,button ) );
	TrimBuffer( buttonbuffer );
}

bool Gamepad::ButtonIsEmpty() const noexcept
{
	return buttonbuffer.empty();
}

void Gamepad::Flush() noexcept
{
	buttonbuffer = std::queue<Gamepad::ButtonEvent>();
}

OnButtonPressed and OnButtonReleased take care of updating the buttonbuffer. ButtonIsEmpty returns true if the buttonbuffer is empty. Flush clears the buttonbuffer by assigning to an empty queue.

static float Normalize(float value, float min, float max)
{ // Normalize value between min/max range.
	const float average = (min + max) / 2.0f;
	const float range = (max - min) / 2.0f;
	return (value - average) / range;
}

This nonmember function, Normalize, follows a formula to turn our controller's axes values to numbers ranging from -1.0f to 1.0f because XInput's default controller axis values are large values for the type SHORT.

float Gamepad::ApplyDeadzone( float value, float maxValue, float deadzone)
{
	if (value < -(deadzone))
	{
		value += deadzone; // increase neg vals to remove deadzone discontinuity
	}
	else if (value > deadzone)
	{
		value -= deadzone; // decrease pos vals to remove deadzone discontinuity
	}
	else
	{
		return 0.0f; // hey values are zero for once
	}
	const float normValue = value / (maxValue - deadzone);
	return std::max(-1.0f, std::min(normValue, 1.0f));
}

The ApplyDeadone function takes in a value of the axis, maxValue of the axis when normalized(typically 1.0f), and a deadzone. Since deadzones essentaily delete a small area around the origin at which input can be read that means that if there were no further processing then a deadzone of .2f would mean our axis could only read up to .8f in any direction.

Using The Class

Let's write a small program to make use of the class.

#include <iostream>
#include <chrono>
#include <thread>
#include <iomanip>
#include <cstdlib>
#include "Gamepad.h"

Here are our include directives. We of course include iostream to use the std::cout. We will use thread with chrono as a way to sleep this_thread during a while loop. The input output stream manipulations iomanip will be used for rounding off the axis values.

static auto Clear() -> void;

auto main(int argc, char** argv) -> int
{
	using Button = Gamepad::Button;
	Gamepad gamepad(1u);
	std::cout << sizeof(gamepad) << '\n';
	bool aPressed = false;
	if (!gamepad.IsConnected())
	{
		std::cout << "Controller not connected\n";
		std::cout.flush();
		std::cin.get();
		return -1;
	}

We created a gampad object with a controllerID of 1 and bool called a aPressed to check if the A button on the controller is pressed. If the gamepad is not connected it will return.

	while (true)
	{
		if (gamepad.Update())
		{
			::Clear();
			std::cout << std::fixed << std::setprecision(2)
				<< "Left Trigger: " << gamepad.LeftTrigger()
				<< ", Right Trigger: " << gamepad.RightTrigger() << '\n'
				<< "Left Stick " << "X: " << gamepad.LeftStick().x
				<< ", Y: " << gamepad.LeftStick().y << '\n'
				<< "Right Stick " << "X: " << gamepad.RightStick().x
				<< ", Y: " << gamepad.RightStick().y << std::endl;

Here we open a loop and call gamepad::Update in an if statement to make sure it's connected and to refresh the controller state structure and axis information. Later, std::fixed and std::setprecision(2) is inserted into the stream to limit decimal numbers to two places past the decimal.

			if (gamepad.IsButtonPressed(Button::A))
			{
				aPressed = true;
			}
			else
			{                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                              
				aPressed = false;
			}

			if (aPressed == true)
			{
				gamepad.Vibrate(20000, 20000);
			}
			else
			{
				gamepad.ResetVibration();
			}

This code of course uses the Gamepad::IsButtonPressed member function to cause the controller to vibrate while the A button is pressed.

			if (gamepad.IsButtonPressed(Button::BACK))
				break;
		}
		std::this_thread::sleep_for(std::chrono::milliseconds(10));
	}
	std::cin.get();
	return 0;
}

This final section check if the back button is pressed and exits the loop ending the program. At the very end of the loop std::this_thread::sleep_for is called to delay each run of the loop by 10 milliseconds.

That concludes this tutorial. Hopefully it helped you, and if you have any questions please leave a comment.

Resources/Useful Links:

Here is the full source code:

main.cpp

#include <iostream>
#include <chrono>
#include <thread>
#include <iomanip>
#include <cstdlib>
#include "Gamepad.h"

static auto Clear() -> void;

auto main(int argc, char** argv) -> int
{
	using Button = Gamepad::Button;
	Gamepad gamepad(1u);
	std::cout << sizeof(gamepad) << '\n';
	bool aPressed = false;
	if (!gamepad.IsConnected())
	{
		std::cout << "Controller not connected\n";
		std::cout.flush();
		std::cin.get();
		return -1;
	}

	while (true)
	{
		if (gamepad.Update())
		{
			::Clear();
			std::cout << std::fixed << std::setprecision(2)
				<< "Left Trigger: " << gamepad.LeftTrigger()
				<< ", Right Trigger: " << gamepad.RightTrigger() << '\n'
				<< "Left Stick " << "X: " << gamepad.LeftStick().x
				<< ", Y: " << gamepad.LeftStick().y << '\n'
				<< "Right Stick " << "X: " << gamepad.RightStick().x
				<< ", Y: " << gamepad.RightStick().y << std::endl;

			if (gamepad.IsButtonPressed(Button::A))
			{
				aPressed = true;
			}
			else
			{                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                              
				aPressed = false;
			}

			if (aPressed == true)
			{
				gamepad.Vibrate(20000, 20000);
			}
			else
			{
				gamepad.ResetVibration();
			}

			if (gamepad.IsButtonPressed(Button::BACK))
				break;
		}
		std::this_thread::sleep_for(std::chrono::milliseconds(10));
	}
	std::cin.get();
	return 0;
}

inline static auto Clear() -> void
{
#ifdef _WIN32 || _WIN64
	std::system("cls");
#else
	std::system("Clear");
#endif
}

Gamepad.h

#pragma once

#define WIN32_LEAN_AND_MEAN
#define NOMINMAX
#include <Windows.h>

#pragma comment(lib, "xinput.lib")
#include <XInput.h>
#include <queue>
#include <string>
#include <optional>

class Gamepad
{
public:
	enum class Button : unsigned int
	{
		DPAD_UP = XINPUT_GAMEPAD_DPAD_UP,
		DPAD_DOWN = XINPUT_GAMEPAD_DPAD_DOWN,
		DPAD_LEFT = XINPUT_GAMEPAD_DPAD_LEFT,
		DPAD_RIGHT = XINPUT_GAMEPAD_DPAD_RIGHT,
		START = XINPUT_GAMEPAD_START,
		BACK = XINPUT_GAMEPAD_BACK,
		LEFT_THUMB = XINPUT_GAMEPAD_LEFT_THUMB,
		RIGHT_THUMB = XINPUT_GAMEPAD_RIGHT_THUMB,
		LEFT_SHOULDER = XINPUT_GAMEPAD_LEFT_SHOULDER,
		RIGHT_SHOULDER = XINPUT_GAMEPAD_RIGHT_SHOULDER,
		A = XINPUT_GAMEPAD_A,
		B = XINPUT_GAMEPAD_B,
		X = XINPUT_GAMEPAD_X,
		Y = XINPUT_GAMEPAD_Y,
	};
	class ButtonEvent
	{
	public:
		enum class Type
		{
			PRESS,
			RELEASE,
		};
	private:
		Type type;
		Button button;
	public:
		ButtonEvent( Type type, Button button ) noexcept
			: 
			type( type ),
			button ( button )
		{}
		bool IsPress() const noexcept
		{
			return type == Type::PRESS;
		}
		bool IsRelease() const noexcept
		{
			return type == Type::RELEASE;
		}
		Button getButton() const noexcept
		{
			return button;
		}
	};
	struct Axis
	{
		float x, y;
	};
	using Deadzone = Axis;
public:
	// id is a number of 1-4 but is represented by 0-3 in the API
	Gamepad( unsigned int id, const Deadzone& deadzone = {
		XINPUT_GAMEPAD_LEFT_THUMB_DEADZONE,
		XINPUT_GAMEPAD_RIGHT_THUMB_DEADZONE
	} );
	Gamepad( const Gamepad& ) = delete;
	Gamepad& operator=( const Gamepad& ) = delete;
	Gamepad( Gamepad&& ) noexcept = delete;
	Gamepad& operator=( Gamepad&& ) noexcept = delete;

	UINT GetControllerID() const noexcept;
	XINPUT_GAMEPAD* GetGamepad() noexcept;
	const XINPUT_GAMEPAD* GetGamepad() const noexcept;
	bool IsConnected();
	bool Update();
	void Vibrate( unsigned short leftSpeed, unsigned short rightSpeed );
	void Vibrate( unsigned short bothSpeed );
	void ResetVibration();
	bool IsButtonPressed( Button button ) const;
	std::optional<ButtonEvent> ReadButtonBuffer() noexcept;
	Axis& LeftStick() noexcept;
	const Axis& LeftStick() const noexcept;
	Axis& RightStick() noexcept;
	const Axis& RightStick() const noexcept;
	float LeftTrigger() const noexcept;
	float RightTrigger() const noexcept;
	bool GetAudioDeviceIDs(
		const std::wstring& pRenderDeviceId, unsigned int* pRenderCount,
		const std::wstring& pCaptureDeviceId, unsigned int* pCaptureCount
	) const;
	XINPUT_CAPABILITIES* GetCapabilities( unsigned long flags = 0u );
	const XINPUT_CAPABILITIES* GetCapabilities(unsigned long flags = 0u) const;
	void SetDeadzone( const Deadzone &deadzone ) noexcept;
	Deadzone& GetDeadzone() noexcept;
	const Deadzone& GetDeadzone() const noexcept;
	bool ButtonIsEmpty() const noexcept;
	void Flush() noexcept;
	// XInputGetBatteryInformation is not supported for 9.1.0
	// XINPUT_BATTERY_INFORMATION* getBatteryInfo();
	// void Enable(bool value) const; no supported for 9.1.0
private: // axis & trigger values
	Axis leftStick;
	Axis rightStick;
	float leftTrigger, rightTrigger;
private:
	void OnButtonPressed( Button button ) noexcept;
	void OnButtonReleased( Button button ) noexcept;
	static float ApplyDeadzone( float value, float maxValue, float deadzone );
	template <typename buf>
	static void TrimBuffer( std::queue<buf>& buffer ) noexcept
	{
		while ( buffer.size() > bufferSize )
			buffer.pop(); // [[yesdiscard]] xD
	}
private:
	static constexpr unsigned int bufferSize = 16u;
	static constexpr float maxAxisValue = 1.0f;
	static constexpr float triggerThreshold = XINPUT_GAMEPAD_TRIGGER_THRESHOLD / 255.0f;

	unsigned int controllerID;
	Deadzone deadzone;

	mutable XINPUT_STATE state;
	mutable XINPUT_VIBRATION vibration;
	mutable XINPUT_BATTERY_INFORMATION battery;
	mutable XINPUT_CAPABILITIES capabilities;

	std::queue<ButtonEvent> buttonbuffer;
};

Gamepad.cpp

#include "Gamepad.h"
#include <algorithm>
#include <climits>
#include <cmath>

static float Normalize( float value, float min, float max );

Gamepad::Gamepad( unsigned int id, const Deadzone& deadzone )
	: controllerID( id ), deadzone( deadzone )
{
	ZeroMemory( &state, sizeof(XINPUT_STATE) );
	ZeroMemory( &vibration, sizeof(XINPUT_VIBRATION) );
	ZeroMemory( &battery, sizeof(XINPUT_BATTERY_INFORMATION) );
	ZeroMemory( &capabilities, sizeof(XINPUT_CAPABILITIES) );
}

UINT Gamepad::GetControllerID() const noexcept
{
	return this->controllerID - 1;
}

XINPUT_GAMEPAD* Gamepad::GetGamepad() noexcept
{
	return &this->state.Gamepad;
}

const XINPUT_GAMEPAD *Gamepad::GetGamepad() const noexcept
{
	return &this->state.Gamepad;
}

/*XINPUT_BATTERY_INFORMATION* Gamepad::getBatteryInfo()
{
	XInputGetBatteryInformation(controllerID, XINPUT_DEVTYPE_GAMEPAD, &battery);
	return &battery;
}*/

bool Gamepad::IsConnected()
{
	return ( XInputGetState( this->controllerID - 1, &state ) == ERROR_SUCCESS )
		? true : false;
}

bool Gamepad::Update()
{
	if (!IsConnected())
		return false; // don't update the controller if it's not connected

	if (state.Gamepad.wButtons != 0u) 
	{
		OnButtonPressed( static_cast<Gamepad::Button>(state.Gamepad.wButtons) );
	}
	else
	{
		if (!ButtonIsEmpty())
			OnButtonReleased( buttonbuffer.front().getButton() );
	}

	static constexpr std::numeric_limits<short> limitShrt{};
	static constexpr auto limitMin = limitShrt.min();
	static constexpr auto limitMax = limitShrt.max();
	// Normalize input from the left and right stick's x and y axes.
	const float normLX = Normalize( static_cast<float>(state.Gamepad.sThumbLX),
								   limitMin, limitMax);
	const float normLY = Normalize( static_cast<float>(state.Gamepad.sThumbLY),
								   limitMin, limitMax);

	const float normRX = Normalize( static_cast<float>(state.Gamepad.sThumbRX),
								   limitMin, limitMax);
	const float normRY = Normalize( static_cast<float>(state.Gamepad.sThumbRY),
								   limitMin, limitMax);
	
	// Apply the deadzones to the normalized form of each stick axes.
	leftStick.x = ApplyDeadzone(  normLX, maxAxisValue, Normalize(deadzone.x, limitMin, limitMax) );
	leftStick.y = ApplyDeadzone(  normLY, maxAxisValue, Normalize(deadzone.y, limitMin, limitMax) );
	rightStick.x = ApplyDeadzone( normRX, maxAxisValue, Normalize(deadzone.x, limitMin, limitMax) );
	rightStick.y = ApplyDeadzone( normRY, maxAxisValue, Normalize(deadzone.y, limitMin, limitMax) );
	// Normalize trigger input of BYTE/char limit.
	static constexpr std::numeric_limits<unsigned char> limitChr{};
	leftTrigger = (
		static_cast<float>(state.Gamepad.bLeftTrigger)
		/ static_cast<float>(limitChr.max())
	);
	rightTrigger = (
		static_cast<float>(state.Gamepad.bRightTrigger)
		/ static_cast<float>(limitChr.max())
	);
	return true;
}

/*void Gamepad::Enable(bool value) const
{
	// XInputEnable is meant for use when an application loses/gains focus
	// for example the WM_ACTIVATEAPP message.
	// you may pass FALSE when you wish for vibrations to stop and vice versa
	XInputEnable(static_cast<BOOL>(value));
}*/

void Gamepad::Vibrate( unsigned short leftSpeed, unsigned short rightSpeed )
{
	vibration.wLeftMotorSpeed = leftSpeed;
	vibration.wRightMotorSpeed = rightSpeed;
	XInputSetState( GetControllerID(), &vibration );
}

void Gamepad::Vibrate( unsigned short lrSpeed )
{
	vibration.wLeftMotorSpeed = lrSpeed;
	vibration.wRightMotorSpeed = lrSpeed / 2u;
	XInputSetState( GetControllerID(), &vibration );
}

void Gamepad::ResetVibration()
{
	vibration.wLeftMotorSpeed = 0u;
	vibration.wRightMotorSpeed = 0u;
	XInputSetState(GetControllerID(), &vibration);
}

bool Gamepad::IsButtonPressed( Button button ) const
{
	return ( state.Gamepad.wButtons & static_cast<unsigned short>(button) ) != 0u;
}

std::optional<Gamepad::ButtonEvent> Gamepad::ReadButtonBuffer() noexcept
{
	if ( buttonbuffer.size() > 0u )
	{
		ButtonEvent e = buttonbuffer.front();
		buttonbuffer.pop();
		return e;
	}
	return {};
}

Gamepad::Axis& Gamepad::LeftStick() noexcept
{
	return leftStick;
}

const Gamepad::Axis& Gamepad::LeftStick() const noexcept
{
	return leftStick;
}

Gamepad::Axis& Gamepad::RightStick() noexcept
{
	return rightStick;
}

const Gamepad::Axis& Gamepad::RightStick() const noexcept
{
	return rightStick;
}

float Gamepad::LeftTrigger() const noexcept
{
	return leftTrigger;
}

float Gamepad::RightTrigger() const noexcept
{
	return rightTrigger;
}

bool Gamepad::GetAudioDeviceIDs( const std::wstring& pRenderDeviceId, unsigned int* pRenderCount,
	const std::wstring& pCaptureDeviceId, unsigned int* pCaptureCount ) const
{ // I'm not quite sure how to use this one but this should wrap it okay
	const auto result = XInputGetAudioDeviceIds( GetControllerID(),
		const_cast<wchar_t*>(pRenderDeviceId.c_str()), pRenderCount, 
		const_cast<wchar_t*>(pCaptureDeviceId.c_str()), pCaptureCount );
	if (result != ERROR_SUCCESS)
		return false;
	return true;
}

XINPUT_CAPABILITIES* Gamepad::GetCapabilities( unsigned long flags )
{ // returns const pointer to XINPUT_CAPABILITIES structure that details  
  // useful charateristics about the controller with the dwUserIndex parameter
  // you may pass XINPUT_FLAG_GAMEPAD as the value of flags to limit use to Xbox 360 controllers
	const auto result = XInputGetCapabilities(GetControllerID(), flags,
											  &this->capabilities);
	return &this->capabilities;
}

const XINPUT_CAPABILITIES* Gamepad::GetCapabilities( unsigned long flags ) const
{
	const auto result = XInputGetCapabilities(GetControllerID(), flags,
											  &this->capabilities);
	return &this->capabilities;
}

void Gamepad::SetDeadzone( const Deadzone& deadzone ) noexcept
{
	this->deadzone = deadzone;
}

void Gamepad::OnButtonPressed( Button button ) noexcept
{
	using ButtonEventType = ButtonEvent::Type;
	buttonbuffer.push( ButtonEvent( ButtonEventType::PRESS,button ) );
	TrimBuffer( buttonbuffer );
}

void Gamepad::OnButtonReleased( Button button ) noexcept
{
	using ButtonEventType = ButtonEvent::Type;
	buttonbuffer.push( ButtonEvent( ButtonEventType::RELEASE,button ) );
	TrimBuffer( buttonbuffer );
}

Gamepad::Deadzone& Gamepad::GetDeadzone() noexcept
{
	return deadzone;
}

const Gamepad::Deadzone& Gamepad::GetDeadzone() const noexcept
{
	return deadzone;
}

bool Gamepad::ButtonIsEmpty() const noexcept
{
	return buttonbuffer.empty();
}

void Gamepad::Flush() noexcept
{
	buttonbuffer = std::queue<Gamepad::ButtonEvent>();
}

float Gamepad::ApplyDeadzone( float value, const float maxValue, float deadzone)
{
	if (value < -(deadzone))
	{
		value += deadzone; // increase neg vals to remove deadzone discontinuity
	}
	else if (value > deadzone)
	{
		value -= deadzone; // decrease pos vals to remove deadzone discontinuity
	}
	else
	{
		return 0.0f; // hey values are zero for once
	}
	const float normValue = value / (maxValue - deadzone);
	return std::max(-1.0f, std::min(normValue, 1.0f));
}

static float Normalize(float value, float min, float max)
{ // Normalize value between min/max range.
	const float average = (min + max) / 2.0f;
	const float range = (max - min) / 2.0f;
	return (value - average) / range;
}

Post a Comment

Click to generate a new captcha.

0 Comments