├── README.md └── stateMachine ├── State.hx ├── StateMachine.hx └── StateMachineEvent.hx /README.md: -------------------------------------------------------------------------------- 1 | Haxe State Machine 2 | ================= 3 | 4 | About 5 | ----- 6 | 7 | A Haxe port of AS3 State Machine. 8 | 9 | This package allows you to create simple and hierarchical StateMachines. 10 | State Machines are composed of states, and each state has (optional) callbacks for entering and exiting state. It's also possible to restrict the transition from states using the from property. 11 | 12 | Learn more about state machines and its applications on http://ai-depot.com/FiniteStateMachines/FSM.html 13 | 14 | 15 | Usage 16 | ----- 17 | Available states can be set with addState and initial state can be set using initialState setter. 18 | 19 | The following example creates a state machine for a player model with 3 states (Playing, paused and stopped) 20 | 21 | playerSM = new StateMachine(); 22 | playerSM.addState("playing",{ enter: onPlayingEnter, exit: onPlayingExit, from:["paused","stopped"] }); 23 | playerSM.addState("paused",{ enter: onPausedEnter, from:["playing"]}); 24 | playerSM.addState("stopped",{ enter: onStoppedEnter, from:"*"}); 25 | 26 | playerSM.addEventListener(StateMachineEvent.TRANSITION_DENIED,transitionDeniedFunction); 27 | playerSM.addEventListener(StateMachineEvent.TRANSITION_COMPLETE,transitionCompleteFunction); 28 | 29 | playerSM.initialState = "stopped"; 30 | 31 | 32 | It's also possible to create hierarchical state machines using the argument "parent" in the addState method. This example shows the creation of a hierarchical state machine for the monster of a game (Its a simplified version of the state machine used to control the AI in the original Quake game) 33 | 34 | monsterSM = new StateMachine(); 35 | 36 | monsterSM.addState("idle",{enter:onIdle, from:["attack"]}); 37 | monsterSM.addState("attack",{enter:onAttack, from:["idle"]}); 38 | monsterSM.addState("melee attack",{parent:"atack", enter:onMeleeAttack, from:["attack"]}); 39 | monsterSM.addState("smash",{parent:"melee attack", enter:onSmash}); 40 | monsterSM.addState("punch",{parent:"melee attack", enter:onPunch}); 41 | monsterSM.addState("missle attack",{parent:"attack", enter:onMissle}); 42 | monsterSM.addState("die",{enter:onDead, from:"attack", enter:onDie}); 43 | 44 | monsterSM.initialState = "idle"; 45 | 46 | License 47 | ------- 48 | 49 | Haxe-FSM is released under the Open Source MIT license, which gives you the possibility to use it and modify it in every circumstance. -------------------------------------------------------------------------------- /stateMachine/State.hx: -------------------------------------------------------------------------------- 1 | package stateMachine; 2 | 3 | class State 4 | { 5 | public var name:String; 6 | public var from:Dynamic; 7 | public var enter:StateMachineEvent->Void; 8 | public var exit:StateMachineEvent->Void; 9 | 10 | @:isVar public var parent(get, set):State; 11 | public var children:Array; 12 | 13 | public function new(name:String, from:Dynamic = null, enter:StateMachineEvent->Void = null, exit:StateMachineEvent->Void = null, parent:State = null) 14 | { 15 | this.name = name; 16 | if (!from) from = "*"; 17 | this.from = from; 18 | this.enter = enter; 19 | this.exit = exit; 20 | this.children = []; 21 | if (parent != null) 22 | { 23 | this.parent = parent; 24 | } 25 | } 26 | 27 | inline function set_parent(parent:State):State 28 | { 29 | this.parent = parent; 30 | this.parent.children.push(this); 31 | return this.parent; 32 | } 33 | 34 | inline function get_parent():State 35 | { 36 | return parent; 37 | } 38 | 39 | public var root(get, never):State; 40 | inline function get_root():State 41 | { 42 | var parentState:State = parent; 43 | if(parentState != null) 44 | { 45 | while (parentState.parent != null) 46 | { 47 | parentState = parentState.parent; 48 | } 49 | } 50 | return parentState; 51 | } 52 | 53 | public var parents(get, never):Array; 54 | inline function get_parents():Array 55 | { 56 | var parentList:Array = []; 57 | var parentState:State = parent; 58 | if(parentState != null) 59 | { 60 | parentList.push(parentState); 61 | while (parentState.parent != null) 62 | { 63 | parentState = parentState.parent; 64 | parentList.push(parentState); 65 | } 66 | } 67 | return parentList; 68 | } 69 | 70 | public function toString():String 71 | { 72 | return this.name; 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /stateMachine/StateMachine.hx: -------------------------------------------------------------------------------- 1 | package stateMachine; 2 | 3 | import flash.utils.Dictionary; 4 | import openfl.events.EventDispatcher; 5 | 6 | class StateMachine extends EventDispatcher 7 | { 8 | public var id:String; 9 | /* @private */ 10 | private var _state:String; 11 | /* @private */ 12 | public var states(get, null):Map; 13 | /* @private */ 14 | private var _outEvent:StateMachineEvent; 15 | /* @private */ 16 | private var parentState:State; 17 | /* @private */ 18 | private var parentStates:Array; 19 | /* @private */ 20 | private var path:Array; 21 | 22 | /** 23 | * Creates a generic StateMachine. Available states can be set with addState and initial state can 24 | * be set using initialState setter. 25 | * @example This sample creates a state machine for a player model with 3 states (Playing, paused and stopped) 26 | *
 27 | 	 *	playerSM = new StateMachine();
 28 | 	 *
 29 | 	 *	playerSM.addState("playing",{ enter: onPlayingEnter, exit: onPlayingExit, from:["paused","stopped"] });
 30 | 	 *	playerSM.addState("paused",{ enter: onPausedEnter, from:"playing"});
 31 | 	 *	playerSM.addState("stopped",{ enter: onStoppedEnter, from:"*"});
 32 | 	 *	
 33 | 	 *	playerSM.addEventListener(StateMachineEvent.TRANSITION_DENIED,transitionDeniedFunction);
 34 | 	 *	playerSM.addEventListener(StateMachineEvent.TRANSITION_COMPLETE,transitionCompleteFunction);
 35 | 	 *	
 36 | 	 *	playerSM.initialState = "stopped";
 37 | 	 * 
38 | * 39 | * It's also possible to create hierarchical state machines using the argument "parent" in the addState method 40 | * @example This example shows the creation of a hierarchical state machine for the monster of a game 41 | * (Its a simplified version of the state machine used to control the AI in the original Quake game) 42 | *
 43 | 	 *	monsterSM = new StateMachine()
 44 | 	 *	
 45 | 	 *	monsterSM.addState("idle",{enter:onIdle, from:"attack"})
 46 | 	 *	monsterSM.addState("attack",{enter:onAttack, from:"idle"})
 47 | 	 *	monsterSM.addState("melee attack",{parent:"atack", enter:onMeleeAttack, from:"attack"})
 48 | 	 *	monsterSM.addState("smash",{parent:"melle attack", enter:onSmash})
 49 | 	 *	monsterSM.addState("punch",{parent:"melle attack", enter:onPunch})
 50 | 	 *	monsterSM.addState("missle attack",{parent:"attack", enter:onMissle})
 51 | 	 *	monsterSM.addState("die",{enter:onDead, from:"attack", enter:onDie})
 52 | 	 *	
 53 | 	 *	monsterSM.initialState = "idle"
 54 | 	 *	
55 | */ 56 | public function new() 57 | { 58 | super(); 59 | states = new Map(); 60 | } 61 | 62 | /** 63 | * Adds a new state 64 | * @param stateName The name of the new State 65 | * @param stateData A hash containing state enter and exit callbacks and allowed states to transition from 66 | * The "from" property can be a string or and array with the state names or * to allow any transition 67 | **/ 68 | public function addState(stateName:String, stateData:Dynamic=null):Void 69 | { 70 | #if (debug) 71 | if ( states.exists(stateName) ) trace("[StateMachine]", id, "Overriding existing state " + stateName); 72 | #end 73 | if(stateData == null) stateData = {}; 74 | states[stateName] = new State(stateName, stateData.from, stateData.enter, stateData.exit, states[stateData.parent]); 75 | } 76 | 77 | /** 78 | * Sets the first state, calls enter callback and dispatches TRANSITION_COMPLETE 79 | * These will only occour if no state is defined 80 | * @param stateName The name of the State 81 | **/ 82 | public var initialState(never, set):String; 83 | public function set_initialState(stateName:String):String 84 | { 85 | if (_state == null && states.exists(stateName )){ 86 | _state = stateName; 87 | 88 | var _callbackEvent:StateMachineEvent = new StateMachineEvent(StateMachineEvent.ENTER_CALLBACK); 89 | _callbackEvent.toState = stateName; 90 | 91 | if(states[_state].root != null){ 92 | parentStates = states[_state].parents; 93 | var j:Int = states[_state].parents.length-1; 94 | while( j>=0 ){ 95 | if(parentStates[j].enter != null){ 96 | _callbackEvent.currentState = parentStates[j].name; 97 | parentStates[j].enter(_callbackEvent); 98 | } 99 | j--; 100 | } 101 | } 102 | 103 | if(states[_state].enter != null){ 104 | _callbackEvent.currentState = _state; 105 | states[_state].enter(_callbackEvent); 106 | } 107 | _outEvent = new StateMachineEvent(StateMachineEvent.TRANSITION_COMPLETE); 108 | _outEvent.toState = stateName; 109 | dispatchEvent(_outEvent); 110 | } 111 | return _state; 112 | } 113 | 114 | /** 115 | * Getters for the current state and for the Dictionary of states 116 | */ 117 | public var state(get, never):State; 118 | inline function get_state():State 119 | { 120 | return states[_state]; 121 | } 122 | 123 | inline function get_states():Map 124 | { 125 | return states; 126 | } 127 | 128 | public function getStateByName( name:String ):State 129 | { 130 | for( s in states ){ 131 | if( s.name == name ) 132 | return s; 133 | } 134 | 135 | return null; 136 | } 137 | /** 138 | * Verifies if a transition can be made from the current state to the state passed as param 139 | * @param stateName The name of the State 140 | **/ 141 | public function canChangeStateTo(stateName:String):Bool 142 | { 143 | return (stateName != _state && 144 | (states[stateName].from.indexOf(_state)!=-1 || states[stateName].from == "*" || states[stateName].parent == state)); 145 | } 146 | 147 | /** 148 | * Discovers the how many "exits" and how many "enters" are there between two 149 | * given states and returns an array with these two integers 150 | * @param stateFrom The state to exit 151 | * @param stateTo The state to enter 152 | **/ 153 | public function findPath(stateFrom:String, stateTo:String):Array 154 | { 155 | // Verifies if the states are in the same "branch" or have a common parent 156 | var fromState:State = states[stateFrom]; 157 | var c:Int = 0; 158 | var d:Int = 0; 159 | while (fromState != null) 160 | { 161 | d=0; 162 | var toState:State = states[stateTo]; 163 | while (toState != null) 164 | { 165 | if(fromState == toState) 166 | { 167 | // They are in the same brach or have a common parent Common parent 168 | return [c,d]; 169 | } 170 | d++; 171 | toState = toState.parent; 172 | } 173 | c++; 174 | fromState = fromState.parent; 175 | } 176 | // No direct path, no commom parent: exit until root then enter until element 177 | return [c,d]; 178 | } 179 | 180 | /** 181 | * Changes the current state 182 | * This will only be done if the intended state allows the transition from the current state 183 | * Changing states will call the exit callback for the exiting state and enter callback for the entering state 184 | * @param stateTo The name of the state to transition to 185 | **/ 186 | public function changeState(stateTo:String):Void 187 | { 188 | // If there is no state that maches stateTo 189 | if ( !states.exists(stateTo) ) { 190 | #if (debug) 191 | trace("[StateMachine]", id, "Cannot make transition: State " + stateTo +" is not defined"); 192 | #end 193 | return; 194 | } 195 | 196 | // If current state is not allowed to make this transition 197 | if(!canChangeStateTo(stateTo)) 198 | { 199 | #if (debug) 200 | trace("[StateMachine]", id, "Transition to " + stateTo +" denied"); 201 | #end 202 | _outEvent = new StateMachineEvent(StateMachineEvent.TRANSITION_DENIED); 203 | _outEvent.fromState = _state; 204 | _outEvent.toState = stateTo; 205 | _outEvent.allowedStates = states[stateTo].from; 206 | dispatchEvent(_outEvent); 207 | return; 208 | } 209 | 210 | // call exit and enter callbacks (if they exits) 211 | path = findPath(_state,stateTo); 212 | if(path[0]>0) 213 | { 214 | var _exitCallbackEvent:StateMachineEvent = new StateMachineEvent(StateMachineEvent.EXIT_CALLBACK); 215 | _exitCallbackEvent.toState = stateTo; 216 | _exitCallbackEvent.fromState = _state; 217 | 218 | if(states[_state].exit != null){ 219 | _exitCallbackEvent.currentState = _state; 220 | states[_state].exit(_exitCallbackEvent); 221 | } 222 | parentState = states[_state]; 223 | var i:Int=0; 224 | while( i0) 239 | { 240 | var _enterCallbackEvent:StateMachineEvent = new StateMachineEvent(StateMachineEvent.ENTER_CALLBACK); 241 | _enterCallbackEvent.toState = stateTo; 242 | _enterCallbackEvent.fromState = oldState; 243 | 244 | if(states[stateTo].root != null) 245 | { 246 | parentStates = states[stateTo].parents; 247 | var k:Int = path[1]-2; 248 | while( k>=0 ) 249 | { 250 | if(parentStates[k] != null && parentStates[k].enter != null){ 251 | _enterCallbackEvent.currentState = parentStates[k].name; 252 | parentStates[k].enter(_enterCallbackEvent); 253 | } 254 | k--; 255 | } 256 | } 257 | 258 | if(states[_state].enter != null){ 259 | _enterCallbackEvent.currentState = _state; 260 | states[_state].enter(_enterCallbackEvent); 261 | } 262 | } 263 | #if (debug) 264 | trace("[StateMachine]", id, "State Changed to " + _state); 265 | #end 266 | 267 | // Transition is complete. dispatch TRANSITION_COMPLETE 268 | _outEvent = new StateMachineEvent(StateMachineEvent.TRANSITION_COMPLETE); 269 | _outEvent.fromState = oldState; 270 | _outEvent.toState = stateTo; 271 | dispatchEvent(_outEvent); 272 | } 273 | } 274 | -------------------------------------------------------------------------------- /stateMachine/StateMachineEvent.hx: -------------------------------------------------------------------------------- 1 | package stateMachine; 2 | 3 | import flash.events.Event; 4 | 5 | class StateMachineEvent extends Event 6 | { 7 | public static inline var EXIT_CALLBACK:String = "exit"; 8 | public static inline var ENTER_CALLBACK:String = "enter"; 9 | public static inline var TRANSITION_COMPLETE:String = "transition complete"; 10 | public static inline var TRANSITION_DENIED:String = "transition denied"; 11 | 12 | public var fromState : String; 13 | public var toState : String; 14 | public var currentState : String; 15 | public var allowedStates : Dynamic; 16 | 17 | public function new(type:String, bubbles:Bool=false, cancelable:Bool=false) 18 | { 19 | super(type, bubbles, cancelable); 20 | } 21 | } 22 | --------------------------------------------------------------------------------