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.
|