• Aucun résultat trouvé

Keyboard with DirectInput

Dans le document [ Team LiB ] (Page 114-122)

DirectInput provides fast asynchronous access to key states. A single call can retrieve the state of the whole keyboard, so subsequent tests are just table lookups. The operation is thus very similar to the GetKeyboardState Win32 call. But before we delve into keyboard reading code, we need to discuss how DirectInput works.

DirectInput encapsulates keyboards, joysticks, mice, and any other exotic input peripheral under a common interface called a device.

The operation is really straightforward. We first need to boot DirectInput. This implies creating a DirectInput object, from which all other objects dealing with input processing can be derived. The DirectInput object can thus be used to create devices, which are the logical interfaces to peripherals. Once a device has been created, we need to specify several parameters, such as the format of the data we want to interchange with the device, and the cooperative level, which tells DirectInput if the device is to be shared among different applications or if we need it exclusively.

DirectInput devices can then be polled asynchronously. We query the state of the device, not waiting for a specific event like a key or button press. This means DirectInput will take a snapshot of the current state of the device and return it to the application so it can be processed. As a summary, here is a list of the steps involved in setting up a keyboard DirectInput:

Create the DirectInput object.

Set the cooperative level you will use with the operating system.

4.

Read data as needed.

5.

Let's now move on to a specific example, beginning with the DirectInput code needed to boot the API. The code in this section has been tested in both DirectX8 and DirectX9. DirectInput is almost identical in both versions.

LPDIRECTINPUT8 g_pDI=NULL;

HRESULT hr=DirectInput8Create(GetModuleHandle(NULL),DIRECTINPUT_VERSION, IID_IDirectInput8,(VOID**)&g_pDI,NULL)))

In the preceding code, the first parameter is used to send the instance handle to the application that is creating the DirectInput object.

Then, we need to pass the DirectInput version we are requesting. The macro DIRECTINPUT_VERSION is a handy way to pass the current version number. Next, we need to pass the unique interface identifier for the object we are requesting. We use IID_IDirectInput8 to request a DirectInput object, but we can use other parameters to define ANSI or Unicode versions of the interface. We then pass the pointer so we can receive the already initialized object, and the last parameter is used to perform Component Object Model (COM) aggregation. You probably won't want to aggregate your DirectInput object to anything else, so leave this as NULL.

Now we have a DirectInput object ready for use. It is now time for the real keyboard code. We will first request the device and set some parameters that define how we will communicate with it. Then, we will examine the source code used to read data from a keyboard.

The first step is to actually request a device from the DirectInput object. This is achieved with the line:

HRESULT hr =g_pDI->CreateDevice(GUID_SysKeyboard, &g_pKeyboard, NULL);

The call must receive the Global Unique Identifier (GUID) for the desired device. DirectInput is built on top of the COM, an

object-oriented programming model. In COM, GUIDs are used to identify specific objects or interfaces. Internally, GUIDs are just 128-bit structures, but they are used to represent functions, objects, and generally any DirectX construct. In this case, classic GUIDs for the different devices are

GUID_SysKeyboard: The default system keyboard.

GUID_SysMouse: The default system mouse.

Additional GUIDs can be assigned to joysticks. However, these GUIDs should not be written directly, but as the result of a call to DirectInput8::EnumDevices. We will be covering joysticks in the next section. For our keyboard, GUID_SysKeyboard will do the job. The second parameter is just the pointer to the newly created device, and the last parameter is again reserved for aggregation and must thus be set to NULL.

Now, we must tell the keyboard how we want to exchange data. This is achieved with the call to SetDataFormat, as shown here:

HRESULT hr = g_pKeyboard->SetDataFormat( &c_dfDIKeyboard );

The call must receive a parameter of type LPCDIDATAFORMAT, which is a structure defined as:

typedef struct DIDATAFORMAT {

This structure controls the number of objects we will be requesting, the format of each one, and so on. Because it is a complex structure to fill, DirectInput already comes with several predefined data formats that we can use directly. For a keyboard, the format

c_dfDiKeyboard tells DirectInput we will be requesting the full keyboard, stored in an array of 256 bytes.

In addition, we need to tell DirectInput about the cooperative level we will be using with this device. This is achieved by using the line:

HRESULT hr=g_pKeyboard->SetCooperativeLevel(hWnd, DISCL_FOREGROUND| DISCL_EXCLUSIVE);

Here we pass the window handle as the first parameter, and the second parameter is the OR of a series of flags that control the cooperative level. In this case, we are telling DirectInput that we want exclusive access and that this access should only be valid if the application is in the foreground. As our application moves to the background, the device is automatically unacquired.

Additionally, we need to acquire the keyboard, so we can begin querying its state. The following line will do that for us:

g_pKeyboard->Acquire();

And now we are ready to begin using the keyboard. Here is the code snippet that declares both DirectInput and the keyboard, and makes sure the device is ready. Error checking has been omitted for clarity:

HRESULT hr;

Reading the keyboard is even easier than preparing it. All we have to do is prepare a 256-byte array and pass it to DirectInput with the keyboard acquired to query its state:

BYTE diks[256]; // DirectInput keyboard state buffer ZeroMemory( diks, sizeof(diks) );

hr = g_pKeyboard->GetDeviceState( sizeof(diks), diks );

Notice how we clear the buffer and then pass it to DirectInput. As with GetAsyncKeyState, specific key codes must be used after the

read to query for each key. In this case, all keys are represented by symbolic constants, such as:

DIK_RETURN The return key DIK_SPACE The space key DIK_A ... DIK_Z The alphabetic keys DIK_F1 ... DIK_F10 The function keys

Now, to query a specific key, we must test for the most significant bit of the corresponding array position. If that position is set to one, the key is currently being pressed. Thus, to check whether the Return key is activated, the following code can be used:

bool return_pressed=(buffer[DIK_RETURN] & 0x80)!=0);

As usual, we can read combinations, so we can check whether several keys are pressed simultaneously. Because there is only one DirectInput read at the very beginning, these are just array lookups.

Reading the keyboard is really straightforward. But we must be careful with acquiring and unacquiring the device, which can make our input controller malfunction. Sometimes, especially in some cooperative level modes, we can lose contact with the keyboard

momentarily. This is called unacquiring the device. The most popular reason for this is that our application moved to the background, thus losing the keyboard access in favor of another application, which is now in the foreground. Some other events might make us lose track of our device as well. If this happens, we will discover it in the next GetDeviceState call, which will fail. We must then reacquire the keyboard so we can continue querying its state. This is achieved as follows:

BYTE diks[256]; // DirectInput keyboard state buffer ZeroMemory( diks, sizeof(diks) );

hr = g_pKeyboard->GetDeviceState( sizeof(diks), diks );

if( FAILED(hr) ) {

hr = g_pKeyboard->Acquire();

while( hr == DIERR_INPUTLOST || hr== DIERR_OTHERAPPHASPRIO) hr = g_pKeyboard->Acquire();

}

Notice how we detect the error and keep calling Acquire until we regain access to the device.

Once we have finished with our application, it is time to release all DirectInput objects peacefully. Releasing the keyboard is a two-step process. First, we unacquire the device, and then release its data structures. Second, we must delete the main DirectInput object.

Overall, the destruction sequence is achieved by using the following code:

if( g_pKeyboard ) g_pKeyboard->Unacquire();

SAFE_RELEASE( g_pKeyboard );

SAFE_RELEASE( g_pDI );

Notice that we are using the SAFE_RELEASE macros provided with DirectX to ensure that all data structures and allocated memory are deleted.

[ Team LiB ]

[ Team LiB ]

Mouse

Since their inception in the late 1960s as a CAD input device, mice have been adapted for many uses including computer games. They are especially popular in PC games, but game consoles do not usually support them. Unlike a keyboard or joystick, the mouse not only generates button or key presses, but 2D positions as well. This provides a wider range of input choices at the cost of a higher learning curve for the player.

Mice can be used in a variety of scenarios, from unit picking in a real-time strategy title to the popular mouselook found in most first-person shooters. In all cases, the operation of the mouse can be divided into transmitting positional information (thanks to the internal mouse sensors) and sending button press and release messages.

Let's examine how a mouse operates under DirectInput. The source code is very similar to the keyboard request because DirectInput treats all devices the same. This is beneficial for the programmer because most inner details are hidden. Let's assume we have the main DirectInput object up and running, and start with the device creation pass:

LPDIRECTINPUTDEVICE g_pMouse;

HRESULT hr;

hr = g_pDI->CreateDevice(GUID_SysMouse, &g_pMouse, NULL);

As you can see, it is extremely similar to requesting a keyboard; the only difference being the GUID we pass to request the desired device. Then, the data format is set as follows:

hr = g_pMouse->SetDataFormat(&c_dfDIMouse);

In this case, the c_dfDIMouse parameter tells DirectInput we will be passing a DIMOUSESTATE structure to IDirectInputDevice::GetDeviceState. This structure has the following signature:

This structure returns the X and Y positions, and an optional Z axis, which is usually assigned to a wheel. Then, the button array works like the keyboard array. Buttons are pressed if the high-order bit is set. A variant of this structure is the DIMOUSESTATE2, set by the parameter c_dfDIMouse2. The only difference is that the latter supports eight buttons instead of the four supported by DIMOUSESTATE.

These are especially useful in specific mice used for CAD systems, for example.

After the data format has been set, we need to set the cooperative level. No surprises here, as the code is exactly identical to the keyboard version:

hr = g_pMouse->SetCooperativeLevel(hWnd,

DISCL_EXCLUSIVE | DISCL_FOREGROUND);

In addition, we need to acquire the ready-to-use device with the line:

g_pMouse->Acquire();

Here is the full source code in review:

LPDIRECTINPUTDEVICE g_pMouse;

HRESULT hr = g_pDI->CreateDevice(GUID_SysMouse, &g_pMouse, NULL);

hr = g_pMouse->SetDataFormat(&c_dfDIMouse);

hr = g_pMouse->SetCooperativeLevel(hWnd,

DISCL_EXCLUSIVE | DISCL_FOREGROUND);

g_pMouse->Acquire();

Reading from this mouse is achieved with the GetDeviceState call, which will return a LPDIMOUSESTATE structure. The source code would be:

DIMOUSESTATE dims; // DirectInput mouse state structure

ZeroMemory( &dims, sizeof(dims) );

hr = g_pMouse->GetDeviceState( sizeof(DIMOUSESTATE), &dims );

if( FAILED(hr) ) {

hr = g_pMouse->Acquire();

while( hr == DIERR_INPUTLOST || hr == DIERR_OTHERAPPHASPRIO ||

hr == DIERR_NOTACQUIRED) hr = g_pMouse->Acquire();

}

Notice how I have added the unacquiring prevention code to avoid losing track of our mouse due to unexpected events. Other than that, the code is very similar to reading a keyboard. To access the mouse attributes, all we have to do is this:

int MouseX = dims.lX;

int MouseY = dims.lY;

bool lbutton = (dims.rgbButtons[0] & 0x80)!=0);

Usually, button 0 is assigned to the left mouse button, button 1 is assigned to the right one, and button 2 is assigned to the middle button (if available). Regarding positions, remember that a mouse is a relative pointing device. When first acquired, the mouse's position is reset to (0,0). Then, each new read will return the displacement from the last one. Thus, if we move the mouse vertically, we will see

displacements in the Y direction. But when we stop the movement, the read mouse value will go back to (0,0). Remember, the mouse does not work with positions but instead works with displacements. Last, but not least, the mouse is usually configured so negative X points to the left, and positive Y points away from our body as we are sitting at a table.

Additionally, remember to release the mouse as soon as you have finished using it. The code is again very similar to the keyboard release code:

if( g_pMouse ) g_pMouse->Unacquire();

SAFE_RELEASE( g_pMouse );

SAFE_RELEASE( g_pDI );

Mouselook

A popular use of the mouse is to implement the classic mouselook used in many first-person shooters. The mouselook is easy to code once you understand how a mouse operates. All we have to do is use the keys to change our position, and use the mouse to reorient our viewpoint. I will explain the effect fully, so we can combine what we have learned about keyboards and mice.

The game must have at least four degrees of freedom. We must have a position consisting of an X and Z value, and then a yaw and pitch angle. Y values are often added to the mix so we can climb different heights, but roll is generally not needed. We will use the following mapping:

Let's first focus on the keyboard and forget about the orientation for a second. Assuming a standard DirectInput keyboard, we would need the following code to implement the desired behavior:

int strafe= (buffer[DIK_RIGHT] & 0x80)!=0) - (buffer[DIK_LEFT] & 0x80)!=0);

int fwd= (buffer[DIK_UP] & 0x80)!=0) - (buffer[DIK_DOWN] & 0x80)!=0);

Notice how we have elegantly encapsulated the cursor control. By subtracting opposite directions, we get two numbers, strafe and fwd, in the range –1..1. These numbers are then used to drive our position update routine:

pos.x += fwd*FWDSPEED*elapsed*cos(yaw) + strafe*STRAFESPEED*elapsed*cos(yaw+3.1416/2);

pos.z += fwd*FWDSPEED*elapsed*sin(yaw) + strafe*STRAFESPEED*elapsed*sin(yaw+3.1416/2);

The fwd and strafe variables control how each member is multiplied—by –1, 0, or 1—to perform the desired effect. Now, let's take care of pitch and yaw through the mouse:

yaw+= YAWSPEED*elapsed*dims.lX;

pitch+= PITCHSPEED* elapsed*dims.lY;

