Friday, 5 January 2007

MDX Custom Action Mapping (Part III)

[Warning: This article is obsolote. I´d suggest you to read the Refreshed version here.]

We continue with the MDX Custom Action Mapping series where we left it. This is, by now, the last part of the tutorial. Hope it helps you...

Bind up the physical object

What do we put in the file to identify the 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.

Steping back to the previous part, we could think of using Inmediate Mode, and storing in the file the name of the field in JoystickState struct we wanted to map the game action to, regardless of the physical device it´s attached to. That might be a solution, but once we have the JoystickState structure, how do we get the value of a field whose name is stored in a variable? One way to do that is to make an unsafe pointer access to the struct, if you know the address and size of the field you want to retrieve. This is, you know, so C++, that we´ll discard it by now.
There´s a simpler and better way...so don´t use inmediate mode. That´s why ActionMapping does so too, I guess.

Let´s have a look again at buffered mode. Specially to the BufferedData object. The significant difference in this way of retrieving data is the field Offset . This field does really have a meaning! It is the Physical Device Object´s offset, if Im not wrong, and it will serve us as a link between GameActions and the physical devices. So, what we want to store in the file is the PhysicalOffset, like this:

Map Action="Steering Action"
PhysicalDeviceGuid="1820-12820-2147-94579-3426-4575"
PhysicalObjectOffset="138452"

So there´s the link. We choose BufferedMode and go ahead !

Designing the Action class

It´s a good Idea to defina a class like this:

public class Action
{
public eGameAction mGameAction;
public Device mPhysicalDevice;
public int mPhysicalObjectOffset;

public int mLastDataReaded;
}

It will handle the mapping between a GameAction and a Physical Object, and store the last readed value. Then, to handle the whole Action Map, we could define structures like this (optional, make your own for your purposes):

1.- Collections.Generic.Dictionary<> > mActionMap;

This collection has a first dictionary, keyed by the DirectInput Device, and whose value is another dictionary, with all the Actions mapped for that device, keyed by the offset of the object.

2.- Collections.Generic.Dictionary mActions;

This is a plain list of the actions available in the application. Initialized for every eGameAction, regardless it´s mapped to a device or not.

Initializing and Storing the Action Map

The best way to store the action map is an XML file in the user´s ApplicationData special folder. This way, the config will be made for every machine the application is installed.

To save the settings, simplly loop for every device in mActionMap 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:

1.- Recover a GameAction based on it´s name: Just as we said earlier, use Enum.Parse ( typeof( eGameActions), name);
2.- Recover a device instance by it´s Guid: Just loop throught the Available Devices searching one with the same guid.

The DoSteps method

Once per frame, a DoSteps / OnFrameMove / whatever you like method should be called to update all the data.

foreach( Device dev in this.mActionMap.Keys)
{
dev.Poll()
BufferedDataCollection coll = dev.GetBufferedDate();
if( coll == null )
continue;

Collections.Generic.Dictionary<> deviceActions = this.mActionMap[dev];

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 unpressed, 128 pressed)
deviceActions[ bdata.offset ].mLastDataReaded = bdata.data;
}
}
}

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, 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:

1.- Define a Timer object in your configuration dialog, which is started when the user presses the "Assign" button.
2.- The timer will fire up every 100 ms or so. In the Timer_tick event, do the actual "listening" process:
2.0.- Increment a Time Counter. If it reaches the amount of time for listening, get out.
2.1.- Loop through every device
2.2.- Make device.GetBufferedData ()
2.3.- Assign first retrieved data to selected GameAction

In this algorithm you should also apply a Thresold system, 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.

Finally, reading data from the application

Once in your application, you can simple access the mActions collection to get the data. For example:

1.- Reading the Steering game action:

int wheelPosition = this.mInput.mActions[ eGameActions.Steering ].mLastDataReaded;

2.- Reading a button

if ( this.mInput.mActions [ eGameActions.MoveForward ].mLastDataReaded != 0 )
this.WalkForward();


That´s it ! Hope all this stuff helped you !

Feel free to email any questions.

Best regards,

Iñaki Ayucar

No comments: