DirectX - Joystick

DirectX's DirectInput component provides access to keyboards, mice, joysticks, and other human interface devices. Here, I'll focus on interfacing with joystick devices.

  1. Header Files
  2. Libraries
  3. Initialization
  4. Selecting a Joystick
  5. Setting Joystick Properties
  6. Enumerating the Axes
  7. Polling the Joystick
  8. Closing the Joystick
  9. Supporting Multiple Joysticks

Header Files

You'll need to include the DirectInput header file in order to use any of the DirectInput functions:

#include <dinput.h>

Libraries

In order to link a final application binary, you'll need to include the following following libraries in your project:

dinput8.lib dxguid.lib

Initialization

Before you can access the joystick, you must first initialize the DirectInput library:

LPDIRECTINPUT8 di;
HRESULT hr;

// Create a DirectInput device
if (FAILED(hr = DirectInput8Create(GetModuleHandle(NULL), DIRECTINPUT_VERSION, 
                                   IID_IDirectInput8, (VOID**)&di, NULL))) {
    return hr;
}

Selecting a Joystick

DirectInput uses the EnumDevices() method to iterate through all of the available input devices on the system. In our case, we'll use the function callback method:

LPDIRECTINPUTDEVICE8 joystick;

// Look for the first simple joystick we can find.
if (FAILED(hr = di->EnumDevices(DI8DEVCLASS_GAMECTRL, enumCallback,
                                NULL, DIEDFL_ATTACHEDONLY))) {
    return hr;
}

// Make sure we got a joystick
if (joystick == NULL) {
    printf("Joystick not found.\n");
    return E_FAIL;
}

Here's the code for enumCallback():

BOOL CALLBACK
enumCallback(const DIDEVICEINSTANCE* instance, VOID* context)
{
    HRESULT hr;

    // Obtain an interface to the enumerated joystick.
    hr = di->CreateDevice(instance->guidInstance, &joystick, NULL);

    // If it failed, then we can't use this joystick. (Maybe the user unplugged
    // it while we were in the middle of enumerating it.)
    if (FAILED(hr)) { 
        return DIENUM_CONTINUE;
    }

    // Stop enumeration. Note: we're just taking the first joystick we get. You
    // could store all the enumerated joysticks and let the user pick.
    return DIENUM_STOP;
}

Setting Joystick Properties

Now that we have a joystick, we can set its properties and query its capabilities.

DIDEVCAPS capabilities;

// Set the data format to "simple joystick" - a predefined data format 
//
// A data format specifies which controls on a device we are interested in,
// and how they should be reported. This tells DInput that we will be
// passing a DIJOYSTATE2 structure to IDirectInputDevice::GetDeviceState().
if (FAILED(hr = joystick->SetDataFormat(&c_dfDIJoystick2))) {
    return hr;
}

// Set the cooperative level to let DInput know how this device should
// interact with the system and with other DInput applications.
if (FAILED(hr = joystick->SetCooperativeLevel(NULL, DISCL_EXCLUSIVE |
                                              DISCL_FOREGROUND))) {
    return hr;
}

// Determine how many axis the joystick has (so we don't error out setting
// properties for unavailable axis)
capabilities.dwSize = sizeof(DIDEVCAPS);
if (FAILED(hr = joystick->GetCapabilities(&capabilities))) {
    return hr;
}

Enumerating the Axes

The EnumObjects() function also requires a callback:

// Enumerate the axes of the joyctick and set the range of each axis. Note:
// we could just use the defaults, but we're just trying to show an example
// of enumerating device objects (axes, buttons, etc.).
if (FAILED(hr = joystick->EnumObjects(enumAxesCallback, NULL, DIDFT_AXIS))) {
    return hr;
}

BOOL CALLBACK
enumAxesCallback(const DIDEVICEOBJECTINSTANCE* instance, VOID* context)
{
    HWND hDlg = (HWND)context;

    DIPROPRANGE propRange; 
    propRange.diph.dwSize       = sizeof(DIPROPRANGE); 
    propRange.diph.dwHeaderSize = sizeof(DIPROPHEADER); 
    propRange.diph.dwHow        = DIPH_BYID; 
    propRange.diph.dwObj        = instance->dwType;
    propRange.lMin              = -1000; 
    propRange.lMax              = +1000; 
    
    // Set the range for the axis
    if (FAILED(joystick->SetProperty(DIPROP_RANGE, &propRange.diph))) {
        return DIENUM_STOP;
    }

    return DIENUM_CONTINUE;
}

Polling the Joystick

In order to get information on the joystick's current position and the state of its buttons, you must poll it. Here's an example of a poll function:

HRESULT
poll(DIJOYSTATE2 *js)
{
    HRESULT     hr;

    if (joystick == NULL) {
        return S_OK;
    }


    // Poll the device to read the current state
    hr = joystick->Poll(); 
    if (FAILED(hr)) {
        // DInput is telling us that the input stream has been
        // interrupted. We aren't tracking any state between polls, so
        // we don't have any special reset that needs to be done. We
        // just re-acquire and try again.
        hr = joystick->Acquire();
        while (hr == DIERR_INPUTLOST) {
            hr = joystick->Acquire();
        }

        // If we encounter a fatal error, return failure.
        if ((hr == DIERR_INVALIDPARAM) || (hr == DIERR_NOTINITIALIZED)) {
            return E_FAIL;
        }

        // If another application has control of this device, return successfully.
        // We'll just have to wait our turn to use the joystick.
        if (hr == DIERR_OTHERAPPHASPRIO) {
            return S_OK;
        }
    }

    // Get the input's device state
    if (FAILED(hr = joystick->GetDeviceState(sizeof(DIJOYSTATE2), js))) {
        return hr; // The device should have been acquired during the Poll()
    }

    return S_OK;
}

Closing the Joystick

When you're done using the joystick, you should close the device.

if (joystick) { 
    joystick->Unacquire();
}

Supporting Multiple Joysticks

There is some good information of supporting multiple joysticks in the MSDN article entitled Microsoft DirectInput and Developing for Microsoft SideWinder Digital Game Controllers. It's an older article, but the information is sound.