Game Development Community

More SCXML

by Greg Beauchesne · 07/27/2009 (12:25 am) · 3 comments

(Previous entry)

So, as mentioned in the summary, I've been working on SCXML for Torque. As a quick review, the current process is as follows:

  1. Create an SCXML file describing a state machine. All the scripting sections use TorqueScript.
  2. Transform the code from SCXML into a regular TorqueScript .cs file using XSL.
  3. Execute the state machine in Torque. The heavy lifting is handled by the C++ state machine engine.

So what does this actually look like? Here's a very simple example. Consider a player character in a game. They can be either alive or dead. We can model this as follows:
<scxml xmlns="http://www.w3.org/2005/07/scxml"
	version="1.0"
	name="FSMDemo"
	profile="torquescript"
	initial="alive">
	
	<state id="alive">
		<transition event="die" target="dead" />
	</state>

	<state id="dead" />
</scxml>

So we have one state called "alive" and another called "dead". If we are in the "alive" state and receive a "die" event, we move to the "dead" state. After being run through the translator, this script becomes the following:
if (!isObject(FSMDemo)) new XFSMTemplate(FSMDemo) {
	stateType = "scxml";
	new XFSMTransition() {
		isDefault = true;
		internalName = "N65543";
		target = "alive";
	};
	new XFSMState() {
		internalName = "alive";
		stateType = "state";
		new XFSMTransition() {
			internalName = "N65548";
			event = "die";
			target = "dead";
		};
	};
	new XFSMState() {
		internalName = "dead";
		stateType = "state";
	};
};

This gives you a little idea of how it works under the hood. As far as the tree is concerned, it's pretty much a direct translation of the XML. XFSMTemplate is the root class, and XFSMState and XFSMTransition objects are added to it to form the hierarchy. (The whole "X" prefix thing was mainly an attempt to avoid namespace clashes, and "SCXMLFSM" was not only way too long and jumbled to be a good prefix, but it would be a misnomer as well, since the implementation technically does not require SCXML at all. These classes will probably be renamed in the future, and if anyone can think of something better, I'd love to hear it.) The internalName properties for states come from the id element of <state>, whereas the ones for transitions are automatically generated by the XSL transformer.

In any case, we can see it in action:
==>new XFSMInstance(FSM) { template = FSMDemo; debug = true; };
XFSMInstance(FSM): Initializing state machine
==>FSM.update();
XFSMInstance(FSM): Performing microstep with transitions "N65543"
XFSMInstance(FSM): Following transition "N65543"
XFSMInstance(FSM): Entering state "alive"
==>FSM.postEvent("die");
XFSMInstance(FSM): External event "die" posted
==>FSM.update();
XFSMInstance(FSM): Processing event "die" from the external event queue
XFSMInstance(FSM): Performing microstep with transitions "N65548"
XFSMInstance(FSM): Exiting state "alive"
XFSMInstance(FSM): Following transition "N65548"
XFSMInstance(FSM): Entering state "dead"

Well, it works. We create an instance of the state machine with "new XFSMInstance()". We set template to FSMDemo to use the FSMDemo definition from above, and set debug to true to enable the debugging messages you see there. Events are added to the event queue with postEvent(), and then update() performs the actual work. But this is a pretty boring state machine. There's not really any TorqueScript to speak of, and it only uses regular top-level states.

So let's add some more to it:
<scxml xmlns="http://www.w3.org/2005/07/scxml"
	version="1.0"
	name="FSMDemo"
	profile="torquescript"
	initial="spawn">
	
	<state id="spawn">
		<onentry>
			<script><![CDATA[
				%this.health = 100;
				echo("Spawning player!");
			]]></script>
		</onentry>
		<transition target="alive" />
	</state>
	
	<state id="alive" initial="idle">
		<state id="idle">
			<transition event="speedUp" target="walk" />
		</state>
		<state id="walk">
			<transition event="speedUp" target="run" />
			<transition event="stop" target="idle" />
		</state>
		<state id="run">
			<transition event="speedUp">
				<script><![CDATA[
					echo("Can't speed up any more!");
				]]></script>
			</transition>
			<transition event="stop" target="idle" />
		</state>
		
		<transition event="damage">
			<script><![CDATA[
				%this.health -= 25;
				echo("Took damage! Health is down to " @ %this.health);
			]]></script>
			<raise event="die" />
		</transition>
		
		<transition event="die" cond="%this.health &lt;= 0" target="dead">
			<script><![CDATA[
				echo("You have died.");
			]]></script>
		</transition>
	</state>

	<state id="dead" />
</scxml>
Now we have some script code, and "alive" is now a compound state with 3 child states. The nesting can go as deep as you like, but for now I'm still keeping it simple. The "damage" transition is in the "alive" state, and so it applies to all of "idle", "walk", and "run", whereas the transitions triggering on the events "speedUp" and "stop" are one level deeper, causing those states to behave differently in response.

The scripts must be surrounded by <![CDATA[ ]]> markers to prevent the XML parser from trying to interpret the code inside as XML. The only exception to this is guard conditions (such as on the "die" transition). Because guard conditions in SCXML are attributes, <![CDATA[ ]]> can't be used and so we must resort to &lt;= to get "<=" out of the code.

I will spare you the TorqueScript object tree in this one (they can get pretty big), but now that there's some script code in there, I will show you that part of the .cs output:
function FSMDemo::onStateEntry(%template, %this, %state, %source)
{
	%_data = %this._data;
	switch$ (%state) {
	case "spawn":
		%this.health = 100;
		echo("Spawning player!");
	}
}

