Keycode Description
VK_SHIFT VK_RSHIFT, VK, LSHIFT Either of the two Shift keys
VK_MENU Either of the Alt keys
VK_CTRL VK_RCTRL, VK_LCTRL Any of the Ctrl keys VK_UP, VK_DOWN, VK_LEFT, VK_RIGHT The cursor keys
VK_F1...VK_F12 The function keys
VK_ESCAPE The Esc key
VK_SPACE The Spacebar
VK_RETURN The Enter/Return key
VK_HOME, VK_END, VK_PRIOR, VK_NEXT The numeric keypad keys
VK_BACK The Backspace key
VK_TAB The Tab key
VK_INSERT, VK_DELETE The Insert and Delete keys
The return value encodes the state of the key passed as a parameter. The most significant bit is activated if the key is currently pressed, whereas the least significant bit is activated if this key was activated the last time GetAsyncKeyState was called. Here is an example of how to check whether the left Shift key is pressed:
If (GetAsyncKeyState(VK_LSHIFT)) {
// whatever }
Notice that, due to the nature of the call, we can check multiple keys. The next example shows how to test for the left Shift AND Return combination:
If ((GetAsyncKeyState(VK_LSHIFT)) && (GetAsyncKeyState(VK_RETURN))) {
// whatever }
As you can see, each key test requires a system call, which can be troublesome for those systems checking a lot of different keys. Now, let's compare this call with a whole keyboard check, which can be performed by using the call:
bool GetKeyboardState(PBYTE *lpKeyState);
Here the result only encodes if the function succeeded, and the real meat is returned as an array passed as a reference. Then, successive checks such as the following perform the individual test, which is nothing but a simple array lookup:
if (keystate[VK_RSHIFT]) {
// right shift was pressed }
Again, for games that check many keys (such as a flight simulator), this option can be better than repeated calls to GetAsyncKeyState. The programmer only needs to be aware that an initial call to GetKeyboardState is required to load the array.
Another possible pitfall to watch out for is that this second mode does not immediately check the keys when you perform the test. Keys are checked at the call to GetKeyboardState. If there is a significant delay between this test and the array lookup, undesirable side
effects might occur because the array will contain "old" key values.
Keyboard with DirectInput
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.
1.
Create the keyboard device.
2.
Set the data format for reading it.
3.
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 {
DWORD dwSize; DWORD dwObjSize; DWORD dwFlags; DWORD dwDataSize; DWORD dwNumObjs; LPDIOBJECTDATAFORMAT rgodf; } DIDATAFORMAT, *LPDIDATAFORMAT;
typedef const DIDATAFORMAT *LPCDIDATAFORMAT;
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;
hr = DirectInput8Create( GetModuleHandle(NULL), DIRECTINPUT_VERSION, IID_IDirectInput8, (VOID**)&g_pDI, NULL );
hr = g_pDI->CreateDevice( GUID_SysKeyboard, &g_pKeyboard, NULL ); hr = g_pKeyboard->SetDataFormat( &c_dfDIKeyboard );
hr = g_pKeyboard->SetCooperativeLevel( hDlg, dwCoopFlags ); hr = g_pKeyboard->Acquire();
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 );
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 ]
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:
typedef struct DIMOUSESTATE { LONG lX;
LONG lY; LONG lZ;
BYTE rgbButtons[4];
} DIMOUSESTATE, *LPDIMOUSESTATE;
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);
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 );