Direct Input Custom Action Mapping (refresh)





0/5 (0 vote)
Direct Input Custom Action Mapping (refresh)
ManagedDirectx is quite a bit outdated, and no longer supported by Microsoft, but it will make it for this example on custom action mapping. I´d suggest you to go XNA or SlimDX if you want to do some serious .NET graphics or game development.
What´s this post about? It´s about having a decent controller configuration system. The first choice, of course, is to start looking at DirectInput´s Action Mapping. If you prefer to do that on your own (to get rid of the standard and no too customizable DX config dialog, for example), keep reading.
The main task we want to do in our ActionMapping
is to be able to save to disk and recover a controller configuration, which assigns an InputDevice
and an Object
of that device to a GameAction
defined by us.
PART 1: Define Game Actions
We will put all of our actions in an enumeration.
enum eGameActions
{
MoveForward,
MoveBackward,
TurnLeft,
TurnRight,
Shoot,
. . .
NumberOfActions // Not an action, just to know the total count of actions
}
A quick note for beginners: There´s a very useful class in the .NET Framework called System.Enum
. This class has static
methods to loop through members of an enumeration and more. Things like:
Enum.GetNames ( typeof (eGameActions) )
: Will return astring[]
with: "MoveForward
", "MoveBackward
" and so on.Enum.IsDefined( typeof(eGameActions), 6 )
: Will returnfalse
becauseeGameActions
doesn't have that member.Enum.Parse(typeof(eGameActions), string)
: Will try to convert anystring
representation of an enumeration, to the enumeration itself.
PART 2: Immediate or Buffered Mode?
The next step is to make an important choice: Immediate or Buffered mode? Pasting here the DX SDK description of both modes:
“DirectInput supplies two types of data: buffered and immediate. Buffered data is a record of events that are stored until an application retrieves them. Immediate data is a snapshot of the current state of a device. You might use immediate data in an application that is concerned only with the current state of a device - for example, a flight combat simulation that responds to the current position of the joystick and the state of one or more buttons. Buffered data might be the better choice where events are more important than states - for example, in an application that responds to movement of the mouse and button clicks. You can also use both types of data, as you might, for example, if you wanted to get immediate data for joystick axes but buffered data for the buttons.”
DX standard Action Mapping works in Buffered Mode only, but we will want to provide a way of using both. Why? Because we want an Input library for all of our projects, regardless of whether they fit best with a buffered or immediate mode.
Immediate Mode
Take a look at how data is reported under the Immediate Mode: all you get is a struct
of the type JoystickState
, MouseState
or KeyboardState
, which has all the data you need under some default fields, defined by DirectX (like AxisX
, AxixY
, etc.). These fields are always the same, no matter which device you are accessing. It´s the device´s builder (i.e. Logitech) who decides what physical objects are mapped to what DX default fields. An example:
- For a joystick, it´s quite trivial to map its objects to fields, because
JoystickState
was originally designed for that: joysticks (as its name states). So, theAxisX
field will almost always be mapped to the X-Axis of the joystick. - What happens for a driving wheel? That´s something
DirectInput
was not originally designed for, and when these kind of devices came out, instead of adaptingDInput
for them, DX guys decided to use existingstruct
s to handle new devices. So, there´s no default field in theJoystickState
structure for theWheelAxis
object. In this way, some device builders will map wheel axis toAxisX
, while others will do to the Rx Axis, and so on...
Buffered Mode
In buffered mode, you don´t get access to the whole structure of data. Instead of that, you call the GetBufferedData()
method, which retrieves a collection of BufferedData
objects, one for each changing object in the device. That means, if the device is absolutely stall, no data will be returned.
One tip: To set the buffered mode, you have to manually change the property: Device.Properties.BufferSize = 16
PART 3: Making the Relationship
We need a way to save and recover from a file something like this: Action="Steering Action" PhysicalDevice="Logitech G25" PhysicalObject="Wheel Axis"
. We will use XML based files to store the information. How?
- The first attribute is easy, just
gameAction.ToString()
tosave
, andEnum.Parse(typeof(eGameActions), attributeInnerText);
to recover from the file. - The second attribute is not hard either. Instead of saving device´s name, we will save device´s
Guid
: Write the guid asDeviceGuid.ToString()
and recover it as:DeviceGuid = new Guid(attributeGuid.InnerText );
- The third attribute.... aaaahhh. This is a little bit more tricky.
We need a way to identify the device´s object we want to map the action to.
Bind Up the Physical Object
What do we put in the XML file to identify the device´s physical object? Its name? Its ID? Its offset? Any of them would work if we´d only need to recover info about a physical device, as its name, properties, etc. You can do that, looping through the collection Device.Objects
, and searching by any of those terms. The problem is that we don´t only need that, we need to retrieve data from that object.
In Buffered Mode, physical objects are identified through an Offset
provided by the GetBufferedData
method (inside the BufferedData
class). If you look into it, you will realize that this offset is, in fact, the offset inside the JoystickState
structure provided by the Immediate Mode. So, it seems we have found a unique identifier for our physical objects, that works in both immediate and buffered mode: THE OBJECT´S OFFSET.
So, our XML configuration file will handle information like the following:
Map Action="Steering Action" PhysicalDeviceGuid="1820-12820-2147-94579-3426-4575"
PhysicalObjectOffset="138452"
PART 4: Designing the ActionMap Class
It´s a good Idea to define an ActionMap
class, to handle the mapping between a GameAction
and a Physical Object, and store information about the read data. The first part, to manage the mapping with the physical object could be something like:
public class ActionMap
{
public eGameActions mActionType;
public string mActionName = "";
public eGameActionCategories mCategory = eGameActionCategories.None;
public DeviceState mDeviceState = null;
public int mObjectOffset = -1;
And the second part, to store information about the read
data, could have this shape:
public int mCurrentValue = 0;
public bool mIsFFAxis = false;
private bool mReadAsImmediateData = false;
public float mFormattedValue = 0;
public float mCurrentValue01 = 0;
public bool mCurrentValueBool = false;
private int mRangeMin = 0;
private int mRangeMax = 0;
private bool mInvertReading = false;
private eBoolBase mBoolBasedOn = eBoolBase.RangeMax;
What is all that information? The first variable is, of course, the current or last read value. It is an int
as every DInput
value is read as integer. The second value tells us if this action is mapped to an analog object with Force Feedback enabled. The third value will allow us to configure this specific action to be read as Immediate (instead of buffered, the default behaviour).
Starting from there, I'd recommend you to store another versions of the data. FormattedValue
, for example. It is useful to provide this container to your application, so it can transform the read data as you want it, storing the result there for your comfort. It is very common to have other typical formatting of your data too, like the value expressed in the range 0..1, or expressed as a boolean (useful for buttons).
In order to make these conversions, you will need another step.
PART 5: Action Calibration
Why every Controller Configuration has a Calibration step? Because you cannot know the range of the object the user selected for an Action. Analog objects, for instance, like pedals, joysticks or steering wheels, usually report an integer value between 0 .. 65535. But some of them will use other ranges. Buttons are retrieved as int
too, with the values 0
or 128
only. To make things even worse, objects are sometimes read as inverted, what means that a button can be reported as 0
when un-pressed and 128
when pressed, or just the opposite. The same for analogs.
So, it´s clear that you need calibration, a way to know the valid range for objects and if they have to be read as inverted or not. I´d suggest you to store those values in mRangeMin
, mRangeMax
, mInvertReading
. With that information, you have all you need to transform the int
value read to the range 0..1.
The last thing we need is a way to convert it to boolean, so we can quickly check from our application if a button is pressed or not, for example, without having to worry about its range, inverted property, or anything. What I usually do here is to define what to compare the int
value to, to decide if its pressed (boolean = true
) or not (bool = false
). You can do this in many ways, but I like to use an enum
for such purpose:
public enum eBoolBase
{
RangeMax,
RangeMin,
Zero,
NotZero,
}
Using this, you can make an Action
to be true
when its max range value is reached, or when it´s non-zero, or whatever you want.
PART 6: The Whole ActionMap
The whole Action Map for our application could be handled by a structure like the following (make your own for your purposes):
Dictionary<DeviceState, Dictionary<int, List<ActionMap>>>
Or you can have the reverse, indexing first by the ActionMap
, and taking the Device
and Object
´s Offset
later. Choose your favorite option.
Then, just define the ToXml()
and FromXml()
methods to store and recover all your configuration. The best place to store this configuration is the ApplicationData
special folder. This way, the config will be made for every machine the application is installed, keeping a different configuration for each Windows user.To save the settings, just loop for every device in your structure saving its Guid
, and a list of actions, just as we´ve seen before.
To read the settings, just load the XML file, loop through its nodes, and do the following:
- Recover a
GameAction
based on its name: Just as we said earlier, useEnum.Parse ( typeof( eGameActions), name);
- Recover a device instance by its
Guid
: Just loop through the Available Devices searching one with the sameguid
.
PART 7: Updating Your Data at Runtime
Once per frame, a DoSteps
/ OnFrameMove
/ Update
/ whatever you like method should be called to update all the data. It should do something similar to this:
// Read Buffered Data
foreach (Device dev in this.mActionMap.Keys)
{
dev.Poll();
BufferedDataCollection coll = dev.GetBufferedDate();
if (coll == null)
continue;
foreach (BufferedData bdata in coll)
{
if (deviceActions.ContainsKey(bdata.offset))
{
// Action is mapped. Save it´s value.
// Axis will report integer (usually 0..65535) and
// buttons will report integer (0 or 128)
bdata.Data is what you need
}
}
}
// Read Immediate Data
foreach (DeviceState st in this.mDeviceStates)
{
foreach (List<ActionMap> lista in dic.Values)
{
foreach (ActionMap action in lista)
{
if (!action.ReadAsImmediateData)
continue;
// Use action.Offset to access the JoystickState structure
}
}
}
PART 8: User Configuration of the Action Map
DirectX Action Mapping has its own user interface to configure the mapping. It´s dark, mysterious, ugly, uncomfortable, strange, a little bit chaotic, uncustomizable, and as we are no longer using standard Action Mapping, we can no longer use it. So, make your own config dialog, with the appearance you want, and the behaviour you want.
Now, with your custom action mapping, making a new assignment is as easy as changing Action.PhysicalDevice
and Action.PhysicalObjectOffset
properties.
Listening to a Device
Most of the games make a controller configuration based on "Listen for any device´s object moving". If that happens, object is assigned to game´s action. In the config dialog, there will be a list of available game actions. When user selects one and press the "Assign" button, the application should stay for a while listening for devices. To do so:
- Define a
Timer
object in your configuration dialog, which is started when the user presses the "Assign
" button. - Set the timer to fire up every 100 ms or so. In the
Timer_tick
event, do the actual "listening" process:- Increment a Time Counter. If it reaches the amount of time for listening, get out.
- Loop through every device
- Make
device.GetBufferedData ()
- Assign first retrieved data to selected
GameAction
In this algorithm, you should also apply a Threshold
, because analog devices are almost always reporting small changes. So keep track of the first values returned in BufferedData
for every physical object and when newer values come, calculate the difference between actual and first value. If the difference is bigger than Threshold
, make the assignment.
Take care!