28.5 The keyboard

Let's talk about how we handle the keys in the Pop Framework. In order to process keys, your CView can either have an OnChar handler or an OnKeyDown handler. But it turns out, the OnChar handler is only good for letter keys. So in order to process arrow keys we use VisualStudio to add to CPopView the OnKeyDown handler for the WM_KEYDOWN message.

When you press a key, the view that has the focus receives an OnKeyDown message. If you continue holding the key down, the focus view will continue receiving OnKeyDown messages (but it won't necessarily get repeated OnChar messages). The timing between these messages varies greatly from machine to machine, and you should never write code that depends on any assumptions about how often or how seldom the repeated OnKeyDown messages will come in when somebody holds down a key. One reason for the variation is that most computers have a BIOS setting for how often to generate 'autorepeat' or 'typematic' messages when a key is held down. Another reason for the variation is the behavior of Windows itself. If it matters to you if a key is being held down, it is up to you to keep track of this yourself rather than depend on repeated OnKeyDown messages.

When you release the key, the focus view receives an OnKeyUp message. One thing that can cause problems here is that if you press a key while a given view has focus, hold the key down, switch the focus (by selecting another view or by opening a menu popup or dialog), and then release the key, the initial view window will not receive the OnKeyUp message.

The UINT nChar argument to the key handlers is a keycode. For letter and number keys, this code is simply the ASCII code for that key. In C, C++ and Java we write an ASCII code for a character by putting the character in single quotes. Thus 'A' is the ASCII for the A key.

The keycodes for the non-ASCII keys are defined in a file called winuser.h. These codes have names that start with VK_. Thus VK_LEFT is the Left arrow key, VK_F2 is the F2 key, VK_SHIFT is the Shift key and so on. We list the keycodes in Appendix A; the numerical values of the keycodes should never make any difference to you, but it is good to have the listing just so as to know exactly what all the names are. (Without the list, you have to guess, for instance, whether the Control key, which has 'Ctrl' printed on it, is VK_CTRL or VK_CONTROL.) One gotcha in key programming is that you cannot automatically use a keycode like VK_A in place of the standard symbolism 'A'.

It can be useful to be able to tell if an OnKeyDown message is from the user's first press of a key or if it's from the user holding the key down. As mentioned above, most keyboards generate repeated OnKeyDown messages for any key you hold down. In the Pop Framework we won't actually care about this feature; what will matter to us more is whether the critters have already had a chance to detect a given keypress message.

As was mentioned in Chapter 12: Listeners, the Pop Framework has a special class called cController to encapsulate our key handling code, and each cGame object has a cController member. This means that in our CPopView, we do very little work inside the key handlers; we just have them pass the buck to the cGame. The game stores the key information in the controller, and then makes this information available to each critter. You could also override the methods so that the game did something extra for certain keypresses. The gain of having the key information in the controller is twofold: the controller organizes the keydown/keyup pairing and makes the key information available to any critter that needs it. In some games, you might well want to have more than one critter responding to the keys (e.g. left and right flippers in a pinball game).

Here's the code where the Pop Framework reads the keys:

void CPopView::OnKeyDown(UINT nChar, UINT nRepCnt, UINT nFlags) 
        /* We will use the Windows-defined VK_??? symbols to stand 
        for the various nChar values. Most of these are defined in 
        the Visual C++ include file winuser.h. A few that should be 
        defined are not, so we fix this by #defining them in the 
        controller.h header file. */ 

        /*Although the documentation says nRepCnt gives you the 
        number of repeated typematic messages from a keypress, 
        this may not always to be true. Alternately, the doc says 
        Bit number 14 of nFlags tells me if a prior OnKeyDown 
        message has already been sent from this particular keypress. 
        If this bit is on, the key is being held down. You access 
        bit 14 via (fFlags & (1<<14)). But in any case we are not 
        very interested in detecting repeated typematic OnKeyDown 
        messages, instead we plan to make cController set a 
        GOTTWICEBIT bitflag to signal when a given key press has 
        been accessed more than once by the critters. This is more 
        of an issue than whether we have a typematic repeat. */ 

    UINT control = (0x8000 & 
        /* Is control key down? The GetAsyncKeyState method returns 
        a short unsigned int that has a one in its high bit if 
        the key in question is down. I trust this more than using 
        the nFlags. 
            No matter how you detect Ctrl, on the Microsoft Natural 
        Keyboard, the control key blocks the INSERT key, so we can't 
        count on using the Ctrl+INSERT combination for anything. In 
        the same critical vein, note that when the Alt key is down, 
        you do not get any OnKeyDown messages at all. This contradicts 
        the Microsoft doc that says the nFlags have a bit (#13) to 
        tell you if the Alt key is down. */ 
    UINT shift = (0x8000 & 
        /* Is shift key down? Again, GetAsyncKeyState seems more 
        reliable than nFlags. */ 
    nFlags = control | shift; 
    pgame()->onKeyDown(this, nChar, nFlags); 

The view reacts by passing the keydown messages to the cController which will detect on its own when a given key has been released. In its update(dt) method, cController uses the a global Windows method SHORT GetAsyncKeyState (int vKey).

We put the scope resolution operator in front of this method name with nothing to the left of it to remind ourselves that the method is indeed global and not a member of any class and call it ::GetAsyncKeyState. The 16-bit signed integer variable returned sets 'the most significant bit' to tell you if the key is down or not, so we mask with 0X8000, which in binary is a one followed by 15 zeroes. This call works both with keys and with mouse buttons. (The left mouse button has key code VK_LBUTTON.) The GetAsyncKeyState method gets your key information regardless of which window currently has the focus. It's a direct hardware access method.

[A troubling issue with the keyboard in Windows is the behavior of the group of four Arrow keys, not the 'digital keypad' on the right, but the little group with Up and then below it Left Down Right. The Left key here fails to fire if the Up and Space are pressed. By 'failing to fire' we mean that the Left key passes a nChar value of 0 to the OnKeyDown. If you hold down Space and Left, the Up key is then blocked in the same way. What makes this especially puzzling is that the Right key will fire when the Up and Space are pressed. Also this blocking doesn't occur for the digital keypad Left Arrow keys, only for the keys on the extra little group of four Arrow keys.]

We use the cController::onKeyDown method to tell the controller when we press a key.

void cController::onKeyDown(UINT nChar, UINT nFlags) 
    if (_keystate[nChar] == KEYOFF) 
        _keystateage[nChar] = 0.0; 
        _keystate[nChar] = KEYON | nFlags; 
        _keystate[nChar] |= TYPEMATICBIT; 
            /* Notice when you get repeated keypresses by holding a 
                key down */ 

The cController::update takes a dt argument for two reasons: first, cController remembers the last dt in case anyone needs it; and second, cController tracks how long each currently pressed key has been held down ? this is useful for having, say, the rotation of a spaceship start out slow and then speed up a bit if you press, say, the Left Arrow key and continue holding it down.

Here's a simplified version of the code for cController::update. Note that since we have the cController::onKeyDown method to turn the keys 'on,' we use cController::update only to 'age' keys or to turn them off. Checking the key states 'by hand' with the ::GetAsyncKeyState method is safer than relying on the Windows OnKeyUp message, which may not get sent to the view you are currently in.

void cController::update(Real dt) 
    for(int vkindex=0; vkindex< VKKEYCOUNT; vkindex++) 
        if (keystate[vkindex] & KEYON) 
            if (!(0x8000 & ::GetAsyncKeyState(vkindex))) 
                //Key isn't down. 
                keystate[vkindex] = KEYOFF; //Turn off the keystate. 
                    keystateage[vkindex] = 0.0; 
            else //Key is down. 
                keystateage[vkindex] += dt; //Age the keystate. 
                if (keystate[vkindex] & GOTONCEBIT) 
                    //keystate[vkindex] |= GOTTWICEBIT; 
                    keystate[vkindex] |= GOTONCEBIT; 

The place where the controller information may be used by the critters is this line of the cGame::step(dt) method: _pbiota->listen(dt). These cascade down to cListener calls that have access to the game's cController object. Figure 12.1 in Chapter 12: Listeners is a sequence diagram of the process.

    Part I: Software Engineering and Computer Games
    Part II: Software Engineering and Computer Games Reference