Part B - Direct3D

User Control

Introduce user control of movable objects
Introduce user action to key mapping


Sample | Framework | Design | Coordinator | Window | User Input | Input Device | Exercise



The fifth stage in building a digital game, before turning to the mathematics of three-dimensional representation, is adding user control.  The user exercises control over the speed of the movable objects through key presses and releases.  The user decides which action maps to which key at configuration time.  Depending upon the keys pressed, the the framework performs specific actions. 

This chapter presents the code that implements user control of motion and maps actions processed in the Design object to keys pressed by the user. 


User Control Sample

The User Control Sample permits instantaneous increases and decreases in speed of the two sprites moving across the photograph of Stonehenge.  If the user does nothing, the sprites drift across the screen at the uniform pre-defined speed until they reach its edge, at which point they reverse their directions of travel.  If the user presses a speedup key, the sprite associated with that key adjusts its speed of travel. 

User Control Sample

The dialog box lists the keys translated by the framework and the actions available to the application.  When the user selects an action, the dialog box displays the key that currently corresponds to that action in the selection line of the key press combo box.  The user can change that key simply by selecting another key from the list. 

The framework initializes the speedup keys as WASD. 

  • W - vertically moving sprite increase speed upwards
  • A - horizontally moving sprite increase speed leftwards
  • S - vertically moving sprite increase speed downwards
  • D - horizontally moving sprite increase speed rightwards

Framework

The components upgraded to incorporate user control include:

  • Design - responds with actions according to user key presses
  • Coordinator - flows the key presses and releases to the UserInput component
  • UserInput - configures the action-key map and sets up and manages the keyboard device
  • Window - converts key-related messages into key presses or releases

The framework has one new component:

  • InputDevice - represents the keyboard device, which stores the key states

User Control Components

Topics

Three topics are covered in this sample:

  • polling actions
  • storing key states
  • mapping actions to keys

Polling Actions

The Design object, through the Coordinator object and the APIUserInput object interrogates the Keyboard object to determine whether the user has initiated the action in question.  action.  The APIUserInput object uses the action-key mapping to convert the action to its corresponding key.  If the key is in a pressed state, the Design object implements the action. 

Storing Key States

The operating system sends a message to the application for each key press and release.  The APIWindow object in its window procedure sends this message through the Coordinator and APIUserInput object to the Keyboard object.  The Keyboard object stores the key's state for subsequent polling. 

Action-Key Mapping

The mapping of actions to keys involves two independent lists: a list of actions and a list of keys.  Each item in each list has a user friendly description associated with it.  It is these descriptions that the combo boxes display.  The mapping is one from an action to a key.  While each action has an associated key, every key does not necessarily associate with an action. 

Translation Settings

