Xbox 360 Controller Test
From Josh Jones
Contents |
Introduction
Just for fun, I thought I'd share how the new Xbox 360 controller interfaces with DirectX via the Xinput interface.
To do this I'm using a sandbox program of mine codenamed "Crossbaby" for its cross-platformness. One thing that computers have problems with in general is dealing with any kind of joystick interface. This includes gamepads, joysticks, steering wheels, etc. (I will call them "joysticks" from here out) There are no standards in place so one joystick adapter can be totally different from another.
I have 3 different PS2 adapters from 3 different companies. Each adapter assigns the buttons, analog axies, etc., each a different way. So what's the big deal? Well, that means that for every PC game I have I have to configure the hell out of the joystick! Sometimes the game just doesn't care about some axis. Halo PC has one of the best joystick configuration interfaces i've ever seen on a computer. Wouldn't it be nice if we could just plug in a joystick and not have to worry about it?
Now comes Microsoft with their almighty Xbox 360 controllers with USB interfaces. Compatable with PCs and Xbox 360s. I'm a big fan of great gamepads so I thought i'd see what the big fuss is about.
The controller itself is very nice to hold. It has 4 axis for the left and right thumbsticks, 2 axis for the left and right triggers, 10 buttons (4 colored buttons, start, back, left & right thumbsticks, and the left and right shoulder buttons) The middle Xbox button can't be used with the official PC driver, the indicator lights are used automatically.
Implementation
Its going to take a few months for this to catch on to most big game titles out there, but this controller can be used 2 different ways as far as DirectX is concerned. I will refer to these two different interfaces as DirectInput and Xinput.
99% of all Windows games that have joystick support use DirectInput. If you were to plug the Xbox 360 controller into a computer without installing the interface drivers it would be detected as a standard HID or Human Interface Device. This is what DirectInput would use. Installing the official Xbox 360 drivers adds the Xinput interface ability to the controller.
When you play a game on a console. There is nothing stopping you from just removing the controller(s) from the system at any time. I dare you to try this with any popular PC game out there. Basically the joystick has to be plugged in before the game starts for the game to detect it. If you remove the joystick while the game is running, the game will still work, but plugging the joystick back in will not work. You have to usually close the game and reload it. How annoying!
The Xinput interface allows you to plug and play during gameplay as if you were playing on a console system. I'm pretty sure the interface on the Xbox 360 is almost identical to the interface on DirectX so it makes sense.
Differences
One of the biggest differences I've noticed other than the whole plug and play thing, is the trigger buttons. With DirectInput the left and right trigger buttons share an axis. Its hard to visualize without seeing it in the Game Controllers section of the Control Panel, but with no buttons being pressed the axis is at 50%. With both buttons pressed down the axis is at 50%. Weird!
Xinput allows the left and right triggers to be treated separately.
My Implementation
DirectInput only detects 10 buttons on the controller, but I'd like to be able to treat the left and right triggers as buttons as well, so I proclaim there are 12 buttons now! DirectInput detects only 5 axis on the controller since the left and right triggers share an axis in DirectInput land. I proclaim 6 axis! Take that subspace! :)
How it works
This is how it will work from the game player's perspective. You can start the game with or without the controller plugged in; I chose to leave mine out for this example.
Once its plugged in the program will detect it and assign a "player" to it:
Notice how the "1" indicator light turns on to show which controller is assigned as player 1. Just like the Xbox 360, there can only be 4 Xbox 360 controllers connected at the same time. DirectInput can supposedly support 52, but I can't even imagine the wires involved with that. :) Right now i'm doing a little hand waving with the "player 1" concept. Just understand that each controller gets assigned a number automatically and you can use this number to check the status of the controller (which buttons are pressed, what the left thumbstick position is, etc.).
Now I'm going to remove the controller again. The program has been running this whole time.
Ta-da! Works like a charm. Now for a stress test:
The Code
Most of this is taken directly from the samples in the DirectX SDK (October 2005)
First you need the new header file:
#include <XInput.h> // For Xbox 360 Controllers
You also need to include the Xinput.lib file for the linker or you'll get funky linker errors.
Here is a handy define that we'll be using all over the place:
//! XInput handles up to 4 controllers #define DXUT_MAX_CONTROLLERS 4
Here is the structure that will hold all the status data for a controller:
typedef struct DXUT_GAMEPAD_t
{
// From XINPUT_GAMEPAD
WORD wButtons;
BYTE bLeftTrigger;
BYTE bRightTrigger;
SHORT sThumbLX;
SHORT sThumbLY;
SHORT sThumbRX;
SHORT sThumbRY;
// Device properties
XINPUT_CAPABILITIES caps;
bool bConnected; // If the controller is currently connected
bool bInserted; // If the controller was inserted this frame
bool bRemoved; // If the controller was removed this frame
// Thumb stick values converted to range [-1,+1]
float fThumbRX;
float fThumbRY;
float fThumbLX;
float fThumbLY;
// Left and right triggers as axis [0, 1]
float fTriggerL;
float fTriggerR;
// Records which buttons were pressed this frame.
// These are only set on the first frame that the button is pressed
WORD wPressedButtons;
bool bPressedLeftTrigger;
bool bPressedRightTrigger;
// Last state of the buttons
WORD wLastButtons;
bool bLastLeftTrigger;
bool bLastRightTrigger;
} DXUT_GAMEPAD, *DXUT_GAMEPAD_PTR;
Finally declare an array of 4 of these structures since you could potentially have 4 controllers plugged in at once:
//! All of the Xinput Joysticks DXUT_GAMEPAD m_XinputGamePads[DXUT_MAX_CONTROLLERS];
Some more handy defines for the next function:
#define DXUT_GAMEPAD_TRIGGER_THRESHOLD 30
#define DXUT_INPUT_DEADZONE ( 0.24f * FLOAT(0x7FFF) ) // Default to 24% of the +/- 32767 range.
// This is a reasonable default value but can be altered if needed.
#define AXIS_THRESHOLD 32767.0f
#define AXIS_TRIGGER_THRESHOLD 255.0f
#define DXUT_GAMEPAD_NUMBUTTONS 12
#define DXUT_GAMEPAD_NUMAXIS 6
#define DXUT_GAMEPAD_NUMHATS 1
#define DXUT_GAMEPAD_NUMBALLS 0
#define DXUT_GAMEPAD_NAME "XBOX 360 For Windows (Controller)"
This is the function that will be called once per game loop, or update loop. Here is how to use the function:
- dwPort is a number 0-3 that represents which "player" the controller is for.
- pGamePad is an index to one of the 4 DXUT_GAMEPAD structs we just created.
- bThumbstickDeadZone defines if you want the axis to utilize a dead zone.
- bSnapThumbstickToCardinals, if true, treats an analog stick like a D-pad.
Here is how I call this function in my update loop:
// Get the state of the Xbox 360 gamepads
for( DWORD iUserIndex=0; iUserIndex<DXUT_MAX_CONTROLLERS; iUserIndex++ )
DXUTGetGamepadState( iUserIndex, &m_XinputGamePads[iUserIndex], true, false );
The function itself:
HRESULT CJoystick_DirectX::DXUTGetGamepadState( DWORD dwPort, DXUT_GAMEPAD* pGamePad, bool bThumbstickDeadZone, bool bSnapThumbstickToCardinals )
{
if( dwPort >= DXUT_MAX_CONTROLLERS || !pGamePad )
return E_FAIL;
if(!m_bUsingXbox360Interface)
return S_OK;
static LPXINPUTGETSTATE s_pXInputGetState = NULL;
static LPXINPUTGETCAPABILITIES s_pXInputGetCapabilities = NULL;
if( !s_pXInputGetState || !s_pXInputGetCapabilities )
{
LPSTR wszPath = new CHAR[MAX_PATH];
if( GetSystemDirectory( wszPath, MAX_PATH ) )
{
char* temp = "\\";
StringCchCat( wszPath, MAX_PATH, temp );
StringCchCat( wszPath, MAX_PATH, XINPUT_DLL );
HINSTANCE hInst = LoadLibrary( wszPath );
if( hInst )
{
s_pXInputGetState = (LPXINPUTGETSTATE)GetProcAddress( hInst, "XInputGetState" );
s_pXInputGetCapabilities = (LPXINPUTGETCAPABILITIES)GetProcAddress( hInst, "XInputGetCapabilities" );
}
}
delete [] wszPath;
}
if( s_pXInputGetState == NULL )
return E_FAIL;
XINPUT_STATE InputState;
DWORD dwResult = s_pXInputGetState( dwPort, &InputState );
// Track insertion and removals
BOOL bWasConnected = pGamePad->bConnected;
pGamePad->bConnected = (dwResult == ERROR_SUCCESS);
pGamePad->bRemoved = ( bWasConnected && !pGamePad->bConnected );
pGamePad->bInserted = ( !bWasConnected && pGamePad->bConnected );
if(pGamePad->bRemoved)
std::cout << "Removed Xbox 360 Gamepad: Player " << dwPort+1 << std::endl;
// Don't update rest of the state if not connected
if( !pGamePad->bConnected )
return S_OK;
// Store the capabilities of the device
if( pGamePad->bInserted )
{
std::cout << "Found Xbox 360 Gamepad: Assigning player " << dwPort+1 << std::endl;
ZeroMemory( pGamePad, sizeof(DXUT_GAMEPAD) );
pGamePad->bConnected = true;
pGamePad->bInserted = true;
if( s_pXInputGetCapabilities )
s_pXInputGetCapabilities( dwPort, XINPUT_DEVTYPE_GAMEPAD, &pGamePad->caps );
}
// Copy gamepad to local structure (assumes that XINPUT_GAMEPAD at the front in CONTROLER_STATE)
memcpy( pGamePad, &InputState.Gamepad, sizeof(XINPUT_GAMEPAD) );
if( bSnapThumbstickToCardinals )
{
// Apply deadzone to each axis independently to slightly snap to up/down/left/right
if( pGamePad->sThumbLX < DXUT_INPUT_DEADZONE && pGamePad->sThumbLX > -DXUT_INPUT_DEADZONE )
pGamePad->sThumbLX = 0;
if( pGamePad->sThumbLY < DXUT_INPUT_DEADZONE && pGamePad->sThumbLY > -DXUT_INPUT_DEADZONE )
pGamePad->sThumbLY = 0;
if( pGamePad->sThumbRX < DXUT_INPUT_DEADZONE && pGamePad->sThumbRX > -DXUT_INPUT_DEADZONE )
pGamePad->sThumbRX = 0;
if( pGamePad->sThumbRY < DXUT_INPUT_DEADZONE && pGamePad->sThumbRY > -DXUT_INPUT_DEADZONE )
pGamePad->sThumbRY = 0;
}
else if( bThumbstickDeadZone )
{
// Apply deadzone if centered
if( (pGamePad->sThumbLX < DXUT_INPUT_DEADZONE && pGamePad->sThumbLX > -DXUT_INPUT_DEADZONE) &&
(pGamePad->sThumbLY < DXUT_INPUT_DEADZONE && pGamePad->sThumbLY > -DXUT_INPUT_DEADZONE) )
{
pGamePad->sThumbLX = 0;
pGamePad->sThumbLY = 0;
}
if( (pGamePad->sThumbRX < DXUT_INPUT_DEADZONE && pGamePad->sThumbRX > -DXUT_INPUT_DEADZONE) &&
(pGamePad->sThumbRY < DXUT_INPUT_DEADZONE && pGamePad->sThumbRY > -DXUT_INPUT_DEADZONE) )
{
pGamePad->sThumbRX = 0;
pGamePad->sThumbRY = 0;
}
}
// Convert [-1,+1] range
pGamePad->fThumbLX = pGamePad->sThumbLX / AXIS_THRESHOLD;
pGamePad->fThumbLY = pGamePad->sThumbLY / AXIS_THRESHOLD;
pGamePad->fThumbRX = pGamePad->sThumbRX / AXIS_THRESHOLD;
pGamePad->fThumbRY = pGamePad->sThumbRY / AXIS_THRESHOLD;
// Convert to [0, 1] range
pGamePad->fTriggerL = pGamePad->bLeftTrigger / AXIS_TRIGGER_THRESHOLD;
pGamePad->fTriggerR = pGamePad->bRightTrigger / AXIS_THRESHOLD;
// Get the boolean buttons that have been pressed since the last call.
// Each button is represented by one bit.
pGamePad->wPressedButtons = ( pGamePad->wLastButtons ^ pGamePad->wButtons ) & pGamePad->wButtons;
pGamePad->wLastButtons = pGamePad->wButtons;
// Figure out if the left trigger has been pressed or released
bool bPressed = ( pGamePad->bLeftTrigger > DXUT_GAMEPAD_TRIGGER_THRESHOLD );
pGamePad->bPressedLeftTrigger = ( bPressed ) ? !pGamePad->bLastLeftTrigger : false;
pGamePad->bLastLeftTrigger = bPressed;
// Figure out if the right trigger has been pressed or released
bPressed = ( pGamePad->bRightTrigger > DXUT_GAMEPAD_TRIGGER_THRESHOLD );
pGamePad->bPressedRightTrigger = ( bPressed ) ? !pGamePad->bLastRightTrigger : false;
pGamePad->bLastRightTrigger = bPressed;
m_pJoysticks[dwPort];
return S_OK;
}
That magical function takes care of almost everything. To use the joystick you just make checks against that structure. Here is an example used for testing to see if a button is pressed down or not. For convenience I use numbers to reference my buttons, but you can do it however you want.
// Xinput controllers
if( (index >= 0) && (index <= 3) )
{
if( m_XinputGamePads[index].bConnected )
{
switch( button )
{
case 0: // A Button
return( (m_XinputGamePads[index].wButtons & XINPUT_GAMEPAD_A) ? true : false );
case 1: // B Button
return( (m_XinputGamePads[index].wButtons & XINPUT_GAMEPAD_B) ? true : false );
case 2: // X Button
return( (m_XinputGamePads[index].wButtons & XINPUT_GAMEPAD_X) ? true : false );
case 3: // Y Button
return( (m_XinputGamePads[index].wButtons & XINPUT_GAMEPAD_Y) ? true : false );
case 4: // Left shoulder
return( (m_XinputGamePads[index].wButtons & XINPUT_GAMEPAD_LEFT_SHOULDER) ? true : false );
case 5: // Right shoulder
return( (m_XinputGamePads[index].wButtons & XINPUT_GAMEPAD_RIGHT_SHOULDER) ? true : false );
case 6: // Back
return( (m_XinputGamePads[index].wButtons & XINPUT_GAMEPAD_BACK) ? true : false );
case 7: // Start
return( (m_XinputGamePads[index].wButtons & XINPUT_GAMEPAD_START) ? true : false );
case 8: // Left thumb
return((m_XinputGamePads[index].wButtons & XINPUT_GAMEPAD_LEFT_THUMB) ? true : false );
case 9: // Right thumb
return( (m_XinputGamePads[index].wButtons & XINPUT_GAMEPAD_RIGHT_THUMB) ? true : false );
case 10: // Left trigger
return( (m_XinputGamePads[index].bLeftTrigger >= DXUT_GAMEPAD_TRIGGER_THRESHOLD) ? true : false );
case 11: // Right rigger
return( (m_XinputGamePads[index].bRightTrigger >= DXUT_GAMEPAD_TRIGGER_THRESHOLD) ? true : false );
}
}
else
return false;
An example of a function that contains this logic would be similar to this:
// Check for the first gamepad's A button
// First parameter is the gamepad index, the second is the button to check
if(isButtonPressed(0, 0))
playerJumped(); // Some made up example function
Thanks
Did you enjoy this? You think you want to change or add anything? Let me know: twonjosh@yahoo.com