Created
July 24, 2015 04:32
-
-
Save fcingolani/43b833e0b03d392ccc9f to your computer and use it in GitHub Desktop.
FlxFSM - HaxeFlixel Finite State Machine. Backported to HaxelFlixel 3.x from 4.x (https://github.com/HaxeFlixel/flixel-addons/blob/dev/flixel/addons/util/FlxFSM.hx).
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package; | |
import PlayState; | |
import flixel.interfaces.IFlxDestroyable; | |
import flixel.util.FlxDestroyUtil; | |
import flixel.util.FlxPool; | |
import flixel.util.FlxSignal; | |
/** | |
* A generic FSM State implementation. Extend this class to create new states. | |
*/ | |
class FlxFSMState<T> implements IFlxDestroyable | |
{ | |
public function new() { } | |
/** | |
* Called when state becomes active. | |
* | |
* @param Owner The object the state controls | |
* @param FSM The FSM instance this state belongs to. Used for changing the state to another. | |
*/ | |
public function enter(owner:T, fsm:FlxFSM<T>):Void { } | |
/** | |
* Called every update loop. | |
* | |
* @param Owner The object the state controls | |
* @param FSM The FSM instance this state belongs to. Used for changing the state to another. | |
*/ | |
public function update(elapsed:Float, owner:T, fsm:FlxFSM<T>):Void { } | |
/** | |
* Called when the state becomes inactive. | |
* | |
* @param Owner The object the state controls | |
*/ | |
public function exit(owner:T):Void { } | |
public function destroy():Void { } | |
} | |
/** | |
* Sample bitflags for FSM's type | |
*/ | |
@:enum | |
abstract FSMType(Int) from Int to Int | |
{ | |
var any = 1; | |
var actor = 2; | |
var ai = 4; | |
var animation = 8; | |
var area = 16; | |
var audio = 32; | |
var collision = 64; | |
var damage = 128; | |
var effect = 256; | |
var environment = 512; | |
var game = 1024; | |
var machine = 2048; | |
var menu = 4096; | |
var npc = 8192; | |
var particle = 16384; | |
var physics = 32768; | |
var pickup = 65536; | |
var player = 131072; | |
var projectile = 262144; | |
var text = 524288; | |
} | |
/** | |
* Helper typedef for FlxExtendedFSM's pools | |
*/ | |
typedef StatePool<T> = Map<String, FlxPool<FlxFSMState<T>>> | |
/** | |
* A generic Finite-state machine implementation. | |
*/ | |
class FlxFSM<T> implements IFlxDestroyable | |
{ | |
/** | |
* The owner of this FSM instance. Gets passed to each state. | |
*/ | |
public var owner(default, set):T; | |
/** | |
* Current state | |
*/ | |
public var state(default, set):FlxFSMState<T>; | |
/** | |
* Class of current state | |
*/ | |
public var stateClass:Class<FlxFSMState<T>>; | |
/** | |
* The age of the active state | |
*/ | |
public var age:Float; | |
/** | |
* Name of this FSM. Used for locking/unlocking when in a stack. | |
*/ | |
public var name:String; | |
/** | |
* Binary flag. Used for locking/unlocking when in a stack. | |
*/ | |
public var type:Int; | |
/** | |
* The stack this FSM belongs to or null | |
*/ | |
public var stack:FlxFSMStack<T>; | |
/** | |
* Optional transition table for this FSM | |
*/ | |
public var transitions:FlxFSMTransitionTable<T>; | |
/** | |
* Optional map object containing FlxPools for FlxFSMStates | |
*/ | |
public var pools:StatePool<T>; | |
public function new(?owner:T, ?state:FlxFSMState<T>) | |
{ | |
this.age = 0; | |
this.owner = owner; | |
this.state = state; | |
this.type = FSMType.any; | |
this.transitions = new FlxFSMTransitionTable<T>(); | |
this.pools = new StatePool<T>(); | |
} | |
/** | |
* Updates the active state instance. | |
*/ | |
public function update(elapsed:Float):Void | |
{ | |
if (state != null && owner != null) | |
{ | |
age += elapsed; | |
state.update(elapsed, owner, this); | |
} | |
if (transitions != null && pools != null) | |
{ | |
var newStateClass = transitions.poll(stateClass, this.owner); | |
if (newStateClass != stateClass) | |
{ | |
var curName = Type.getClassName(stateClass); | |
var newName = Type.getClassName(newStateClass); | |
if (pools.exists(newName) == false) | |
{ | |
pools.set(newName, new FlxPool<FlxFSMState<T>>(newStateClass)); | |
} | |
var returnToPool = state; | |
state = pools.get(newName).get(); | |
if (pools.exists(curName)) | |
{ | |
pools.get(curName).put(returnToPool); | |
} | |
} | |
} | |
} | |
/** | |
* Calls exit on current state | |
*/ | |
public function destroy():Void | |
{ | |
owner = null; | |
state = null; | |
stack = null; | |
name = null; | |
transitions = null; | |
pools = null; | |
type = FSMType.any; | |
} | |
private function set_owner(owner:T):T | |
{ | |
if (this.owner != owner) | |
{ | |
if (this.owner != null && state != null) | |
{ | |
state.exit(this.owner); | |
} | |
this.owner = owner; | |
if (this.owner != null && state != null) | |
{ | |
age = 0; | |
state.enter(this.owner, this); | |
} | |
} | |
return this.owner; | |
} | |
private function set_state(state:FlxFSMState<T>):FlxFSMState<T> | |
{ | |
var newClass = Type.getClass(state); | |
if (this.stateClass != newClass) | |
{ | |
if (owner != null && this.state != null) | |
{ | |
this.state.exit(owner); | |
} | |
this.state = state; | |
if (this.state != null && owner != null) | |
{ | |
age = 0; | |
this.state.enter(owner, this); | |
} | |
this.stateClass = newClass; | |
} | |
return state; | |
} | |
} | |
/** | |
* Used for grouping FSM instances and updating them according to the stack's updateMode. | |
*/ | |
class FlxFSMStack<T> extends FlxFSMStackSignal implements IFlxDestroyable | |
{ | |
/** | |
* Test if the stack is empty | |
*/ | |
public var isEmpty(get, never):Bool; | |
private var _stack:Array<FlxFSM<T>>; | |
private var _alteredStack:Array<FlxFSM<T>>; | |
private var _hasLocks:Bool; | |
private var _lockedNames:Array<String>; | |
private var _lockedTypes:Int; | |
private var _lockRemaining:Bool; | |
public function new() | |
{ | |
super(); | |
_stack = []; | |
_lockedNames = []; | |
_lockedTypes = 0; | |
_hasLocks = false; | |
FlxFSMStackSignal._lockSignal.add(lockType); | |
} | |
/** | |
* Updates the states that have not been locked | |
*/ | |
public function update(elapsed:Float) | |
{ | |
if (_alteredStack != null) // Stack was edited during the last loop. Adopt the changes | |
{ | |
_stack = _alteredStack.copy(); | |
_alteredStack = null; | |
} | |
for (fsm in _stack) | |
{ | |
if (_hasLocks) | |
{ | |
if (_lockRemaining == false && (fsm.type & _lockedTypes) == 0 && _lockedNames.indexOf(fsm.name) == -1) | |
{ | |
fsm.update(elapsed); | |
} | |
} | |
else | |
{ | |
fsm.update(elapsed); | |
} | |
} | |
if (_lockedNames.length != 0) | |
{ | |
_lockedNames = []; | |
} | |
_lockRemaining = false; | |
_lockedTypes = 0; | |
_hasLocks = false; | |
} | |
/** | |
* Locks the specified FSM for the duration of the update loop | |
* @param name | |
*/ | |
public function lock(name:String):Void | |
{ | |
if (_lockedNames.indexOf(name) == -1) | |
{ | |
_lockedNames.push(name); | |
_hasLocks = true; | |
} | |
} | |
/** | |
* Locks the remaining FSMs for the duration of the update loop | |
*/ | |
public function lockRemaining():Void | |
{ | |
_lockRemaining = true; | |
_hasLocks = true; | |
} | |
/** | |
* Locks by type, so that if `FSM.type & bitflag != 0`, the FSM gets locked. | |
* @param bitflag You can use `FSMType` abstract for values or build your own. | |
*/ | |
public function lockType(bitflag:Int):Void | |
{ | |
_lockedTypes |= bitflag; | |
_hasLocks = true; | |
} | |
/** | |
* Adds the FSM to the front of the stack | |
* @param FSM | |
*/ | |
public function unshift(FSM:FlxFSM<T>) | |
{ | |
if (_alteredStack == null) | |
{ | |
_alteredStack = _stack.copy(); | |
} | |
FSM.stack = this; | |
_alteredStack.unshift(FSM); | |
} | |
/** | |
* Removes the first FSM from the stack | |
* @return The removed FSM | |
*/ | |
public function shift():FlxFSM<T> | |
{ | |
if (_alteredStack == null) | |
{ | |
_alteredStack = _stack.copy(); | |
} | |
var FSM = _alteredStack.shift(); | |
FlxDestroyUtil.destroy(FSM); | |
return FSM; | |
} | |
/** | |
* Adds the FSM to the end of the stack | |
* @param FSM | |
*/ | |
public function push(FSM:FlxFSM<T>) | |
{ | |
if (_alteredStack == null) | |
{ | |
_alteredStack = _stack.copy(); | |
} | |
FSM.stack = this; | |
_alteredStack.push(FSM); | |
} | |
/** | |
* Removes the first FSM from the stack | |
* @return The removed FSM | |
*/ | |
public function pop():FlxFSM<T> | |
{ | |
if (_alteredStack == null) | |
{ | |
_alteredStack = _stack.copy(); | |
} | |
var FSM = _alteredStack.pop(); | |
lock(FSM.name); // FSM isn't updated during the remainder the loop current | |
FlxDestroyUtil.destroy(FSM); | |
return FSM; | |
} | |
/** | |
* Removes the FSM from the stack and destroys it | |
* @param The removed FSM | |
*/ | |
public function remove(FSM:FlxFSM<T>) | |
{ | |
if (_alteredStack == null) | |
{ | |
_alteredStack = _stack.copy(); | |
} | |
if (_alteredStack.remove(FSM)) | |
{ | |
lock(FSM.name); // FSM isn't updated during the remainder the current loop | |
FlxDestroyUtil.destroy(FSM); | |
} | |
} | |
/** | |
* Removes the FSM with given name from the stack and destroys it | |
* @param The removed FSM | |
*/ | |
public function removeByName(name:String) | |
{ | |
for (fsm in _stack) | |
{ | |
if (fsm.name == name) | |
{ | |
remove(fsm); | |
} | |
} | |
} | |
/** | |
* Destroys every member in stack and self | |
*/ | |
public function destroy():Void | |
{ | |
for (fsm in _stack) | |
{ | |
FlxDestroyUtil.destroy(fsm); | |
} | |
lockRemaining(); | |
FlxFSMStackSignal._lockSignal.remove(lockType); | |
} | |
private function get_isEmpty():Bool | |
{ | |
return (_stack.length == 0); | |
} | |
} | |
/** | |
* Base class for `FlxFSMStack<T>` | |
* Only function is to create a static `FlxTypedSignal` that's shared between stacks. | |
* Otherwise signals would be type specific, and `FlxFSMStack<A>` could not dispatch | |
* to `FlxFSMStack<B>` | |
*/ | |
private class FlxFSMStackSignal | |
{ | |
private static var _lockSignal:FlxTypedSignal< Int->Void >; | |
public function new() | |
{ | |
if (FlxFSMStackSignal._lockSignal == null) | |
{ | |
FlxFSMStackSignal._lockSignal = new FlxTypedSignal < Int->Void > (); | |
} | |
} | |
/** | |
* Sends a message to all active FSMStacks to lock given types. | |
* @param type You can use `FSMType` abstract for values or build your own. | |
*/ | |
public function globalLock(type:Int):Void | |
{ | |
FlxFSMStackSignal._lockSignal.dispatch(type); | |
} | |
} | |
/** | |
* Contains the information on when to transition from a given state to another. | |
*/ | |
class FlxFSMTransitionTable<T> | |
{ | |
private var _table:Array<Transition<T>>; | |
private var _startState:Class<FlxFSMState<T>>; | |
private var _garbagecollect:Bool = false; | |
public function new() | |
{ | |
_table = new Array<Transition<T>>(); | |
} | |
/** | |
* Polls the transition table for active states | |
* @param FSM The FlxFSMState the table belongs to | |
* @return The state that should become or remain active. | |
*/ | |
public function poll(currentState:Class<FlxFSMState<T>>, owner:T):Class<FlxFSMState<T>> | |
{ | |
if (currentState == null && _startState != null) | |
{ | |
return _startState; | |
} | |
if (_garbagecollect) | |
{ | |
_garbagecollect = false; | |
var removeThese = []; | |
for (transition in _table) | |
{ | |
if (transition.remove == true) | |
{ | |
if (transition.from == currentState) | |
{ | |
_garbagecollect = true; | |
} | |
else | |
{ | |
removeThese.push(transition); | |
} | |
} | |
} | |
for (transition in removeThese) | |
{ | |
_table.remove(transition); | |
} | |
} | |
for (transition in _table) | |
{ | |
if (transition.from == currentState || transition.from == null) | |
{ | |
if (transition.evaluate(owner) == true) | |
{ | |
return transition.to; | |
} | |
} | |
} | |
return currentState; | |
} | |
/** | |
* Adds a transition condition to the table. | |
* @param From The state the condition applies to | |
* @param To The state to transition | |
* @param Condition Function that returns true if the transition conditions are met | |
*/ | |
public function add(from:Class<FlxFSMState<T>>, to:Class<FlxFSMState<T>>, condition:T->Bool) | |
{ | |
if (hasTransition(from, to, condition) == false) | |
{ | |
var row = new Transition<T>(); | |
row.from = from; | |
row.to = to; | |
row.condition = condition; | |
_table.push(row); | |
} | |
return this; | |
} | |
/** | |
* Adds a global transition condition to the table. | |
* @param To The state to transition | |
* @param Condition Function that returns true if the transition conditions are met | |
*/ | |
public function addGlobal(to:Class<FlxFSMState<T>>, condition:T->Bool) | |
{ | |
if (hasTransition(null, to, condition) == false) | |
{ | |
var row = new Transition<T>(); | |
row.to = to; | |
row.condition = condition; | |
_table.push(row); | |
} | |
return this; | |
} | |
/** | |
* Add a transition directly | |
* @param transition | |
*/ | |
public function addTransition(transition:Transition<T>) | |
{ | |
if (_table.indexOf(transition) == -1) | |
{ | |
_table.push(transition); | |
} | |
} | |
/** | |
* Sets the starting State | |
* @param With | |
*/ | |
public function start(with:Class<FlxFSMState<T>>) | |
{ | |
_startState = with; | |
return this; | |
} | |
/** | |
* Replaces given state class with another. | |
* @param Target State class to replace | |
* @param Replacement State class to replace with | |
*/ | |
public function replace(target:Class<FlxFSMState<T>>, replacement:Class<FlxFSMState<T>>) | |
{ | |
for (transition in _table) | |
{ | |
if (transition.to == target) | |
{ | |
transition.remove = true; | |
if (transition.from == null) | |
{ | |
addGlobal(replacement, transition.condition); | |
} | |
else | |
{ | |
add(transition.from, replacement, transition.condition); | |
} | |
_garbagecollect = true; | |
} | |
if (transition.from == target) | |
{ | |
transition.remove = true; | |
add(replacement, transition.to, transition.condition); | |
_garbagecollect = true; | |
} | |
} | |
} | |
/** | |
* Removes a transition condition from the table | |
* @param From From State | |
* @param To To State | |
* @param Condition Condition function | |
* @return True when removed, false if not in table | |
*/ | |
public function remove(?from:Class<FlxFSMState<T>>, ?to:Class<FlxFSMState<T>>, ?condition:Null<T->Bool>) | |
{ | |
switch([from, to, condition]) | |
{ | |
case [f, null, null]: | |
for (transition in _table) | |
{ | |
if (from == transition.from) | |
{ | |
transition.remove = true; | |
_garbagecollect = true; | |
} | |
} | |
case [f, t, null]: | |
for (transition in _table) | |
{ | |
if (from == transition.from && to == transition.to) | |
{ | |
transition.remove = true; | |
_garbagecollect = true; | |
} | |
} | |
case [null, t, c]: | |
for (transition in _table) | |
{ | |
if (to == transition.to && condition == transition.condition) | |
{ | |
transition.remove = true; | |
_garbagecollect = true; | |
} | |
} | |
case [f, t, c]: | |
for (transition in _table) | |
{ | |
if (from == transition.from && to == transition.to && condition == transition.condition) | |
{ | |
transition.remove = true; | |
_garbagecollect = true; | |
} | |
} | |
} | |
} | |
/** | |
* Tells if the table contains specific transition or transitions. | |
* @param From From State | |
* @param To To State | |
* @param Condition Condition function | |
* @return True if match found | |
*/ | |
public function hasTransition(?from:Class<FlxFSMState<T>>, ?to:Class<FlxFSMState<T>>, ?condition:Null<T->Bool>):Bool | |
{ | |
switch([from, to, condition]) | |
{ | |
case [f, null, null]: | |
for (transition in _table) | |
{ | |
if (from == transition.from && transition.remove == false) | |
{ | |
return true; | |
} | |
} | |
case [f, t, null]: | |
for (transition in _table) | |
{ | |
if (from == transition.from && to == transition.to && transition.remove == false) | |
{ | |
return true; | |
} | |
} | |
case [null, t, c]: | |
for (transition in _table) | |
{ | |
if (to == transition.to && condition == transition.condition && transition.remove == false) | |
{ | |
return true; | |
} | |
} | |
case [f, t, c]: | |
for (transition in _table) | |
{ | |
if (from == transition.from && to == transition.to && condition == transition.condition && transition.remove == false) | |
{ | |
return true; | |
} | |
} | |
} | |
return false; | |
} | |
} | |
class Transition<T> | |
{ | |
public function new() { } | |
/** | |
* If this function returns true, the transition becomes active. | |
* Note: you can override this function if you don't want to use functions passed as variables. | |
* @param target | |
* @return | |
*/ | |
public function evaluate(target:T):Bool | |
{ | |
return condition(target); | |
} | |
/** | |
* The state this transition applies to, or null for global transition, ie. from any state | |
*/ | |
public var from:Class<FlxFSMState<T>>; | |
/** | |
* The state this transition leads to | |
*/ | |
public var to:Class<FlxFSMState<T>>; | |
/** | |
* Function used for evaluation. | |
* The evaluation function may be overridden, in which case this param may be redundant. | |
*/ | |
public var condition:T->Bool; | |
/** | |
* Used to mark this transition for removal | |
*/ | |
public var remove:Bool = false; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment