DirectInput Technical Articles
The DirectInput Mapper: Programming for Multiple Users and Devices on a Single System
 

C. Shane Evans
Microsoft Corporation

January 2001

Summary: This article explores programming with the new DirectInput Mapper features, especially as they relate to multiple users playing a game together on a single machine. (27 printed pages)

Contents

Overview
The Application Code
Data Structures
The action map
Player action and device enumeration structures
Application Initialization
The Application Window Process
Game Loop and Handling Users' Input
Application Cleanup
The CInputDeviceManager Class
Data Members
Error Codes
Initialization: CInputDeviceManager Constructor and the Create() Method
Enumeration and assignment
Device reassignment
Cleanup: The Cleanup() and CleanupDevices() Methods
Conclusion

Overview

To get the most of this document, you should have a solid understanding of DirectInput and its features in Microsoft® DirectX® 8.0. For basic information, you should begin by reading the SDK documentation, and studying the SDK tutorial applications.

DirectX 8.0 introduces a major new feature for input developers—The DirectInput Mapper. The DirectInput Mapper, or "the Mapper" for short, is a big step toward an input paradigm that's truly agnostic of device type. The Mapper makes it possible for you to author input code in the game-native terms of the actions users can perform. Using the Mapper means that you don't need to write special-case code for different types of devices like joysticks, gamepads, driving wheels, and so on. In addition, if you use the Mapper:

  • You don't need to specifically target a few key devices to get them to work well.
  • You tell DirectInput what actions you have, and it figures out which devices make sense, and how to apply your actions to selected devices.
  • You get device reconfiguration services and persisted user preferences "for free."

In short, if you're into input code, the Mapper is an excellent tool. Keep in mind that DirectX 8.0 is the first time Microsoft has attempted a technology of this nature, and as such, this first outing is on the conservative side. Action Mapping itself is robust, but there are areas that need growth, such as Macros, Shift Buttons, and Force Feedback tie-ins to name a few. This article focuses on what we believe will be a common case, multi-user games. By multi-user we mean a console gaming model where there are two or more players interacting with a game that's executing on a single machine. (The term should not be confused with multiplayer, which closely relates to network gaming.) One of the most common multi-user game scenarios on a console is a sports title in which two or more are people playing on teams, each person with their own controller. The console world has the advantage of not being associated with a wide variety of device types. Console developers have a single controller in mind, or licensed controllers that conform to the same input specification. In addition, the console gaming experience isn't as closely associated with the concept of persisted preferences as the PC gaming model, especially in the case of a sit-down-and-play sports game. Making sense out of persisting settings becomes challenging when users are constantly swapping controllers.

Does the Mapper perfectly reconcile the console and PC multi-user experiences? No, but it does give you the information you need to design a solution suitable for the needs of your application and its users. The application that this paper is based upon, MultiMapper, shipped with the DirectX 8.0 SDK. The MultiMapper sample is not a game, it's an input loop and a manager class with a little window dressing. It displays no real game graphics, keeps no score, and provides only the simplest user interface. Its entire purpose is to showcase a Mapper-based input paradigm in a multi-user context.

This document is a walk-through of the MultiMapper sample, drawing code and comments directly from the sample as needed to describe the problems and solutions that the sample demonstrates. The contents of this paper have largely been entered as comments, but it does embellish the comments with details in many places. Code review is organized into two primary sections. The Application Code discusses the global data structures, application initialization and cleanup, window messages, and the game loop. However, most of the important code is contained within a helper class, called CInputDeviceManager. The CInputDeviceManager Class covers the class's internal data members, initialization and cleanup, error codes, and device enumeration and assignment.

The Application Code

The application contains code to define an action map, create and display a window with a simple user interface, create the CInputDeviceManager class, and process user input. These are actually the simplest parts of the sample overall, the manager class itself handles all of the device enumeration and assignment.

Details on the tasks performed by the application code are covered in the following topics:

Data Structures
The action map
Player action and device enumeration structures
Application Initialization
The Application Window Process
Game Loop and Handling Users' Input
Application Cleanup

Data Structures

The action map

The action map is the cornerstone of a Mapper-enabled application, which contains all the information DirectInput needs to abstract the input devices attached to a particular machine. The action map communicates an association between an application-defined value (the uAppData member of a DIACTION structure) and a value that represents a common action in a given game genre (dwSemantic, also from DIACTION).

The MultiMapper sample uses the following constants for its uAppData values, which correspond to the actions that the user can perform in this "game." This sample pretends to be a space simulator game, so relevant inputs are for turning the ship, thrust control, firing weapons, and so on. Also, note that some constants are defined for the axes, which receive axis data from joysticks and similar analog controls.

#define INPUT_LEFTRIGHT_AXIS   1L
#define INPUT_UPDOWN_AXIS      2L
#define INPUT_TURNLEFT         3L
#define INPUT_TURNRIGHT        4L
#define INPUT_FORWARDTHRUST    5L
#define INPUT_REVERSETHRUST    6L
#define INPUT_FIREWEAPONS      7L
#define INPUT_ENABLESHIELD     8L
#define INPUT_DISPLAYGAMEMENU  9L
#define INPUT_QUITGAME        10L

The action map itself is an array of DIACTION structures. The g_rgGameAction array is the global collection game actions that DirectInput uses to initially map real device inputs into game actions. In the first column are the app-defined codes shown in the preceding list of defined constants. These are the constants the game sees in its input loop when the user physically activates a control on a device. The second column is the physical action that is to be mapped to the app-defined semantic. For instance, in the array below, if the user hits the left arrow key on the keyboard, the input loop will receive an input code equal to INPUT_TURNLEFT. The last column is a text string that DirectInput uses for displaying a configuration dialog box.

#define NUMBER_OF_SEMANTICS 17