So now we have our new pos.x, pos.z, yaw, and pitch player structure updated. Obviously, we need to keep two devices alive, and we need to define all constants in caps to set the desired speed. Notice how each constant is multiplied by an elapsed time factor, which stores the time it took to render the last frame. This way we ensure device-independent performance. For completeness, here is the source code required to compute all specific camera parameters ready to be plugged into an OpenGL or DirectX pipeline:

point campos(pos.x,pos.y,pos.z);

point camlookat(pos.x+cos(yaw)*cos(pitch),pos.y+sin(pitch), pos.z+sin(yaw)*cos(pitch));

The lookat coordinate computation is just a spherical mapping using the pitch and yaw. Depending on how your axes are laid out, a sign might change or you might need to add a constant angle like Pi to the pitch values.

[ Team LiB ]

[ Team LiB ]

Joysticks

The joystick was introduced in the 1970s as a way to represent positional data easily. The first models were restricted to binary tests: The joystick returned a 0 or 1 to represent whether it was being activated or not. Thus, most joysticks allowed nine positional values: one for the joystick being centered and eight for N, S, E, W, SW, SE, NW, and NE. Usually, the joystick position was mapped to an integer value, with an extra bit used to represent the button press. Most joysticks from eight-bit computers were like this.

As software development houses created better simulations, joysticks began to improve as well. Continuous-output joysticks appeared to satisfy the flight simulation community, but because they offered better control, they became mainstream. Today, all joysticks map the sticks' inclination to a continuous range of values, so we can control our characters precisely.

Controlling a joystick is slightly more complex than working with a keyboard or a mouse. Joysticks come in a variety of shapes and configurations, so the detection and data retrieval process is a bit more involved. Some gamepads have two controllers, whereas others have a stick and a point-of-view (POV). In addition, the number of buttons varies from model to model, making the process of detecting a joystick nontrivial.

In DirectInput, we must ask the API to enumerate the device so it autodetects the joystick so we can use it. For this example, I will assume we already have the DirectInput object ready. The first step is to ask DirectInput to enumerate any joystick it is detecting. This is achieved by using the call:

HRESULT hr = g_pDI->EnumDevices( DI8DEVCLASS_GAMECTRL, EnumJoysticksCallback,

NULL, DIEDFL_ATTACHEDONLY ) ) )

The first parameter tells DirectInput which kind of device we want to detect. It can be a mouse (DI8DEVCLASS_POINTER) or a keyboard (DI8DEVCLASS_KEYBOARD). In this case, we request a game controller, which is valid for both gamepads and joysticks of all kinds.

Now comes the tricky part: DirectInput detects all candidate devices. Imagine that we have two joysticks (for example, in a two-player game). DirectInput would need to return two devices. Thus, instead of doing some parameter magic to allow this, DirectInput works with callbacks. A callback is a user-defined function that gets called from inside the execution of a system call. In this case, we provide the EnumJoysticksCallback, a function we write that will get triggered once for each detected joystick. The internals of that function will have to retrieve GUIDs, allocate the device objects, and so on. This is a bit more complicated than returning a list of pointers, but on the other hand, it allows greater flexibility. We will examine our callback in a second. Let's first complete the call profile by stating that the third parameter is a user-defined parameter to be passed to the callback (usually NULL), whereas the last parameter is the enumeration flags.

DIEDFL_ATTACHEDONLY is used to state that we only want to detect those devices that are properly attached and installed, the same way DIEDFL_FORCEFEEDBACK is used to restrict the enumeration to force feedback joysticks. Here is the source code for the EnumJoysticksCallback function:

BOOL CALLBACK EnumJoysticksCallback( const DIDEVICEINSTANCE* pdidInstance, VOID* pContext ) {

Notice how the callback is receiving the device instance, so we only have to create the device using that instance. This is a relatively simple example, where we return to the application as soon as we have found one joystick. If the user had two joysticks attached to the computer, this code would only enumerate the first one. A variant could be used to store each and every joystick in a linked list, so the user can then select the joystick he actually wants to use from a drop-down list.

After the joystick has been enumerated, we can set the data format and cooperative level. No news here—just a rehash of the code required for keyboards and mice:

HRESULT hr = g_pJoystick->SetDataFormat( &c_dfDIJoystick );

HRESULT hr = g_pJoystick->SetCooperativeLevel( hWnd, DISCL_EXCLUSIVE |

DISCL_FOREGROUND );

An extra piece of code must be used to set the output range for the joystick. Because it is a device with analog axes, what will be the range

An extra piece of code must be used to set the output range for the joystick. Because it is a device with analog axes, what will be the range

Dans le document [ Team LiB ] (Page 114-122)