Main Site Links Resources Tutorials
News VB Gaming Code Downloads DirectX 7
Contact Webmaster VB Programming Product Reviews DirectX 8
  General Multimedia Articles DirectX 9
      Miscellaneous

DirectXInput: Action Mapping
Author: Jack Hoxley
Written: 20th June 2002
Contact: [EMail]
Download: IN3_ActMap.Zip (15kb)


Contents of this lesson
1. Introduction
2. About Genre's
3. Initialization and Configuration
4. Interpreting and understanding 
5. Conclusion


1. Introduction

Welcome to the third tutorial in the DirectXInput series. DirectInput is one of the simplest parts of the DirectX API - most people will be content with using just the keyboard mouse and joystick to control their applications. Once these three have been learnt there's not much else to do!

However, there is one last thing - Action Mapping, a new addition in DirectX8, and probably about as advanced and interesting as input-programming gets. I'm surprised its taken this long to work its way into the DirectX API really; DirectInput has always been about abstracting the input interfaces - you access the mouse and DI will sort out if its a 1-button/10 button, 2 axis/3 axis mouse and give you the data you want. Action Mapping goes one step beyond this - you tell it what data you want and it'll do the rest - whatever device it comes from.

For example, in the sample that this tutorial covers (a simple racing game) the input can come from the keyboard, mouse or joystick - and your application doesn't need to differentiate between them. When using action mapping an 'Accelerate' or 'Turn Left' message is just that, an 'Accelerate' message from the joystick is not different from an 'Accelerate' message from the keyboard.


2. About Genre's