The framework uses enumeration constants for keys and for actions.  There are two new identifiers for the action and key combo boxes.

 // Translation.h

 // shared with dialog resource (follows windows conventions)
 #define IDC_KEY 105
 #define IDC_ACT 106
 // ...
 // key symbols (do not initialize these constants)
 typedef enum Key {
     KEY_A, KEY_B, KEY_C, KEY_D, KEY_E, KEY_F, KEY_G, KEY_H, KEY_I, KEY_J,
     KEY_K, KEY_L, KEY_M, KEY_N, KEY_O, KEY_P, KEY_Q, KEY_R, KEY_S, KEY_T,
     KEY_U, KEY_V, KEY_W, KEY_X, KEY_Y, KEY_Z, KEY_1, KEY_2, KEY_3, KEY_4,
     KEY_5, KEY_6, KEY_7, KEY_8, KEY_9, KEY_0,
     KEY_F1, KEY_F2, KEY_F3, KEY_F4, KEY_F5, KEY_F6, KEY_F7, KEY_F8,
     KEY_F9, KEY_F10, KEY_F11, KEY_F12, KEY_SPACE, KEY_ENTER,
     KEY_UP, KEY_DOWN, KEY_PGUP, KEY_PGDN, KEY_LEFT, KEY_RIGHT,
     KEY_NUM1, KEY_NUM2, KEY_NUM3, KEY_NUM4, KEY_NUM5,
     KEY_NUM6, KEY_NUM7, KEY_NUM8, KEY_NUM9,
     KEY_ESCAPE, KEY_SEMICOLON, KEY_APOSTROPHE, KEY_O_BRACKET, KEY_C_BRACKET,
     KEY_BACKSLASH, KEY_COMMA, KEY_PERIOD, KEY_SLASH, KEY_TIMES, KEY_GRAVE,
     KEY_MINUS, KEY_UNDERSCORE, KEY_EQUALS, KEY_PLUS
 } Key;

 // key descriptions listed in the user dialog
 #define KEY_DESCRIPTIONS { \
     L"A", L"B", L"C", L"D", L"E", L"F", L"G", L"H", L"I", L"J", \
     L"K", L"L", L"M", L"N", L"O", L"P", L"Q", L"R", L"S", L"T", \
     L"U", L"V", L"W", L"X", L"Y", L"Z", L"1", L"2", L"3", L"4", \
     L"5", L"6", L"7", L"8", L"9", L"0", \
     L"F1", L"F2", L"F3", L"F4", L"F5", L"F6", L"F7", L"F8", \
     L"F9", L"F10", L"F11", L"F12", L"Space", L"Enter", \
     L"Up", L"Down", L"PageUp", L"PageDown", L"Left", L"Right", \
     L"NumPad 1", L"NumPad 2", L"NumPad 3", L"NumPad 4", L"NumPad 5", \
     L"NumPad 6", L"NumPad 7", L"NumPad 8", L"NumPad 9", \
     L"Escape", L";", L"'", L"[", L"]", \
     L"\\", L",", L".", L"/", L"*", L"`", \
     L"-", L"_", L"=", L"+" \
 }
 //-------------------------------- Action Enumerations ---------------------
 // to add an action
 // - add the enumeration constant for the new action
 // - add the friendly description of the new action
 // - add the default mappings for the new action below
 // enumeration constants
 typedef enum Action {
     MDL_PLUS_X,
     MDL_MINUS_X,
     MDL_PLUS_Y,
     MDL_MINUS_Y,
 } Action;

 // user friendly descriptions of actions - used in the dialog box
 // (descriptions should not exceed MAX_DESC characters)
 #define ACTION_DESCRIPTIONS {\
     L"Translate X +", \
     L"Translate X -", \
     L"Translate Y +", \
     L"Translate Y -", \
 }
 // initial mappings of actions to keys
 #define ACTION_KEY_MAP {KEY_D, KEY_A, KEY_S, KEY_W}

Note the instructions for adding a new action to the framework.

Resource Definition Script

The resource-definition script for the dialog box includes combo boxes that list the user actions and the keys translated: 

 // Dialog.rc
 // ...
 BEGIN
     // ...
     LTEXT           "Action", IDC_STATIC, 10, 65, 55, 10
     COMBOBOX        IDC_ACT, 10, 75, 125, 66, CBS_DROPDOWNLIST | \
                     WS_VSCROLL | WS_TABSTOP
     LTEXT           "Key Press", IDC_STATIC, 138, 65, 52, 10
     COMBOBOX        IDC_KEY, 138, 75, 52, 127, CBS_DROPDOWNLIST | \
                     WS_DISABLED | WS_VSCROLL | WS_TABSTOP
     DEFPUSHBUTTON   "Go", IDC_GO, 40, 205, 50, 15, WS_DISABLED
     PUSHBUTTON      "Cancel", IDCANCEL, 110, 205, 50, 15
     // ...
 END

Design

The Design component checks if the user has initiated or terminated any actions that affect the motion of the drawable objects.  The update() method on the Design object retrieves the key state associated with each potential action and, if the user has initiated or terminated that action, implements the corresponding response:

 void Design::update() {
     // ...
     // add changes initiated by the user
     if (pressed(MDL_MINUS_X))
         dx -= delta;
     if (pressed(MDL_PLUS_X))
         dx += delta;
     if (pressed(MDL_MINUS_Y))
         dy -= delta;
     if (pressed(MDL_PLUS_Y))
         dy += delta;
     // ...
 }

Note that this method refers to actions by their enumeration constants (and not by the key constants which differ with differing configurations).