function FSMDemo::onStateExit(%template, %this, %state, %source)
{
}

function FSMDemo::onTransition(%template, %this, %trans, %source)
{
	%_data = %this._data;
	switch$ (%trans) {
	case "N65585":
		echo("Can't speed up any more!");
	case "N65597":
		%this.health -= 25;
		echo("Took damage! Health is down to " @ %this.health);
		%this.postInternalEvent("die");
	case "N65607":
		echo("You have died.");
	}
}

function FSMDemo::conditionMatch(%template, %this, %trans, %source)
{
	%_data = %this._data;
	switch$ (%trans) {
	case "N65607":
		return %this.health <= 0;
	}
	return true;
}
I've manually cleaned up the indentation here. Script code added to the state machine via the <script> tag retains the spacing it had in the SCXML file, so it can get a little hard to read, but for the most part you're not supposed to be directly reading the generated script files anyway. You can see that the code from the different states has been chopped up and rearranged so that it is handled by one of several functions in the FSMDemo namespace. The IDs are matched with a switch$ statement. If this ends up being too slow (from what I can tell, Torque implements switches as just a series of if-then-else constructs) then I can look into a different implementation. The nice part is that if I do so, I won't have to change any of the SCXML files. In fact, the SCXML files I had been using in the scripted version of the state machine code required very few changes to work with the C++ version.

So now that we have this state machine, we can try it. For the sake of brevity, I have shut off the debug output in favor of calling getConfiguration(), which returns a list of the states the machine is currently in:
==>new XFSMInstance(FSM) { template = FSMDemo; };
==>FSM.update(); echo(FSM.getConfiguration());
Spawning player!
alive idle
==>FSM.postEvent("damage"); FSM.update(); echo(FSM.getConfiguration());
Took damage! Health is down to 75
alive idle
==>FSM.postEvent("speedUp"); FSM.update(); echo(FSM.getConfiguration());
alive walk
==>FSM.postEvent("damage"); FSM.update(); echo(FSM.getConfiguration());
Took damage! Health is down to 50
alive walk
==>FSM.postEvent("speedUp"); FSM.update(); echo(FSM.getConfiguration());
alive run
==>FSM.postEvent("damage"); FSM.update(); echo(FSM.getConfiguration());
Took damage! Health is down to 25
alive run
==>FSM.postEvent("speedUp"); FSM.update(); echo(FSM.getConfiguration());
Can't speed up any more!
alive run
==>FSM.postEvent("stop"); FSM.update(); echo(FSM.getConfiguration());
alive idle
==>FSM.postEvent("damage"); FSM.update(); echo(FSM.getConfiguration());
Took damage! Health is down to 0
You have died.
dead
Looks good from where I'm standing!

In addition to the debug output above, I've also been trying to put a lot of error checking into this to help out with creating the state machines. Because the structure of the state machine is known to the system (rather than being 100% code) it can warn you of duplicate state IDs, invalid transitions, and unreachable states in advance so that you're not trying to track them down at run time. There's also a microstep limit so the game won't completely freeze if the state machine has an infinite loop of transitions - further processing gets deferred until the next call to update().

SCXML is still a Working Draft, so there are some ambiguous holes in its specification, but I have tried to support what features I can, mostly in the things most applicable to Torque. Here's a list of the elements my implementation can and cannot currently handle:

Fully- or nearly fully-supported:
  • <scxml> (the "exmode" attribute is ignored)
  • <state> (the "src" attribute is not supported; state machines must all be in one file)
  • <parallel>
  • <history>
  • <final>
  • <initial>
  • <transition> (everything but anchors is supported)
  • <if> / <else> / <elseif>
  • <log>
  • <raise>
  • <script>
Partially supported:
  • <send> (can only send events back to the current state machine)
  • <cancel> (works with <send>)
  • <assign> ("dataid" attribute not supported)
Not supported (but maybe could be in the future):
  • <datamodel>
  • <invoke>
  • <anchor>
At some point in the near future, I will probably need testers, if anyone is interested in trying it out. Before that time, though, there are still some more validation tests I have to write, and I will need to put together documentation and a package that makes it straightforward to use. Moreover, I would like feedback on what you like, what you don't, what you feel is missing, etc.

#1
07/27/2009 (1:34 pm)
very interesting, what is a real use of this in real the life?
#2
07/27/2009 (2:09 pm)
@Javier,

From what I am reading he is building the FSM scripts from a defined XML file. Which will make adding new states and changes a lot easier.

@Greg,

I may be interested in testing when you get to that point, or even before:)
#3
07/27/2009 (5:42 pm)
Randy: Exactly. For me this arose because I was trying to create a character that was built out of multiple independent sprites in TGB. I wanted to keep the logic simple and make the animation more complex, but I also wanted to keep everything in one place. This also made it pretty easy to do things like temporarily hide the individual sprites whenever I wanted to do a full-body animation and then return to the multi-sprite mode when it was done.

Javier: Among other things, it can be used in AI logic, animation (as explained above), modal GUI navigation, and dialog trees (as I understand it, SCXML was originally designed to be used as part of automated voice systems).

I didn't show any in this entry, but I think parallel states are one of the more useful features because they can have functionality that cuts across each other without having to tightly integrate the code with one another.