JavaScript EditorFree JavaScript Editor     Ajax Editor 



Main Page
  Previous Section Next Section

DirectInput Overture

DirectInput is basically a miracle—just like DirectDraw. Without DirectInput, you would be on the phone with every input device manufacturer in the world, begging them for drivers (one for DOS, Win16, Win32, and so on) and having a really bad day--trust me! DirectInput takes all these problems away. Of course, because it was designed by Microsoft, it creates whole new problems, but at least they are localized at one company!

DirectInput is just like DirectDraw. It's a hardware-independent virtual input system that allows hardware manufacturers to create conventional and non-conventional input devices that all act as interfaces in the same way. This is good for you because you don't need a driver for every single input device that your users might own. You talk to DirectInput in a generic way, and the DirectInput driver translates this code into hardware-specific calls, as does DirectDraw.

Take a look at Figure 9.3 to see an illustration of DirectInput as it relates to the hardware drivers and physical input devices.

Figure 9.3. DirectInput system-level schematic.

graphics/09fig03.gif

As you can see, you are always insulated by the HAL (Hardware Abstraction Layer). There's not much to emulate with the HEL (Hardware Emulation Layer), so it's not as important as it was with DirectDraw. In any case, that's the basic idea of DirectInput. Let's take a look at what it supports:

Every single input device that exists.

That's pretty much the truth. As long as there is a DirectInput driver for a device, DirectInput can talk to it, and hence you can too. Of course, it's up to the hardware vendor to write a driver, but that's their job. With that in mind, you can expect support for the following devices:

  • Keyboards

  • Mice

  • *Joysticks

  • *Paddles

  • *Game pads

  • *Steering wheels

  • *Flight yokes

  • *Head-mounted display trackers

  • *6-DOF (degree of freedom) space balls

  • *Cybersex suits (as soon as they hit the mass market in early 2005)

The devices with asterisks are all considered to be joysticks as far as DirectInput is concerned. There are so many subclasses of joystick-like input devices that DirectInput just decided to call them all devices. Each of these devices can have one or more input objects, which might be axial, rotational, momentary, pressure, and so on. Get it? For example, a joystick with two axes (X and Y) and two momentary switches has four input objects—that's it.

DirectInput doesn't really care whether the device in question is a joystick because the device could represent a steering wheel just as easily. However, in reality, DirectInput does subclass a little. Anything that's not a mouse or keyboard is a joystick-like device, whether you hold it, squeeze it, turn it, or step on it. Coolio?

DirectInput differentiates between all these devices by forcing the manufacturer (and hence the driver) to give each device a unique GUID to represent it. This way, at least there is a unique name of sorts for every single device that exists or will exist, and DirectInput can query the system for any device using this name. Once the device is found, however, it's just a bunch of input objects. I'm belaboring this point because it seems to confuse people. Okay, let's move on…

The Components of DirectInput

DirectInput version 8.0 consists of a number of COM interfaces, as do any of the DirectX subsystems. Take a look at Figure 9.4. You can see that there is the main interface, IDirectInput8, and then only one other main interface, IDirectInputDevice8.

Figure 9.4. The interfaces of DirectInput.

graphics/09fig04.gif

Let's take a look at these interfaces:

  • IDirectInput8This is the main COM object that you must create to start up DirectInput. Luckily there is a wrapper to do DirectInput8Create() that does all the COM stuff for you. Once you have created the IDirectInput8 interface, you will make calls to it to set up the properties of DirectInput and to create and acquire any input devices that you may want to work with.

  • IDirectInputDevice8This interface is created from the main IDirectInput8 interface and is the conduit that you use to communicate with a device, whether it be a mouse, keyboard, joystick, or whatever. They are all IDirectInput8 devices. This latest interface supports joysticks and force feedback devices. In addition, it allows polled devices to be plugged; some joysticks need to be polled.

The General Steps for Setting Up DirectInput

