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 make in our ActionMapping is to be able to save to disk and recover a controller configuration, which assign 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 a string[] with: "MoveForward", "MoveBackward" and so on.
- Enum.IsDefined( typeof(eGameActions), 6 ): Will return false because eGameActions doesn't have that member.
- Enum.Parse(typeof(eGameActions), string): Will try to convert any string representation of an enumeration, to the enumeration itself.
PART 2: Inmediate or Buffered mode?
The next step is to make an important choice:
Inmediate 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 they fit best with a buffered or immediate mode.
Immediate Mode
Take a look at how data is reported under the Inmediate 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). This fields are always the same, no matter which device you are accessing to. 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 it´s objects to fields, because JoystickState was originally designed for that: joysticks (as it´s name states). So, the AxisX 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 this kind of devices came out, instead of adapting DInput for them, DX guys decided to use existing structs to handle new devices. So, there´s no default field in the JoystickState structure for the WheelAxis object. In this way, some device builders will map wheel axis to AxisX, 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 info. How?
- The first attribute is easy, just gameAction.ToString() to save, and Enum.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 as DeviceGuid.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? It´s name? It´s ID? It´s offset? Any of them would work if we´d only need to recover info about a physical device, as it´s name, properties, etc. You can do that, looping through the collection Device.Objects, and searching by any of that 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 a 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 physica 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? First variable is, of course, the current or last read value. It is an
int as every DInput value is read as integer. Second value tells us if this action is mapped to an analog object with Force Feedback enabled. 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 this 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 it´s 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 it reached it´s max range value, 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 structre saving it´s Guid, and a list of actions, just as we´ve seen before.
To read the settings, just load the xml file, loop through it´s nodes, and do the following:
- Recover a GameAction based on it´s name: Just as we said earlier, use Enum.Parse ( typeof( eGameActions), name);
- Recover a device instance by it´s Guid: Just loop throught the Available Devices searching one with the same guid.
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 it´s own user interface to configure the mapping. It´s dark, misterious, ugly, unconfortable, 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 makes 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 Thresold, 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 Thresold, make the assignment.
Take Care!