JavaScript EditorFree JavaScript Editor     Ajax Editor 



Main Page
  Previous Section Next Section

Joysticks

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 );
HRESULT hr = g_pJoystick->SetCooperativeLevel( hWnd, DISCL_EXCLUSIVE |
                                         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 );

Response Curves

We have seen how analog joysticks map the controller position to a continuous range of values so we can detect subtle variations. This behavior can become our best ally or a nightmare depending on how we handle it.

For example, imagine a game like Mario, where there is no speed control: Mario is simply running left, running right, or standing still. So how do we implement that using an analog controller? We must discretize the output range so we only get three possible values:

-1 for running left

0 for standing still

1 for running right

Assuming that the analog output is in the range -100..100, we need to define a transfer function that maps a number in the range -100..100 to a number in the range -1..1. Choosing that transfer function—often called response curve—accurately is key to keeping good communication with the player. Imagine that we do something like this:

-1 for values [-100..-1]

0 for value 0

1 for values [1..100]

This means the slightest variation or decentering from the controller will trigger a movement, making the game unplayable. For these kinds of response curves (which convert the analog range to a discrete one), we must supply enough dynamic range so that each value will be mapped correctly. For example, a much better solution would be

-1 for values [-100..-25]

0 for values [-24..24]

1 for values [25..100]

This way minimal decalibrations will not trigger the joystick by mistake. This response curve can be seen in Figure 5.1.

Figure 5.1. Response curve without (left) and with (right) dead zone.

graphics/05fig01.gif

Most games use analog control these days. The analog controller returns a value in a continuous range, and we use that value to implement various degrees of influence (be it speed, turning, etc.) in our game world. An airplane will dive faster if we pull the controller completely, the car will accelerate more aggressively, and so on. However, analog control also needs a response curve. Without a response curve, a car will keep turning if the controller is just minimally decalibrated. Although this is most likely a controller problem, our code must deal with it so the player enjoys a seamless playing experience.

As you might have guessed, we begin by neutralizing the zone around the center of the controller to prevent movements due to bad calibration. But will the neutralization speed map linearly to the controller's position (as in Figure 5.2, left), or will it use a curve (such as the one on the right in Figure 5.2)? Using curves such as parabolas will be useful to implement inertia effects, where the player needs to control the amount of inclination carefully. It makes games a bit harder to master because small errors in the control yield large variations in the effect.

Figure 5.2. Types of response curves.

graphics/05fig02.gif

      Previous Section Next Section
    



    JavaScript EditorAjax Editor     JavaScript Editor