There are a number of steps involved in getting DirectInput up and running, connecting to one or more devices, and finally getting data from the device(s). First is the setup of DirectInput, and next is the setup of each input device. The second part is almost identical for each device, so we can generalize it. That's cool, huh? Here's what you have to do:

  1. Create the main DirectInput interface IDirectInput8 with a call to DirectInput8Create(). This returns the IDirectInput8 interface.

  2. (Optional) Query for device GUIDs. During this step, you will query DirectInput for input devices belonging to the class keyboard, mouse, joystick, or generic device (one that doesn't fall into the previous list). This is accomplished with (get ready to throw up) a callback function and an enumeration. Basically, you request DirectInput to enumerate all devices of some type/subtype. DirectInput filters them through a callback, which you then build up with a database of GUIDs. Disgusting, huh? Well, luckily this is really only an issue for joystick-like devices, because you can usually bank on a generic mouse and keyboard and there are stock GUIDs for them. I'll show you how this step works when I cover joysticks a little later.

  3. For each device that you want to use in your application, you must create it with a call to CreateDevice() passing a GUID. CreateDevice() is an interface function of IDirectInput8, so you must obtain the IDirectInput8 interface before making this call. Also, this step will come after step 2 if you don't know the GUID of the device that you are trying to create. There are two built-in GUIDs, one for the keyboard and one for the mouse:

    GUID_SysKeyboard— This is globally defined and will always work as the primary keyboard device GUID.

    GUID_SysMouse— This is globally defined and will always work as the primary mouse device GUID.

    TIP

    A while ago, an astute reader sent me an email asking about how to detect and use more than one mouse. I hadn't really thought about it, but if the driver supports more than one mouse, you should be able to use it under DirectInput. In this case, you would have to query for the secondary mouse GUID to create it.

  4. Once you have created the device, you must set the cooperative level for each. This is accomplished with the call IDirectInputDevice8::SetCooperativeLevel(). Note the C++ syntax here—it simply means that this SetCooperativeLevel() is an interface method or function of IDirectInputDevice8. The cooperation levels are much like those in DirectDraw, but there are fewer of them. We will take a closer look at them when we walk through the keyboard example.

  5. Set the data format of each device with a call to SetDataFormat() from the IDirectInputDevice8 interface. This is a bit confusing in practice, but not that bad conceptually. The data format is how you want the data packet for each device event to be formatted. That was nice of DirectInput! This just gives you more flexibility, that's all. Thank goodness, there are some globally predefined data formats that you can use that are reasonably intelligent, so you don't have to set one up yourself.

  6. Set any properties of the device that you desire with IDirectInputDevice8:: SetProperty(). This is device context-sensitive, meaning that some devices have some properties and some don't. Thus, you have to know what you're trying to set. In this case, you'll only use this to set some of the range properties of the joystick device, but be aware that in most cases, anything that is configurable on a device is configured with a call to SetProperty(). As usual, the call is fairly horrific, but I'll show you exactly how to do it when we cover the joystick example.

  7. Acquire each device with a call to IDirectInputDevice8::Acquire(). This basically attaches or associates the device(s) with your application and tells DirectInput that in the future you'll be requesting data from the device.

  8. (Optional) Poll the device(s) with a call to IDirectInputDevice8::Poll(). Some input devices need to be polled rather than you generating interrupts and keeping the input state current. Many joysticks fall into this class, so it's always a good idea to poll joysticks whether they need it or not. Polling doesn't hurt and costs nothing if it isn't needed (the function just returns).

  9. Read the device data for each device with a call to IDirectInputDevice8::GetDeviceState(). The data returned from the call will be different for each device, but the call is exactly the same. This call retrieves the data from the device and places it into a buffer so you can read it.

That's all there is to it! Seems like a lot, but it's really a small price to pay to access any input device without having to worry about the device driver for it!

Data Acquisition Modes

Last, but not least, I want to briefly alert you to the existence of immediate and buffered data modes. DirectInput can send you immediate state information or buffer input, time-stamped in a message format. I haven't had much use for the buffered input model, but it's there if you want to use it (read the DirectX SDK if you're interested). We'll use the immediate mode of data acquisition, which is the default.

Creating the Main DirectInput Object

Now let's see how to create the main DirectInput COM object IDirectInput8. Then we'll take a look at how to work with the keyboard, mouse, and joystick.

The interface pointer to the main DirectInput object is defined in DINPUT.H as follows:

LPDIRECTINPUT8 lpdi; // main directinput interface

To create the main COM interface, use the standalone function DirectInput8Create(), shown here:

HRESULT WINAPI DirectInput8Create(
 HINSTANCE hinst, // the main instance of the app
 DWORD dwVersion, // the version of directinput you want
 REFIID riidltf, // reference id for desired interface
 LPVOID *lplpDirectInput,  // ptr to storage
                                   // for interface ptr
 LPUNKNOWN punkOuter);  // COM stuff, always NULL

The parameters are as follows:

hinst is the instance handle of your application. This is one of the few function calls that needs this handle. It's the same handle that is passed into WinMain() at the start of your application, so just save it in a global and stuff it in here.

dwVersion is a constant that describes which version of DirectInput you want to be compatible with. If you assume that some of your game will be played on DirectX 3.0 machines, this will be of concern, but just send DIRECTINPUT_VERSION for the latest version of DirectInput and that will do.

riidltf is a constant that selects the version of the interface that you want to create. Normally, this will be IID_IDirectInput8.

lplpDirectInput is the address of the interface pointer that will receive the COM interface to DirectInput.

And lastly, punkOuter is for COM aggregation and is not of concern. Set it to NULL.

DirectInput8Create() returns DI_OK (DirectInput OK) if successful, or something else if not. As usual, though, we are going to use the macros SUCCESS() and FAILURE() rather than test for DI_OK because it's now the preferred method under DirectX to test for a problem. But it's pretty safe to use DI_OK if you want.

Here's an example of creating the main DirectInput object:

#include "DINPUT.H" // need this and DINPUT.LIB, DINPUT8.LIB

// the rest of your includes, defines etc.

// globals...

LPDIRECTINPUT8 lpdi = NULL; // used to point to com interface

if (FAILED(DirectInput8Create(main_instance,
                              DIRECTINPUT_VERSION,
                              IID_IDirectInput8,
                              (void **)&lpdi,NULL)))
   return(0);

NOTE

It's very important that you include DINPUT.H and DINPUT.LIB in your application, or else the compiler and linker won't know what to do. Also, if you've never read my instructions on compiling, please include the .LIB file directly in the application project. Setting a search path in the library search settings is usually not enough.


And that's that. If the function was successful, at this point you'll have a pointer to the main DirectInput object that you can then use to create devices.

As with all COM objects, when your application is complete and you are releasing resources, you must make a call to Release() to decrement the reference count of the COM object. Here's how:

// the shutdown
lpdi->Release();

And if you want to be technical:

// the shutdown
if (lpdi)
    lpdi->Release();

Of course, you would do this after releasing the devices created. Remember always to make your calls to Release() in the reverse order you created your objects, like unwinding a stack.

The 101-Key Control Pad

Because setting up one DirectInput device is similar to setting up all other devices, I'm going to really go into detail with the keyboard and then speed things up with the mouse and joystick. So make sure to read this section carefully, because it will be applicable to the other devices as well.

Creating the Keyboard Device

The first step in getting any device up and running is creating it with a call to IDIRECTINPUT8::CreateDevice(). Remember, this function basically gives you an interface to the particular device that you requested (the keyboard in this first example), which you can then work with. Let's take a look at the function:

HRESULT CreateDevice(
 REFGUID rguid, // the GUID of the device to create
 LPDIRECTINPUTDEVICE8 *lplpDirectInputDevice,  // ptr to the
                                              // IDIRECTINPUTDEVICE8
                                              // interface to receive ptr
 LPUNKNOWN pUnkOuter); // COM stuff, always NULL

Simple enough, huh, baby? The first parameter, rguid, is the GUID of the device you want to create. You can either query for the GUID of interest or use one of the defaults for the most common devices:

GUID_SysKeyboard— The keyboard.

GUID_SysMouse— The mouse.

WARNING

Danger, Will Robinson! Remember, these are in DINPUT.H, so that must be included along with DINPUT.LIB, DINPUT8.LIB. Furthermore, for all this GUID stuff, you should also place a #define INITGUID at the top of your application before all other includes (but only once), as well as including the header OBJBASE.H with your application. You can also include the .LIB file DXGUID.LIB with your program, but OBJBASE.H is preferred. In any case, you can always look at the chapter demos on the CD and see a working example of what to include and what not to include; it's just one of the stupid details.


The second parameter is the receiver of the new interface, and of course the last is just NULL. The function returns DI_OK if successful and something else if not.

All right, based on your new-found knowledge of CreateDevice(), let's see if you can create the keyboard device. The first thing you need is a variable to hold the interface pointer that will be created during the call. All devices are of type IDIRECTINPUTDEVICE8:

IDIRECTINPUTDEVICE8 lpdikey = NULL; // ptr to keyboard device

Now, let's create the device with a call to CreateDevice() from the main COM object. Here's all the code, including the creation of the main COM object and all necessary inclusion/defines:

// this needs to come first
#define INITGUID


// includes
#include <OBJBASE.H> // need this one for GUIDS
#include "DINPUT.H"  // need this for directinput and
                     // DINPUT.LIB

// globals...

LPDIRECTINPUT8 lpdi = NULL; // used to point to com interface
IDIRECTINPUTDEVICE8 lpdikey = NULL; // ptr to keyboard device

if (FAILED(DirectInput8Create(main_instance,
                              DIRECTINPUT_VERSION,
                              IID_IDirectInput8,
                              (void **)&lpdi,NULL)))
   return(0)



// now create the keyboard device
if (FAILED(lpdi->CreateDevice(GUID_SysKeyboard, &lpdikey, NULL)))
   { /* error */ }

// do all the other stuff....

At this point, lpdikey points to the keyboard device, and you can call methods of the interface to set the cooperation level, data format, and so on. Of course, when you're done with the device, you release it with a call to Release(). However, this call will be before you've released the main DirectInput object lpdi, so put something like this in your shutdown:

// release all devices
if (lpdikey)
   lpdikey->Release();

// .. more device releases, joystick, mouse etc.
// now release main COM object
if (lpdi)
    lpdi->Release();
Setting the Keyboard's Cooperative Level

Once you've created your device (the keyboard in this case), you must set its cooperation level, just like the main DirectDraw object. But in the case of DirectInput, there isn't as much of a selection. Table 9.1 lists the various possibilities for the cooperation level.

Table 9.1. Cooperation Flags for DirectInput SetCooperativeLevel()
Value Description
DISCL_BACKGROUND Your application can use a DirectInput device when it's either in the background or active in the foreground.
DISCL_FOREGROUND The application requires foreground access. If foreground access is granted, the device is automatically unacquired when the associated window moves to the background.
DISCL_EXCLUSIVE Once you acquire the device, no other application can request exclusive access to it. However, other applications can still request non-exclusive access.
DISCL_NONEXCLUSIVE The application requires non-exclusive access. Access to the device will not interfere with other applications that are accessing the same device.

These give me a headache. It's like, "Background, foreground, exclusive, non-exclusive—you're killing me!" However, after reading the definitions a few times, it becomes clear how the various flags work. In general, if you set DISCL_BACKGROUND, your application will receive input whether it's active or minimized. Setting DISCL_FOREGROUND will only send your application input when it's on top.

The exclusive/non-exclusive setting controls whether your application has total control of the device and no other application can have access. For example, the mouse and keyboard are implicitly exclusive devices; when your application acquires them, no other application can use them until it gains focus. This creates some paradoxes.

First, you can only acquire the keyboard in non-exclusive mode because Windows itself always has to be able to get the Alt-key combinations. Second, you can acquire the mouse in exclusive mode if you want, but you will lose normal mouse messages to your application (this may be your intent) and the mouse cursor will disappear. Like you care, because you will most probably render one yourself. Finally, most force feedback joysticks (and joysticks in general) should be acquired in exclusive mode. However, you can set normal joysticks for non-exclusive.

Thus, the moral of the story is to set the flags to DISCL_BACKGROUND | DISCL_NONEXCLUSIVE. The only time you'll really need to set exclusive access is for force feedback devices. Of course, with this setting it's possible that you could lose the device to something that wants exclusive access when it becomes the active application. In that case you will have to reacquire the device, but we'll get to that in a moment.

For now, just set the cooperation level with IDIRECTINPUTDEVICE8::SetCooperativeLevel(...), shown here:

HRESULT SetCooperativeLevel(HWND hwnd, // the window handle
       DWORD dwFlags); // cooperation flags

And here's the call to set the cooperation level for your keyboard (all devices are done in the same way):

if (FAILED(lpdikey->SetCooperativeLevel(main_window_handle,
     DISCL_BACKGROUND | DISCL_NONEXCLUSIVE)))
   { /* error */ }

The only way this won't work is if there is another application that has exclusive/ foreground access and is the current application. Then you just have to wait or tell the user to kill the application that is hogging the input device.

Setting the Data Format of the Keyboard

The next step to getting the keyboard ready to send input is to set the data format. This is done with a call to IDIRECTINPUTDEVICE8::SetDataFormat(), shown here:

HRESULT SetDataFormat(LPCDIDATAFORMAT lpdf); // ptr to data format structure

Bummer… That single parameter is the problem. Here's the data structure:

// directinput dataformat
typedef struct
 {
 DWORD dwSize;    // size of this structure in bytes
 DWORD dwObjSize; // size of DIOBJECTDATAFORMAT in bytes
 DWORD dwFlags;   // flags:either DIDF_ABSAXIS or
                  // DIDF_RELAXIS for absolute or
                  // relative reporting
 DWORD dwDataSize;// size of data packets
 DWORD dwNumObjs; // number of objects that are defined in
                  // the following array of object
 LPDIOBJECTDATAFORMAT rgodf; // ptr to array of objects
  } DIDATAFORMAT, *LPDIDATAFORMAT;

This a really complex structure to set up and is overkill for your purposes. Basically, it allows you to define how the data from the input device will be formatted at the device object level. Luckily, though, DirectInput comes with some custom-made data formats that work for just about everything, and you will use one of them. Take a look at Table 9.2 to see these formats.

Table 9.2. Generic Data Formats Available to DirectInput
Value Description
c_dfDIKeyboard Generic keyboard
c_dfDIMouse Generic mouse
c_dfDIJoystick Generic joystick
c_dfDIJoystick2 Generic force feedback

Once you set the data format to one of these types, DirectInput will send each data packet back in a certain format. DirectInput again has some predefined formats to make this easy, as shown in Table 9.3.

Table 9.3. DirectInput Data Structures Used to Send Data When Using Generic Data Formats
Name Description
DIMOUSESTATE This data structure holds a mouse message.
DIJOYSTATE This data structure holds a standard joystick-like device message.
DIJOYSTATE2 This data structure holds a standard force feedback device message.

I'll show you the actual structures when we get to the mouse and the joystick. However, you're probably wondering where the damn keyboard structure is. Well, it's so simple that there isn't a type for it. It's nothing more than an array of 256 bytes, each representing one key, thus making the keyboard look like a set of 101 momentary switches.

Hence, using the default DirectInput data format and data type is very similar to using the Win32 function GetAsyncKeyState(). In any case, all you need is a type like this:

typedef _DIKEYSTATE UCHAR[256];

TRICK

If DirectX is missing something and I want to create a "DirectXish" version of it, I will usually create the missing data structure or function, but with a leading underscore to remind me six months from now that I invented it.


So with all that in mind, let's set the data format for the poor little keyboard:

// set data format
if (FAILED(lpdikey->SetDataFormat(&c_dfDIKeyboard)))
   { /* error */ }

Notice that I used the & operator to get the address of the global c_dfDIKeyboard because the function wants a pointer to it.

Acquiring the Keyboard

You're getting there! Almost done. You've created the DirectInput main COM object, created a device, set the cooperation level, and set the data format. The next step is to acquire the device from DirectInput. To do this, use the function method IDIRECTINPUTDEVICE8::Acquire() with no parameters. Here's an example:

// acquire the keyboard
if (FAILED(lpdikey->Acquire()))
   { /* error */ }

And that's all there is to it. Now you're ready to get input from the device! It's time to celebrate. I think I'll have a Power Bar. :)

Retrieving Data from the Keyboard

Retrieving data from all devices is mostly the same, plus or minus a couple of details that may be device-specific. In general, you must do the following:

  1. (Optional) Poll the device like a joystick.

  2. Read the immediate data from the device with a call to IDIRECTINPUTDEVICE8:: GetDeviceState().

TIP

Remember, any method you can call with a IDIRECTINPUTDEVICE interface can be called with a IDIRECTINPUTDEVICE2 interface.


Here's what the GetDeviceState() function looks like:

HRESULT GetDeviceState(
   DWORD cbData,    // size of state data structure
   LPVOID lpvData); // ptr to memory to receive data

The first parameter is the size of the receiving data structure that the data will be stuffed into: 256 for keyboard data, sizeof(DIMOUSESTATE) for the mouse, sizeof(DIJOYSTATE) for the plain joystick, and so on. The second parameter is just a pointer to where you want the data to be stored. Hence, here's how you might read the keyboard:

// here's our little helper typedef
typedef _DIKEYSTATE UCHAR[256];

_DIKEYSTATE keystate[256]; // this will hold the keyboard data

Now let's read the keyboard:

if (FAILED(lpdikey->GetDeviceState(sizeof(_DIKEYSTATE),
         (LPVOID)keystate)))
       { /* error */ }

Of course, you would do this once for each game loop at the top of the loop, before any processing has occurred.

Once you have the data, you'll want to test for keypresses, right? Just as there are constants for the GetAsyncKeyState() function, there are constants for the keyboard switches that resolve to their positions in the array. They all start with DIK_ (DirectInput key, I would imagine) and are defined in DINPUT.H. Table 9.4 contains a partial list of them (please refer to the DirectX SDK docs for the complete list).

Table 9.4. The DirectInput Keyboard State Constants
Symbol Description
DIK_ESCAPE The Esc key
DIK_0-9 Main keyboard 0 through 9
DIK_MINUS Minus key
DIK_EQUALS Equals key
DIK_BACK Backspace key
DIK_TAB Tab key
DIK_A-Z Letters A through Z
DIK_LBRACKET Left bracket
DIK_RBRACKET Right bracket
DIK_RETURN Return/Enter on main keyboard
DIK_LCONTROL Left control
DIK_LSHIFT Left shift
DIK_RSHIFT Right shift
DIK_LMENU Left Alt
DIK_SPACE Spacebar
DIK_F1-15 Function keys 1 through 15
DIK_NUMPAD0-9 Numeric keypad keys 0 through 9
DIK_ADD + on numeric keypad
DIK_NUMPADENTER Enter on numeric keypad
DIK_RCONTROL Right control
DIK_RMENU Right Alt
DIK_HOME Home on arrow keypad
DIK_UP Up arrow on arrow keypad
DIK_PRIOR PgUp on arrow keypad
DIK_LEFT Left arrow on arrow keypad
DIK_RIGHT Right arrow on arrow keypad
DIK_END End on arrow keypad
DIK_DOWN Down arrow on arrow keypad
DIK_NEXT PgDn on arrow keypad
DIK_INSERT Insert on arrow keypad
DIK_DELETE Delete on arrow keypad
Bolded entries simply mean to follow the sequence. For example, DIK_0-9 means that there are constants DIK_0, DIK_1, DIK_2, and so forth.

To test if any key is down, you must test the 0x80 bit in the 8-bit byte of the key in question; in other words, the uppermost bit. For example, you'd use the following if you wanted to test whether the Esc key was pressed:

if (keystate[DIK_ESCAPE] & 0x80)
    { // it's pressed */ }
else
    { /* it's not */ }

TIP

You could probably get away without the & and the bit test, but Microsoft doesn't guarantee that other bits won't be high even if the key isn't down. It's a good idea to do the bit test just to be safe.


The and operator is a bit ugly; you can make it look better with a macro like this:

#define DIKEYDOWN(data,n) (data[n] & 0x80)

Then you can just write this:

if (DIKEYDOWN(keystate, DIK_ESCAPE))
    { /* do it to it baby! */ }

Much cleaner, huh? And of course, when you're done with the keyboard, you must "unacquire" it with Unacquire() and release it (along with the main DirectInput COM object) like this:

// unacquire keyboard
if (lpdikey)
   lpdikey->Unacquire();

// release all devices
if (lpdikey)
   lpdikey->Release();

// .. more device unacquire/releases, joystick, mouse etc.
// now release main COM object
if (lpdi)
    lpdi->Release();

This is the first time that I've talked about the function Unacquire(), but it is so closely tied to releasing the object that I thought it appropriate for use here. However, if you simply want to unacquire a device but not release it, you can surely call Unacquire() on a device and reacquire it later. You might want to do this if you want another application to have access to the device when you aren't using it.

WARNING

Of course, if you want to release the keyboard but keep the joystick (or some other combination), don't release the main COM object until you're completely ready to kill DirectInput.


For an example of using the keyboard, take a look at DEMO9_1.CPP|EXE (DEMO9_1_16B.CPP|EXE 16-bit version) on the CD. Figure 9.5 shows a screen shot of the action. The program uses all the techniques we have covered to set up the keyboard, and it lets you move a character around. To compile the program, remember to include DDRAW.LIB, DINPUT.LIB, and WINMM.LIB for VC++ users. Also, if you look at the header section of the program, you'll notice that it uses T3DLIB1.H. Thus, it obviously needs T3DLIB1.CPP in the project to compile. By the end of the chapter, you will create an entire input library (which I have already completed and named T3DLIB2.CPP|H, but I'll show that to you later in the chapter).

Figure 9.5. DEMO09 1.EXE in action

graphics/09fig05.gif

Problem During Reading: Reacquisition

I hate to talk about problems that can occur with DirectX because there are so many of them. They aren't bugs, but simply manifestations of running in a cooperative OS like Windows. One such problem that can occur with DirectInput is due to a device getting yanked from you or acquired by another application.

In this case, it's possible that you had the device during the last frame of animation, and now it's gone. Alas, you must detect this and be able to reacquire the device. Luckily, there is a way to test for it that is fairly simple. When you read a device, you test to see if it was acquired by another application. If so, you simply reacquire it and try reading the data again. You can tell because the GetDeviceState() function will return an error code.

The real error codes that GetDeviceState() returns are shown in Table 9.5.

Table 9.5. Error Codes for GetDeviceState()
Value Description
DIERR_INPUTLOST Device has lost input and will lose acquisition on next call.
DIERR_INVALIDPARAM One of the parms to the function was invalid.
DIERR_NOTACQUIRED You have totally lost the device.
DIERR_NOTINITIALIZED The device is not ready.
E_PENDING Data not yet available: chill.

So what you want to do is test for DIERR_INPUTLOST during a read and then try to reacquire if that error occurs. Here's an example:

HRESULT result; // general result

while(result = lpdikey->GetDeviceState(
         sizeof(_DIKEYSTATE),
         (LPVOID)keystate) == DIERR_INPUTLOST)
     {
     // try an re-acquire the device
     if (FAILED(result = lpdikey->Acquire()))
        {
        break; // serious error
        } // end if

     } // end while

// at this point, there is either a serious error or the data is valid
if (FAILED(result))
   { /* error */}

TIP

Although I'm showing you an example of reacquiring the keyboard, chances are this will never happen. In most cases, you will only lose joystick-like devices.


Trapping the Mouse

The mouse is one of the most amazingly useful input devices ever created. Can you imagine being the guy who invented the mouse and having so many people laugh at how ridiculous they thought it was? I hope the inventor is laughing on some island over margaritas! The point is, sometimes it's the most unusual things that work best, and the mouse is a good example of that. Anyway, let's get serious now… let me take this bra off my head. :)