Coordinator

The Coordinator component serves as the intermediary for processing key presses and releases associated with action that affect the motion of objects in a scene.  The Coordinator object passes key presses and releases to the APIUserInput object for recording and retrieves key states for the Design object during updating. 

User Control Components

The iCoordinator interface exposes the two pressed() methods to the framework:

 class iCoordinator {
     // ...
     virtual void pressed(int, bool)      = 0;
     virtual bool pressed(Action a) const = 0;
 };
 // ...

The Coordinator class defines these methods:

 // Coordinator.h

 class Coordinator : public iCoordinator, public Base {
     // ...
   public:
     // ...
     void pressed(int, bool);
     bool pressed(Action a) const;
     // ...
 };

The implementation passes the key press/release data to and from the APIUserInput object:

 void Coordinator::pressed(int key, bool down) {
     userInput->pressed(key, down);
 }

 bool Coordinator::pressed(Action a) const {
     return userInput->pressed(a);
 }

Window

The Window component processes all key presses and releases trapped by the operating system.  The updates to this component affect only its window procedure.

The wndProc procedure passes each key press and release to the Coordinator object:

 LRESULT CALLBACK wndProc(HWND hwnd, UINT msg, WPARAM wp, LPARAM lp) {

     switch (msg) {
       // ...
       case WM_KEYDOWN:
         switch (wp) {
           // ...
           default:
             coordinator->pressed(wp, true);
         }
         break;
       case WM_KEYUP:
         coordinator->pressed(wp, false);
         break;
       // ...
     }
     // ...
 }

User Input

The User Input component serves as an intermediary between the Coordinator and Keyboard objects for passing key state information and manages the action to key mapping, including its configuration. 

User Input Component

The iAPIUserInput interface exposes four new virtual methods to the framework:

 // iAPIUserInput.h

 class iAPIUserInput {
   public:
     // ...
     virtual void pressed(int, bool)             = 0;
     virtual bool pressed(Action) const          = 0;
     virtual void showActionMapping(void*)       = 0;
     virtual void updateActionKeyMapping(void*)  = 0;
     // ...
 };
 // ...

The APIUserInput class defines:

  • a pointer to the array that identifies the key associated with a specified action
  • a pointer to the array of strings that holds the user friendly descriptions of the framework's actions
  • a pointer to the array of strings that holds the user friendly descriptions of the framework's keys
  • an instance variable that holds the currently selected action
  • a pointer to the keyboard object
 // APIUserInput.h

 class APIUserInput : public iAPIUserInput, public APIBase {
     // ...
     unsigned         nActions;
     unsigned*        action;
     unsigned         nKbdObjs;
     wchar_t          (*actionDesc)[MAX_DESC + 1];
     wchar_t          (*kbdObjDesc)[MAX_DESC + 1];
     int              action_;  // currently selected action
     iAPIInputDevice* keyboard; // points to the keyboard object
     // ...
   public:
     // ...
     void pressed(int, bool);
     bool pressed(Action) const;
     // ...
     void populateActionList(void*);
     void populateMappableObjectLists(void*);
     void showActionMapping(void*);
     void updateActionKeyMapping(void*);
     // ...
 };

The parentheses are necessary to distinguish the pointer to an array from an array of pointers.

Implementation

Construct

The constructor initializes the pointer to the keyboard object, allocates memory for the three arrays that hold the action-key data, stores the macro definitions in these arrays, and sets the previously selected action to the first action in the list:

 APIUserInput::APIUserInput() {
     // ...
     keyboard = nullptr;
     action_  = 0;
     // allocate memory for configurable action descriptions
     const wchar_t* actDesc[] = ACTION_DESCRIPTIONS;
     nActions = sizeof actDesc / sizeof(wchar_t*);
     actionDesc = new wchar_t[nActions][MAX_DESC + 1];
     for (unsigned i = 0; i < nActions; i++)
         strcpy(actionDesc[i], actDesc[i], MAX_DESC);

     // allocate memory for configurable keyboard object descriptions
     const wchar_t* kbdDesc[] = KEY_DESCRIPTIONS;
     nKbdObjs= sizeof kbdDesc / sizeof(wchar_t*);
     kbdObjDesc = new wchar_t[nKbdObjs][MAX_DESC + 1];
     for (unsigned i = 0; i < nKbdObjs; i++)
         strcpy(kbdObjDesc[i], kbdDesc[i], MAX_DESC);

     // populate key-action translation list and initialize other lists 
     action = new unsigned[nActions];
     Key kbdKey[] = ACTION_KEY_MAP;
     for (unsigned i = 0; i < nActions; i++) {
         action[i] = kbdKey[i] + 1;
     }
 }