Action Mapping is built upon the principle of Genre's - any game that you write should be able to pick its input to match with that typical of one of the genre's. This doesn't mean that if you're writing a racing game you HAVE to use the racing-game genre's - if the flight-sim genre matches better you can use that. Also, if a particular genre doesn't have all the controls that you need you can always add additional control handles (as we'll see later).

The following tree shows all the genre's defined in DirectInput8:
• Action Genres
     • Hand-to-Hand ( _FIGHTINGH_ , DIVIRTUAL_FIGHTING_HAND2HAND)
     • Shooting ( _FPS_ , DIVIRTUAL_FIGHTING_FPS)
     • Third Person Action ( _TPS_ , DIVIRTUAL_FIGHTING_THIRDPERSON)
• Arcade Genres
     • Platform ( _ARCADEP_ , DIVIRTUAL_ARCADE_PLATFORM)
     • Side-to-Side ( _ARCADES_ , DIVIRTUAL_ARCADE_SIDE2SIDE)
• CAD Genres
     • 2D Object ( _2DCONTROL_ , DIVIRTUAL_CAD_2DCONTROL)
     • 3D Model ( _CADM_ , DIVIRTUAL_CAD_MODEL)
     • 3D Navigation ( _CADF_ , DIVIRTUAL_CAD_FLYBY)
     • 3D Object ( _3DCONTROL_ , DIVIRTUAL_CAD_3DCONTROL)
• Control Genres
     • Browser ( _BROWSER_ , DIVIRTUAL_BROWSER_CONTROL)
     • Remote Control ( _REMOTE_ , DIVIRTUAL_REMOTE_CONTROL)
• Driving Genres
     • Combat Racing ( _DRIVINGC_ , DIVIRTUAL_DRIVING_COMBAT)
     • Mechanical Fighting ( _MECHA_ , DIVIRTUAL_DRIVING_MECHA)
     • Racing ( _DRIVINGR_ , DIVIRTUAL_DRIVING_RACE)
     • Tank ( _DRIVINGT_ , DIVIRTUAL_DRIVING_TANK)
• Flight Genres
     • Air Combat ( _FLYINGM_ , DIVIRTUAL_FLYING_MILITARY)
     • Civilian Flight ( _FLYINGC_ , DIVIRTUAL_FLYING_CIVILIAN)
     • Helicopter Flight ( _FLYINGH_ , DIVIRTUAL_FLYING_HELICOPTER)
     • Space Combat ( _SPACESIM_ , DIVIRTUAL_SPACESIM)
• Sports Genres
     • Baseball Batting ( _BASEBALLB_ , DIVIRTUAL_SPORTS_BASEBALL_BAT)
     • Baseball Fielding ( _BASEBALLF_ , DIVIRTUAL_SPORTS_BASEBALL_FIELD)
     • Baseball Pitching ( _BASEBALLP_ , DIVIRTUAL_SPORTS_BASEBALL_PITCH)
     • Basketball Defense ( _BBALLD_ , DIVIRTUAL_SPORTS_BASKETBALL_DEFENSE)
     • Basketball Offense ( _BBALLO_ , DIVIRTUAL_SPORTS_BASKETBALL_OFFENSE)
     • Fishing ( _FISHING_ , DIVIRTUAL_SPORTS_FISHING)
     • Football Defense ( _FOOTBALLD_ , DIVIRTUAL_SPORTS_FOOTBALL_DEFENSE)
     • Football Offense ( _FOOTBALLO_ , DIVIRTUAL_SPORTS_FOOTBALL_OFFENSE)
     • Football Play ( _FOOTBALLP_ , DIVIRTUAL_SPORTS_FOOTBALL_FIELD)
     • Football Quarterback ( _FOOTBALLQ_ , DIVIRTUAL_SPORTS_FOOTBALL_QBCK)
     • Golf ( _GOLF_ , DIVIRTUAL_SPORTS_GOLF)
     • Hockey Defense ( _HOCKEYD_ , DIVIRTUAL_SPORTS_HOCKEY_DEFENSE)
     • Hockey Goalie ( _HOCKEYG_ , DIVIRTUAL_SPORTS_HOCKEY_GOALIE)
     • Hockey Offense ( _HOCKEYO_ , DIVIRTUAL_SPORTS_HOCKEY_OFFENSE)
     • Hunting ( _HUNTING_ , DIVIRTUAL_SPORTS_HUNTING)
     • Mountain Biking ( _BIKINGM_ , DIVIRTUAL_SPORTS_BIKING_MOUNTAIN)
     • Racquet ( _RACQUET_ , DIVIRTUAL_SPORTS_RACQUET)
     • Skiing ( _SKIING_ , DIVIRTUAL_SPORTS_SKIING)
     • Soccer Defense ( _SOCCERD_ , DIVIRTUAL_SPORTS_SOCCER_DEFENSE)
     • Soccer Offense ( _SOCCERO_ , DIVIRTUAL_SPORTS_SOCCER_OFFENSE)
• Strategy Genres
     • Role Playing ( _STRATEGYR_ , DIVIRTUAL_STRATEGY_ROLEPLAYING)
     • Turn Based ( _STRATEGYT_ , DIVIRTUAL_STRATEGY_TURN)

A fairly long list! The two values in the parenthesis: first one represents a string you can use to search the SDK help files and/or VB object browser for a complete list of the controls for that genre, second one is the genre's 'name' - the use for which you'll see in a little while.


3. Initialization and Configuration

90% of the work to get action mapping is done when the application starts or when the end-user decides to alter their control settings.

The first step is to construct a default set of controls for the application - you assign one of the controls from a genre listed above to a particular internal constant and give it a name. Secondly we query the system about the proposed action map, it will then return a list of devices attached to the system that are compatible/necessary for the given action map. We then create an instance of each device that the system "recommended", configuring them as we go.

Before we actually execute any code we need  a list of constants - some are just for convenience, others are requirements. It is necessary to have a list of internal constants representing each control - later on we'll send these values to the action map, and DirectInput will use these as the basis for returning information back to us.

'//PROGRAM CONSTANTS AND VARIABLES
'controls whether the app. is running or not

Private bRunning As Boolean
'we need a GUID to make sure DI8 responds properly (Use DX.CreateNewGUID() to make your own)
Private Const AppGUID As String = "{506BD635-CEAF-494D-B3D2-4F0E1F1469FC}"
'in a game this would probably be the players name / handle
Private Const AppUserName As String = "DirectX4VB.Com Sample User"
'there are a list of these in the SDK and Object Browser (F2) - also, see list in tutorial.
Private Const AppGenre As Long = DIVIRTUAL_DRIVING_RACE

'//DEFINE COMMAND CONSTANTS
'these are the values that we'll receive back from DI8

Const CAR_ACCELERATE As Long = 1
Const CAR_BRAKE As Long = 2
Const CAR_STEER As Long = 3
Const CAR_NITRO As Long = 4
Const CAR_STEER_LEFT As Long = 5
Const CAR_STEER_RIGHT As Long = 6
Const CAR_ACCEL_OR_BRAKE As Long = 7
'these are general commands
Const DISPLAY_OPTIONS As Long = 8
Const EXIT_PROGRAM As Long = 9

Above is all fairly straightforward, pay particular attention to AppGUID and AppUserName - these should be changed if you use Action Mapping in your own programs. AppGUID is a unique value generated by DX.CreateNewGUID( ) - you should create a new one for each application you make. The reason for this being that DirectInput is clever and saves action maps to the hard drive for future sessions, and it uses this GUID (and the user name) to differentiate between multiple maps. If, for example, you used the same GUID for all your games and one person installed more than one game on their system you'd start to get conflicts (one game would overwrite another's settings).