The standard PC mouse has either two or three buttons and two axes of motion: X and Y. As the mouse is moved around, it builds up packets of information describing state changes and then sends them to the PC serially (in most cases). The data is then processed by a driver and finally sent on up to Windows or DirectX. As far as we are concerned, the mouse is black magic. All we want to know is how to determine when it moves and when a button is pressed. DirectInput does this and more.

There are two ways to communicate with the mouse: absolute mode and relative mode. In absolute mode, the mouse returns its position relative to the screen coordinates based on where the mouse pointer is. Thus, in a screen resolution of 640x480, you would expect the position of the mouse to vary from 0-639, 0-479. Figure 9.6 shows this graphically.

Figure 9.6. The mouse in absolute mode.

graphics/09fig06.gif

In relative mode, the mouse driver sends the position of the mouse as relative deltas at each clock tick rather than as an absolute position. This is shown in Figure 9.7. In reality, all mice are relative; it's the driver that keeps track of the absolute position of the mouse. Hence, I am going to work with the mouse in relative mode because it's more flexible.

Figure 9.7. The mouse in relative mode.

graphics/09fig07.gif

Now that you know a little about the mouse, let's see what you need to do to get it working under DirectInput:

  1. Create the mouse device with CreateDevice().

  2. Set the cooperation level with SetCooperativeLevel().

  3. Set the data format with SetDataFormat().

  4. Acquire the mouse with Acquire().

  5. Read the mouse state with GetDeviceState().

  6. Repeat step 5 until done.

