The Keyboard

Keyboards are the main input device for PC-based games, but are also available for mobile phones, some consoles, and palm devices. That makes them, in all probability, the most widely available input device. Unfortunately, such a popular input device is not very well suited for games. Keyboard mappings take time to learn, and the general idea of a keyboard is altogether impractical for small children.

Being a multifunction peripheral that can be used to type documents and to play games, it is not surprising that keyboards can be read using a variety of methods, depending on the specific requirements of the application. Some methods retrieve full strings, others work on a key-by-key basis, and so on. But for gaming purposes, two types of routines are relevant. First, there are the synchronous routines, which wait until a key is pressed and then report it to the application. Second, there are asynchronous routines, which return immediately after being called, and give the application information about which keys were pressed, if any.

Synchronous read modes are used to type information, such as the character name in a role-playing game (RPG). They work by polling the controller until new key input messages arrive. But they are not very well suited for real gameplay. The game code must continually check to see whether keys were pressed, and whatever the response, keep drawing, executing the AI, and so on. So, asynchronous controllers are the way to go. They provide fast tests to check the keyboard state efficiently.

Asynchronous routines can also belong to two different families. Some of them are designed to test the state of individual keys, so the programmer passes the key code as a parameter and gets the state as a result. Others, like the ones exposed by DirectInput, retrieve the whole keyboard state in a single call, so the programmer can then access the data structure and check for the state of each key without further hardware checks. The second type of routine is generally more efficient because there is less overhead involved.

As an example, we will focus on a single-key asynchronous call for the PC platform. The call is Windows specific and is part of the Win32 API. The syntax is

 short GetAsyncKeyState(int keycode); 

This call receives a key code and returns a short value, which encodes different state information. The key code we pass as a parameter can either be a capitalized character, such as "K", or an extended key code, which is used to read special characters. By using extended key codes, we can read specific keys, such as Delete, the function keys, Tabs, and so on. Table 5.1 provides a list of the main special key codes for this call.

Table 5.1. Keycodes for the GetAsyncKeyState call

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:

  1. Create the DirectInput object.

  2. Create the keyboard device.

  3. Set the data format for reading it.

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

  5. Read data as needed.

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 ); 

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.



Core Techniques and Algorithms in Game Programming2003
Core Techniques and Algorithms in Game Programming2003
ISBN: N/A
EAN: N/A
Year: 2004
Pages: 261

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net