DIACTION g_rgGameAction[NUMBER_OF_SEMANTICS] =
{
    // Device input (joystick, etc.) that is pre-defined by DirectInput, 
    // according to genre type. The genre for this app is space simulators.
    { INPUT_LEFTRIGHT_AXIS,  DIAXIS_SPACESIM_LATERAL,         0, _T("Turn"), },
    { INPUT_UPDOWN_AXIS,     DIAXIS_SPACESIM_MOVE,            0, _T("Move"), },
    { INPUT_FIREWEAPONS,     DIBUTTON_SPACESIM_FIRE,          0, _T("Shoot"), },
    { INPUT_ENABLESHIELD,    DIBUTTON_SPACESIM_GEAR,          0, _T("Enable shields"), },
    { INPUT_DISPLAYGAMEMENU, DIBUTTON_SPACESIM_DISPLAY,       0, _T("Display"), },

    // Keyboard input mappings
    { INPUT_TURNLEFT,        DIKEYBOARD_LEFT,    0, _T("Turn left"), },
    { INPUT_TURNRIGHT,       DIKEYBOARD_RIGHT,   0, _T("Turn right"), },
    { INPUT_FORWARDTHRUST,   DIKEYBOARD_UP,      0, _T("Forward thrust"), },
    { INPUT_REVERSETHRUST,   DIKEYBOARD_DOWN,    0, _T("Reverse thrust"), },
    { INPUT_FIREWEAPONS,     DIKEYBOARD_F,       0, _T("Shoot"), },
    { INPUT_ENABLESHIELD,    DIKEYBOARD_S,       0, _T("Enable shields"), },
    { INPUT_DISPLAYGAMEMENU, DIKEYBOARD_D, DIA_APPFIXED, _T("Display"), },
    { INPUT_QUITGAME,        DIKEYBOARD_ESCAPE,  DIA_APPFIXED, _T("Quit Game"), },

    // Mouse input mappings
    { INPUT_LEFTRIGHT_AXIS,  DIMOUSE_XAXIS,      0, _T("Turn"), },
    { INPUT_UPDOWN_AXIS,     DIMOUSE_YAXIS,      0, _T("Move"), },
    { INPUT_FIREWEAPONS,     DIMOUSE_BUTTON0,    0, _T("Shoot"), },
    { INPUT_ENABLESHIELD,    DIMOUSE_BUTTON1,    0, _T("Enable shields"), },
};

Player action and device enumeration structures

The PLAYERDATA structure contains all the game-state data for a single player in the game. None of this data is used for anything other than showing brief visual feedback, but a structure like this approximates one you would use in a game. MultiMapper uses an array of these structures—one for each player—to track the state for all players. The array is initialized in the window procedure when the WM_CREATE message is received.

typedef struct _PLAYERDATA {
    BOOL  bTurningRight;
    BOOL  bReverseThrust;
    BOOL  bTurningLeft;
    BOOL  bForwardThrust;
    BOOL  bFiringWeapons;
    BOOL  bEnableShields;
    BOOL  bDisplayingMenu;
    DWORD dwLRAxisData;
    DWORD dwUDAxisData;
} PLAYERDATA, *LPPLAYERDATA;

The ENUMDATA structure carries information needed by the EnumDevicesBySemantics callback method. The ENUMDATA structure is used in various places to add devices to the manager class's internal lists, build and set action maps, and to return the status (device found or not) of the enumeration. The structure includes a pointer to the manager class, flags to indicate how the manager class should initialize itself, and a window handle for the calling application. The remaining members are pretty straight-forward: the number of players in the game, a pointer to a multi-sz string for all the player's names, and a pointer to an action format for the players. Lastly, this structure contains the BOOL field, bRet, to contain a return value that indicates whether or not a device has been assigned to a player.

typedef struct _ENUMDATA {
    CInputDeviceManager* pDevMan;
    DWORD                dwCreateFlags;
    HWND                 hWnd;
    DWORD                dwPlayerNum;
    TCHAR*               pszPlayerName;
    LPDIACTIONFORMAT     lpdiaf;
    BOOL                 bRet;
} ENUMDATA, *LPENUMDATA;

Application Initialization

When the application begins, the WinMain function starts like any other Windows application. It initializes WNDCLASS structure and creates a simple popup window in which the minimalist user interface (UI) will be shown. Once the window is created and visible, the code invokes a modal dialog to query the user for the number of players who will be interacting with the game. There's nothing terribly special going on until the number of players is determined and WinMain calls the application's CreateInputStuff function.

CreateInputStuff creates the DirectInput helper class with a default initialization flag (covered later, in Initialization: CInputDeviceManager Constructor and the Create() Method). The DIACTIONFORMAT structure it passes to the helper class specifies what types of devices are needed (through the genre). The genres are defined in the documents and the dinput.h header file. The DIACTIONFORMAT structure is also used to specify the game action array.