Now we move onto the actual inialisation routine, aptly named InitDirectInput( ). Because this function can be called multiple times (it's called each time the user changes the control settings) we need to start off by erasing any history of existing action maps and/or devices:

'//Clear Any Existing data / format memory for new data
lActionCount = 0
ReDim DIActionFmt.ActionArray(0) As DIACTION

If lDeviceCount > 0 Then
   
For I = 0 To lDeviceCount
       
If Not DevList(I) Is Nothing Then
           
'we have a device allocated here. kill it!!
            DevList(I).Unacquire
'shut it down
           
Set DevList(I) = Nothing 'delete it
       
End If
    Next
I
End If

Next we're going to setup the default action map. The actions you specify now will be the only ones visible to the end-user - if you didn't set a default for DIAXIS_DRIVINGR_STEER then it wouldn't automatically appear, and the user would not be allowed to use axis-steering.

'//Add all of the actions to the list
'there aren't any set patterns for the _DRIVINGR_ type constants
'but DIKEYBOARD_ and DIMOUSE_ DIJOFS_ constants can only be mapped
'to their respective device (when the user looks at the settings window)

AddAction CAR_STEER, DIAXIS_DRIVINGR_STEER, 0, "Steer Vehicle"
AddAction CAR_ACCEL_OR_BRAKE, DIAXIS_DRIVINGR_ACCELERATE, 0, "Accelerate"
AddAction CAR_ACCEL_OR_BRAKE, DIAXIS_DRIVINGR_BRAKE, 0, "Brake"
AddAction CAR_ACCELERATE, DIBUTTON_DRIVINGR_ACCELERATE_LINK, 0, "Accelerate"
AddAction CAR_BRAKE, DIBUTTON_DRIVINGR_BRAKE, 0, "Brake"
AddAction CAR_NITRO, DIBUTTON_DRIVINGR_BOOST, 0, "Turbo Speed"
AddAction DISPLAY_OPTIONS, DIKEYBOARD_D, 0, "Display Options Window"
AddAction EXIT_PROGRAM, DIKEYBOARD_ESCAPE, 0, "Exit Sample"
AddAction CAR_STEER_LEFT, DIKEYBOARD_LEFT, 0, "Steer Left"
AddAction CAR_STEER_RIGHT, DIKEYBOARD_RIGHT, 0, "Steer Right"


'//Finally, Configure the final map
DIActionFmt.guidActionMap = AppGUID
DIActionFmt.lGenre = AppGenre
DIActionFmt.lBufferSize = 10
'10 input events can be stored
DIActionFmt.lAxisMax = 100
DIActionFmt.lAxisMin = -100
DIActionFmt.ActionMapName = "DirectX4VB.Com Sample Action Mapper"
DIActionFmt.lActionCount = lActionCount




'//A custom procedure that helps with creating the default action set.
Private Sub AddAction(UserActionName As Long, _
                                 ActualActionName
As Long, _
                                 flags
As Long, _
                                 FriendlyName
As String)

'resize the array appropriately
ReDim Preserve DIActionFmt.ActionArray(lActionCount) As DIACTION

'fill in the new entry
With DIActionFmt.ActionArray(lActionCount)
    .lAppData = UserActionName
    .lSemantic = ActualActionName
    .lFlags = flags
    .ActionName = FriendlyName
End With

'increment the counter
lActionCount = lActionCount + 1

End Sub

once we've configured our default control setup we can show it to the user and allow them to further customize it to their style. The sample does this now, but a real-world application would probably want to hide this next part in an options screen.

'This next part displays the DirectInput dialog allowing
'the user to configure the controls...

Dim Params As DICONFIGUREDEVICESPARAMS
ReDim Params.ActionFormats(0)
ReDim Params.UserNames(0)

Params.ActionFormats(0) = DIActionFmt
Params.FormatCount = 1
Params.UserCount = 1
Params.UserNames(0) = AppUserName

DI.ConfigureDevices 0, Params, DICD_EDIT

'we call this so that our internal data reflects any changes
'made by the end user while the dialog box was displayed.

DIActionFmt = Params.ActionFormats(0)

An important note here: ConfigureDevices( ) actually displays a default DirectInput dialog box to the end user. Whilst it is a very nice, functional, window I highly doubt it will fit into the majority of real-world game environments. The following image is the window:

A very tasteful black-and-green :)