Note that the action array uses 1-based indexing for the associated key.  Zero-valued action entries identify actions with no assigned key. 

Pressed

The pressed() methods set the key state for the key received and return the key state for the action received:

 // pressed sets the key state
 void APIUserInput::pressed(int key, bool down) {
     if (keyboard) keyboard->pressed(key, down);
 }
 // pressed returns the on/off status of Action a
 bool APIUserInput::pressed(Action a) const {
     bool rc = false;
     if (keyboard && action[a]) rc = keyboard->pressed(action[a] - 1); 
     return rc;
 }

The query corrects for the 1-based indexing of associated keys.

Populate Action List

The populateActionList() method populates the action list and sets the cursor to the previously selected action:

 void UserDialog::populateActionList(void* hwndw) {
     HWND hwnd = (HWND)hwndw; // handle to current window

     SendDlgItemMessage(hwnd, IDC_ACT, CB_RESETCONTENT, 0, 0L);
     for (int a = 0; a < nActions; a++) {
         int i = SendDlgItemMessage(hwnd, IDC_ACT, CB_ADDSTRING, 0,
          (LPARAM)actionDesc[a]);
         SendDlgItemMessage(hwnd, IDC_ACT, CB_SETITEMDATA, i,
          (LPARAM)action[a]);
     }
     SendDlgItemMessage(hwnd, IDC_ACT, CB_SETCURSEL, action_, 0L);
 }

Populate Mappable Key List

The populateMappableKeyList() method populates the key list combo box and sets the cursor to the key associated with the action currently displayed in the action combo box:

 void UserDialog::populateMappableKeyList(void* hwndw) {
     HWND hwnd = (HWND)hwndw; // handle to current window

     SendDlgItemMessage(hwnd, IDC_KEY, CB_RESETCONTENT, 0, 0L);
     for (int i = 0; i < nKbdObjs; i++)
         SendDlgItemMessage(hwnd, IDC_KEY, CB_ADDSTRING, 0,
          (LPARAM)kbdObjDesc[i]);

     // retrieve the index for the selected action
     int f = SendDlgItemMessage(hwnd, IDC_ACT, CB_GETCURSEL, 0, 0L);
     // find the current key for the selected action
     unsigned data = SendDlgItemMessage(hwnd, IDC_ACT, CB_GETITEMDATA, f, 0L);
     if (data == CB_ERR) data = 0;
     // set the cursor to the current object for the selected action
     SendDlgItemMessage(hwnd, IDC_KEY, CB_SETCURSEL, data, 0L);
     EnableWindow(GetDlgItem(hwnd, IDC_KEY), TRUE);
 }

Show Action Mapping

The showActionMapping() method retrieves the selected action and sets the cursor in the key combo box to show the key associated with the selectedt action:

 void UserDialog::showActionMapping(void* hwndw) {
     HWND hwnd = (HWND)hwndw; // handle to current window

     int a = SendDlgItemMessage(hwnd, IDC_ACT, CB_GETCURSEL, 0, 0L);
     if (a == CB_ERR)
         error(L"UserDialog::41 Action selection failed");
     else {
         int k = SendDlgItemMessage(hwnd, IDC_ACT, CB_GETITEMDATA, a, 0L);
         SendDlgItemMessage(hwnd, IDC_KEY, CB_SETCURSEL, k, 0L);
     }
 }

Update Action Key Mapping

