DirectInput OvertureDirectInput 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.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:
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 DirectInputDirectInput 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.Let's take a look at these interfaces:
The General Steps for Setting Up DirectInputThere 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:
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 ModesLast, 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 ObjectNow 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:
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 PadBecause 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 DeviceThe 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:
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 LevelOnce 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. 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 KeyboardThe 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. 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. 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 KeyboardYou'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 KeyboardRetrieving 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:
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). 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) 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 actionProblem During Reading: ReacquisitionI 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. 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 MouseThe 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.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.Now that you know a little about the mouse, let's see what you need to do to get it working under DirectInput:
NOTE If these steps look unfamiliar, please read the previous keyboard section. Creating the Mouse DeviceLooks 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 MouseNow, set the cooperation level: if (FAILED(lpdimouse->SetCooperativeLevel( main_window_handle, DISCL_BACKGROUND | DISCL_NONEXCLUSIVE))) { /* error */ } Setting the Data Format of the MouseNow 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 MouseThe 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 MouseAt 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 ServiceWhen 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.Working the JoystickThe 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.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:
Enumerating for JoysticksI 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.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:
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. 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. 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. 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.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: 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 JoystickOnce 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 LevelSetting 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 FormatNow 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 JoystickBecause 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.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 JoystickNow, 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 JoystickJoysticks 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 DataNow 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 ServiceWhen 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.Massaging Your InputNow 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.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:
And here are the details of the device variables and event mappings: +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]) +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. |