The joystick was introduced in the 1970s as a way to represent positional data easily. The first models were restricted to binary tests: The joystick returned a 0 or 1 to represent whether it was being activated or not. Thus, most joysticks allowed nine positional values: one for the joystick being centered and eight for N, S, E, W, SW, SE, NW, and NE. Usually, the joystick position was mapped to an integer value, with an extra bit used to represent the button press. Most joysticks from eight-bit computers were like this.
As software development houses created better simulations, joysticks began to improve as well. Continuous-output joysticks appeared to satisfy the flight simulation community, but because they offered better control, they became mainstream. Today, all joysticks map the sticks' inclination to a continuous range of values, so we can control our characters precisely.
Controlling a joystick is slightly more complex than working with a keyboard or a mouse. Joysticks come in a variety of shapes and configurations, so the detection and data retrieval process is a bit more involved. Some gamepads have two controllers, whereas others have a stick and a point-of-view (POV). In addition, the number of buttons varies from model to model, making the process of detecting a joystick nontrivial.
In DirectInput, we must ask the API to enumerate the device so it autodetects the joystick so we can use it. For this example, I will assume we already have the DirectInput object ready. The first step is to ask DirectInput to enumerate any joystick it is detecting. This is achieved by using the call:
HRESULT hr = g_pDI->EnumDevices( DI8DEVCLASS_GAMECTRL, EnumJoysticksCallback,
NULL, DIEDFL_ATTACHEDONLY ) ) )
The first parameter tells DirectInput which kind of device we want to detect. It can be a mouse (DI8DEVCLASS_POINTER) or a keyboard (DI8DEVCLASS_KEYBOARD). In this case, we request a game controller, which is valid for both gamepads and joysticks of all kinds. Now comes the tricky part: DirectInput detects all candidate devices. Imagine that we have two joysticks (for example, in a two-player game). DirectInput would need to return two devices. Thus, instead of doing some parameter magic to allow this, DirectInput works with callbacks. A callback is a user-defined function that gets called from inside the execution of a system call. In this case, we provide the EnumJoysticksCallback, a function we write that will get triggered once for each detected joystick. The internals of that function will have to retrieve GUIDs, allocate the device objects, and so on. This is a bit more complicated than returning a list of pointers, but on the other hand, it allows greater flexibility. We will examine our callback in a second. Let's first complete the call profile by stating that the third parameter is a user-defined parameter to be passed to the callback (usually NULL), whereas the last parameter is the enumeration flags. DIEDFL_ATTACHEDONLY is used to state that we only want to detect those devices that are properly attached and installed, the same way DIEDFL_FORCEFEEDBACK is used to restrict the enumeration to force feedback joysticks. Here is the source code for the EnumJoysticksCallback function:
BOOL CALLBACK EnumJoysticksCallback( const DIDEVICEINSTANCE* pdidInstance, VOID* pContext ) {
HRESULT hr;
hr = g_pDI->CreateDevice( pdidInstance->guidInstance, &g_pJoystick, NULL ); if( FAILED(hr) ) return DIENUM_CONTINUE;
return DIENUM_STOP; }
Notice how the callback is receiving the device instance, so we only have to create the device using that instance. This is a relatively simple example, where we return to the application as soon as we have found one joystick. If the user had two joysticks attached to the computer, this code would only enumerate the first one. A variant could be used to store each and every joystick in a linked list, so the user can then select the joystick he actually wants to use from a drop-down list.
After the joystick has been enumerated, we can set the data format and cooperative level. No news here—just a rehash of the code required for keyboards and mice:
HRESULT hr = g_pJoystick->SetDataFormat( &c_dfDIJoystick );
DISCL_FOREGROUND );
An extra piece of code must be used to set the output range for the joystick. Because it is a device with analog axes, what will be the range of output values? Will it be –1..1 or –1000..1000? We need to make sure the behavior of the joystick is initialized properly. In our case, we will make the joystick respond with a value from –100..100, much like a percentage. To do so, we need to use a second callback. But first we need the following call, which requests the objects associated with the joystick:
g_pJoystick->EnumObjects(EnumObjectsCallback, (VOID*)hWnd, DIDFT_ALL);
Objects can be axes, buttons, POVs, and so on. Then, the call will respond via the provided callback. Here is the source code for that callback, which performs the axis initialization:
BOOL CALLBACK EnumObjectsCallback( const DIDEVICEOBJECTINSTANCE* pdidoi, VOID* pContext )
{
HWND hDlg = (HWND)pContext;
if( pdidoi->dwType & DIDFT_AXIS ) {
DIPROPRANGE diprg;
diprg.diph.dwSize = sizeof(DIPROPRANGE); diprg.diph.dwHeaderSize = sizeof(DIPROPHEADER); diprg.diph.dwHow = DIPH_BYID;
diprg.diph.dwObj = pdidoi->dwType; // Specify the enumerated axis diprg.lMin = -100;
diprg.lMax = +100;
if( FAILED( g_pJoystick->SetProperty( DIPROP_RANGE, &diprg.diph ) ) ) return DIENUM_STOP;
} }
As with the earlier callback, this routine is called once for each object. Then, the if sentence checks whether the returned object is actually an axis, and if so, uses the SetProperty call to specify its response range. The SetProperty call can be used for more exotic functions, such as calibrating the joystick. The first parameter supports many other symbolic constants that can be used for this purpose.
Fortunately, reading from the joystick is not as complex as initializing it. Here the source code is not much different from keyboards or mice. The only difference is that we need to call Poll() before actually reading from the joystick. Joysticks are polled devices, meaning they do not generate interrupts, and thus need to be polled prior to retrieving their state. Other than that, the code is straightforward:
hr = g_pJoystick->Poll(); if( FAILED(hr) ) {
hr = g_pJoystick->Acquire();
while( hr == DIERR_INPUTLOST || hr== DIERR_OTHERAPPHASPRIO) hr = g_pJoystick->Acquire();
return S_OK; }
DIJOYSTATE js;
hr = g_pJoystick->GetDeviceState( sizeof(DIJOYSTATE), &js ));
This code returns a DIJOYSTATE structure with all the joystick state information. The profile of the call is as follows: typedef struct DIJOYSTATE {
LONG lX; LONG lY; LONG lZ; LONG lRx; LONG lRy; LONG lRz; LONG rglSlider[2]; DWORD rgdwPOV[4]; BYTE rgbButtons[32]; } DIJOYSTATE, *LPDIJOYSTATE;
This structure should suffice for most uses. However, there is a more involved structure with lots of extra parameters available under the DIJOYSTATE2 name. All you have to do is change the SetDataFormat call accordingly:
HRESULT hr = g_pJoystick->SetDataFormat( &c_dfDIJoystick2 );