1
0
forked from 0ad/0ad
0ad/binaries/data/mods/public/simulation/helpers/FSM.js
Ykkrosh 2b57f4f998 # Initial support for formation movement.
Support asynchronous path queries.
Allow escaping when stuck in impassable terrain tiles.
Split Update message in multiple phases, to cope with ordering
requirements.
Support automatic walk/run animation switching.

This was SVN commit r8058.
2010-09-03 09:55:14 +00:00

258 lines
6.4 KiB
JavaScript

// Hierarchical finite state machine implementation.
//
// FSMs are specified as a JS data structure;
// see e.g. AnimalAI.js for an example of the syntax.
//
// FSMs are implicitly linked with an external object.
// That object stores all FSM-related state.
// (This means we can serialise FSM-based components as
// plain old JS objects, with no need to serialise the complex
// FSM structure itself or to add custom serialisation code.)
function FSM(spec)
{
// The (relatively) human-readable FSM specification needs to get
// compiled into a more-efficient-to-execute version.
//
// In particular, message handling should require minimal
// property lookups in the common case (even when the FSM has
// a deeply nested hierarchy), and there should never be any
// string manipulation at run-time.
this.decompose = { "": [] };
/* 'decompose' will store:
{
"": [],
"A": ["A"],
"A.B": ["A", "A.B"],
"A.B.C": ["A", "A.B", "A.B.C"],
"A.B.D": ["A", "A.B", "A.B.D"],
...
};
This is used when switching between states in different branches
of the hierarchy, to determine the list of sub-states to leave/enter
*/
this.states = { };
/* 'states' will store:
{
...
"A": {
"_name": "A",
"_parent": "",
"_refs": { // local -> global name lookups (for SetNextState)
"B": "A.B",
"B.C": "A.B.C",
"B.D": "A.B.D",
},
},
"A.B": {
"_name": "A.B",
"_parent": "A",
"_refs": {
"C": "A.B.C",
"D": "A.B.D",
},
"MessageType": function(msg) { ... },
},
"A.B.C": {
"_name": "A.B.C",
"_parent": "A.B",
"_refs": {},
"enter": function() { ... },
"MessageType": function(msg) { ... },
},
"A.B.D": {
"_name": "A.B.D",
"_parent": "A.B",
"_refs": {},
"enter": function() { ... },
"leave": function() { ... },
"MessageType": function(msg) { ... },
},
...
}
*/
function process(fsm, node, path, handlers)
{
var state = {};
fsm.states[path.join(".")] = state;
var newhandlers = {};
for (var e in handlers)
newhandlers[e] = handlers[e];
state._name = path.join(".");
state._parent = path.slice(0, -1).join(".");
state._refs = {};
for (var key in node)
{
if (key === "enter" || key === "leave")
{
state[key] = node[key];
}
else if (key.match(/^[A-Z]+$/))
{
state._refs[key] = (state._name ? state._name + "." : "") + key;
// (the rest of this will be handled later once we've grabbed
// all the event handlers)
}
else
{
newhandlers[key] = node[key];
}
}
for (var e in newhandlers)
state[e] = newhandlers[e];
for (var key in node)
{
if (key.match(/^[A-Z]+$/))
{
var newpath = path.concat([key]);
var decomposed = [newpath[0]];
for (var i = 1; i < newpath.length; ++i)
decomposed.push(decomposed[i-1] + "." + newpath[i]);
fsm.decompose[newpath.join(".")] = decomposed;
var childstate = process(fsm, node[key], newpath, newhandlers);
for (var r in childstate._refs)
{
var cname = key + "." + r;
state._refs[cname] = childstate._refs[r];
}
}
}
return state;
}
process(this, spec, [], {});
}
FSM.prototype.Init = function(obj, initialState)
{
this.deferFromState = undefined;
obj.fsmStateName = "";
obj.fsmNextState = undefined;
this.SwitchToNextState(obj, initialState);
};
FSM.prototype.SetNextState = function(obj, state)
{
obj.fsmNextState = state;
};
FSM.prototype.ProcessMessage = function(obj, msg)
{
// warn("ProcessMessage(obj, "+uneval(msg)+")");
var func = this.states[obj.fsmStateName][msg.type];
if (!func)
{
error("Tried to process unhandled event '" + msg.type + "' in state '" + obj.fsmStateName + "'");
return;
}
func.apply(obj, [msg]);
while (obj.fsmNextState)
{
var nextStateName = this.LookupState(obj.fsmStateName, obj.fsmNextState);
obj.fsmNextState = undefined;
if (nextStateName != obj.fsmStateName)
this.SwitchToNextState(obj, nextStateName);
}
};
FSM.prototype.DeferMessage = function(obj, msg)
{
// We need to work out which sub-state we were running the message handler from,
// and then try again in its parent state.
var old = this.deferFromState;
var from;
if (old) // if we're recursively deferring and saved the last used state, use that
from = old;
else // if this is the first defer then we must have last processed the message in the current FSM state
from = obj.fsmStateName;
// Find and save the parent, for use in recursive defers
this.deferFromState = this.states[from]._parent;
// Run the function from the parent state
var state = this.states[this.deferFromState];
var func = state[msg.type];
if (!func)
error("Failed to defer event '" + msg.type + "' from state '" + obj.fsmStateName + "'");
func.apply(obj, [msg]);
// Restore the changes we made
this.deferFromState = old;
// TODO: if an inherited handler defers, it calls exactly the same handler
// on the parent state, which is probably useless and inefficient
// NOTE: this will break if two units try to execute AI at the same time;
// as long as AI messages are queue and processed asynchronously it should be fine
};
FSM.prototype.LookupState = function(currentStateName, stateName)
{
// print("LookupState("+currentStateName+", "+stateName+")\n");
for (var s = currentStateName; s; s = this.states[s]._parent)
if (stateName in this.states[s]._refs)
return this.states[s]._refs[stateName];
return stateName;
};
FSM.prototype.SwitchToNextState = function(obj, nextStateName)
{
var fromState = this.decompose[obj.fsmStateName];
var toState = this.decompose[nextStateName];
if (!toState)
error("Tried to change to non-existent state '" + nextStateName + "'");
// Find the set of states in the hierarchy tree to leave then enter,
// to traverse from the old state to the new one.
// If any enter/leave function returns true then abort the process
// (this lets them intercept the transition and start a new transition)
for (var equalPrefix = 0; fromState[equalPrefix] === toState[equalPrefix]; ++equalPrefix)
{
}
for (var i = fromState.length-1; i >= equalPrefix; --i)
{
var leave = this.states[fromState[i]].leave;
if (leave)
{
obj.fsmStateName = fromState[i];
if (leave.apply(obj))
return;
}
}
for (var i = equalPrefix; i < toState.length; ++i)
{
var enter = this.states[toState[i]].enter;
if (enter)
{
obj.fsmStateName = toState[i];
if (enter.apply(obj))
return;
}
}
obj.fsmStateName = nextStateName;
}
Engine.RegisterGlobal("FSM", FSM);