12.1 How the critters listen to the user input

The cController utility class

The Microsoft void CView::OnKeyDown(UINT nChar, UINT nRepCnt, UINT nFlags) method is triggered whenever a key is pressed. The nFlags argument is a collection of bitflags designed to tell you whether the Ctrl, Alt, and/or Shift keys are down, and whether this is a repeated typematic keypress. The nRepCnt is also supposed to hold the number of repeated typematic messages that a key press has generated, where 'typematic' refers to the feature that has most keys trigger additional OnKeyDown messages if you continue to hold them down. In point of fact, the nFlags and nRepCnt arguments don't reliably behave as Microsoft's documentation says they do, so we work around them, as you can see if you check the CPopView override of the OnKeyDown method.

To give clean access to the user input, we have a class called cController that serves to hold the current state of the keyboard and mouse and allow the programmer to access this information with some conveniently designed accessor methods. The various possible keys are represented by integer keycodes, ordinarily the keycode for any key has a name of the form VK_???, such as VK_A, VK_LEFT, VK_SPACE, and so on. Check Appendix A for a complete list of the VK_ code names used in the Pop Framework. Under the current versions of Windows, there are 166 distinct recognized keys; we #define this number to be VKKEYCOUNT.

The cController maintains an unsigned integer keystate for each key; the keystate uses bitflags to represent if the key is depressed, and whether the Shift or Ctrl keys were down when the key was first pressed. Also we maintain some bitflags that enable us to tell when a key has been down for more than one cycle of the cGame::step call. Here's a partial listing of its prototype.

class cController : public CObject 
    UINT _keystate[VKKEYCOUNT]; 
    Real _keystateage[VKKEYCOUNT]; 
    virtual void update(Real dt); /* cController uses update to 
        check for when keys are no longer depressed and for when 
        keys have been made available to the listeners more than 
        once. */ 
    BOOL keyon(int vkcode); 
    BOOL keyonplain(int vkcode); 
    BOOL keyoncontrol(int vkcode); 
    BOOL keyonshift(int vkcode); 
    BOOL keyoncontrolshift(int vkcode); 
        /* The following *single accessors only return TRUE once per 
            keypress, useful for impulse controls. */ 
    BOOL keyonsingle(int vkcode) 
    BOOL keyonplainsingle(int vkcode); 
    BOOL keyoncontrolsingle(int vkcode); 
    BOOL keyonshiftsingle(int vkcode); 
    BOOL keyoncontrolshiftsingle(int vkcode); 
        /* Sometimes, as when using an arrow key to spin a player, it 
            is useful to know how long a key has been depressed. */ 
    Real keystateage(int vkcode); 

It's worth mentioning that there is a VK_LBUTTON as well; we use this to signal when the left mouse button is depressed. And the same is true for the right button. In other words, we can treat the mouse buttons like keyboard keys. A complicating factor with the mouse is that one often needs to know the mouse's current cursor position; we deal with this by having the cGame maintain a cVector _cursorpos that gets updated by the active view within its CPopView::OnSetCursor call. Windows forces an OnSetCursor call in the window underlying the current mouse cursor position during or before every call to OnIdle.

The sequence from keypress to critter

In order to play the game, we need for the player critter to be able to take input from the keyboard and/or the mouse. We get at the input in a somewhat indirect fashion.

  • When you press a key, an OnKeyDown message goes to the active CPopView.

  • The CPopView::OnKeyDown message handler sends a cGame::onKeyDown message to the active cGame object.

  • The cGame object stores the key information in its cController *_pcontroller member.

  • The cGame::step method calls cCritter::feellistener for the player.

  • The player critter's cListener* _plistener member calls the cListener::listen(Real dt, cCritter *pownercritter) method.

  • The cListener::listen uses the pcritter->pgame()->pcontroller() accessor to get at the cController *_pcontroller to see which keys are down, whether the Ctrl and Shift keys are also down, how long the keys have been down, whether the mouse buttons are down, and so on.

  • Depending on the keystates, the cListener::listen may do something like using cCritter::setAcceleration to change the player's acceleration.

The reason the flow is so indirect is because we want for a given keystroke to be available to any critter in the game that has a listener ? this would be a factor for two-person games, for instance. In addition, rather than processing keystrokes immediately as they happen, we want for the processing to happen at a certain predictable spot within the cGame::step cycle ? otherwise we may have trouble keeping up the illusion that our critters are behaving in a parallel fashion.

We have a little more about the interaction between the keyboard and the controller in the Keyboard section of Chapter 28: Mouse, Cursors and Keyboard.

We can sketch the flow in a sequence diagram as shown in Figure 12.1.

Figure 12.1. Sequence diagram for a key press


If this complexity bothers you, don't worry about it; the whole point of all this framework coding was to give the programmer an unobtrusive and reliable interface to the user's keyboard and mouse input. In the next section, we'll talk about how the cListener objects use this interface.

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