February 1998
May the Force Feedback Be with You: Grappling with DirectX and DirectInput
Download FeelForce.exe (16KB)
Jason Clark supports software core development for Microsoft.
He believes that logic is a pure art and a pure science.
He can be reached at jclark@microsoft.com.
|
In case you haven't noticed, games and multimedia programs for Windows® have really started to rock. Hardware is getting faster, and Windows has become more flexible. Since Microsoft released DirectX®, game developers have had increasingly less incentive to develop their software for other platforms. Many software publishers have migrated their game development completely to Windows.
Developing games for PCs has never been easy. With the myriad of graphics and sound cards available, developers have learned the art of balancing functionality with compatibility. They've had to deal with nasty issues such as page swapping, segmented memory architecture, and bit manipulations. And with the growing popularity of multiplayer games, developers must also deal with considerations such as networking and communications. Game development has become easier with the introduction of DirectX. Much of this grunt work has been greatly simplified by the DirectX objects now available to developers. I would like to take some of the mystery out of DirectX. Is a DirectX-based program a normal Windows-based application? Do you have to know COM? Is it worth using DirectX for a simple program? Do you have to use all of DirectX? I'm sure there are more questions, but you get the point. This article, the first in a series, will introduce DirectX and then let you get your feet wet with one of its components, DirectInput®. A demo application illustrates DirectInput, focusing on force feedback functionality. Each successive article will expand on the demo app until it has touched on each major component of DirectX.
DirectX Demystified
Getting Started with DirectX
Use the Force
Dissecting DirectInput
Creating a DirectInputObject
|
|
The first parameter is the instance of your process. The second is the version of DirectInput that your application requires. Normally you pass the DIRECTINPUT_VERSION macro, which will be defined as the current version. The third parameter is the most important, and it's potentially confusing if COM is fairly new to you. It is the address of
a pointer to an IDirectInput interface. Your application should define a variable (possibly global) of type LPDIRECTINPUT and pass its address as the third parameter to DirectInputCreate.
The last parameter is known as punkOuter, and has to do with a COM technique known as aggregation. You can safely ignore this parameter and pass NULL. The return value is an HRESULT, which is a standard return type for COM. It can be compared to the possible return values for the function, or you can apply one of the COM macros SUCCESS or FAILED to check it. Using DirectInputCreate, you can easily create your high-level object and retrieve a pointer to its main interface. This is another design approach of DirectX that you should get used to. Each DirectX component provides a helper function, such as DirectInputCreate or DirectDrawCreate, which can be used to create your high-level object. You will probably use the Se helper functions to create DirectX objects in your own applications. However, the Se functions are actually creating COM objects, which can also be done by using a standard Win32 API function called CoCreateInstance. This leads to the second way that you can create a DirectInput object. Using CoCreateInstance to create a COM object in Win32 is very common. If your application is already using CoCreateInstance to create other COM objects, you may want to use it to create your DirectX objects as well. Because COM objects are registered to the system when they are installed, all you need to know is the Object's GUID to create an instance of it. All of the GUIDs that you need to create DirectX objects are declared in the header files, such as DINPUT.H, and they are defined in the DXGUID.LIB lib file. You can pass one of the Se predefined GUIDs to CoCreateInstance and have Windows create the Object for you. CoCreateInstance is defined as follows: |
|
The first parameter is the GUID for the Object you want to create. In this case, DirectX defines the GUID for you as the GUID structure variable named CLSID_DirectInput. The second parameter is the familiar pUnkOuter. Again, you can ignore this feature and pass NULL. The third parameter, dwClsContext, defines where the COM object is to be created. DirectX only supports inproc servers, so you will pass the value CLSCTX_INPROC_SERVER.
The fourth parameter to CoCreateInstance is where the real difference in the two approaches comes in. Remember that COM objects expose interfaces. Like the Objects the Mselves, the interfaces are identified using a GUID. Using the first approach, you have no choice about which interface you get; you always get IDirectInput. Using CoCreateInstance, you can request any interface supported by the Object that you are requesting simply by passing the predefined GUID for that interface. In the case of DirectInput, this is all but moot because the DirectInput object's only useful interface is IDirectInput. I brought up this approach because other DirectX components support more than one useful interface. (For example, the DirectDraw® object can be manipulated by its IDirectDraw or IDirectDraw2 interface.) The last parameter to CoCreateInstance is the actual address of your application's interface pointer variable. You now have the Object and an interface to the Object. The CoCreateInstance approach requires one more step: you must make your first call into an interface function to initialize the Object. DirectInputCreate provides you with an already-initialized DirectInput object, but CoCreateInstance has no specific knowledge of DirectInput, so you must call the Initialize member function of the IDirectInput interface. Assuming you defined your pointer to the IDirectInput interface variable as follows |
|
your call to Initialize would look like this: |
|
Now you have effectively done, using CoCreateInstance, what CreateDirectInput did in the first approach. Though if you choose to take this "standard" approach to creating your objects, you will also have to pay attention to other standard COM needs such as the required call to CoInitialize and CoUninitialize.
Using the DirectInput Object
|
|
This should look familiar, as it's similar to the CoCreateInstance and DirectInputCreate APIs. But I am not yet completely ready to move on to the DirectInputDevice object. The reason is that you need the GUID for a device before you can create a DirectInputDevice object to use it.
The DirectInput library predefines two GUIDs for creating DirectInputDevice objects: GUID_SysKeyboard and GUID_SysMouse. Either of the Se can be passed directly to the CreateDevice function and you will be given a DirectInputDevice for that device. Notice the surprising lack of a predefined GUID for joysticks. In Windows, you usually have a system keyboard and a system mouse. Even if a system is an oddball and has two mice, it still has one system mouse defined as the composite movements of both mice. This is also the case with keyboards. On the Other hand, the system itself has no use for a joystick. You can install a joystick or two (or three, or four), and the system doesn't care beyond the driver level. It will not assign one of the Se devices a special system status, and it will not use one of the devices in its daily business. So it would not be logical (or truthful) for DirectInput to define a GUID for a system joystick. So how do you find the GUIDs for the joysticks attached to the system? To find the M, you have to enumerate devices. Enumerating system devices and capabilities is common in DirectX. To enumerate input devices in your system, use EnumDevices, part of the IDirectInput interface. EnumDevices is defined as follows: |
|
Notice that this function is similar to other enumeration APIs in Windows, such as EnumWindows. You pass the function a callback as the second parameter, and an application-defined 32-bit value, which is passed to the callback function as the third parameter. You also pass the type of device you wish to enumerate as the first parameter. For joysticks this would be DIDEVTYPE_JOYSTICK (see Figure 4 for a complete list of device types). Pass flags that detail what you want to enumerate as the last parameter. Currently supported flags are DIEDFL_ATTACHEDONLY and DIEDFL_ALLDEVICES, which are mutually exclusive, and DIEDFL_FORCEFEEDBACK, which indicates force feedback devices and can be bitwise ORed with the Other two flags.
While EnumDevices is enumerating input devices for the system, it is repeatedly calling your callback function. This function is defined as follows: |
|
Since this function is defined by your application and passed to EnumDevices, this is an excellent place for you to call CreateDevice until you have created enough DirectInputDevice objects to suit your needs. But the function does not have to be implemented this way. It could just as easily store all of the GUIDs for the enumerated devices in a list to be used by the code following the call to EnumDevices.
Your callback function receives two parameters. The second is the application-defined 32-bit value passed to EnumDevices. More importantly, the first parameter passed is a pointer to a structure containing lots of useful information about a single device that matches the criteria for the enumeration. This is the DIDEVICEINSTANCE structure. The most important piece of information for this purpose is the device's GUID, which is stored in the guidInstance member of the structure. After your application is completely finished with DirectInput, it should call the Release member of the IDirectInput interface. This tells the DirectInput object that it can free itself. With DirectX, it is a good idea to get used to freeing your objects, starting with the lower-level objects and ending with your high-level objects. Normally an application will call Release as part of its cleanup or shutdown routine. Once again, get used to this because it is a necessary part of using every DirectX component, and every COM component for that matter. Now that you have used the CreateDevice member function to get an interface to a DirectInputDevice object, you are ready to begin dealing with actual physical devices connected to the system.
Using the DirectInputDevice Object
|
|
In this example, lpdid would be your pointer to the IDirectInputDevice interface.
Once you have set the data format for the device object, you need to set the cooperation level of the device. Cooperation levels warrant some explaining since they are common throughout DirectX. Most DirectX objects that deal directly with system hardware have a function called SetCooperativeLevel as a member of one of their interfaces. This is important because it defines the level of control that your application wields over the hardware relative to other processes in the system. Like other DirectX objects, very little can be done with a DirectInputDevice object until its cooperation level has been set. To understand cooperation levels, you need to be acquainted with the Acquire function. This function is called to obtain actual access to the physical device (not to be confused with the logical DirectInputDevice object). Conversely, the Unacquire function releases access to the physical device. Now back to SetCooperativeLevel. Here is the function's definition: The hwnd is your application's main window (more on this in a moment). The flags are some combination of the following values ORed together: DISCL_BACKGROUND, DISCL_FOREGROUND, DISCL_EXCLUSIVE, DISCL_
NONEXCLUSIVE.
If DISCL_EXCLUSIVE is ORed into the flags parameter, then your process will be the Only one granted access to the physical device when you have acquired the device. On the Other hand, if DISCL_NONEXCLUSIVE is selected, then multiple processes in the system can acquire and use the device concurrently. If DISCL_BACKGROUND is ORed into the flags parameter, your process will not lose a physical device to another process. However, a system event such as the Ctrl+Alt+ Del combination can still implicitly "unacquire" a device for your process. If DISCL_ FOREGROUND is used, your process will automatically unacquire the physical device if your main window ceases to be the active window. This is the significance of passing your application's main window handle to SetCooperativeLevel. DirectX coordinates device sharing automatically based on whether the provided window is currently the active window in the system. So what is the significance of all of this? Let me give you some examples. If the cooperative mode for a force feedback joystick device is DISCL_FOREGROUND | DISCL_EXCLUSIVE, the application will be able to read from the joystick and cause force feedback effects to play (force feedback requires exclusive-level cooperation) as long as the application is active. As soon as the user selects another application, your application loses control of the physical device and the new active application can access the device if it wants to. This means that if you are debugging an application and you switch to the debugger's window, your application loses the joystick because its window became inactive. What would happen if your cooperative level for the same joystick was DISCL_BACKGROUND | DISCL_EXCLUSIVE? Your application would have access to the joystick at all times, regardless of the state of its window. But now other processes in the system cannot acquire the joystick until your process unacquires itregardless of what the user does! It would seem obvious that you would always choose DISCL_FOREGROUND | DISCL_EXCLUSIVE for a release product, and DISCL_BACKGROUND|DISCL_EXCLUSIVE for the debug version. But this is not always an option. For example, DirectInputDevice will fail a call to SetCooperativeLevel for exclusive use if the device is the system keyboard. This is because the Operating system wants to allow the user the freedom to switch from one process to the next, no matter what. Similarly, DirectInputDevice won't allow a cooperative level of DISCL_BACKGROUND|DISCL_EXCLUSIVE for the system mouse. Windows does not want an application to be able to completely shut a user out from the Operating system. I touched on the Acquire function briefly. Let's finish that discussion. The physical device must be acquired, using Acquire, before you can read from it or send information to it. To explicitly unacquire a device, which you do when you are temporarily or permanently finished with the device, use the Unacquire function. But Unacquire is not the Only way that you can lose control of a device. A device can be implicitly unacquired if the application's main window ceases to be the active window in the system and the DISCL_FOREGROUND flag was used when setting the cooperation level. This means that between the time that the application calls Acquire and the time that it actually tries to read from the device, it could have lost its acquire. You need to catch such errors by checking return values, and you should be prepared to reacquire the device using Acquire at any time. One final point regarding Acquire and Unacquire: while your application has a device acquired at the exclusive cooperative level, DirectX owns the device. For example, a button on your application's window will not respond to the mouse, if the mouse is acquired (exclusively) by DirectX. This means that you should unacquire a device if you want Windows to respond to the device. Said another way, call Unacquire if you don't want DirectInput to be reading data from a device. After setting the cooperation level of the device, you then configure other settings for the device. Acquire the device, then begin regular polling for input data using the GetDeviceState function. When finished with the device object, call Unacquire, and then Release to free up the DirectInputDevice object. Details differ from device to device; I'll cover the joystick and keyboard here, which should provide enough base knowledge for you to read input from other devices.
The Keyboard
|
|
cKeyboardData is the 256-char buffer. It's almost that simple. Do remember that if GetDeviceState ever returns a value of DIERR_INPUTLOST, you must acquire the device using Acquire. This is going to happen every time the user switches away from your application.
In the interest of being complete, it's important that I mention that it is possible to request that DirectInput buffer keyboard information. This requires you to supply a buffer and set the buffer size for the device using SetProperty. I don't have room to cover this technique here, this would be useful if your application is not able to check the keyboard state fairly often. It is possible that the user could press and release a key between two calls to GetDeviceState by the application. This keypress would be lost if DirectInput were not buffering keyboard data for you. So that is the keyboard in a nutshell. Now let's discuss the joystick.
The Joystick
|
|
Assuming success, you can then call release on your first interface and use your shiny new IDirectInputDevice2 interface. But why do you need this? The IDirectInputDevice2 interface provides all of the functionality of IDirectInputDevice with two important added features: Support for polling devices and support for force feedback devices. I will cover both of the Se in a minute.
Next, there are some setup considerations. Remember that SetDataFormat defines the type of data later returned by GetDeviceState. For a joystick device, pass one of the predefined variables: c_dfDIJoystick or c_dfDIJoystick2. the Se set the DIJOYSTATE or DIJOYSTATE2 structures as the return data type. Which one you choose depends largely on what type of joystick features you intend to use. Reading through the elements of the Se structures should help to clarify this. Here I will use the DIJOYSTATE structure, and will therefore set the data format of the device with the predefined c_dfDIJoystick structure variable. Like all input devices, you set the data format and cooperative levels for joysticks. The joystick tends to require a bit more attention than the keyboard. This is because there are roughly a gazillion joysticks available today so your application should make sure that the device at its disposal is suitable for its needs. If not, it can adjust its needs or alert the user to the fact that the joystick is lame! Either way, a device's capabilities can and should be ascertained with a call to the GetCapabilities member function of the IDirectInputDevice interface. This brings me to another point of discussion that applies to all of DirectX. DirectX offers a wide array of support for a variety of devices. To avoid defaulting to lowest common denominator support, the DirectX objects support features or groups of features that have been predefined for a given device. Odds are that you develop software on a vastly different machine from mine. Our machines are going to support differing levels of DirectX functionality. Writing good software using DirectX requires that you check the capabilities of the hardware, period. At the very least you can fail the application if a feature is missing. Best case scenario, of course, would be that your application intelligently adjusts itself for a missing feature. Before you begin retrieving input from your device, you need to set properties for the device. This includes such details as the range of values you want, to what portion of the joystick is the deadzone (middle position). This is all done with the SetProperty function. This function is potentially confusing, so let's take it slowly. SetProperty sets one feature of the device. First, you must fill in a data structure with the information regarding the setting you wish to change. Consult the documentation in the Platform SDK for all of the structures available. Each possible structure starts out with the elements of a DIPROPHEADER structure, which you fill in with information describing the feature you wish to change. Then you fill in the remainder of the structure with data specific to the setting you're changing. Finally, call SetProperty, passing the GUID for the change and a pointer to the DIPROPHEADER portion of your structure. The following code snippet would set the vertical range of a joystick to 100 by 100: |
|
The most confusing parts of this structure are the diph.dwObj and diph.dwHow elements. The diph.dwHow member describes what kind of info the diph.dwObj member holds. The diph.dwObj member actually describes which property is being set. Most of the time, the "How" value will be DIPH_BYOFFSET, and the "Obj" value will be a predefined offset into the structure that was passed into SetDataFormat.
That said, I should point out that it is possible to enumerate objects of a device, which include buttons and other features. This is done with the EnumObjects function. In doing so, you are provided with an object ID. You can pass this ID in the diph.dwObj member, at which point you would fill the diph.dwHow member with DIPH_BYID. If this method is confusing, use the "by offset" approach with preexisting values such as DIJOFS_Y until you have the need to take the second approach. Before reading data from the device, at the very least you need to set the min and max values for the X and Y axis of your device. Once your device's properties have been set, you can acquire the device and begin retrieving data from it. Retrieving data from a joystick is different than from a keyboard or a mouse because it is a polled device. Keyboards and mice cause hardware interrupts, which are handled by drivers in the system and are used to update the data that DirectInput returns via calls to GetDeviceState. Polled devices (such as most joysticks) do not create hardware interrupts. As a result, DirectInput must be told to retrieve state information from the device. This is done by making a call to the Poll member function of the IDirectInputDevice2 interface. This is also an appropriate time to check if your device is in need of reacquiring. Once your device has been successfully polled, you can retrieve the state data by calling GetDeviceState. If you used the c_dfDIJoystick variable when calling SetDataFormat, then GetDeviceState is going to fill in a DIJOYSTATE structure with information about your joystick's current state. The contents of this structure depend largely on the features of the physical device and the settings you specified through SetProperty. For example, if the lY member of the structure is equal to 50, and you have set your Y axis range from 100 to 100, the joystick is vertical halfway between the center and the top of its range. You will notice that this structure contains members for rudders, sliders, thrusts, and point-of-view hats, as well as support for up to 32 buttons. Your application should make sure that the device's ranges are set to values that are logical for the needs of the application. As for retrieving data from a joystick device, an application should simply poll the device in a regular period. Now you're ready to move on to making force feedback effects for a joystick. Although this process begins with the DirectInputDevice object, much of the work is done with an object called DirectInputEffect. For the most part, DirectInputDevice retrieves input from a device and creates instances of the DirectInputEffect object. The guts of force feedback exist in the DirectInputEffect object.
Using DirectInputEffect
The dwSize member is the size in bytes of the structure. The dwFlags tells what type of coordinate style you will use for the effect, as well as whether you will use the by offset or by ID approach to describing axes (exactly like SetProperty described previously). Normally, you will set the flags to DIEFF_CARTESIAN|DIEFF_OBJECTOFFSETS. This means that axes are described by offset and are in terms of XYZ coordinates.
The dwDuration describes how long the effect will play in microseconds. Note that dwDuration can be set to INFINITE. The dwSamplePeriod describes, in microseconds, how long it takes the effect to play one cycle. Different devices support different sample periods. In practice, the SideWinder joystick seems to support periods no longer than one second and no shorter than 1/80 second. The dwGain value can be viewed as a master volume for the effect, in that it describes in relative terms how forceful the effect is. The range for this value is from 0 to 10000. The dwTriggerButton and dwTriggerRepeatInterval values are used to set the button that causes the effect to be played, and how often it should repeat itself. Of course, an effect can be set with no button association by filling the dwTriggerButton value with DIEB_NOTRIGGER. Otherwise, the dwFlags member defines its value type in that it can describe a button by ID or by offset. Because the by offset approach does not require a call to EnumObjects, you will commonly be assigning values such as DIJOFS_ BUTTON0 and DIJOFS_BUTTON1. The cAxes member tells how many axes should be affected by this effect. rgdwAxes points to an array of DWORDs that describe the axes involved. This array should have one element per axis. Like the buttons before the M, the axes are described by offset or by ID. Common by offset values include DIJOFS_X and DIJOFS_Y. Likewise, the rglDirection member points to an array of longs, one per axis. For Cartesian values, a Y value of 1 and an X value of 1 would be the same as a Y value of 10 and an X value of 10. That is to say, it would describe a diagonal force direction that moves the joystick away from the user and to the right. You should adjust the relative magnitudes of the values if you want a force in a direction that is not a multiple of 45 degrees. For example, a Y value of 10 and an X value of 1 would describe a direction in the same quadrant as the last example, but it would be significantly closer to the Y axis. Effects can also have an envelope described for the M. Filling in a DIENVELOPE structure and then filling in the lpEnvelope member with its address does this. An envelope causes the effect's magnitude or strength to be affected over a period of time, where the attack level is a starting modifier to the effect, and the attack time describes in microseconds how long it takes the effect to reach the sustain or unmodified strength. The fade level is the level of the effect at the end of the envelope, and the fade time is the number of microseconds that it takes to begin fading. An envelope can be used to make an effect initially strong, and then slowly fade, for example. See Figure 6 for an example of how an envelope changes an effect. |
Figure 6 Envelope Effects |
The Demo Sample
|
Figure 7 Demo App |
From the February 1998 issue of Microsoft Systems Journal.
|