The updateActionKeyMapping() method retrieves the cursor for the selected action and the cursor for the selected key and then associates the selected key with the selected action:

 void UserDialog::updateActionKeyMapping(void* hwndw) {
     HWND hwnd = (HWND)hwndw; // handle to current window

     unsigned a = SendDlgItemMessage(hwnd, IDC_ACT, CB_GETCURSEL, 0, 0L);
     unsigned k = SendDlgItemMessage(hwnd, IDC_KEY, CB_GETCURSEL, 0, 0L);
     // retrieve the current mapping for the selected action
     unsigned d = SendDlgItemMessage(hwnd, IDC_ACT, CB_GETITEMDATA, a, 0L);
     // erase the current mapping for the key object
     for (unsigned i = 0; i < nActions; i++) {
         unsigned j = SendDlgItemMessage(hwnd, IDC_ACT, CB_GETITEMDATA, i, 0L);
         if (k == j) SendDlgItemMessage(hwnd, IDC_ACT, CB_SETITEMDATA, i, 0);
     }
     // store the selected key in the data item of the selected action
     if (a != CB_ERR && k != CB_ERR)
         SendDlgItemMessage(hwnd, IDC_ACT, CB_SETITEMDATA, a, k);
 }

Save User Choices

The saveUserChoices() method retrieves the selected action-key mappings, stores them in the action array, and saves the selected action for use in the next re-configuration:

 bool UserDialog::saveUserChoices(void* hwndw) {
     // ...
     //----- key mappings for model actions ---------------------------------
     // define the action mappings for the configurable objects
     for (unsigned a = 0; a < nActions; a++) {
         // extract the key mapping from the data parameter of the line item
         unsigned k = SendDlgItemMessage(hwnd, IDC_ACT, CB_GETITEMDATA, a,
          0L);
         action[a] = k;
     }
     // store index of currently visible action for future initialization
     action_ = SendDlgItemMessage(hwnd, IDC_ACT, CB_GETCURSEL, 0, 0L);
     return rcd;
 }

Window Procedure

The dlgProc() procedure shows the key corresponding to the currently selected action and updates the action-key mapping for the currently selected action when the user selects a key from the key combo box:

 BOOL CALLBACK dlgProc(HWND hwnd, UINT msg, WPARAM wp, LPARAM lp) {
    // ...
    switch (msg) {
      // ...
      case WM_COMMAND:          // user accessed a dialog box control
        switch (LOWORD(wp)) {   // which control?
          // ...
          case IDC_ACT:  // user accessed the action combo box
            // show the current key for the selected action
            apiDialog->showActionMapping(hwnd);
            break;
          case IDC_KEY:  // user accessed the key combo box
            if (HIWORD(wp) == CBN_SELCHANGE)
                // associate the selected key with the selected action
                apiDialog->updateActionKeyMapping(hwnd);
            break;
          // ...
        }
        break;
    }
    return rc;
}

Input Device

The Input Device component manages all of the input devices in the framework.  Its Keyboard class keeps track of the current key state for the user's keyboard. 

The iAPIInputDevice interface exposes two pressed() methods to the APIUserInput class:

 class iAPIInputDevice {
     virtual void pressed(int, bool)        = 0;
     virtual bool pressed(unsigned a) const = 0;
     virtual void Delete() const            = 0;
     friend class APIUserInput;
 };
 iAPIInputDevice* CreateAPIKeyboard();

The Keyboard class contains the key flags and defines the pressed() methods:

 class Keyboard : public iAPIInputDevice, public APIBase {
     bool key[256];                          // the current key states
     Keyboard(const Keyboard& k);            // prevents copying
     Keyboard& operator=(const Keyboard& k); // prevents assignment
   public:
     Keyboard();
     void pressed(int, bool);
     bool pressed(unsigned a) const { return key[a % 256]; }
     void Delete() const            { delete this; }
 };

The Keyboard constructor initializes all of the keys to a released state:

 Keyboard::Keyboard() { for (int i = 0; i < 256; i++) key[i] = false; }