NOTE

If these steps look unfamiliar, please read the previous keyboard section.


Creating the Mouse Device

Looks like bedrock, baby. Let's give it a try. First, you need an interface pointer to receive the device once you create it. Use a IDIRECTINPUTDEVICE8 pointer for this:

// of course you need all the other stuff

LPDIRECTINPUTDEVICE8 lpdimouse = NULL; // the mouse device
// assuming that lpdi is valid

// create the mouse device
if (FAILED(lpdi->CreateDevice(GUID_SysMouse,
    &lpdimouse, NULL)))
   { /* error */ }

Step 1 is handled. Note that you used the device constant GUID_SysMouse for this type. This gives you the default mouse device.

Setting the Cooperation Level of the Mouse

Now, set the cooperation level:

if (FAILED(lpdimouse->SetCooperativeLevel(
           main_window_handle,
           DISCL_BACKGROUND | DISCL_NONEXCLUSIVE)))
   { /* error */ }
Setting the Data Format of the Mouse

Now for the data format. Remember, there are a number of standard data formats predefined by DirectInput (shown in Table 9.2); the one you want is c_dfDIMouse. Plug it into the function and set the data format:

// set data format
if (FAILED(lpdimouse->SetDataFormat(&c_dfDIMouse)))
   { /* error */ }

Okay, now you need to take a pause for a moment. With the keyboard data format c_dfDIKeyboard, the data structure returned was an array of 256 UCHARS. However, with the mouse, the data format defines something that is more mouse-like. :) Referring back to Table 9.3, the data structure that you'll be working with is called DIMOUSESTATE and is shown here:

// the mouse data structure
typedef struct DIMOUSESTATE
    {
    LONG lX;     // X-axis
    LONG lY;     // Y-axis
    LONG lZ;     // Z-axis (wheel in most cases)
    BYTE rgbButtons[4]; // buttons, high bit means down
    } DIMOUSESTATE, *LPDIMOUSESTATE;