HRESULT CreateInputStuff( HWND hWnd, DWORD dwNumUsers )
{
    HRESULT hr;

    // Setup action format for suitable input devices for this app
    DIACTIONFORMAT diaf;
    ZeroMemory( &diaf, sizeof(DIACTIONFORMAT) );
    diaf.dwSize        = sizeof(DIACTIONFORMAT);
    diaf.dwActionSize  = sizeof(DIACTION);
    diaf.dwDataSize    = NUMBER_OF_SEMANTICS * sizeof(DWORD);
    diaf.dwNumActions  = NUMBER_OF_SEMANTICS;
    diaf.guidActionMap = g_AppGuid;
    diaf.dwGenre       = DIVIRTUAL_SPACESIM;
    diaf.rgoAction     = g_rgGameAction;
    diaf.dwBufferSize  = BUFFER_SIZE;
    diaf.lAxisMin      = -100;
    diaf.lAxisMax      = 100;
    _tcscpy( diaf.tszActionMap, _T("MultiMapper Sample Application") );

    // Create a new input device manager
    g_pInputDeviceManager = new CInputDeviceManager();
    if( !g_pInputDeviceManager )
    {
        return E_OUTOFMEMORY;
    }

    hr = g_pInputDeviceManager->Create( hWnd, &dwNumUsers, &diaf, 
APP_DICREATE_DEFAULT );

    if( FAILED(hr) )
    {
        TCHAR msg[MAX_PATH];
        int iRet;

Device "ownership" in this application is determined by the presence of the DIEBSFL_RECENTDEVICE flag during device enumeration for a specific user. If this flag is present, the device was recently used by that user, and MultiMapper assumes that the same user would prefer to use that device again.

Given that logic, it's possible that a single user could own too many devices for the other players to get into the game. If so, MultiMapper reinitializes the manager class with a flag that overrides the default behavior and provides each user with a device that has a default configuration. The initialization flags are covered in Initialization: CInputDeviceManager Constructor and the Create() Method.

        switch(hr)
        {
        case E_APPERR_DEVICESTAKEN:
            sprintf( msg, 
                 _T("You have entered more users than there are suitable 
devices, " \
                 "or some users are claiming too many devices.\n\n" \
                 "Click Yes to give each user a default device, or click No " \
                 "to close the application"));

            iRet = MessageBox( hWnd, msg, _T("Devices Are Taken"), 
                               MB_YESNO | MB_ICONEXCLAMATION );

            if(iRet == IDYES)
            {
                g_pInputDeviceManager->Cleanup();
                g_pInputDeviceManager->Create( hWnd, &dwNumUsers, 
                                               &diaf, APP_DICREATE_FORCEINIT );
            }
            else
                return E_FAIL;
            
            break;

Another possible error case occurs if there are more users attempting to play than there are devices attached to the machine. In this case, the number of players is automatically lowered and the helper class reinitialized to make playing the game possible.

        case E_APPERR_TOOMANYUSERS:
            DWORD dwNumDevices = g_pInputDeviceManager->GetNumDevices();
            dwNumUsers = dwNumDevices;
            TCHAR* str = _T( "There are not enough devices attached to 
the system " \
                             "for the number of users you entered.\n\n" \
                             "The number of users has been automatically changed " \
                             "to %i (the number of devices available 
on the system).");

            sprintf( msg, str, dwNumDevices);
            MessageBox( hWnd, msg, _T("Too Many Users"), 
                        MB_OK | MB_ICONEXCLAMATION );

            g_pInputDeviceManager->Cleanup();
            g_pInputDeviceManager->Create( hWnd, &dwNumUsers, &diaf, 
APP_DICREATE_DEFAULT );                           
            break;
        }
    }

    return S_OK;
}

A more robust application might attempt to split each player's controls across various parts of the keyboard, but for simplicity, the MultiMapper sample doesn't take this approach.

The Application Window Process

The window process for MultiMapper is extremely simple, handling only three messages: WM_PAINT, WM_CHAR, and WM_DESTROY. The sample processes WM_PAINT to display a portion of the minimalist visual feedback in response to actions made on the currently selected control devices. The rest of the user interface is drawn within the PaintPlayerStatus function, discussed later in this article.

        case WM_PAINT:
        {
            // Output message to user
            CHAR* str;
            HDC hDC = GetDC( hWnd );
            
            SetTextColor( hDC, RGB(255,255,255) );
            SetBkColor( hDC, RGB(0,0,0) );
            SetBkMode( hDC, OPAQUE );
            
            str = _T("Name");
            TextOut( hDC, 0, 0, str, lstrlen(str) );
            str = _T("Turn");
            TextOut( hDC, COL_SPACING*1, 0, str, lstrlen(str) );
            str = _T("Thrust");
            TextOut( hDC, COL_SPACING*2, 0, str, lstrlen(str) );
            str = _T("Weapon");
            TextOut( hDC, COL_SPACING*3, 0, str, lstrlen(str) );
            str = _T("Shield");
            TextOut( hDC, COL_SPACING*4, 0, str, lstrlen(str) );

            str = _T("Looking for game input... press Escape to exit.");
            TextOut( hDC, 0, 140, str, lstrlen(str) );

            str = _T("Press D to display input device settings.");
            TextOut( hDC, 0, 158, str, lstrlen(str) );

            ReleaseDC( hWnd, hDC );
            break;
        }

MultiMapper employs global, that is non player-specific, keystrokes to enable any user to close the application or display the configuration UI. Because the keyboard may or may not be configured for a specific player, handling global actions in an action map becomes slightly problematic in the event that no users have taken the keyboard. Rather than attempting to embed global actions in a keyboard action map irrespective of player assignments, MultiMapper handles global keystrokes as WM_CHAR messages. In this case, it's actually easier to use Windows messages than an action map.

        case WM_CHAR:
            if( VK_ESCAPE == (TCHAR)wParam )
                SendMessage( hWnd, WM_DESTROY, 0, 0 );
            else if ( 0x64 == (TCHAR)wParam ) // 0x64 == 'D'
                InvokeDefaultUI(hWnd);
            break;

The WM_DESTROY message is sent to the application when a user presses the Escape key, or has chosen to close the window through standard Windows UI. When received, the code invokes the Cleanup code for the application. This is covered in more detail in Application Cleanup.

        case WM_DESTROY:
            Cleanup();
            PostQuitMessage( 0 );
            break;

Game Loop and Handling Users' Input

The GameLoop function is the aptly-named input loop for the application. The function begins by calling the CInputDeviceManager::GetNumUsers method to retrieve the number of users playing the game. The function then iterates through all players, calling the CInputDeviceManager::GetDevicesForPlayer method to retrieve an array of IDirectInputDevice8 interface pointers that represent the devices currently assigned to each person.

BOOL GameLoop( HWND hWnd )
{
    BOOL bRet;
    DIDEVICEOBJECTDATA didObjData;
    ZeroMemory( &didObjData, sizeof(didObjData) );
    static PLAYERDATA pd[MAX_USERS];    // State of each player
    LPDIRECTINPUTDEVICE8* pdidDevices;  
    
    DWORD dwNumPlayers = g_pInputDeviceManager->GetNumUsers();
            
    // Loop through all devices for all players and check game input.
    for( DWORD i=0; i < dwNumPlayers; i++ )
    {
        // Get access to the list of input devices.
        g_pInputDeviceManager->GetDevicesForPlayer( i, &pdidDevices );

Once a user's device array is retrieved, GameLoop calls the ParsePlayerInput function to retrieve the buffered actions stored for the user since the last time the input loop was invoked. The ParsePlayerInput code is discussed in detail later in this paper.

        bRet = ParsePlayerInput(hWnd, pdidDevices, &pd[i]);
        if( !bRet )
        {
            return bRet;
        }

After a player's input state is compiled into a PLAYERDATA structure, the GameLoop function removes logical conflicts. For instance, it doesn't often make sense to be able to simultaneously move your player left and right at the same moment. These conflicts depend on the game logic, and not on the DirectInput semantic mappings. You may want to design your application to do this differently.

        if( pd[i].bTurningLeft && pd[i].bTurningRight )
        {
            pd[i].bTurningLeft = pd[i].bTurningRight = FALSE;
        }
        if( pd[i].bForwardThrust && pd[i].bReverseThrust )
        {
            pd[i].bForwardThrust = pd[i].bReverseThrust = FALSE;
        }

At this point, the sample has a clear picture of the player's state in the game. It then paints the status for the player by calling the PaintPlayerStatus function. In a real game, this could just as easily be a call to update the scene.

        PaintPlayerStatus( hWnd, i, pd[i] );

The previously mentioned ParsePlayerInput method retrieves buffered actions from all the devices owned for a user, and parses those actions into the player state structure, PLAYERDATA. The data represents a snapshot of the in-game actions being performed by a player. The function receives parameters for the retrieval and storage of game actions. It also receives a window handle for use in displaying the configuration user interface, in the event that a device control has been mapped for this purpose. This augments the global keystroke for the same task, as some games may want to include a control strictly to display the current configuration of a device.

BOOL ParsePlayerInput(HWND hWnd, LPDIRECTINPUTDEVICE8* ppdidDevices, LPPLAYERDATA ppd)
{
    HRESULT hr;
    DWORD   dwItems;
    DIDEVICEOBJECTDATA adod[BUFFER_SIZE];

    DWORD   dwDevice = 0;

The function loops through all of the devices assigned to this user. The first action taken on each device is a call to the IDirectInputDevice8::Poll method. The Poll method is technically optional as many devices are USB, and USB is an interrupt-driven bus. However, the Poll method is essentially a no-op on interrupt-driven devices, so the MultiMapper sample polls all devices before attempting to read data from all devices without sacrificing performance. It's possible that the Poll method could fail because the device has become unacquired, and if so, the sample immediately attempts to reacquire the device and returns control to the calling function. In this case, data for the device is lost for that iteration of the input loop. A common example of this is when a user tabs away from the active application and the loss is rarely a serious problem.

    while ( ppdidDevices[dwDevice] )
    {
        // Poll the device to read the current state
        if( FAILED(ppdidDevices[dwDevice]->Poll() ) )  
        {
            // DirectInput is telling us that the input stream has been
            // interrupted. We aren't tracking any state between polls, so
            // we don't have any special reset that needs to be done. We
            // just re-acquire and try again.
            hr = ppdidDevices[dwDevice]->Acquire();
            if( DIERR_INPUTLOST  == hr) 
            {
                hr = ppdidDevices[dwDevice]->Acquire();
            }

            // hr may be DIERR_OTHERAPPHASPRIO or other errors.  This
            // may occur when the app is minimized or in the process of 
            // switching, so just try again later 
            return TRUE; 
        }        

Now that the device has been polled, the code retrieves buffered actions from the device by calling IDirectInputDevice8::GetDeviceData method. The actions are stored in a new member of the DIDEVICEOBJECTDATA structure, uAppData. The uAppData member is what now makes parsing input so easy. DirectInput sets the uAppData member to the application-defined value given in the previous call to IDirectInputDevice::SetActionMap. In a single DIDEVICEOBJECTINSTANCE structure, the uAppData member represents an action that should take place in the game. The array of DIDEVICEOBJECTINSTANCE structures collectively describes which game actions the user has performed since the last iteration of the input loop.

When GetDeviceData returns, the application loops through the array of actions, using a simple switch statement to determine which actions should take place. Each game action is caught by a case statement that places state data into the corresponding member of the user's PLAYERDATA structure for use by PaintPlayerStatus to display visual feedback in response to events in the game.

        dwItems = BUFFER_SIZE;
        hr = ppdidDevices[dwDevice]->GetDeviceData( sizeof(DIDEVICEOBJECTDATA),
                                                    adod, &dwItems, 0 );
        if( SUCCEEDED(hr) )
        {           
            // Get the actions. The number of input events is stored in
            // dwItems, and all the events are stored in the "adod" array. Each
            // event has a type stored in uAppData, and actual data stored in
            // dwData.
            for( DWORD j=0; j<dwItems; j++ )
            {
                // Non-axis data is recieved as "button pressed" or "button
                // released". Parse input as such.
                BOOL bState = (adod[j].dwData != 0 ) ? TRUE : FALSE;

                switch (adod[j].uAppData)
                {
                case INPUT_LEFTRIGHT_AXIS: // Parse the left-right axis data
                    (*ppd).dwLRAxisData  = adod[j].dwData;
                    (*ppd).bTurningRight = (*ppd).bTurningLeft  = FALSE;
                    if( (int)(*ppd).dwLRAxisData > 0 )
                        (*ppd).bTurningRight = TRUE;
                    else if( (int)(*ppd).dwLRAxisData < 0 )
                        (*ppd).bTurningLeft = TRUE;
                    break;

                case INPUT_UPDOWN_AXIS: // Parse the up-down axis data
                    (*ppd).dwUDAxisData   = adod[j].dwData;
                    (*ppd).bReverseThrust = (*ppd).bForwardThrust = FALSE;

                    if( (int)(*ppd).dwUDAxisData > 0 )
                        (*ppd).bReverseThrust = TRUE;
                    else if( (int)(*ppd).dwUDAxisData < 0 )
                        (*ppd).bForwardThrust = TRUE;
                    break;
                    
                case INPUT_TURNLEFT:        (*ppd).bTurningLeft    = bState; break;
                case INPUT_TURNRIGHT:       (*ppd).bTurningRight   = bState; break;
                case INPUT_FORWARDTHRUST:   (*ppd).bForwardThrust  = bState; break;
                case INPUT_REVERSETHRUST:   (*ppd).bReverseThrust  = bState; break;
                case INPUT_FIREWEAPONS:     (*ppd).bFiringWeapons  = bState; break;
                case INPUT_ENABLESHIELD:    (*ppd).bEnableShields  = bState; break;
                
                case INPUT_QUITGAME:        return FALSE;                
                
                case INPUT_DISPLAYGAMEMENU: 
                    (*ppd).bDisplayingMenu = bState; 
                    InvokeDefaultUI(hWnd);
                    return TRUE;
                }
            }
        }

        // Go to the next device in the list.
        dwDevice++;
        }

    return TRUE;
}

Application Cleanup

The Cleanup function is the global cleanup function for the application. The window process calls Cleanup when it receives a WM_DESTROY message. This function simply invokes the manager class's internal cleanup method (which destroys all of the DirectInput objects), then deletes the manager class itself.

VOID Cleanup()
{
    if( g_pInputDeviceManager )
    {
        g_pInputDeviceManager->Cleanup();
        delete g_pInputDeviceManager;
        g_pInputDeviceManager = NULL;
    }
}

The CInputDeviceManager Class

Most of what's interesting about the MultiMapper sample takes place within the CInputDeviceManager class. This class manages the action maps provided by the application, performs device enumeration, and takes care of device assignment and reassignment. Each of these tasks is dissected in the following sections.

Data Members
Error Codes
Initialization: CInputDeviceManager Constructor and the Create() Method
Enumeration and assignment
Device reassignment
Cleanup: The Cleanup() and CleanupDevices() Methods

Data Members

The CInputDeviceManager class uses several private data members to perform various DirectInput-related tasks. It stores a single IDirectInput8 interface from which it spawns IDirectInputDevice8 interfaces for the input devices on the system. The device interfaces are stored in a two-dimensional array, in which each row could be thought of as a one-dimensional array of devices assigned to a user.

The class includes data members to track the current number of players and an array of player names (stored in multi-sz format). Additionally, there are data members to hold the client window handle, an action format structure, creation flags used during class initialization, and the count of all input devices attached to the system at the last enumeration.

    LPDIRECTINPUT8       m_pDI;
    LPDIRECTINPUTDEVICE8 m_ppdidDevices[MAX_USERS][MAX_DEVICES];
    DWORD                m_dwNumUsers;
    TCHAR*               m_szUserNames;
    HWND                 m_hWnd;
    DIACTIONFORMAT       m_diaf;
    DWORD                m_dwCreateFlags;
    DWORD                m_dwTotalDevices;

Error Codes

The CInputDeviceManager class employs two custom COM error codes. According to COM rules, these are defined with the MAKE_HRESULT macro, using SEVERITY_ERROR and FACILITY_ITF for the error code severity and facility, respectively.

By default during enumeration, the class attempts to assign any device marked as recent for a user to the current user. In some cases, a single player can take ownership of enough devices to prevent all users from playing. If this occurs, the class returns the E_APPERR_DEVICESTAKEN error code.

#define E_APPERR_DEVICESTAKEN MAKE_HRESULT(SEVERITY_ERROR,FACILITY_ITF,998) 

E_APPERR_TOOMANYUSERS is returned by the manager class when the number of players exceeds the number of devices present on the system. For example, this code is returned if the users request a game for four players on a machine that only has a keyboard and mouse.

#define E_APPERR_TOOMANYUSERS MAKE_HRESULT(SEVERITY_ERROR,FACILITY_ITF,999)

Initialization: CInputDeviceManager Constructor and the Create() Method

The CInputDeviceManager class is initialized in two stages. The first stage, during the class's constructor, zero-initializes the class's internal data members sets the static list of player names.

CInputDeviceManager::CInputDeviceManager()
{
    m_pDI                  = NULL;
    m_dwCreateFlags        = 0;
    m_dwTotalDevices       = 0;

    // Uses canned names for players.
    m_szUserNames  = _T("Player 1\0Player 2\0Player 3\0Player 4\0\0");

    ZeroMemory( m_ppdidDevices, sizeof(m_ppdidDevices) );
}

The second stage of initialization takes place when the client calls the CInputDeviceManager::Create method. The method creates DirectInput, and enumerates devices for each player. Device assignment takes place in the enumeration callback. Prior to invoking device enumeration, the code prepares an ENUMDATA structure that carries information needed by the callback function and receives a return value that indicates the success or failure of the enumeration and device assignment process.

HRESULT CInputDeviceManager::Create( HWND hWnd, DWORD* lpdwNumUsers, 
                                     DIACTIONFORMAT* pdiaf, DWORD dwFlags )
{
    HRESULT hr;

    // Copy passed in arguments for internal use.
    m_hWnd          = hWnd;
    memcpy( &m_diaf, pdiaf, sizeof(DIACTIONFORMAT) );
    m_dwNumUsers    = *lpdwNumUsers;
    m_dwCreateFlags = dwFlags;

    // Create the main DirectInput object.
    hr = DirectInput8Create( GetModuleHandle(NULL), DIRECTINPUT_VERSION, 
                             IID_IDirectInput8, (VOID**)&m_pDI, NULL );
    if( FAILED(hr) )
    {
        return E_FAIL;
    }

    TCHAR* pszNameScan = m_szUserNames;
    for(DWORD dwPlay = 0; dwPlay < m_dwNumUsers; dwPlay++)
    {
        ENUMDATA ed;
        ZeroMemory( &ed, sizeof(ed) );
        ed.pDevMan = this;
        ed.dwPlayerNum = dwPlay;
        ed.pszPlayerName = pszNameScan;
        ed.bRet = FALSE; // Will be TRUE if a device was found.
        ed.lpdiaf = &m_diaf;
        ed.hWnd = m_hWnd;
        ed.dwCreateFlags = m_dwCreateFlags;

The class can be initialized in two ways, as controlled by the dwFlags parameter passed to the Create method. In the default mode, the class assigns recently used devices to the appropriate user, based upon the user name.

        if( m_dwCreateFlags == APP_DICREATE_DEFAULT)
        {
            // Enumerate "suitable" devices for this user.
            hr = m_pDI->EnumDevicesBySemantics( pszNameScan, &m_diaf, 
                                        EnumSuitableDevicesCB, 
                                        (LPVOID)&ed, 
                                        DIEDBSFL_THISUSER | DIEDBSFL_AVAILABLEDEVICES);
        }

This default approach is prone to a case where a single user has ownership of enough devices to limit the number of players who can interact with the game. If this happens, the class can be reinitialized with the APP_DICREATE_FORCEINIT creation flag. When Create is called with APP_DICREATE_FORCEINIT, the class allocates a single device to each user, imposing a default configuration in favor of past user preferences. This task is performed by re-enumerating the devices, but omitting the DIEDBSFL_THISUSER flag, which causes DirectInput to enumerate devices regardless of device ownership.

        else // m_dwCreateFlags == APP_DICREATE_FORCEINIT
        {
            // Enumerate available devices for any user.
            hr = m_pDI->EnumDevicesBySemantics( NULL, &m_diaf, 
                                                EnumSuitableDevicesCB, 
                                                (LPVOID)&ed, 
DIEDBSFL_AVAILABLEDEVICES);
        }

        if( FAILED(hr))
        {
            return hr;
        }

After enumeration, the return value stored in the bRet member of the ENUMDATA structure indicates the success or failure of the device assignment attempt. A zero value indicates failure to assign a device to the user. If the number of devices attached to the system is less than the number of players attempting to play the game, the method returns E_APPERR_TOOMANYUSERS. Otherwise, it's likely that one of the users owns too many devices to fairly determine which devices should go to the other players. In this case, the method returns E_APPERR_DEVICESTAKEN.

        if(!ed.bRet)
        {
            if(m_dwTotalDevices < m_dwNumUsers)
                return E_APPERR_TOOMANYUSERS;
            else
                return E_APPERR_DEVICESTAKEN;
        }

Enumeration and assignment

MultiMapper uses the IDirectInput8::EnumDevicesBySemantics method in two ways: by default, to enumerate and assign devices based on user preferences, and during forced initialization, where it enumerates for devices suitable for any user. The distinction between these two cases is the presence or absence of the DIEDBSFL_THISUSER flag passed to EnumDevicesBySemantics. The class-defined EnumDevicesBySemanticsCB callback method receives the callbacks in either case. Amongst the standard parameters the callback receives is pContext, which in this case turns out to be a pointer to an application-defined ENUMDATA structure. This structure plays a key role in device assignment and in reporting the success or failure of device assignment to the caller. The ENUMDATA.pszPlayerName member is important here too, as it communicates to the callback for which user enumeration is now taking place.

Because the manager class enumerates all devices for each player, DirectInput calls the method frequently during startup (#_of_calls = #_of_players * installed_devices - #_of_owned_devices). For completeness, the beginning of the method definition follows.

Note   A lot of code related to debug output has been removed from this function.
BOOL CALLBACK CInputDeviceManager::EnumSuitableDevicesCB( 
      LPCDIDEVICEINSTANCE  pdidi,
      LPDIRECTINPUTDEVICE8 pdidDevice, 
      DWORD  dwFlags,
      DWORD  dwRemainingDevices,
      LPVOID pContext )
{
    HRESULT hr;
    ENUMDATA ed = *(LPENUMDATA)pContext;
    DWORD dwBuildFlags;    

    // Temp array to hold non-recent/new devices for later allocation.
    // A linked list would be a better choice, but for simplicity, we'll 
    // use a simple array.
    static LPDIRECTINPUTDEVICE8 lprgDevTemp[MAX_DEVICES]; 
    static DIDEVICEINSTANCE     didTemp[MAX_DEVICES];
    static int iIndex = 0;
        

Devices of type DI8DEVTYPE_DEVICECTRL have specialized purposes such as voice communication, that are not generally considered appropriate to control high-priority game actions. Because the sample doesn't target these types of devices, they are discarded and enumeration continues with the next device.

    if( DI8DEVTYPE_DEVICECTRL == GET_DIDEVICE_TYPE(pdidi->dwDevType) )
    {
        return DIENUM_CONTINUE;     
    }

The method attempts to assign devices recently used by a player in a past invocation of the game to the same player this time. Devices marked by the DIEDBS_RECENTDEVICE were last owned by this user and are therefore probably desirable to the user again. As such, the sample attempts to assign the device to the user for which enumeration is now taking place. To begin, MultiMapper calls the IDirectInputDevice8::SetCooperativeLevel method to request exclusive foreground access to the device. Cooperative level has nothing to do with setting an action map, so your own application could set the cooperative level elsewhere. MultiMapper then calls IDirectInputDevice8::BuildActionMap to build an action map for the user, then subsequently sets the action map to the device by calling IDirectInputDevice8::SetActionMap. The SetActionMap call is what effectively causes the user to take ownership of the device. Once the device is assigned, calling EnumDevicesBySemantics with the DIEDBSFL_THISUSER flag will not enumerate any devices owned by other users (based on a simple comparison of user name strings).

Note that devices are only marked by the DIEDBS_RECENTDEVICE flag when the caller invokes EnumDevicesBySemantics method and passes the DIEDSBFL_THISUSER flag.

    if( dwFlags & DIEDBS_RECENTDEVICE )
    {
        // Set the cooperative level.
        hr = pdidDevice->SetCooperativeLevel( ed.hWnd, 
DISCL_EXCLUSIVE|DISCL_FOREGROUND );
        if( FAILED(hr) )
        {
            return DIENUM_CONTINUE;     
        }

        // Set the action map for this player to remove it from 
DirecInput's internal
        // list of available devices.
        dwBuildFlags = (ed.dwCreateFlags == APP_DICREATE_DEFAULT) 
? DIDBAM_DEFAULT : DIDBAM_HWDEFAULTS;
        pdidDevice->BuildActionMap( ed.lpdiaf, ed.pszPlayerName, dwBuildFlags );
        hr = pdidDevice->SetActionMap( ed.lpdiaf, ed.pszPlayerName, DIDSAM_DEFAULT ); 

        // Add the device to the device manager's internal list
        ed.pDevMan->AddDeviceForPlayer( ed.dwPlayerNum, pdidi, pdidDevice );

        // If BuildActionMap fails, so will SetActionMap. Using a single error 
        // check here for simplicity. These may fail if no controls on this device 
        // are appropriate. However, other devices may be just fine, so continue 
        // the enumeration.
        if( FAILED(hr) )
        {
            return DIENUM_CONTINUE;
        }
        
        // Set return value in ENUMDATA struct to indicate success and 
        // stop enumeration.
        ((LPENUMDATA)pContext)->bRet = TRUE;
    }

Devices marked by the DIEDBS_NEWDEVICE or DIEDBS_MAPPEDPRI1 flags are added to a flat list that is eventually used in the event that no recent devices are found for this user. These represent devices that are new for that user, or that can have game-critical actions assigned to them. In the event that no recent devices exist for this player, any devices marked with these flags are likely candidates for use.

    else if( dwFlags & ( DIEDBS_MAPPEDPRI1 | DIEDBS_NEWDEVICE ) )
    {   // This is neither a RECENT nor a NEW device, add it to an internal list
        // of devices. If we get to the end of the devices without finding a
        // recently used or new device, we'll try one of these. 
        lprgDevTemp[iIndex] = pdidDevice;
        didTemp[iIndex]     = *(LPDIDEVICEINSTANCE)pdidi;
        lprgDevTemp[iIndex++]->AddRef();

    }

The callback function is passed a count of the remaining devices to be enumerated with each call. Using this information, it's easy to tell when no more devices will be found and hence, no more callbacks made for this user. If no recent devices have yet been found for the current user, the method takes the first device in the flat list (generally the most suitable) and ignores the others. This is done for the sake of simplicity; real applications would probably iterate through action maps for these devices and choose the one with the most game-critical actions assigned. When default enumeration is taking place, user-preferences are respected. During forced-assignment enumeration, the sample imposes hardware defaults to ensure that each user receives a fully-configured device.

    if( 0 == dwRemainingDevices &&  (((LPENUMDATA)pContext)->bRet != TRUE) ) 
    {
        dwBuildFlags = (ed.dwCreateFlags == APP_DICREATE_DEFAULT) ? DIDBAM_DEFAULT 
: DIDBAM_HWDEFAULTS;

        // Set the action map for this player to remove it from 
DirectInput's internal
        // list of available devices.
        if( lprgDevTemp[0] )
        {
            // Add the device to the device manager's internal list
            ed.pDevMan->AddDeviceForPlayer( ed.dwPlayerNum, 
&didTemp[0], lprgDevTemp[0] );

            lprgDevTemp[0]->BuildActionMap( ed.lpdiaf, 
ed.pszPlayerName, dwBuildFlags );
            hr = lprgDevTemp[0]->SetActionMap( ed.lpdiaf, 
ed.pszPlayerName, DIDSAM_DEFAULT ); 

            // If BuildActionMap fails, so will SetActionMap. Using a single error
            // check here for simplicity. These may fail if no controls on this
            // device are appropriate. However, other devices may be just fine, so
            // continue the enumeration.
            if( FAILED(hr) )
                return DIENUM_CONTINUE;

            ((LPENUMDATA)pContext)->bRet = TRUE;
        }

        // Release all of the remaining devices in the list.
        for (int i=1 ; i<iIndex ; i++ )
        {
            if( lprgDevTemp[i] )
            {
                lprgDevTemp[i]->Release();
            }
        }

        iIndex = 0; // reset counter for next pass.
    }

    return DIENUM_CONTINUE;
}

Device reassignment

Device reassignment takes place whenever the DirectInput Mapper Default UI returns from the ConfigureDevices call. After ConfigureDevices returns, the probability that some devices now have different configurations or have changed hands relatively high, as is the possibility that devices previously unowned are now being used by someone. Device reassignment and reconfiguration turns out to be an easy task when you do it through enumeration. Because the ConfigureDevices call reassigns devices automatically before it returns, each of the devices on the system will return the most current user name for their DIPROP_USERNAME property.

VOID CInputDeviceManager::ReassignDevices()
{
    // Flat array for all devices on the machine.
    LPDIRECTINPUTDEVICE8 prgAllDev[MAX_DEVICES];

    TCHAR szNameDev[MAX_PATH];
    DWORD dwDev;

This method starts by flushing the manager class's internal lists (while preserving who owns which devices) then it enumerates all devices on the machine by calling EnumDevicesBySemantics with a NULL username and by omitting the DIEDBSFL_THISUSER flag. The BuildFlatListCB callback method shown later in this section simply adds all of the enumerated devices to an array.

    // Clean-out the class-internal array of devices for each user.
    CleanupDevices( APP_CLEANUP_PRESERVEASSIGNMENT );

    // Build a simple flat list of all devices currently attached to the machine.
    // This array will be used to reassign devices to each user.
    // 
    // Using a NULL username and omitting the DIEDBSFL_THISUSER flag enumerates
    // all devices.
    ZeroMemory( prgAllDev, sizeof(prgAllDev) );
    m_pDI->EnumDevicesBySemantics( NULL, &m_diaf, BuildFlatListCB, 
                                   prgAllDev, DIEDBSFL_ATTACHEDONLY); 
   

Once the list is built, the code rolls through all devices in the array to retrieve the current user name (through DIPROP_USERNAME). If the device has a username, the method calls CInputDeviceManager::AddDeviceForPlayer method to give that device to the appropriate user.

    DIPROPSTRING dips;
    dips.diph.dwSize       = sizeof(DIPROPSTRING); 
    dips.diph.dwHeaderSize = sizeof(DIPROPHEADER); 
    dips.diph.dwObj        = 0; // device property 
    dips.diph.dwHow        = DIPH_DEVICE; 

    // Now we've got an array with every device attached to the system. 
    // Loop through them all and assign them to a player in the temp array.
    dwDev = 0;
    while( prgAllDev[dwDev] )
    {
        prgAllDev[dwDev]->GetProperty( DIPROP_USERNAME,
                                       &dips.diph );
        
        // Convert the string from Unicode to ANSI.
        WideCharToMultiByte( CP_ACP, 0, dips.wsz, -1,
                             szNameDev, MAX_PATH,NULL, NULL);

        // Determine who this device is now assigned to (as a DWORD value)
        // If the device is unassigned (i.e. no username), we skip it.
        if( strlen(szNameDev) )
        {
            DWORD dwAssignedTo;
            dwAssignedTo = (DWORD) ( (byte)szNameDev[strlen(szNameDev)-1] - 
((byte)'1') );
            
            DIDEVICEINSTANCE didi;
            ZeroMemory( &didi, sizeof(didi) );
            didi.dwSize = sizeof(didi);
            prgAllDev[dwDev]->GetDeviceInfo( &didi );

            // Get the device ready to go again for the user.
            prgAllDev[dwDev]->BuildActionMap( &m_diaf, szNameDev, DIDBAM_DEFAULT );
            prgAllDev[dwDev]->SetActionMap( &m_diaf, szNameDev, DIDSAM_DEFAULT );
            prgAllDev[dwDev]->SetCooperativeLevel( m_hWnd, 
DISCL_EXCLUSIVE|DISCL_FOREGROUND );
            
            // Now add it for the player.
            AddDeviceForPlayer( dwAssignedTo, &didi, prgAllDev[dwDev] );
        }

        dwDev++;
    }

Of course, the remaining devices have no owner, and are returned to the device pool by releasing their interface pointers.

    // All devices have been assigned a to a user in the new array. 
    // Clean up the local flat array
    dwDev = 0;
    while( prgAllDev[dwDev] )
    {
        prgAllDev[dwDev]->Release();
        prgAllDev[dwDev] = NULL;
        dwDev++;
    }
}

For completeness, the BuildFlatListCB callback function for the enumeration invoked by ReassignDevices is shown here. This method merely adds devices to a flat array that represents all the devices present on the machine.

BOOL CALLBACK CInputDeviceManager::BuildFlatListCB( LPCDIDEVICEINSTANCE  pdidi,
                                                    LPDIRECTINPUTDEVICE8 
pdidDevice, 
                                                    DWORD  dwFlags,
                                                    DWORD  dwRemainingDevices,
                                                    LPVOID pContext )
{
    static DWORD dwIndex = 0;
    LPDIRECTINPUTDEVICE8* pdidDevArray;
    pdidDevArray = (LPDIRECTINPUTDEVICE8*)pContext;

    pdidDevArray[dwIndex++] = pdidDevice;
    pdidDevice->AddRef(); // Must AddRef any interfaces we keep.

    if(!dwRemainingDevices)
    {
        dwIndex = 0; 
    }

    return DIENUM_CONTINUE;
}

Cleanup: The Cleanup() and CleanupDevices() Methods

Cleanup of the interfaces used by the CInputDeviceManager class takes place in two phases. Both phases are invoked when the client calls the CInputDeviceManager::Cleanup method.

The CleanupDevices method performs the first phase of cleanup is the process of releasing and invalidating all of the DirectInput devices in use by the class. Device cleanup takes place separately from destroying DirectInput because there are occasions where it is desirable to rebuild the device lists without necessarily reinitializing all of DirectInput. Device reassignment is the perfect case, and the code for CInputDeviceManager::ReassignDevices calls CleanupDevices for this purpose, without calling the Cleanup method.

The CleanupDevices method unacquires and releases the DirectInput device objects used by the class. Device ownership information persists outside the lifetime of the interface, so in the event that device reassignment is taking place, the device's last owner is kept in memory. Because this is a special-case, the default behavior for the method invalidates the current owner by calling the IDirectInputDevice8::SetActionMap method with the DIDSAM_NOUSER flag.

VOID CInputDeviceManager::CleanupDevices(DWORD dwCleanFlags)
{
    DWORD dwD = 0;

    for( DWORD dwP = 0; dwP < m_dwNumUsers; dwP++ )
    {
        while( m_ppdidDevices[dwP][dwD] )
        {     
            m_ppdidDevices[dwP][dwD]->Unacquire();
            if(APP_CLEANUP_DEFAULT == dwCleanFlags)
            {
                m_ppdidDevices[dwP][dwD]->SetActionMap( &m_diaf, NULL, 
DIDSAM_NOUSER );
            }
            m_ppdidDevices[dwP][dwD]->Release();
            m_ppdidDevices[dwP][dwD] = NULL;
            dwD++;
        }

        dwD = 0;
    }

The final cleanup stage takes place in the Cleanup method after CleanupDevices returns. As with any interface cleanup code, this method releases the remaining DirectInput object used by the class.

VOID CInputDeviceManager::Cleanup()
{
    CleanupDevices( APP_CLEANUP_DEFAULT );
    
    if( m_pDI )
    {
        m_pDI->Release();
    }

    m_pDI = NULL;
}

Conclusion

The DirectInput Mapper is a very useful technology for most input development needs. The basic features it provides for abstracting devices, and actions assigned to devices, simplify the lives of input developers and users alike. For a first-effort technology, it's ambitious to tackle the problems related to enabling multiple users to manipulate many devices (and configurations on those devices) transparently to the application.

The solutions employed in DirectX 8.0 provide support for lightweight device ownership, making it possible for dynamic device ownership and configuration changes to approach the simplicity of consoles today. However, the ever-shifting nature of the PC and the input devices that can be attached to it present a special challenge for both the Mapper and its client applications. The techniques shown in the MultiMapper code represent one reasonable way to implement a dynamic device ownership model, but no single approach could possibly meet the needs of every type of game. Hopefully, the basics shown in this paper will provide you with ideas about how to use the Mapper to address the challenges specific to your application and users in a simple, reliable, and easy-to-code way.

Page view tracker