using System; using System.Collections.Generic; using System.Reflection; namespace Otter { /// /// State machine that uses a specific type. This is really meant for using an enum as your list of states. /// If an enum is used, the state machine will automatically populate the states using methods in the parent /// Entity that match the name of the enum values. /// /// /// Say you have an enum named State, and it has the value "Walking" /// When the state machine is added to the Entity, it will match any methods named: /// EnterWalking /// UpdateWalking /// ExitWalking /// And use those to build the states. This saves a lot of boilerplate set up code. /// /// An enum of states. public class StateMachine : Component { #region Private Fields Dictionary states = new Dictionary(); List stateStack = new List(); List pushQueue = new List(); List pushPopBuffer = new List(); // keep track of the order of commands List timers = new List(); Dictionary> transitions = new Dictionary>(); bool firstChange = true; bool populatedMethods; TState changeTo; bool change; bool updating; bool noState = true; TState topState { get { if (stateStack.Count > 0) { return stateStack[0]; } return CurrentState; } } #endregion #region Private Properties State s { get { if (!states.ContainsKey(CurrentState)) { return null; } return states[CurrentState]; } } #endregion #region Public Fields /// /// Determines if the StateMachine will autopopulate its states based off of the values of the Enum. /// public bool AutoPopulate = true; #endregion #region Public Properties /// /// The current state. /// public TState CurrentState { get; private set; } #endregion #region Constructors /// /// Create a new StateMachine. /// public StateMachine() { } #endregion #region Private Methods void Transition(TState from, TState to) { if (transitions.ContainsKey(from)) { if (transitions[from].ContainsKey(to)) { transitions[from][to](); } } } void EnsurePopulatedMethods() { if (AutoPopulate) { // Populate the methods on the state change. if (!populatedMethods) { populatedMethods = true; if (typeof(TState).IsEnum) { foreach (TState value in Enum.GetValues(typeof(TState))) { AddState(value); } } } } } void PushStateImmediate(TState state) { EnsurePopulatedMethods(); stateStack.Insert(0, state); timers.Insert(0, 0); var from = CurrentState; if (stateStack.Count > 1) { timers[1] = Timer; } Timer = 0; CurrentState = topState; s.Enter(); Transition(from, CurrentState); noState = false; } void PopStateImmediate() { EnsurePopulatedMethods(); if (stateStack.Count == 0) return; // No states to pop! stateStack.RemoveAt(0); timers.RemoveAt(0); s.Exit(); var from = CurrentState; CurrentState = topState; Transition(from, CurrentState); if (stateStack.Count > 0) { Timer = timers[0]; } else { noState = true; // No more states... :( Timer = 0; } } void ChangeState() { if (change) { var state = changeTo; change = false; EnsurePopulatedMethods(); if (!firstChange) { if (states.ContainsKey(CurrentState)) { if (states[CurrentState] == states[state]) return; // No change actually happening so return } } Timer = 0; var fromState = CurrentState; if (states.ContainsKey(state)) { if (s != null && !firstChange) { s.Exit(); } CurrentState = state; if (s == null) throw new NullReferenceException("Next state is null."); s.Enter(); noState = false; Transition(fromState, CurrentState); } if (firstChange) { firstChange = false; } } else { pushPopBuffer.ForEach(push => { if (push) { // Push var pushState = pushQueue[0]; pushQueue.RemoveAt(0); PushStateImmediate(pushState); } else { // Pop PopStateImmediate(); } }); pushPopBuffer.Clear(); } } #endregion #region Public Methods /// /// Finds methods that match the enum state in the Entity. This happens in the Added() method of the /// component if AutoPopulate is set to true. /// /// The Entity to get methods from. public void PopulateMethodsFromEntity(Entity e) { if (typeof(TState).IsEnum) { foreach (TState value in Enum.GetValues(typeof(TState))) { AddState(value, e); } } } /// /// Finds methods that match the enum state in the Entity. This happens in the Added() method of the /// component if AutoPopulate is set to true. If no Entity is specified, get the methods from the /// Entity that owns this component. /// public void PopulateMethodsFromEntity() { PopulateMethodsFromEntity(Entity); } /// /// Change the state. Exit will be called on the current state followed by Enter on the new state. /// If the state machine is currently updating then the state change will not occur until after the /// update has completed. /// /// The state to change to. public void ChangeState(TState state) { pushQueue.Clear(); stateStack.Clear(); changeTo = state; change = true; if (updating) { } else { ChangeState(); } } /// /// Push a state onto a stack of states. The state machine will always run the top of the stack. /// /// The state to push. public void PushState(TState state) { if (updating) { pushQueue.Add(state); pushPopBuffer.Add(true); //true means push } else { PushStateImmediate(state); } } /// /// Pop the top state on the stack (if there is a stack.) /// public void PopState() { if (updating) { pushPopBuffer.Add(false); //false means pop } else { PopStateImmediate(); } } /// /// Update the State Machine. /// public override void Update() { base.Update(); if (states.ContainsKey(CurrentState) && !noState) { updating = true; s.Update(); updating = false; ChangeState(); } } /// /// Add a transition callback for when going from one state to another. /// /// The State that is ending. /// The State that is starting. /// The Action to run when the machine goes from the fromState to the toState. public void AddTransition(TState fromState, TState toState, Action function) { if (!transitions.ContainsKey(fromState)) { transitions.Add(fromState, new Dictionary()); } transitions[fromState].Add(toState, function); } /// /// Add a state with three Actions. /// /// The key to reference the State with. /// The method to call when entering this state. /// The method to call when updating this state. /// The method to call when exiting this state. public void AddState(TState key, Action onEnter, Action onUpdate, Action onExit) { states.Add(key, new State(onEnter, onUpdate, onExit)); } /// /// Add a state with just an update Action. /// /// The key to reference the State with. /// The method to call when updating this state. public void AddState(TState key, Action onUpdate) { states.Add(key, new State(onUpdate)); } /// /// Add a state. /// /// The key to reference the State with. /// The State to add. public void AddState(TState key, State value) { states.Add(key, value); } /// /// Add a state using reflection to retrieve the approriate methods on the Entity. /// For example, a key with a value of "Idle" will retrieve the methods "EnterIdle" "UpdateIdle" and "ExitIdle" automatically. /// /// The key to reference the State with. public void AddState(TState key, Entity e = null) { if (e == null) e = Entity; // Use the Component's Entity if no entity specified. if (states.ContainsKey(key)) return; // Dont add duplicate states. var state = new State(); var name = key.ToString(); //Using reflection to find all the appropriate functions! MethodInfo mi; mi = e.GetType().GetMethod("Enter" + name, BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Public | BindingFlags.FlattenHierarchy); if (mi == null) e.GetType().GetMethod("Enter" + name, BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Public); if (mi != null) { state.OnEnter = (Action)Delegate.CreateDelegate(typeof(Action), e, mi); } mi = e.GetType().GetMethod("Update" + name, BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Public | BindingFlags.FlattenHierarchy); if (mi == null) e.GetType().GetMethod("Update" + name, BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Public); if (mi != null) { state.OnUpdate = (Action)Delegate.CreateDelegate(typeof(Action), e, mi); } mi = e.GetType().GetMethod("Exit" + name, BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Public | BindingFlags.FlattenHierarchy); if (mi == null) e.GetType().GetMethod("Exit" + name, BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Public); if (mi != null) { state.OnExit = (Action)Delegate.CreateDelegate(typeof(Action), e, mi); } states.Add(key, state); } #endregion } /// /// Used in StateMachine. Contains functions for enter, update, and exit. /// public class State { #region Public Fields /// /// The method to call when entering this state. /// public Action OnEnter = delegate { }; /// /// The method to call when updating this state. /// public Action OnUpdate = delegate { }; /// /// The method to call when exiting this state. /// public Action OnExit = delegate { }; #endregion #region Public Properties /// /// The Id that this state has been assigned. /// public int Id { get; internal set; } #endregion #region Constructors /// /// Create a new State with three Actions. /// /// The method to call when entering this state. /// The method to call when updating this state. /// The method to call when exiting this state. public State(Action onEnter = null, Action onUpdate = null, Action onExit = null) { Functions(onEnter, onUpdate, onExit); } /// /// Create a new State with just an update Action. /// /// The method to call when updating this state. public State(Action onUpdate) : this(null, onUpdate) { } #endregion #region Public Methods /// /// Set all three of the methods for enter, update, and exit. /// /// The method to call when entering this state. /// The method to call when updating this state. /// The method to call when exiting this state. public void Functions(Action onEnter, Action onUpdate, Action onExit) { if (onEnter != null) { OnEnter = onEnter; } if (onUpdate != null) { OnUpdate = onUpdate; } if (onExit != null) { OnExit = onExit; } } /// /// Call OnUpdate. /// public void Update() { OnUpdate(); } /// /// Call OnEnter. /// public void Enter() { OnEnter(); } /// /// Call OnExit. /// public void Exit() { OnExit(); } #endregion } }