sunlight

DirectInput

Introduction

In this part of the tutorial, we will cover:

  • DirectInput 8
  • Action maps

Previous View Code Download Code Next

Action Maps

DirectInput 7 and below required the developer to work in a time-honoured way to access devices: each device had to be separately handled; joysticks, mice and keyboards had to be handled differently; new devices could not be anticipated; each application had a different user interface for configuration, and so on and so forth.

DirectX 8 maintains compatibility with DirectInput 7, but introduces the concept of action maps to solve all these problems and more. Although it requires more code for simple cases, it makes it so much easier to support all these different devices that we can't really ignore it.

The action map consists of a few definitions and structures. The first is a unique identifier for our application. You can use the program GUIDGEN for this, or you can use the one I have provided.

// {BD0E8406-FB56-4322-8BC6-BD6E481F5564}
GUID g_guidApp = 
{ 0xbd0e8406, 0xfb56, 0x4322, { 0x8b, 0xc6, 0xbd, 0x6e, 0x48, 0x1f, 0x55, 0x64 } };

Next, we need to define a genre for our application. This is to let DirectInput know what kind of controls we're going to need. We're going for the 'Arcade - Side to Side' genre.

DWORD   g_dwGenre = DIVIRTUAL_ARCADE_SIDE2SIDE;

We need a name for our action map: we're going to call it 'Sample'.

LPCTSTR g_tszActionMapName = _T("Sample");

We need a number for each action we are going to perform, which we will get by using an enum:

enum Actions 
{
    ACTIONS_LEFTRIGHT,
    ACTIONS_LEFT,
    ACTIONS_RIGHT,
    ACTIONS_QUIT,
    ACTIONS_NONE
};

We need a few global variables to store the state of the input:

bool    g_bQuit;
bool    g_bLeft, g_bRight;

Finally, we need an array of actions, which defines the mapping from the genre-defined controls to our actions:

DIACTION g_rgActions[] =
{
    // Genre-defined virtual axes
    { ACTIONS_LEFTRIGHT,    DIAXIS_ARCADES_LATERAL,         0, "Left/Right" },

    // Actions mapped to keys as well as to virtual controls
    { ACTIONS_LEFT,         DIKEYBOARD_LEFT,                0, "Left" },
    { ACTIONS_RIGHT,        DIKEYBOARD_RIGHT,               0, "Right" },

    // Actions mapped to keys
    { ACTIONS_QUIT,         DIKEYBOARD_ESCAPE,              0, "Quit" },
};

DWORD g_nActions = sizeof(g_rgActions) / sizeof(DIACTION);

Unfortunately, Unicode support is not complete in DirectX, yet (action names must still be ANSI). Maybe next time...

Initialising DirectInput

Since there will be a fair bit of DirectInput code, we will put the generic code in a separate file.

Add DINPUT.H to your header file includes:

#include <dinput.h>

and add DINPUT8.LIB to your project.