Thus, when you make a call to get the device state with GetDeviceState(), this is the structure that will be returned. No surprises here. Everything is what it would seem.

Acquiring the Mouse

The next step is to acquire the mouse with a call to Acquire(). Here it is:

// acquire the mouse
if (FAILED(lpdimouse->Acquire()))
   { /* error */ }

Cool! This is so easy. Wait until you put a wrapper around all this stuff, which will be even easier!

Reading the Data from the Mouse

At this point, you have created the mouse device, set the cooperation level and the data format, and acquired it. Now you're ready to shake that booty. To make the shake happen, you need to read the data from the mouse with GetDeviceState(). However, you must send the correct parameters based on the new data format, c_dfDIMouse, and the data structure the data will be placed in, DIMOUSESTATE. Here's how you read the mouse:

DIMOUSESTATE mousestate; // this holds the mouse data

// .. somewhere in your main loop

// read the mouse state
if (FAILED(lpdimouse->GetDeviceState(sizeof(DIMOUSESTATE),
         (LPVOID)mousestate)))
       { /* error */ }

TRICK

Notice how smart the function is. Instead of having multiple functions, the function uses a size and ptr to work with any data format that exists now or that you might think of later. This is a good programming technique to remember, young Jedi.


Now that you have the mouse data, let's work with it. Imagine that you want to move an object around based on the motion of the mouse. If the player moves the mouse left, you want the object to move left by the same amount. In addition, if the user presses the left mouse button, it should fire a missile, and the right button should exit the program. Here's the main code:

// obviously you need to do all the other steps...

// defines
#define MOUSE_LEFT_BUTTON   0
#define MOUSE_RIGHT_BUTTON  1
#define MOUSE_MIDDLE_BUTTON 2 // (most of the time)

// globals
DIMOUSESTATE mousestate; // this holds the mouse data

int object_x = SCREEN_CENTER_X, // place object at center
    object_y = SCREEN_CENTER_Y;


// .. somewhere in your main loop
// read the mouse state
if (FAILED(lpdimouse->GetDeviceState(sizeof(DIMOUSESTATE),
         (LPVOID)mousestate)))
       { /* error */ }


// move object
object_x += mousestate.lX;
object_y += mousestate.lY;

// test for buttons
if (mousestate.rgbButtons[MOUSE_LEFT_BUTTON] & 0x80)
    { /* fire weapon */ }
else
if (mousestate.rgbButtons[MOUSE_RIGHT_BUTTON] & 0x80)
    { /* send exit message */ }
Releasing the Mouse from Service

When you're done with the mouse, you need to first unacquire it with a call to Unacquire() and then release the device as usual. Here's the code:

// unacquire mouse
if (lpdimouse)
   lpdimouse->Unacquire();

// release the mouse
if (lpdimouse)
   lpdimouse->Release();

As an example of working with the mouse, I have created a little demo called DEMO9_2.CPP|EXE (DEMO9_2_16B.CPP|EXE 16-bit version). As before, you need to link in DDRAW.LIB, DINPUT.LIB, and WINMM.LIB (for VC++ users), along with T3DLIB1.CPP. Figure 9.8 shows a screen shot of the program in action.

Figure 9.8. DEMO09 2.EXE in action.

graphics/09fig08.gif

Working the Joystick

The joystick is probably the most complex of all DirectInput devices. The term joystick really encompasses all possible devices other than the mouse and keyboard. However, to keep things manageable, I'm going to primarily focus on devices that look like a joystick or game paddle, such as the Microsoft Sidewinder, Microsoft Gamepad, Gravis Flight Stick, and so forth.

Before we get into this, take a look at Figure 9.9. Here you see a joystick and a control pad. Both devices are considered to be joysticks under DirectInput. The only joystick-like device that has a class of its own is the force feedback device, but I'll get to that later.

Figure 9.9. DirectInput devices are collections of device objects.

graphics/09fig09.gif

Anyway, the point that I want to make about the joystick and gamepad is that they're both the same thing as far as DirectInput is concerned. They are both a collection of axes, switches, and sliders. It's just that the axes on the joystick have many positions (they are continuous), and the gamepad has clamped or extreme positions. The point is that each device is a collection of device objects or device things or input objects, depending on your terminology and what reference you use. They're all just input devices that happen to be on the same physical piece of hardware. Get it? I hope so. :)

The steps for setting up a joystick-like device are the same as for the keyboard and mouse, except that there are a couple of added steps. Let's take a look:

  1. Create the joystick device with CreateDevice().

  2. Set the cooperation level with SetCooperativeLevel().

  3. Set the data format with SetDataFormat().

  4. Set the joystick range, dead zone, and other properties with SetProperties(). This step is new.

  5. Acquire the joystick with Acquire().

  6. Poll the joystick with the Poll() function. This step basically makes sure that joysticks without interrupt drivers have valid data when GetDeviceState() is called. This step is new.

  7. Read the mouse state with GetDeviceState().

  8. Repeat step 7 until done.

Enumerating for Joysticks

I always hate explaining callback functions and enumeration functions because they seem so complex. However, by the time you get your hands on this book, you will probably be familiar with these types of functions because DOS programming has been falling by the wayside for quite some time. If you are just learning Windows programming, this will seem like overkill, but once you get over it, you won't have to worry about it anymore.

Basically, a callback function is something similar to the WinProc() in your Windows programs. It's a function Windows calls that you supply to do something. This is fairly straightforward and understandable. Figure 9.10 shows a standard callback like that of the Windows WinProc().

Figure 9.10. A callback function.

graphics/09fig10.gif

However, Win32/DirectX also uses callback functions for enumeration. Enumeration means that Windows (or DirectInput in this case) needs to have the capability to scan the system registry, or whichever database, for something that you're looking for, such as what kind of joysticks are plugged in and available.

There are two ways to do this:

  • You could call a DirectInput function that builds a list for you and stores it in a data structure, and you later parse and extrapolate out the important information.

  • You could supply DirectInput with a callback/enumeration function that it will call for each new device that it finds. You can be the one who builds up the device list by adding each new entry every time your callback function is called.

The second method is how DirectInput works, so you just have to deal with it. Now, you might wonder why you need to do an enumeration at all. Well, you have no idea what types of joystick devices are plugged in, and even if you did, you need the exact GUID of one or more of them. So you need to scan for them no matter what because you need that GUID for the call to CreateDevice().

The function that does the enumeration is called IDIRECTINPUT8::EnumDevices() and is called directly from the main DirectInput COM object. Here's its prototype:

HRESULT EnumDevices(
 DWORD dwDevType,  // type of device to scan for
 LPDIENUMCALLBACK lpCallback, // ptr to callback func
 LPVOID pvRef,   // 32 bit value passed back to you
 DWORD dwFlags); // type of search to do

Let's take a look at the parameters. First, dwDevType indicates what kind of devices you want to scan for; the possibilities are shown in Table 9.6.

Table 9.6. The Basic Device Types for DirectInput
Value Description
DIDEVTYPE_MOUSE A mouse or mouse-like device (such as a trackball).
DIDEVTYPE_KEYBOARD A keyboard or keyboard-like device.
DIDEVTYPE_JOYSTICK A joystick or similar device, such as a steering wheel.
DIDEVTYPE_DEVICE A device that doesn't fall into one of the previous categories.

If you want EnumDevices() to be more specific, you can also give it a subtype that you logically OR with the main type. Table 9.7 contains a partial list of subtypes for mouse and joystick device enumeration.