The pressed() modifier resets the key state for the received key to the received flag if the key is in the translation list:

 void Keyboard::pressed(int kp, bool d) {
     int k = -1;
     switch (kp) {
       case 'A': k = KEY_A; break;
       case 'B': k = KEY_B; break;
       case 'C': k = KEY_C; break;
       case 'D': k = KEY_D; break;
       case 'E': k = KEY_E; break;
       case 'F': k = KEY_F; break;
       case 'G': k = KEY_G; break;
       case 'H': k = KEY_H; break;
       case 'I': k = KEY_I; break;
       case 'J': k = KEY_J; break;
       case 'K': k = KEY_K; break;
       case 'L': k = KEY_L; break;
       case 'M': k = KEY_M; break;
       case 'N': k = KEY_N; break;
       case 'O': k = KEY_O; break;
       case 'P': k = KEY_P; break;
       case 'Q': k = KEY_Q; break;
       case 'R': k = KEY_R; break;
       case 'S': k = KEY_S; break;
       case 'T': k = KEY_T; break;
       case 'U': k = KEY_U; break;
       case 'V': k = KEY_V; break;
       case 'W': k = KEY_W; break;
       case 'X': k = KEY_X; break;
       case 'Y': k = KEY_Y; break;
       case 'Z': k = KEY_Z; break;
       case '1': k = KEY_1; break;
       case '2': k = KEY_2; break;
       case '3': k = KEY_3; break;
       case '4': k = KEY_4; break;
       case '5': k = KEY_5; break;
       case '6': k = KEY_6; break;
       case '7': k = KEY_7; break;
       case '8': k = KEY_8; break;
       case '9': k = KEY_9; break;
       case '0': k = KEY_0; break;
       case VK_F1:  k = KEY_F1;  break;
       case VK_F2:  k = KEY_F2;  break;
       case VK_F3:  k = KEY_F3;  break;
       case VK_F4:  k = KEY_F4;  break;
       case VK_F5:  k = KEY_F5;  break;
       case VK_F6:  k = KEY_F6;  break;
       case VK_F7:  k = KEY_F7;  break;
       case VK_F8:  k = KEY_F8;  break;
       case VK_F9:  k = KEY_F9;  break;
       case VK_F10: k = KEY_F10; break;
       case VK_F11: k = KEY_F11; break;
       case VK_F12: k = KEY_F12; break;
       case VK_SPACE : k = KEY_SPACE; break;
       case VK_RETURN: k = KEY_ENTER; break;
       case VK_UP    : k = KEY_UP;     break;
       case VK_DOWN  : k = KEY_DOWN;   break;
       case VK_PRIOR : k = KEY_PGUP;  break;
       case VK_NEXT  : k = KEY_PGDN;   break;
       case VK_LEFT  : k = KEY_LEFT;   break;
       case VK_RIGHT : k = KEY_RIGHT;  break;
       case VK_NUMPAD1:  k = KEY_NUM1; break;
       case VK_NUMPAD2:  k = KEY_NUM2; break;
       case VK_NUMPAD3:  k = KEY_NUM3; break;
       case VK_NUMPAD4:  k = KEY_NUM4; break;
       case VK_NUMPAD5:  k = KEY_NUM5; break;
       case VK_NUMPAD6:  k = KEY_NUM6; break;
       case VK_NUMPAD7:  k = KEY_NUM7; break;
       case VK_NUMPAD8:  k = KEY_NUM8; break;
       case VK_NUMPAD9:  k = KEY_NUM9; break;
       case VK_ESCAPE    : k = KEY_ESCAPE; break;
       case VK_MULTIPLY  : k = KEY_TIMES; break;
       case VK_ADD       : k = KEY_PLUS; break;
     }
     if (k != -1) key[k] = d;
 }

If the received key is not in the translation list (the subset of Windows keys listed here), this method does not change any key state.


Exercises

  • Introduce a user action that will make the horizontally drifting sprite move on a diagonal: add the action to the Translation.h file and process the key press for that action in the Design::update() method.
  • Introduce another user action that will make the vertically drifting sprite move on a diagonal: add the action to the Translation.h file and process the key press for that action in the Design::update() method.
  • Add logic to the Design::update() function to identify and respond to a collision between the two moving objects.



Previous Reading  Previous: Sprites in Motion Next: 2D Mathematics   Next Reading


  Designed by Chris Szalwinski   Copying From This Site   
Logo