We will need a new function to initialise DirectInput. This function will first need to call DirectInput8Create to create an IDirectInput8 object.
// Initialise DirectInput
BOOL InitDirectInput()
{
    // Create an IDirectInput8 object that we will use to create devices.
    DirectInput8Create(GetModuleHandle(NULL), DIRECTINPUT_VERSION, IID_IDirectInput8,
        (void **)&g_pDI, NULL);

We now have an object that we can use to access the various input devices in the system. We will now look for devices to use; we call this enumeration.

    ZeroMemory(&g_diaf, sizeof(DIACTIONFORMAT));
    g_diaf.dwSize = sizeof(DIACTIONFORMAT);
    g_diaf.dwActionSize = sizeof(DIACTION);
    g_diaf.dwNumActions = g_nActions;
    g_diaf.dwDataSize = g_nActions * sizeof(DWORD);
    g_diaf.rgoAction = g_rgActions;
    g_diaf.guidActionMap = g_guidApp;
    g_diaf.dwGenre = g_dwGenre;
    g_diaf.dwBufferSize = 16;
    g_diaf.lAxisMin = -100;
    g_diaf.lAxisMax = 100;
    _tcscpy(g_diaf.tszActionMap, g_tszActionMapName);
    g_pDI->EnumDevicesBySemantics(NULL, &g_diaf, DIEnumDevicesBySemanticsCallback,
        NULL, DIEDBSFL_ATTACHEDONLY);

We need to provide a callback function that will check to see if the devices meet our specifications; if they do, we will assign the action map.

// Callback function to map appropriate devices
BOOL CALLBACK DIEnumDevicesBySemanticsCallback(LPCDIDEVICEINSTANCE lpddi,  
    IDirectInputDevice8 *lpdid, DWORD dwFlags, DWORD dwRemaining, LPVOID pvRef)
{
    HRESULT     h;

    // Devices of type DI8DEVTYPE_DEVICECTRL are specialized devices not generally
    // considered appropriate to control game actions. We just ignore these.
    if (GET_DIDEVICE_TYPE(lpddi->dwDevType) == DI8DEVTYPE_DEVICECTRL)
        return DIENUM_CONTINUE;

    // Assign exclusive control of this device to us.
    h = lpdid->SetCooperativeLevel(hWndMain, DISCL_EXCLUSIVE | DISCL_FOREGROUND);
    if (FAILED(h))
        return DIENUM_CONTINUE;

    // Build the action map for the device. This will map each action to
    //  the most appropriate function on the device.
    h = lpdid->BuildActionMap(&g_diaf, NULL, DIDBAM_DEFAULT);
    if (FAILED(h))
        return DIENUM_CONTINUE;

    for (DWORD i = 0; i < g_diaf.dwNumActions; i++)
    {
        if (g_diaf.rgoAction[i].dwHow != DIAH_UNMAPPED)
            break;
    }
    if (i < g_diaf.dwNumActions)
    {
        // If any controls were mapped, we will be using this device,
        //  so set the action map on this device.
        h = lpdid->SetActionMap(&g_diaf, NULL, DIDSAM_DEFAULT);
        if (FAILED(h))
            return DIENUM_CONTINUE;

        if (g_pDeviceArray == NULL)
            g_pDeviceArray = new LPDIRECTINPUTDEVICE8[dwRemaining + 1];

        g_pDeviceArray[g_nDevices] = lpdid;
        g_nDevices++;

        // Add a reference to this device, since DirectInput will 
        //  release the device when we return.
        lpdid->AddRef();
    }
    return DIENUM_CONTINUE;
}

We're done. We need a cleanup function:

// Shut down DirectInput
void ExitDirectInput()
{
    for (int iDevice = 0; iDevice < g_nDevices; iDevice++)
        g_pDeviceArray[iDevice]->Release();

    delete [] g_pDeviceArray;
    g_pDeviceArray = NULL;
    g_nDevices = 0;

    if (g_pDI != NULL)
    {
        g_pDI->Release();
        g_pDI = NULL;
    }
}

We also need functions to acquire and unacquire the devices (obtain control and release control of the devices):

// Acquire (obtain control of) the devices
void AcquireDevices()
{
    for (int iDevice = 0; iDevice < g_nDevices; iDevice++)
        g_pDeviceArray[iDevice]->Acquire();
}

// Unacquire (release control of) the devices
void UnacquireDevices()
{
    if (g_pDeviceArray == NULL)
        return;

    for (int iDevice = 0; iDevice < g_nDevices; iDevice++)
        g_pDeviceArray[iDevice]->Unacquire();
}

Now we're ready to handle the input from these devices.

Handling Input

We need a function that will be called periodically to check for input from devices. We need to check each device in turn:

#define INPUT_DATA_LIMIT    20

// Check each device for actions
void CheckInput()
{
    DIDEVICEOBJECTDATA  pdidod[INPUT_DATA_LIMIT];
    DWORD               dwObjCount;

    if (g_pDeviceArray == NULL)
        return;

    for (int iDevice = 0; iDevice < g_nDevices; iDevice++)
    {
        // Poll the device for data.
        g_pDeviceArray[iDevice]->Poll();

We will now ask the device if there is any data available. If so, we will call another function to handle that action:

   
        // Retrieve the data.
        dwObjCount = INPUT_DATA_LIMIT;
        g_pDeviceArray[iDevice]->GetDeviceData(sizeof(DIDEVICEOBJECTDATA),
                                               pdidod,
                                               &dwObjCount, 0);
        for (DWORD i = 0; i < dwObjCount; i++)
            // Handle the actions regardless of what device returned them.
            HandleAction(pdidod[i].uAppData, pdidod[i].dwData);
    }
}

This function (HandleAction) is application-specific, so we will return to our main file. This function will just set the variables g_bLeft, g_bRight and g_bQuit depending on what action just occurred:

#define AXIS_THRESHOLD  20

void HandleAction(UINT nAction, DWORD dwData)
{
    int nAxisPos = (int)dwData;

    switch (nAction)
    {
    case ACTIONS_LEFTRIGHT:
        if (nAxisPos < -AXIS_THRESHOLD)
        {
            g_bLeft = true;
            g_bRight = false;
        }
        else if (nAxisPos > AXIS_THRESHOLD)
        {
            g_bRight = true;
            g_bLeft = false;
        }
        else
        {
            g_bLeft = g_bRight = false;
        }
        break;
    
    case ACTIONS_LEFT:
        g_bLeft = (dwData != 0);
        break;

    case ACTIONS_RIGHT:
        g_bRight = (dwData != 0);
        break;

    case ACTIONS_QUIT:
        g_bQuit = true;
        break;
    
    default:
        break;
    }
}

Now we're ready to move the sprites around with our devices. 

Moving Sprites

We must check the input state during our OnIdle function:

    CheckInput();

We can also quit here if appropriate:

    if (g_bQuit)
    {
        PostMessage(hWndMain, WM_CLOSE, 0, 0);
        return;
    }

We will now simply use the actions to drive one of our sprites around:

    // Move a sprite around with the input.
    static int xSprite;

    if (g_bLeft)
        xSprite--;
    if (g_bRight)
        xSprite++;
    r.left = 64;
    r.right = 128;
    lpBackBuffer->BltFast(288 + xSprite, 150, lpSprites, &r, 
        DDBLTFAST_SRCCOLORKEY | DDBLTFAST_WAIT); 

That was easy, wasn't it? Plug in a joystick or gamepad, and it'll use it transparently. 

Note that the mouse is not used. The keyboard is also not automatically mapped. These two devices are never automatically mapped by the DirectInput mapper, and if we want movement from them, we must enter the default mappings ourself.

In the next section, we'll add some music and sound effects to our program.

The old DirectX 7 DirectInput tutorial is also available.

 

Copyright © David McCabe, 1998 - 2001. All rights reserved.

You will need to download and install the m-math control to display any equations on this Web site. Without this control, you will not see most of the equations. Please do not e-mail me asking why the equations do not display!