Table 9.7. DirectInput Subtypes (Partial)
Value Description
DIDEVTYPEMOUSE_TOUCHPAD Standard touchpad.
DIDEVTYPEMOUSE_TRACKBALL Standard trackball.
DIDEVTYPEJOYSTICK_FLIGHTSTICK General flightstick.
DIDEVTYPEJOYSTICK_GAMEPAD Nintendo-like gamepad.
DIDEVTYPEJOYSTICK_RUDDER Simple rudder control.
DIDEVTYPEJOYSTICK_WHEEL Steering wheel.
DIDEVTYPEJOYSTICK_HEADTRACKER VR head tracker.

NOTE

There are few dozen other subtypes that I haven't listed. The point is, DirectInput can be as general or as specific in the search as you want it to be. However, you're just going to use DIDEVTYPE_JOYSTICK as the value for dwDevType because you just want to find the basic, run-of-the-mill joystick(s).


The next parameter in EnumDevices() is a pointer to the callback function that DirectInput is going to call for each device it finds. I will show you the form of this function in a moment. The next parameter, pvRef, is a 32-bit pointer that points to a value that will be passed to the callback. Thus, you can modify the value in the callback if you want, or use it to pass data back instead of globally.

Finally, dwFlags controls how the enumeration function should scan. That is, should it scan for all devices, just the ones that are plugged in, or just force feedback devices? Table 9.8 contains the scanning codes to control enumeration.

Table 9.8. Enumeration Scanning Control Codes
Value Description
DIEDFL_ALLDEVICES Scans for all devices that have been installed, even if they aren't currently connected.
DIEDFL_ATTACHEDONLY Scans for devices that are installed and connected.
DIEDFL_FORCEFEEDBACK Scans only for force feedback devices.

WARNING

You should use the DIEDFL_ATTACHEDONLY value because it doesn't make sense to allow the player to connect to a device that isn't plugged into the computer.


Now, let's take a more detailed look at the callback function. The way EnumDevices() works is that it sits in a loop internally, calling your callback over and over for each device it finds, as shown in Figure 9.11. Hence, it's possible that your callback could be called many times if there are a lot of devices installed or attached to the PC.

Figure 9.11. The device enumeration flow diagram.

graphics/09fig11.gif

This means that it's up to your callback function to record all these devices in a table or something so you can later review them after the EnumDevices() returns. Cool. With that in mind, let's take a look at the generic prototype for the callback function to be compatible with DirectInput:

BOOL CALLBACK EnumDevsCallback(
 LPDIDEVICEINSTANCE lpddi, // a ptr from DirectInput
                           // containing info about the
                           // device it just found on
                           // this iteration
 LPVOID data); // the ptr sent in pvRef to EnumDevices()

All you need to do is write a function with the previous prototype (but write the control code, of course), pass it as lpCallback to EnumDevices(), and you're all set. Furthermore, the name can be anything you want because you're passing the function by address.

What you put inside the function is up to you, of course, but you probably want to record or catalog the names of all the devices and their GUIDs as they are retrieved by DirectInput. Remember, your function will be called once for each device found. Then, with the list in hand, you can select one yourself or let the user select one from a list, and then use the associated GUID to create the device.

In addition, DirectInput allows you to continue the enumeration or stop it at any time. This is controlled via the value you return from the callback function. At the end of the function, you can return one of these two constants:

  • DIENUM_CONTINUE-- Continues enumeration.

  • DIENUM_STOP-- Stops enumeration.

So if you simply return DIENUM_STOP as the return value of the function, it will enumerate only one device even if more exist. I don't have enough room here to show you a function that catalogs and records all the device GUIDs, but I'm going to give you one that will find the first device and set it up.

The aforementioned enumeration function will enumerate the first device and stop. But before I show it to you, take a quick look at the DIDEVICEINSTANCE data structure that is sent to your callback function for each enumeration. It's full of interesting information about the device:

typedef struct
 {
 DWORD dwSize;      // the size of the structure
 GUID guidInstance; // instance GUID of the device
                    // this is the GUID we need
 GUID guidProduct;  // product GUID of device, general
 DWORD dwDevType;   // dev type as listed in tables 9.1-2
 TCHAR tszInstanceName[MAX_PATH]; // generic instance name
                    // of joystick device like "joystick 1"
 TCHAR tszProductName[MAX_PATH]; // product name of device
                    // like "Microsoft Sidewinder Pro"
 GUID guidFFDriver; // GUID for force feedback driver
 WORD wUsagePage;   // advanced. don't worry about it
 WORD wUsage;       // advanced. don't worry about it
 } DIDEVICEINSTANCE, *LPDIDEVICEINSTANCE;

In most cases, the only fields of interest are tszProductName and guidInstance. Taking that into consideration, here's the enumeration function that you can use to get the GUID of the first joystick device enumerated:

BOOL CALLBACK DInput_Enum_Joysticks(
LPCDIDEVICEINSTANCE lpddi, LPVOID guid_ptr)
{
// this function enumerates the joysticks, but stops at the
// first one and returns the instance guid
// so we can create it, notice the cast
*(GUID*)guid_ptr = lpddi->guidInstance;

// copy product name into global
strcpy(joyname, (char *)lpddi->tszProductName);

// stop enumeration after one iteration
return(DIENUM_STOP);
} // end DInput_Enum_Joysticks

To use the function to enumerate for the first joystick, you would do something like this:

char joyname[80]; // space for joystick name
GUID joystickGUID; // used to hold GUID for joystick

// enumerate attached joystick devices only with
// DInput_Enum_Joysticks() as the callback function
if (FAILED(lpdi->EnumDevices(
           DIDEVTYPE_JOYSTICK,    // joysticks only
           DInput_Enum_Joysticks, // enumeration function
           &joystickGUID,    // send guid back in this var
           DIEDFL_ATTACHEDONLY)))
   { /* error */ }

// notice that we scan for joysticks that are attached only

In a real product, you might want to continue the enumeration function until it finds all devices and then, during a setup or options phase, allow the player to select a device from a list. Then you use the GUID for that device to create the device, which is the next step!

Creating the Joystick

Once you have the device GUID of the device that you want to create, the call to create the device is, as usual, CreateDevice(). Assuming that the call to EnumDevices() has occurred and the device GUID has been stored in joystickGUID, here's how you would create the joystick device:

LPDIRECTINPUTDEVICE8 lpdijoy; // joystick device interface
// create the joystick with GUID
if (FAILED(lpdi->CreateDevice(joystickGUID, &lpdijoy,
                       NULL)))
   { /* error */ }

NOTE

In the demo engines that I create, I tend to use temporary interface pointers to old interfaces to gain access to the latest ones. Thus, in my demos I use a temp pointer, query for the latest, and call it lpdijoy rather than lpdijoy2. I do this because I got sick of having numbered interfaces around. The bottom line is that all of the interfaces used in this book are the latest ones available for the job.


Setting the Joystick's Cooperation Level

Setting the joystick's cooperation level is done in the exact same way as with the mouse and keyboard. However, if you had a force feedback stick, you would want exclusive access to it. Here's the code:

if (FAILED(lpdijoy->SetCooperativeLevel(
           main_window_handle,
           DISCL_BACKGROUND | DISCL_NONEXCLUSIVE)))
   { /* error */ }
Setting the Data Format

Now for the data format. As with the mouse and keyboard, use a standard data format as shown in Table 9.2. The one you want is c_dfDIJoystick (ci_dfDIJoystick2 is for force feedback). Plug it into the function and set the data format:

// set data format
if (FAILED(lpdijoy->SetDataFormat(&c_dfDIJoystick)))
   { /* error */ }

As with the mouse, you need a specific type of data structure to hold the device state data for the joystick. Referring back to Table 9.3, you see that the data structure you'll be working with is called DIJOYSTATE (DIJOYSTATE2 is for force feedback), shown here:

// generic virtual joystick data structure