The three tabs along the top of the screen represent the (valid) devices currently attached to the system - in my case a cheap gamepad, a keyboard and a mouse. If multiple players are active/playing then they will appear in the "Player" drop-down list box.

Once the end user has configured the devices, we can move on to creating the devices. By using the GetDevicesBySemantics( ) function we can retrieve the list of devices that match the current genre and/or are going to be used by the system. With this list we need to go through and create each device and store a pointer to it.

Set DIEnum = DI.GetDevicesBySemantics(AppUserName, _
                                                            DIActionFmt, _
                                                            DIEDBSFL_ATTACHEDONLY)

For I = 1 To DIEnum.GetCount
    'retrieve the device
   
Set DevInst = Nothing 'clear any existing
   
Set DevInst = DIEnum.GetItem(I)

    'resize the arrays
    lDeviceCount = lDeviceCount + 1
    ReDim Preserve DevList(lDeviceCount) As DirectInputDevice8
    ReDim Preserve DevType(lDeviceCount) As Long

    'create the device
    Set DevList(lDeviceCount) = DI.CreateDevice(DevInst.GetGuidInstance)
    DevType(lDeviceCount) = DevInst.GetDevType
    Debug.Print "DEVICE #" & I & " created: " & DevInst.GetInstanceName

    DevList(lDeviceCount).BuildActionMap DIActionFmt, _
                                    AppUserName, DIDBAM_DEFAULT
    DevList(lDeviceCount).SetActionMap DIActionFmt, _
                                    AppUserName, DIDSAM_DEFAULT
    DevList(lDeviceCount).SetCooperativeLevel frmMain.hWnd, _
                                    DISCL_EXCLUSIVE Or DISCL_FOREGROUND

Next I

that's initialization completed now. Assuming the above code executes successfully you are ready to properly make use of action mapping. The last thing I want to cover in this section is termination. It's always a good idea to properly terminate any DirectX interfaces that you use - particularly so with DirectInput. The following code is executed just before the application closes:

