Another common use for FSM's is to provide an abstraction for AI
state. For this purpose, you would like to supply an "input" string
to the FSM, and let the FSM decide which state it should transition
to, rather than explicitly specifying the target state name.
Consider the following FSM state diagram:
↷ straight |
|
↶ straight |
North |
← left |
East |
↓ left |
|
↑ left |
West |
→ left |
South |
↺ straight |
|
↻ straight |
Here the text next to an arrow represents the "input" string given
to the FSM, and the direction of the arrow represents the state
transition that should be made for that particular input string, from
the indicated starting state.
In this example, we have encoded a simple FSM that determines which
compass direction a character will be facing after either turning left
or continuing straight. The input will be either "left" or
"straight", and the result is a transition to a new state that
represents the new compass direction, based on the previous compass
direction. If we request "left" from state North, the FSM transitions
to state West. On the other hand, if we request "left" from state
South, the FSM transitions to state East. If we request "straight"
from any state, the FSM should remain in its current state.
To implement this in Panda3D, we define a number of filter
functions, one for each state. The purpose of this function is to
decide what state to transition to next, if any, on receipt of a
particular input.
A filter function is created by defining a python method named
filterStateName() , where StateName is the name of the FSM
state to which this filter function applies. The filterStateName
method receives two parameters, a string and a tuple of arguments (the
arguments contain the optional additional arguments that might have
been passed to the fsm.request() call; it's usually an
empty tuple). The filter function should return the name of the state
to transition to. If the transition should be disallowed, the filter
function can either return None to quietly ignore it, or it can raise
an exception. For example:
class CompassDir(FSM.FSM):
def filterNorth(self, request, args):
if request == 'straight':
return 'North'
elif request == 'left':
return 'West'
else:
return None
def filterWest(self, request, args):
if request == 'straight':
return 'West'
elif request == 'left':
return 'South'
else:
return None
def filterSouth(self, request, args):
if request == 'straight':
return 'South'
elif request == 'left':
return 'East'
else:
return None
def filterEast(self, request, args):
if request == 'straight':
return 'East'
elif request == 'left':
return 'North'
else:
return None
|
Note that input strings, by convention, should begin with a
lowercase letter, as opposed to state names, which should begin with
an uppercase letter. This allows you to make the distinction between
requesting a state directly, and feeding a particular input string to
an FSM. To feed input to this FSM, you would use the request() call,
just as before:
myfsm.request('left')
myfsm.request('left')
myfsm.request('straight)
myfsm.request('left')
|
If the FSM had been in state North originally, after the above
sequence of operations it would now be in state East.
The defaultFilter method
Although defining a series of individual filter methods gives you
the most flexibility, for many FSM's you may not need this much
explicit control. For these cases, you can simply define a
defaultFilter method that does everything you need. If a particular
filterStateName() method does not exist, then the FSM
will call the method named defaultFilter() instead; you
can put any logic here that handles the general case.
For instance, we could have defined the above FSM using just the
defaultFilter method, and a lookup table:
class CompassDir(FSM.FSM):
nextState = {
('North', 'straight') : 'North',
('North', 'left') : 'West',
('West', 'straight') : 'West',
('West', 'left') : 'South',
('South', 'straight') : 'South',
('South', 'left') : 'East',
('East', 'straight') : 'East',
('East', 'left') : 'North',
}
def defaultFilter(self, request, args):
key = (self.state, request)
return self.nextState.get(key)
|
The base FSM class defines a defaultFilter() method
that implements the default FSM transition rules (that is, allow all
direct-to-state (uppercase) transition requests unless
self.defaultTransitions is defined; in either case, quietly ignore
input (lowercase) requests).
In practice, you can mix-and-match the use of the defaultFilter
method and your own custom methods. The defaultFilter method will be
called only if a particular state's custom filter method does not
exist. If a particular state's filterStateName method is
defined, that method will be called upon a new request; it can do any
custom logic you require (and it can call up to the defaultFilter
method if you like).
|