rate up
1
rate down
2214
views
bookmark
XInput API Gamepad Class

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.

There are no files for this tutorial
##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: .[http://][https://docs.microsoft.com/en-us/windows/win32/xinput/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> #include <Xinput.h> Here will add our include directives. We define *NOMINMAX* before Windows because we will use the std::min and std::max functions later in our function definitions. This removes the MIN and MAX macros normally in the Windows header that would interfere with the standard library min and max functions. The *WIN32_LEAN_AND_MEAN* macro excludes some of the more unimportant parts of the Win32 API. And last but not least we include the XInput header. **PRIVATE MEMBERS** class Gamepad { private: UINT controllerID; XINPUT_STATE state; XINPUT_VIBRATION vibration; //XINPUT_BATTERY_INFORMATION battery;//not used in v1.4 const float maxValue = 1.0f; float deadzoneX; float deadzoneY; float ApplyDeadzone(float value, float maxValue, float deadzone); Now we will declare our private members. 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 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 forcefeedback 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 maxValue variable 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. And lastly the deadzoneX and deadzoneY variables represent a small area in which our left and right joysticks 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 a first person game, for example, to be very jittery. The function ApplyDeadzone(float, float, float) takes in the value of the axis, the max value of that axis, and the deadzone (-1.0f to 1.0f) to be applied. **CONSTRUCTORS** public: Gamepad(UINT id); Gamepad(UINT id, float deadzoneX, float deadzoneY); The first constructor only takes in and id parameter that will be used to initialize controllerID. The second does the same but initializes the deadzones with custom values. As you will see in the definition the default constructor uses Xinput's default deadzone constants in a normalized form. **PUBLIC FUNCTIONS** inline UINT getControllerID() const; XINPUT_GAMEPAD* getGamepad(); //XInputGetBatteryInformation is not supported for v1.4 //XINPUT_BATTERY_INFORMATION* getBatteryInfo(); bool isConnected(); bool Update(); void Vibrate(USHORT leftSpeed, USHORT rightSpeed); void Vibrate(USHORT speed); void resetVibration(); bool isButtonPressed(UINT button) const; 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 . **PUBLIC MEMBERS** float leftStickX, leftStickY; float rightStickX, rightStickY; float leftTrigger, rightTrigger; } These variables will represent the positions of our controller's axes. The Left Stick and Rights Stick values will range from -1.0f to 1.0f and the Triggers will range from 0.0f to 1.0f . ##THE DEFINITION## Now we can move to the Gamepad.cpp source file. #include "Gamepad.h" #include <algorithm> #include <climits> 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. float normalize(float input, float min, float max); Gamepad::Gamepad(UINT id) : controllerID(id), deadzoneX(XINPUT_GAMEPAD_LEFT_THUMB_DEADZONE), deadzoneY(XINPUT_GAMEPAD_RIGHT_THUMB_DEADZONE) { ZeroMemory(&state, sizeof(XINPUT_STATE)); ZeroMemory(&vibration, sizeof(XINPUT_VIBRATION)); } Gamepad::Gamepad(UINT id, float deadzoneX, float deadzoneY) : deadzoneX(deadzoneX), deadzoneY(deadzoneY) { ZeroMemory(&state, sizeof(XINPUT_STATE)); ZeroMemory(&vibration, sizeof(XINPUT_VIBRATION)); } We'll start with a forward declaration of a non-member function called normalize(), which I will explain later. Then we define our default constructor which uses the initializer list to initialize controllerID and the deadzones. The deadzones are initialized with the XInput constants: #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 function body we call ZeroMemory on both the XINPUT_STATE and XINPUT_VIBRATION member structures to give them default values. The next contructor's only difference is that it takes in custom deadzones as floats. UINT Gamepad::getControllerID() const { return controllerID; } XINPUT_GAMEPAD* Gamepad::getGamepad() { return &state.Gamepad; } /*XINPUT_BATTERY_INFORMATION* Gamepad::getBatteryInfo() { XInputGetBatteryInformation(controllerID, XINPUT_DEVTYPE_GAMEPAD, &battery); return &battery; }*/ bool Gamepad::isConnected() { if (XInputGetState(controllerID - 1, &state) == ERROR_SUCCESS) { return true; } else { return false; } } This 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 ERRO_SUCCESS the function returns true, otherwise it returns false. float normalize(float input, float min, float max) { float average = (min + max) / 2; float range = (max - min) / 2; return (input - 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 fo 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; //hey values are zero for once } float normValue = (float)value / (float)(maxValue - deadzone);//scales to 0-1 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. The if statement bool Gamepad::Update() { if (!isConnected()) return false; float normLX = normalize(static_cast<float>(state.Gamepad.sThumbLX), -32767, 32767); float normLY = normalize(static_cast<float>(state.Gamepad.sThumbLY), -32767, 32767); float normRX = normalize(static_cast<float>(state.Gamepad.sThumbRX), -32767, 32767); float normRY = normalize(static_cast<float>(state.Gamepad.sThumbRY), -32767, 32767); if (deadzoneX <= 1.0f || deadzoneY <= 1.0f) { leftStickX = ApplyDeadzone(normLX, maxValue, deadzoneX); leftStickY = ApplyDeadzone(normLY, maxValue, deadzoneY); rightStickX = ApplyDeadzone(normRX, maxValue, deadzoneX); rightStickY = ApplyDeadzone(normRY, maxValue, deadzoneY); } else { leftStickX = ApplyDeadzone(normLX, maxValue, normalize(deadzoneX, SHRT_MIN, SHRT_MAX)); leftStickY = ApplyDeadzone(normLY, maxValue, normalize(deadzoneY, SHRT_MIN, SHRT_MAX)); rightStickX = ApplyDeadzone(normRX, maxValue, normalize(deadzoneX, SHRT_MIN, SHRT_MAX)); rightStickY = ApplyDeadzone(normRY, maxValue, normalize(deadzoneY, SHRT_MIN, SHRT_MAX)); } leftTrigger = static_cast<float>(state.Gamepad.bLeftTrigger) / 255.0f;//normalize input rightTrigger = static_cast<float>(state.Gamepad.bRightTrigger) / 255.0f; 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. Note: Our controller state is being updated because we call isConnected() first. If is connected returns true we continue on to applying our deazones and normalizing the axes. float normLX = normalize(static_cast<float>(state.Gamepad.sThumbLX), -32767, 32767); float normLY = normalize(static_cast<float>(state.Gamepad.sThumbLY), -32767, 32767); Here we create local floats that will store normalized(-1.0f to 1.0f) version of the vale stored in our state.Gampad.sThumbLX location. We we call the normalize function and cast the first parameter value to a float. Our second parameter is the minimum value of the thumb stick's raw value and the third parameter is the maximum. float normRX = normalize(static_cast<float>(state.Gamepad.sThumbRX), -32767, 32767); float normRY = normalize(static_cast<float>(state.Gamepad.sThumbRY), -32767, 32767); We repeat this similarily for the right stick's X and Y axis. if (deadzoneX <= 1.0f || deadzoneY <= 1.0f) { leftStickX = ApplyDeadzone(normLX, maxValue, deadzoneX); leftStickY = ApplyDeadzone(normLY, maxValue, deadzoneY); rightStickX = ApplyDeadzone(normRX, maxValue, deadzoneX); rightStickY = ApplyDeadzone(normRY, maxValue, deadzoneY); } else { leftStickX = ApplyDeadzone(normLX, maxValue, normalize(deadzoneX, SHRT_MIN, SHRT_MAX)); leftStickY = ApplyDeadzone(normLY, maxValue, normalize(deadzoneY, SHRT_MIN, SHRT_MAX)); rightStickX = ApplyDeadzone(normRX, maxValue, normalize(deadzoneX, SHRT_MIN, SHRT_MAX)); rightStickY = ApplyDeadzone(normRY, maxValue, normalize(deadzoneY, SHRT_MIN, SHRT_MAX)); } Now we will apply the deadzone and assign it to our public leftStickX, leftStickY, rightStickX, and rightStickY members. We use the if (deadzoneX <= 1.0f || deadzoneY <= 1.0f) to check if the user entered their own normalized deadzones. If they did not they are using XInput's constants which are not normalized and that is what the else in this if statement is for. leftStickX = ApplyDeadzone(normLX, maxValue, normalize(deadzoneX, SHRT_MIN, SHRT_MAX)); leftStickY = ApplyDeadzone(normLY, maxValue, normalize(deadzoneY, SHRT_MIN, SHRT_MAX)); We apply the deadzone and assign it to our public leftStickX and leftStickY members. The first parameter takes the value of the axis, the second takes the max value of that axis, which will be 1.0f since we normalized it. The third parameter needs a normalized deadzone. void Gamepad::Vibrate(USHORT leftSpeed, USHORT rightSpeed) { vibration.wLeftMotorSpeed = leftSpeed; vibration.wRightMotorSpeed = rightSpeed; XInputSetState(controllerID, &vibration); } Vibrate takes in two different speeds, one for the left and one for the right. The type is USHORT 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 our vibration member. We should call this function whenever updated motor values. void Gamepad::Vibrate(USHORT speed) { vibration.wLeftMotorSpeed = speed; vibration.wRightMotorSpeed = speed; XInputSetState(controllerID, &vibration); } Here is an overloaded version of Vibrate that takes a single speed parameter to assign to both the left and right motors. void Gamepad::resetVibration() { vibration.wLeftMotorSpeed = 0; vibration.wRightMotorSpeed = 0; XInputSetState(controllerID, &vibration); } resetVibration simply resets both motor speeds to 0 and updates the vibration state. bool Gamepad::isButtonPressed(UINT button) const { return (state.Gamepad.wButtons & button) != 0; } isButtonPressed takes in a UINT parameter called button. The parameter can take in the constants for the buttons from XInput seen here: XINPUT_GAMEPAD_A XINPUT_GAMEPAD_B XINPUT_GAMEPAD_X XINPUT_GAMEPAD_Y XINPUT_GAMEPAD_DPAD_LEFT XINPUT_GAMEPAD_DPAD_RIGHT XINPUT_GAMEPAD_DPAD_UP XINPUT_GAMEPAD_DPAD_DOWN XINPUT_GAMEPAD_LEFT_SHOULDER XINPUT_GAMEPAD_RIGHT_SHOULDER XINPUT_GAMEPAD_LEFT_THUMB XINPUT_GAMEPAD_RIGHT_THUMB The functions returns true by using a logical AND on the state's gamepad structure's wButtons member and the button parameter. If the operation is anything other than 0 it returns true. ##USING THE CLASS## Let's write a small program to make use of the class. #include <iostream> #include <chrono> #include <thread> #include <iomanip> #include "Gamepad.h" Here are our include directives. We of course include iostream to use the outputstream. Chrono is used my threading operations as a way of measuring time. We will use thread as a way to sleep this_thread during a while loop. iomanip or input output stream manipulations will be used for rounding off the axis values. int main() { Gamepad gamepad(1); bool aPressed = false; if (!gamepad.isConnected()) { std::cout << "Controller not connected" << std::endl; 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()) { system("cls"); std::cout << std::fixed << std::setprecision(2) << "Left Trigger: " << gamepad.leftTrigger << ", Right Trigger: " << gamepad.rightTrigger << "n" << "Left Stick " << "X: " << gamepad.leftStickX << ", Y: " << gamepad.leftStickY << "n" << "Right Stick " << "X: " << gamepad.rightStickX << ", Y: " << gamepad.rightStickY << 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 insertetd into the stream to limit decimal numbers to two places past the decimal. if (gamepad.isButtonPressed(XINPUT_GAMEPAD_A)) { aPressed = true; } else { aPressed = false; } if (aPressed == true) { gamepad.Vibrate(20000, 20000); } else { gamepad.resetVibration(); } This code of course uses the isButtonPressed(UINT button) function to cause the controller to vibrate while the A button is pressed. if (gamepad.isButtonPressed(XINPUT_GAMEPAD_BACK)) break; } std::this_thread::sleep_for(std::chrono::milliseconds(10)); } 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**: .[https://docs.microsoft.com/en-us/windows/win32/xinput/getting-started-with-xinput][Getting Started With XInput] .[https://docs.microsoft.com/en-us/windows/win32/xinput/xinput-versions][XInput Versions] .[https://github.com/microsoft/DirectXTK/blob/master/Inc/GamePad.h][DirectXTK's Gamepad.h] .[https://github.com/microsoft/DirectXTK/blob/master/Src/GamePad.cpp][DirectXTK's Gamepad.cpp] Here is the full **source code**: **main.cpp** #include <iostream> #include <chrono> #include <thread> #include <iomanip> #include "Gamepad.h" int main() { Gamepad gamepad(1); bool aPressed = false; if (!gamepad.isConnected()) { std::cout << "Controller not connected" << std::endl; return -1; } while (true) { if (gamepad.Update()) { system("cls"); std::cout << std::fixed << std::setprecision(2) << "Left Trigger: " << gamepad.leftTrigger << ", Right Trigger: " << gamepad.rightTrigger << "n" << "Left Stick " << "X: " << gamepad.leftStickX << ", Y: " << gamepad.leftStickY << "n" << "Right Stick " << "X: " << gamepad.rightStickX << ", Y: " << gamepad.rightStickY << std::endl; if (gamepad.isButtonPressed(XINPUT_GAMEPAD_A)) { aPressed = true; } else { aPressed = false; } if (aPressed == true) { gamepad.Vibrate(20000, 20000); } else { gamepad.resetVibration(); } if (gamepad.isButtonPressed(XINPUT_GAMEPAD_BACK)) break; } std::this_thread::sleep_for(std::chrono::milliseconds(10)); } return 0; } **Gamepad.h** #pragma once #define NOMINMAX #include <Windows.h> #include <Xinput.h> class Gamepad { private: UINT controllerID; XINPUT_STATE state; XINPUT_VIBRATION vibration; //XINPUT_BATTERY_INFORMATION battery;//not used in v1.4 const float maxValue = 1.0f; float deadzoneX; float deadzoneY; float ApplyDeadzone(float value, float maxValue, float deadzone); public: Gamepad(UINT id); Gamepad(UINT id, float deadzoneX, float deadzoneY); inline UINT getControllerID() const; XINPUT_GAMEPAD* getGamepad(); //XInputGetBatteryInformation is not supported for v1.4 //XINPUT_BATTERY_INFORMATION* getBatteryInfo(); bool isConnected(); bool Update(); void Vibrate(USHORT leftSpeed, USHORT rightSpeed); void Vibrate(USHORT speed); void resetVibration(); bool isButtonPressed(UINT button) const; float leftStickX, leftStickY; float rightStickX, rightStickY; float leftTrigger, rightTrigger; } **Gamepad.cpp** #include "Gamepad.h" #include <algorithm> #include <climits> float normalize(float input, float min, float max); Gamepad::Gamepad(UINT id) : controllerID(id), deadzoneX(XINPUT_GAMEPAD_LEFT_THUMB_DEADZONE), deadzoneY(XINPUT_GAMEPAD_RIGHT_THUMB_DEADZONE) { ZeroMemory(&state, sizeof(XINPUT_STATE)); ZeroMemory(&vibration, sizeof(XINPUT_VIBRATION)); } Gamepad::Gamepad(UINT id, float deadzoneX, float deadzoneY) : deadzoneX(deadzoneX), deadzoneY(deadzoneY) { ZeroMemory(&state, sizeof(XINPUT_STATE)); ZeroMemory(&vibration, sizeof(XINPUT_VIBRATION)); } UINT Gamepad::getControllerID() const { return controllerID; } XINPUT_GAMEPAD* Gamepad::getGamepad() { return &state.Gamepad; } /*XINPUT_BATTERY_INFORMATION* Gamepad::getBatteryInfo() { XInputGetBatteryInformation(controllerID, XINPUT_DEVTYPE_GAMEPAD, &battery); return &battery; }*/ bool Gamepad::isConnected() { if (XInputGetState(controllerID - 1, &state) == ERROR_SUCCESS) { return true; } else { return false; } } float normalize(float input, float min, float max) { float average = (min + max) / 2; float range = (max - min) / 2; return (input - average) / range; } 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; //hey values are zero for once } float normValue = (float)value / (float)(maxValue - deadzone); return std::max(-1.0f, std::min(normValue, 1.0f)); } bool Gamepad::Update() { if (!isConnected()) return false; float normLX = normalize(static_cast<float>(state.Gamepad.sThumbLX), -32767, 32767); float normLY = normalize(static_cast<float>(state.Gamepad.sThumbLY), -32767, 32767); float normRX = normalize(static_cast<float>(state.Gamepad.sThumbRX), -32767, 32767); float normRY = normalize(static_cast<float>(state.Gamepad.sThumbRY), -32767, 32767); if (deadzoneX <= 1.0f || deadzoneY <= 1.0f) { leftStickX = ApplyDeadzone(normLX, maxValue, deadzoneX); leftStickY = ApplyDeadzone(normLY, maxValue, deadzoneY); rightStickX = ApplyDeadzone(normRX, maxValue, deadzoneX); rightStickY = ApplyDeadzone(normRY, maxValue, deadzoneY); } else { leftStickX = ApplyDeadzone(normLX, maxValue, normalize(deadzoneX, SHRT_MIN, SHRT_MAX)); leftStickY = ApplyDeadzone(normLY, maxValue, normalize(deadzoneY, SHRT_MIN, SHRT_MAX)); rightStickX = ApplyDeadzone(normRX, maxValue, normalize(deadzoneX, SHRT_MIN, SHRT_MAX)); rightStickY = ApplyDeadzone(normRY, maxValue, normalize(deadzoneY, SHRT_MIN, SHRT_MAX)); } leftTrigger = static_cast<float>(state.Gamepad.bLeftTrigger) / 255.0f;//normalize input rightTrigger = static_cast<float>(state.Gamepad.bRightTrigger) / 255.0f; return true; } void Gamepad::Vibrate(USHORT leftSpeed, USHORT rightSpeed) { vibration.wLeftMotorSpeed = leftSpeed; vibration.wRightMotorSpeed = rightSpeed; XInputSetState(controllerID - 1, &vibration); } void Gamepad::Vibrate(USHORT speed) { vibration.wLeftMotorSpeed = speed; vibration.wRightMotorSpeed = speed; XInputSetState(controllerID - 1, &vibration); } void Gamepad::resetVibration() { vibration.wLeftMotorSpeed = 0; vibration.wRightMotorSpeed = 0; XInputSetState(controllerID - 1, &vibration); } bool Gamepad::isButtonPressed(UINT button) const { return (state.Gamepad.wButtons & button) != 0; }