'//CLEAN UP AFTER WE'RE FINISHED
Debug.Print "Terminating DirectInput Sample..."
If lDeviceCount > 0 Then
   
For I = 0 To lDeviceCount
        If Not DevList(I) Is Nothing Then
            'we have a device allocated here. kill it!!
            DevList(I).Unacquire
'shut it down
           
Set DevList(I) = Nothing 'delete it
       
End If
    Next
I
End If

 


4. Interpreting and understanding 

Now that DirectInput is configured and ready to send us input data we need to be ready to receive and process it. When using action mapping we have to use a method similar to polling - whilst for keyboard/button input it works just the same as event-based (which is best) it is necessary for axis-based devices to be polled. For a further discussion of event-based vs polling see the first lesson - keyboard access.

Exactly what you do once you've received input from attached devices is entirely dependent on you. This sample code does no more than output a list to the screen of the input received, a real-world application would then want to apply these controls to the current character (for example).

As mentioned, we're going to use a polling method - so the basic code structure looks like this:

bRunning = InitDirectInput()

'//EXECUTE THE MAIN LOOP
'this loop would be the same loop as used in almost all games

Do While bRunning
    bRunning = UpdateUserInput()
    DoEvents
Loop

This loop would, in a real-world application, have a rendering function, AI, physics, logic etc... system attached. You've already seen the InitDirectInput( ) function (section #3 above). I'm now going to be focusing on the UpdateUserInput( ) function.

The basic idea behind the UpdateUserInput( ) process is to:
1. Loop through all created devices
2. receive any new input from the device
3. Process this input.

the first part, and the general function outline is as simple as this:

Private Function UpdateUserInput() As Boolean
On Error GoTo BailOut:

    '//INTERNAL VARIABLES
   
Dim DevData(20) As DIDEVICEOBJECTDATA
   
Dim I As Long, J As Long
   
Dim nData As Long

    'a scalar value between 0.0 and 1.0 (0.0=no saturation)
   
Const JOYSTICK_SATURATION As Single = 0.4

    '//SCAN DEVICES FOR INPUT
   
For I = 1 To lDeviceCount
       '//ADDITIONAL CODE FITS IN HERE.
   
Next I

    '//This next line is not important for DI8, but just for the samples user-interface    
    txtOutput.SelStart = Len(txtOutput.Text)


    UpdateUserInput =
True
   
Exit Function
BailOut:
   
Debug.Print "Unable to update user input.", Err.Number, Err.Description
    UpdateUserInput =
False
End Function

the DevData( ) array is very important, this is refreshed for each device and gives us a list of up to 20 events that have occurred since we last checked the device (last frame). JOYSTICK_SATURATION is important when we deal with the joystick a bit later on.

Now that we have the main loop setup we can treat each input generically - such as the following code to collect input from the devices:

'aquire and poll the devices as necessary
On Error Resume Next

If DevList(I) Is Nothing Then Debug.Print "Device #" & I & " does not exist"

DevList(I).Acquire
If Err.Number Then GoTo SkipThisDev:

DevList(I).Poll
If Err.Number Then GoTo SkipThisDev:

On Error GoTo BailOut:

'extract the data
nData = DevList(I).GetDeviceData(DevData, DIGDD_DEFAULT)

The first three parts are very important - firstly we should check to make sure the pointer is still valid - it is possible that we could loose a device (another application can steal it for example), in which case the DevList(I) will be a null pointer. Next we must attempt to aquire the device - should it have been somehow unaquired (similar to being lost, but not as bad). Next we have to poll the device - basically tell it to collect the current state of the hardware (it'll go and check the axis positions on the joypad for example). Lastly we use GetDeviceData( ) to get DirectInput to return us a formatted list of events.

We can then loop through this formatted input (it'll be formatted based on our action map) as shown in the following piece of code:

For J = 0 To nData - 1
   
With DevData(J)
    'it isn't too easy to see with this sample, but there will
    'be two messages generated - key down and key up - for the keyboard
    'the latter is signified by lData=0; which can easily get lost in
    'some logic systems.

       
Select Case .lUserData
           
Case CAR_ACCELERATE
            Case CAR_BRAKE
            Case CAR_ACCEL_OR_BRAKE
            Case CAR_STEER
            Case CAR_STEER_LEFT
            Case CAR_STEER_RIGHT
            Case CAR_NITRO
            Case DISPLAY_OPTIONS
            Case EXIT_PROGRAM
        End Select
    End With
Next
J

With this piece of code we will cycle through each event from the device (there will usually only be one or two). We then split apart incoming message using a Select Case tree. Remember the constants we defined at the beginning of this tutorial? well they appear back here again - the wonders of action mapping has meant that regardless of the raw control data all we get back is a generic, custom value.

What you actually do within each branch of the logic tree is completely up to you. However, I shall demonstrate two useful pieces of code for processing input.

'//PROCESSING AXIS-BASED INPUT.
If .lData > DIActionFmt.lAxisMax * JOYSTICK_SATURATION Then
    txtOutput.Text = txtOutput.Text & "'CAR_BRAKE' MESSAGE RECIEVED: BRAKE!" & vbCrLf
ElseIf .lData < DIActionFmt.lAxisMin * JOYSTICK_SATURATION Then
    txtOutput.Text = txtOutput.Text & "'CAR_BRAKE' MESSAGE RECIEVED: ACCELERATE!" & vbCrLf
End If

'//PROCESSING BUTTON-BASED INPUT:
If .lData = 128 Then 
    txtOutput.Text = txtOutput.Text & "{KEY_DOWN} RECEIVED 'CAR_BRAKE' MESSAGE!!" & vbCrLf
End If
If .lData = 0 Then 
    txtOutput.Text = txtOutput.Text & "{KEY_UP} RECEIVED 'CAR_BRAKE' MESSAGE!!" & vbCrLf
End If

You can see a more complete set of examples if you download the source code for this tutorial. But in general they all follow this pattern.

for axis-based input the .lData value will contain the current value on the device's axis scaled according to the values in .lAxisMax and .lAxisMin. Take a joystick for example, each axis goes from -ve to +ve (left->right or top->bottom). Therefore if .lData is a negative number it is somewhere towards the left or top (depending on which axis the user selected, either way does not matter to us). If .lData is a positive number then it lies somewhere towards the right or bottom. Fairly simple really. I've made it more complicated based on some testing and general knowledge that I have of joysticks... even if you push them all the way to the left you will rarely get the maximum -ve number, and if you don't touch the joystick at all then it rarely stays at 0. This may be different with digital controllers and/or more elegant designs (my joystick is laughable at best!). What I did to combat this was to implement a saturation value: in order for the new data to be considered a significant change it MUST be greater than a certain value. .lAxisMax and .lAxisMin determine the max/min possible values, and JOYSTICK_SATURATION is a scalar multiplier.

As seen above, I set JOYSTICK_SATURATION to be 0.4; given that the range defined earlier is [-100,+100] this logic implies that the new value must be less than -40 or greater than +40 to be considered a change. You could allow your users to specify how sensitive they want their controls using this method.


5. Conclusion

That is all there is to a basic DirectInput8-Action Mapping sample. Obviously, you can make it more complicated if necessary - but to get a basic generic input based system working this is all you need. One useful thing to note is that there are an additional set of control constants you can use for DirectPlay Voice Communications and also a set of generic non-controller specific constants (search for _ANY_ ).

The source code for this tutorial can be download here, or from the top of the page.

DirectX 4 VB © 2000 Jack Hoxley. All rights reserved.
Reproduction of this site and it's contents, in whole or in part, is prohibited,
except where explicitly stated otherwise.
Design by Mateo
Contact Webmaster