typedef struct DIJOYSTATE
 {
 LONG lX;   // x-axis of joystick
 LONG lY;   // y-axis of joystick
 LONG lZ;   // z-axis of joystick
 LONG lRx;  // x-rotation of joystick (context sensitive)
 LONG lRy;  // y-rotation of joystick (context sensitive)
 LONG lRz;  // y-rotation of joystick (context sensitive)
 LONG rglSlider[2];// slider like controls, pedals, etc.
 DWORD rgdwPOV[4]; // Point Of View hat controls, up to 4
 BYTE  rgbButtons[32]; // 32 standard momentary buttons
} DIJOYSTATE, *LPDIJOYSTATE;

As you can see, the structure has a lot of data fields. This generic data format is very versatile; I doubt that you would need to ever make your own data format, because I haven't seen many joysticks that have more than this! Anyway, the comments should explain what the data fields are. The axes are in ranges (that can be set), and the buttons are usually momentary with 0x80 (high bit), meaning that they're pressed.

Thus, when you make a call to get the device state with GetDeviceState(), this is the structure that will be returned and the one that you will query.

Almost done. There is one more detail that you have to take into consideration: the details of the values that are going to be sent back to you in this structure. A button is a button. It's either on or off, but the range entries like lX, lY, and lZ may vary from one manufacturer to another. Thus, DirectInput lets you scale them to a fixed range for all cases so that your game input logic can always work with the same numbers. Let's take a look at how to set this and other properties of the joystick.

Setting the Input Properties of the Joystick

Because the joystick is inherently an analog device, the motion of the yoke has finite range. The problem is, you must set it to known values so your game code can interpret it. In other words, when you query the joystick for its position and it returns lX = 2000, lY=-3445, what does it mean? You can't interpret the data because you have no frame of reference, so that's what you need to clarify.

At the very least, you need to set the ranges of any analog axis (even if it's digitally encoded) that you want to read. For example, you might decide to set both the X and Y axes to -1000 to 1000 and -2000 to 2000, respectively, or maybe -128 to 128 for both so you can fit them in a byte. Whatever you decide to do, you must do something. Otherwise, you won't have any way of interpreting the data when you retrieve it, unless you have set the range yourself.

Setting any property of the joystick, including the ranges of the joystick, is accomplished with the SetProperty() function. Its prototype is shown here:

HRESULT SetProperty(
 REFGUID rguidProp,     // GUID of property to change
 LPCDIPROPHEADER pdiph);// ptr to property header struct
                        // containing detailed information
                        // relating to the change

SetProperty() is used to set a number of various properties, such as relative or absolute data format, range of each axis, dead zone (or dead band; area that is neutral), and so forth. Using the SetProperty() function is extremely complex due to the nature of all the constants and nested data structures.

Suffice it to say that you shouldn't call SetProperty() unless you absolutely must. Most of the default values will work fine. I spent many hours looking at the circular data structures going, "What the heck?" (I didn't really say "What the heck?", but this is a PG-rated book.)

Luckily, you only need to set the range of the X-Y axes (and maybe the dead zone) to make things work, so that's all I'm going to show. If you're interested in learning more, refer to the DirectX SDK on this subject. Nonetheless, the following code should get you started on setting other properties if you need to. The structure you need to set up is as follows:

typedef struct DIPROPRANGE
        {
        DIPROPHEADER diph;
        LONG         lMin;
        LONG         lMax;
        } DIPROPRANGE, *LPDIPROPRANGE;

This has another nested structure, DIPROPHEADER:

typedef struct DIPROPHEADER
        {
        DWORD    dwSize;
        DWORD    dwHeaderSize;
        DWORD    dwObj;
        DWORD    dwHow;
        } DIPROPHEADER, *LPDIPROPHEADER;

And both of them have a billion ways of being set up, so please look at the DirectX SDK if you're interested. It would take 10 more pages just to list all the various flags you can send! Anyway, here's the code to set the axes ranges:

// this structure holds the data for the property changes
DIPROPRANGE joy_axis_range;

// first set x axis tp -1024 to 1024
joy_axis_range.lMin = -1024;
joy_axis_range.lMax = 1024;

joy_axis_range.diph.dwSize       = sizeof(DIPROPRANGE);
joy_axis_range.diph.dwHeaderSize = sizeof(DIPROPHEADER);

// this holds the object you want to change
joy_axis_range.diph.dwObj  = DIJOFS_X;

// above can be any of the following:
//DIJOFS_BUTTON(n) - for buttons buttons
//DIJOFS_POV(n)  - for point-of-view indicators.
//DIJOFS_RX - for x-axis rotation.
//DIJOFS_RY - for y-axis rotation.
//DIJOFS_RZ - for z-axis rotation (rudder).
//DIJOFS_X - for x-axis.
//DIJOFS_Y - for y-axis.
//DIJOFS_Z - for the z-axis.
//DIJOFS_SLIDER(n) - for any of the sliders.
// object access method, use this way always
joy_axis_range.diph.dwHow = DIPH_BYOFFSET;

// finally set the property
lpdijoy->SetProperty(DIPROP_RANGE,&joy_axis_range.diph);

// now y-axis
joy_axis_range.lMin = -1024;
joy_axis_range.lMax = 1024;
joy_axis_range.diph.dwSize       = sizeof(DIPROPRANGE);
joy_axis_range.diph.dwHeaderSize = sizeof(DIPROPHEADER);
joy_axis_range.diph.dwObj        = DIJOFS_Y;
joy_axis_range.diph.dwHow        = DIPH_BYOFFSET;

// finally set the property
lpdijoy->SetProperty(DIPROP_RANGE,&joy_axis_range.diph);

At this point, the joystick would have the X and Y axes set to a range of -1024 to 1024. This range is arbitrary, but I like it. Notice that you use a data structure called DIPROPRANGE. This is the structure that you set up to do your bidding. The bad thing about it is that there are a million ways to set up the structure, so it's a real pain. However, using the previous template, you can at least set the range of any axis—just change the joy_axis_range.diph.dwObj and joy_axis_range.diph.dwHow fields to whatever you need.

As a second example of setting properties, let's set the dead zone (or dead band) of the X and Y axes. The dead zone is the amount of neutral area in the center of the stick. You might want the stick to be able to move a bit away from the center and not send any values. This is shown in Figure 9.12.

Figure 9.12. The mechanics of the joystick dead zone.

graphics/09fig12.gif

For example, in the previous example you set the X and Y axis range to -1024 to 1024, so if you wanted a 10 percent dead zone on both axes, you would set it for about 102 units in both the + and - directions, right? Wrong!!! The dead zone is in terms of an absolute range of 0–10,000, no matter what range you set the joystick to. Thus, you have to compute 10 percent of 10,000 rather than 1024-10% x 10000 — = 1000. This is the number you need to use.

WARNING

The dead zone is always in terms of 0-10000 or hundreds of a percent. If you want a dead zone of 50%, use 5000, for 10% use 1000, and so forth.


Because this operation is a little simpler, you need only use the DIPROPWORD structure:

typedef struct DIPROPDWORD
        {
        DIPROPHEADER     diph;
        DWORD            dwData;
        } DIPROPDWORD, *LPDIPROPDWORD;

This is much simpler than the DIPROPRANGE structure used in the previous example. Here's how to do it:

DIPROPDWORD dead_band; // here's our property word

dead_band.diph.dwSize       = sizeof(dead_band);
dead_band.diph.dwHeaderSize = sizeof(dead_band.diph);
dead_band.diph.dwObj        = DIJOFS_X;
dead_band.diph.dwHow        = DIPH_BYOFFSET;

// 100 will be used on both sides of the range +/-
dead_band.dwData            = 1000;

// finally set the property
lpdijoy->SetProperty(DIPROP_DEADZONE,&dead_band.diph);

And now for the Y-axis:

dead_band.diph.dwSize       = sizeof(dead_band);
dead_band.diph.dwHeaderSize = sizeof(dead_band.diph);
dead_band.diph.dwObj        = DIJOFS_Y;
dead_band.diph.dwHow        = DIPH_BYOFFSET;

// 100 will be used on both sides of the range +/-
dead_band.dwData            = 1000;

// finally set the property
lpdijoy->SetProperty(DIPROP_DEADZONE,&dead_band.diph);

And that's all there is to that. Thank Zeus!

Acquiring the Joystick

Now, let's acquire the joystick with a call to Acquire():

// acquire the joystick
if (FAILED(lpdijoy->Acquire()))
   { /* error */ }

Of course, remember to Unacquire() the joystick when you're through with it, right before calling Release() on the interface to release the device itself.

Polling the Joystick

Joysticks are the only devices that need polling (so far). The reason for polling is the following: Some joystick drivers generate interrupts, and the data is always fresh.

Some drivers are less intelligent (or more efficient) and must be polled. Whatever the philosophical viewpoint of the driver developer, you must always call Poll() on the joystick before trying to read the data. Here's the code for doing so:

If (FAILED(lpdijoy->Poll()))
   { /* error */ }
Reading the Joystick State Data

Now you're ready to read the data from the joystick (you should be an expert at this by now). Make a call to GetDeviceState(). However, you must send the correct parameters based on the new data format, c_dfDIJoystick (c_dfDIJoystick2 for force feedback), and the data structure the data will be placed in, DIJOYSTATE. Here's the code:

DIJOYSTATE joystate; // this holds the joystick data

// .. somewhere in your main loop

// read the joystick state
if (FAILED(lpdijoy->GetDeviceState(sizeof(DIJOYSTATE),
         (LPVOID)joystate)))
       { /* error */ }

Now that you have the joystick data, let's work with it. However, you need to take into consideration that the data is in a range. Let's write a little program that moves an object around, much like the mouse example. And if the user presses the fire button (usually index 0), a missile fires:

// obviously you need to do all the other steps...

// defines
#define JOYSTICK_FIRE_BUTTON   0

// globals
DIJOYSTATE joystate; // this holds the joystick data

int object_x = SCREEN_CENTER_X, // place object at center
    object_y = SCREEN_CENTER_Y;

// .. somewhere in your main loop

// read the joystick state
if (FAILED(lpdijoy->GetDeviceState(sizeof(DIJOYSTATE),
         (LPVOID)joystate)))
       { /* error */ }

// move object


// test for buttons
if (mousestate.rgbButtons[JOYSTICK_FIRE_BUTTON] & 0x80)
    { /* fire weapon */ }
Releasing the Joystick from Service

When you're done with the joystick, you need to unacquire and release the device as usual. Here's the code:

// unacquire joystick
if (lpdijoy)
   lpdijoy->Unacquire();

// release the joystick
if (lpdijoy)
   lpdijoy->Release();

WARNING

Releasing before unacquiring can be devastating! Make sure to unacquire and then release.


As an example of working with the joystick, I have created a little demo called DEMO9_3.CPP|EXE (DEMO9_3_16B.CPP|EXE 16-bit version). As before, you need to link in DDRAW.LIB, DINPUT.LIB, and WINMM.LIB (for VC++ users), along with T3DLIB1.CPP. Figure 9.13 shows a screen shot of the program in action.

Figure 9.13. DEMO9 3.EXE in action.

graphics/09fig13.gif

Massaging Your Input

Now that you know how to read each input device, the question becomes one of input system architecture. In other words, you might be obtaining input from a number of input devices, but it would be a pain to have separate control code for each input device.

Thus, you might come up with the idea of creating a generic input record, merging all the input from the mouse, keyboard, and joystick together, and then using this structure to make your decisions. Figure 9.14 shows the concept graphically.

Figure 9.14. Merging input data into a virtual input record.

graphics/09fig14.gif

In the case of plain old DirectInput, let's say that you want the player to be able to play with the keyboard, mouse, and joystick all at the same time. The left mouse button might be the fire button, but so is the Ctrl key on the main keyboard and the first button on the joystick. In addition, if the player moves the mouse to the right, presses the right arrow on the keyboard, or moves the joystick to the right, you want all of these events to make the player move right.

As an example of what I'm talking about, let's build a really simple input system that takes a keyboard, joystick, and mouse input record from DirectInput and then merges them into one record that you can query. And when you do query the record, you won't care whether it was the mouse, keyboard, or joystick that was the source of the input because all the input events will be scaled and normalized.

The system you're going to implement will have the following:

  • X-axis

  • Y-axis

  • Fire button

  • Special button

And here are the details of the device variables and event mappings:

Mouse Mapping

+x-axis: if (lx > 0)

-x-axis: if (lx < 0)

+y-axis: if (ly > 0)

-y-axis: if (ly < 0)

Fire button: Left mouse button (rgbButtons[0])

Special button: Right mouse button (rgbButtons[1])

Keyboard Mapping

+x-axis: Right arrow key

-x-axis: Left arrow key

+y-axis: Up arrow key

-y-axis: Down arrow key

Fire button: Ctrl key

Special button: Esc key

Joystick Mapping (assume a range of –1024 to +1024 on both axes, with a 10 percent dead zone)

+x-axis: lX > 32

-x-axis: lX < -32

+y-axis: lY > 32

-y-axis: lY < -32

Fire button: rgbButtons[0]

Special button: rgbButtons[1]

Now that you know the mappings, make up an appropriate data structure to hold the result:

typedef struct INPUT_EVENT_TYP
        {
        int dx;       // the change in x
        int dy;       // the change in y
        int fire;     // the fire button
        int special;  // the special button
        } INPUT_EVENT, *INPUT_EVENT_PTR;

Using a simple function and logic, you're going to filter all the input into a structure of this type. First, assume that you have retrieved the device data from all input devices with something like this:

// keyboard
if (FAILED(lpdikey->GetDeviceState(256,
         (LPVOID)keystate)))
       { /* error */ }

// mouse
if (FAILED(lpdimouse->GetDeviceState(sizeof(DIMOUSESTATE),
         (LPVOID)mousestate)))
       { /* error */ }


// joystick

If (FAILED(lpdijoy->Poll()))
   { /* error */ }

if (FAILED(lpdijoy->GetDeviceState(sizeof(DIJOYSTATE),
         (LPVOID)joystate)))
       { /* error */ }

At this point, you have the structures keystate[], mousestate, and joystate ready to go. Here's a function that would do the job:

void Merge_Input(INPUT_EVENT_PTR event_data, // the result
                 UCHAR *keydata, // keyboard data
                 LPDIMOUSESTATE mousedata, // mouse data
                 LPDIJOYSTATE   joydata) // joystick data
{
// merge all the data together

// clear the record to be safe
memset(event_data,0,sizeof(INPUT_EVENT));

// first the fire button
if (mousedata->rgbButtons[0] || joydata->rgbButtons[0] ||
    keydata[DIK_LCONTROL])
event_data->fire = 1;

// now the special button
if (mousedata->rgbButtons[1] || joydata->rgbButtons[1] ||
    keydata[DIK_ESCAPE])
event_data->special = 1;

// now the x-axis
if (mousedata->lX > 0 || joydata->lX > 32 ||
    keydata[DIK_RIGHT])
event_data->dx = 1;

// now the -x-axis
if (mousedata->lX < 0 || joydata->lX < -32 ||
    keydata[DIK_LEFT])
event_data->dx = -1;

// and the y-axis
if (mousedata->lY > 0 || joydata->lY > 32 ||
    keydata[DIK_DOWN])
event_data->dy = 1;

// now the -y-axis
if (mousedata->lY < 0 || joydata->lY < -32 ||
    keydata[DIK_UP])
event_data->dy = -1;

} // end Merge_Data

Killer, huh? Of course, you can make this much more sophisticated by checking if the device is actually online, scaling the data, and so on, but you get the idea.

      Previous Section Next Section
    



    JavaScript EditorAjax Editor     JavaScript Editor