diff --git a/README.md b/README.md index da45dc8..2d425e8 100644 --- a/README.md +++ b/README.md @@ -33,33 +33,34 @@ public class MyGameplayScript : MonoBehaviour Init, Play, Win, - Lose + Lose, + Total // Last state, needed to inherit other states } - StateMachine fsm; + protected StateMachine fsm; - void Awake(){ - fsm = new StateMachine(this); //2. The main bit of "magic". - + protected void Awake(){ + fsm = new StateMachine(this); //2. The main bit of "magic". + fsm.AddStates(); //2a. Load the states fsm.ChangeState(States.Init); //3. Easily trigger state transitions } - void Init_Enter() + protected virtual void Init_Enter() // protected and virtual to allow overriding { Debug.Log("Ready"); } - void Play_Enter() + protected virtual void Play_Enter() { Debug.Log("Spawning Player"); } - void Play_FixedUpdate() + protected virtual void Play_FixedUpdate() { Debug.Log("Doing Physics stuff"); } - void Play_Update() + protected virtual void Play_Update() { if(player.health <= 0) { @@ -67,22 +68,48 @@ public class MyGameplayScript : MonoBehaviour } } - void Play_Exit() + protected virtual void Play_Exit() { Debug.Log("Despawning Player"); } - void Win_Enter() + protected virtual void Win_Enter() { Debug.Log("Game Over - you won!"); } - void Lose_Enter() + protected virtual void Lose_Enter() { Debug.Log("Game Over - you lost!"); } } + +public class ChildTest : MyGameplayScript{ +public enum ChildStates + { + First = MyGameplayScript.States.Total,// Extend parent states + Die, + Total // Last state, needed to inherit other states + } + + override protected void Awake(){ + base.Awake(); // Initialize fsm + + fsm.AddStates(); //Load the extended states + //fsm.ChangeState(ChildStates.Die); //Optionally change state + } + + protected virtual void Die_Enter() + { + Debug.Log("You died!"); + } + + override protected void Win_Enter() + { + Debug.Log("The child won!"); + } +} ``` ### State Methods are defined by underscore convention ( `StateName_Method` ) @@ -138,10 +165,11 @@ public class Driver This is a very simple class. It doesn't have to be called `Driver`; the only constraint is that it must contain `StateEvent` fields. When we pass this to our state machine definition, it will take care of everything needed to set up new State event hooks. ```C# -StateMachine fsm; +StateMachine fsm; void Awake(){ - fsm = new StateMachine(this); + fsm = new StateMachine(this); + fsm.AddStates(); } void Play_Enter() diff --git a/StateMachine/Assets/MonsterLove/Runtime/Events/StateEvent.cs b/StateMachine/Assets/MonsterLove/Runtime/Events/StateEvent.cs index daadcd9..71e1599 100644 --- a/StateMachine/Assets/MonsterLove/Runtime/Events/StateEvent.cs +++ b/StateMachine/Assets/MonsterLove/Runtime/Events/StateEvent.cs @@ -26,107 +26,106 @@ // Warning! // This is somewhat fragile Event pattern implementation. Recommended they aren't used outside of the state machine // -namespace MonsterLove.StateMachine -{ - public class StateEvent - { +namespace MonsterLove.StateMachine { + public class StateEvent { private Func getStateInt; private Func isInvokeAllowed; - private Action[] routingTable; + private List routingTable; - public StateEvent(Func isInvokeAllowed, Func stateProvider, int capacity) - { + public StateEvent(Func isInvokeAllowed, Func stateProvider) { this.isInvokeAllowed = isInvokeAllowed; this.getStateInt = stateProvider; - routingTable = new Action[capacity]; + routingTable = new List(0); } - - internal void AddListener(int stateInt, Action listener) - { + + internal void AddListener(int stateInt, Action listener) { routingTable[stateInt] = listener; } - public void Invoke() - { - if (isInvokeAllowed != null && !isInvokeAllowed()) - { + public void Invoke() { + if(isInvokeAllowed != null && !isInvokeAllowed()) { return; } Action call = routingTable[getStateInt()]; - if (call != null) - { + if(call != null) { call(); return; } } + + private void GrowToFill() { + while(routingTable.Count < routingTable.Capacity) { + routingTable.Add(null); + } + } } - - public class StateEvent - { + + public class StateEvent { private Func getStateInt; private Func isInvokeAllowed; - private Action[] routingTable; - - public StateEvent(Func isInvokeAllowed, Func stateProvider, int capacity) - { + private List> routingTable; + + public StateEvent(Func isInvokeAllowed, Func stateProvider) { this.isInvokeAllowed = isInvokeAllowed; this.getStateInt = stateProvider; - routingTable = new Action[capacity]; + routingTable = new List>(0); } - internal void AddListener(int stateInt, Action listener) - { + internal void AddListener(int stateInt, Action listener) { routingTable[stateInt] = listener; } - public void Invoke(T param) - { - if (isInvokeAllowed != null && !isInvokeAllowed()) - { + public void Invoke(T param) { + if(isInvokeAllowed != null && !isInvokeAllowed()) { return; } Action call = routingTable[getStateInt()]; - if (call != null) - { + if(call != null) { call(param); return; } } + + private void GrowToFill() { + while(routingTable.Count < routingTable.Capacity) { + routingTable.Add(null); + } + } } - - public class StateEvent - { + + public class StateEvent { private Func getStateInt; private Func isInvokeAllowed; - private Action[] routingTable; - - public StateEvent(Func isInvokeAllowed, Func stateProvider, int capacity) - { + private List> routingTable; + + public StateEvent(Func isInvokeAllowed, Func stateProvider) { this.isInvokeAllowed = isInvokeAllowed; this.getStateInt = stateProvider; - routingTable = new Action[capacity]; + routingTable = new List>(0); } - internal void AddListener(int stateInt, Action listener) - { + internal void AddListener(int stateInt, Action listener) { routingTable[stateInt] = listener; } - public void Invoke(T1 param1, T2 param2) - { - if (isInvokeAllowed != null && !isInvokeAllowed()) - { + public void Invoke(T1 param1, T2 param2) { + if(isInvokeAllowed != null && !isInvokeAllowed()) { return; } - + Action call = routingTable[getStateInt()]; - if (call != null) - { + if(call != null) { call(param1, param2); return; } } + + private void GrowToFill() { + while(routingTable.Count < routingTable.Capacity) { + routingTable.Add(null); + } + } } } \ No newline at end of file diff --git a/StateMachine/Assets/MonsterLove/Runtime/StateMachine.cs b/StateMachine/Assets/MonsterLove/Runtime/StateMachine.cs index 469b448..88071da 100644 --- a/StateMachine/Assets/MonsterLove/Runtime/StateMachine.cs +++ b/StateMachine/Assets/MonsterLove/Runtime/StateMachine.cs @@ -27,43 +27,45 @@ using UnityEngine; using Object = System.Object; -namespace MonsterLove.StateMachine -{ - public enum StateTransition - { +namespace MonsterLove.StateMachine { + public enum StateTransition { Safe, Overwrite, } - public interface IStateMachine - { + public interface IStateMachine { MonoBehaviour Component { get; } TDriver Driver { get; } bool IsInTransition { get; } } - public class StateMachine : StateMachine where TState : struct, IConvertible, IComparable - { - public StateMachine(MonoBehaviour component) : base(component) - { + public class StateMachine : StateMachine { + public StateMachine(MonoBehaviour component) : base(component) { + + } + } + + public class StateMachine : StateMachine where TState : struct, IConvertible, IComparable where TDriver : class, new() { // Used to maintain backwards compatibility + public StateMachine(MonoBehaviour component) : base(component) { + AddStates(); } } - public class StateMachine : IStateMachine where TState : struct, IConvertible, IComparable where TDriver : class, new() - { - public event Action Changed; + public class StateMachine : IStateMachine where TDriver : class, new() { + private const string ExcludeEnumName = "Total"; + + public event Action Changed; public bool reenter = false; private MonoBehaviour component; - private StateMapping lastState; - private StateMapping currentState; - private StateMapping destinationState; - private StateMapping queuedState; + private StateMapping lastState; + private StateMapping currentState; + private StateMapping destinationState; + private StateMapping queuedState; private TDriver rootDriver; - private Dictionary> stateLookup; - private Func enumConverter; + private Dictionary> stateLookup; private bool isInTransition = false; private IEnumerator currentTransition; @@ -73,26 +75,45 @@ public StateMachine(MonoBehaviour component) : base(component) private static BindingFlags bindingFlags = BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic; -#region Initialization - - public StateMachine(MonoBehaviour component) - { - this.component = component; - - //Compiler shenanigans to get ints from generic enums - Func identity = Identity; - enumConverter = Delegate.CreateDelegate(typeof(Func), identity.Method) as Func; - + #region Initialization + public void AddStates() { //Define States - var enumValues = Enum.GetValues(typeof(TState)); - if (enumValues.Length < 1) - { - throw new ArgumentException("Enum provided to Initialize must have at least 1 visible definition"); + var enumValues = Enum.GetValues(typeof(TStates)); + int enumLength = enumValues.Length; + if(enumValues.Length < 1) { + throw new ArgumentException("Enum provided must have at least 1 visible definition"); + } + else { + string[] enumNames = Enum.GetNames(typeof(TStates)); + foreach(string enumName in enumNames) { + if(enumName == ExcludeEnumName) { + if(enumValues.Length == 1) + throw new ArgumentException("Enum provided must have at least 1 visible definition not named '" + ExcludeEnumName + "'"); + + // Decrement length + enumLength--; + + // Exclude from values + TStates[] tmp = new TStates[enumLength]; + TStates exclude = (TStates)Enum.Parse(typeof(TStates), enumName); + int i = 0; + foreach(var enumValue in enumValues) { + if(EqualityComparer.Default.Equals((TStates)enumValue, exclude)) + continue; + + // Copy + tmp[i] = (TStates)enumValue; + i++; + } + enumValues = tmp; + + break; + } + } } - var enumBackingType = Enum.GetUnderlyingType(typeof(TState)); - if(enumBackingType != typeof(int)) - { + var enumBackingType = Enum.GetUnderlyingType(typeof(TStates)); + if(enumBackingType != typeof(int)) { throw new ArgumentException("Only enums with an underlying type of int are supported"); } @@ -105,64 +126,94 @@ public StateMachine(MonoBehaviour component) // } List eventFields = GetFilteredFields(typeof(TDriver), "MonsterLove.StateMachine.StateEvent"); Dictionary eventFieldsLookup = CreateFieldsLookup(eventFields); - - //Instantiate driver - // driver = new Driver(); - // for each StateEvent: - // StateEvent foo = new StateEvent(isAllowed, getStateInt, capacity); - rootDriver = CreateDriver(IsDispatchAllowed, GetStateInt, enumValues.Length, eventFields); - + + // Resize the driver + for(int i = 0; i < eventFields.Count; i++) { + //driver.Event.routingTable.Capacity = driver.Event.routingTable.Capacity + enumLength + FieldInfo fieldInfo = eventFields[i]; //Event + object obj = fieldInfo.GetValue(rootDriver); + fieldInfo = fieldInfo.FieldType.GetField("routingTable", BindingFlags.Instance | BindingFlags.NonPublic); //Event.routingTable + obj = fieldInfo.GetValue(obj); + PropertyInfo propertyInfo = fieldInfo.FieldType.GetProperty("Capacity"); //Event.routingTable.Capacity + int capacity = (int)propertyInfo.GetValue(obj); + propertyInfo.SetValue(obj, capacity + enumLength); + + //driver.Event.GrowToFill() + fieldInfo = eventFields[i]; //Event + obj = fieldInfo.GetValue(rootDriver); + MethodInfo methodInfo = fieldInfo.FieldType.GetMethod("GrowToFill", BindingFlags.Instance | BindingFlags.NonPublic);//Event.GrowToFill() + methodInfo.Invoke(obj, null); + } + // Create a state mapping for each state defined in the enum - stateLookup = CreateStateLookup(this, enumValues); + Dictionary> states = CreateStateLookup(this, enumValues); + if(stateLookup != null) { + foreach(var kp in states) { + stateLookup[kp.Key] = kp.Value; + } + } + else + stateLookup = states; //Collect methods in target component MethodInfo[] methods = component.GetType().GetMethods(bindingFlags); - //Bind methods to states - for (int i = 0; i < methods.Length; i++) - { - TState state; + // Update bind methods to states + for(int i = 0; i < methods.Length; i++) { + TStates state; string evtName; - if (!ParseName(methods[i], out state, out evtName)) - { + if(!ParseName(methods[i], out state, out evtName)) { continue; //Skip methods where State_Event name convention could not be parsed } - StateMapping mapping = stateLookup[state]; + StateMapping mapping = stateLookup[state]; - if (eventFieldsLookup.ContainsKey(evtName)) - { + if(eventFieldsLookup.ContainsKey(evtName)) { //Bind methods defined in TDriver // driver.Foo.AddListener(StateOne_Foo); FieldInfo eventField = eventFieldsLookup[evtName]; - BindEvents(rootDriver, component, state, enumConverter(state), methods[i], eventField); + BindEvents(rootDriver, component, (Enum)(object)state, (int)(object)state, methods[i], eventField); } - else - { + else { //Bind Enter, Exit and Finally Methods BindEventsInternal(mapping, component, methods[i], evtName); } } + } + + public StateMachine(MonoBehaviour component) { + this.component = component; + + //Find all items in Driver class + // public class Driver + // { + // StateEvent Foo; <- Selected + // StateEvent Boo; <- Selected + // float x; <- Throw exception + // } + List eventFields = GetFilteredFields(typeof(TDriver), "MonsterLove.StateMachine.StateEvent"); + + //Instantiate driver + // driver = new Driver(); + // for each StateEvent: + // StateEvent foo = new StateEvent(isAllowed, getStateInt, capacity); + rootDriver = CreateDriver(IsDispatchAllowed, GetStateInt, eventFields); //Create nil state mapping currentState = null; } - static List GetFilteredFields(Type type, string searchTerm) - { + static List GetFilteredFields(Type type, string searchTerm) { List list = new List(); FieldInfo[] fields = type.GetFields(bindingFlags); - for (int i = 0; i < fields.Length; i++) - { + for(int i = 0; i < fields.Length; i++) { FieldInfo item = fields[i]; - if (item.FieldType.ToString().Contains(searchTerm)) - { + if(item.FieldType.ToString().Contains(searchTerm)) { list.Add(item); } - else - { + else { throw new ArgumentException(string.Format("{0} contains unsupported type {1}", type, item.FieldType)); } } @@ -170,12 +221,10 @@ static List GetFilteredFields(Type type, string searchTerm) return list; } - static Dictionary CreateFieldsLookup(List fields) - { + static Dictionary CreateFieldsLookup(List fields) { var dict = new Dictionary(); - for (int i = 0; i < fields.Count; i++) - { + for(int i = 0; i < fields.Count; i++) { FieldInfo item = fields[i]; dict.Add(item.Name, item); @@ -184,46 +233,39 @@ static Dictionary CreateFieldsLookup(List fields) return dict; } - static Dictionary> CreateStateLookup(StateMachine fsm, Array values) - { - var stateLookup = new Dictionary>(); - for (int i = 0; i < values.Length; i++) - { - var mapping = new StateMapping(fsm, (TState) values.GetValue(i), fsm.GetState); + static Dictionary> CreateStateLookup(StateMachine fsm, Array values) { + var stateLookup = new Dictionary>(); + for(int i = 0; i < values.Length; i++) { + var mapping = new StateMapping(fsm, (Enum)values.GetValue(i), fsm.GetState); stateLookup.Add(mapping.state, mapping); } return stateLookup; } - static TDriver CreateDriver(Func isInvokeAllowedCallback, Func getStateIntCallback, int capacity, List fieldInfos) - { - if (fieldInfos == null) - { + static TDriver CreateDriver(Func isInvokeAllowedCallback, Func getStateIntCallback, List fieldInfos) { + if(fieldInfos == null) { throw new ArgumentException(string.Format("Arguments cannot be null. Callback {0} fieldInfos {1}", isInvokeAllowedCallback, fieldInfos)); } TDriver driver = new TDriver(); - for (int i = 0; i < fieldInfos.Count; i++) - { + for(int i = 0; i < fieldInfos.Count; i++) { //driver.Event = new StateEvent(callback) FieldInfo fieldInfo = fieldInfos[i]; //Event - ConstructorInfo constructorInfo = fieldInfo.FieldType.GetConstructor(new Type[] {typeof(Func), typeof(Func), typeof(int)}); //StateEvent(Func invokeAllowed, Func getState, int capacity) - object obj = constructorInfo.Invoke(new object[] {isInvokeAllowedCallback, getStateIntCallback, capacity}); //obj = new StateEvent(Func isInvokeAllowed, Func stateProvider, int capacity); + ConstructorInfo constructorInfo = fieldInfo.FieldType.GetConstructor(new Type[] { typeof(Func), typeof(Func) }); //StateEvent(Func invokeAllowed, Func getState, int capacity) + object obj = constructorInfo.Invoke(new object[] { isInvokeAllowedCallback, getStateIntCallback }); //obj = new StateEvent(Func isInvokeAllowed, Func stateProvider, int capacity); fieldInfo.SetValue(driver, obj); //driver.Event = obj; } return driver; } - static bool ParseName(MethodInfo methodInfo, out TState state, out string eventName) - { - state = default(TState); + static bool ParseName(MethodInfo methodInfo, out TStates state, out string eventName) { + state = default(TStates); eventName = null; - if (methodInfo.GetCustomAttributes(typeof(CompilerGeneratedAttribute), true).Length != 0) - { + if(methodInfo.GetCustomAttributes(typeof(CompilerGeneratedAttribute), true).Length != 0) { return false; } @@ -231,20 +273,17 @@ static bool ParseName(MethodInfo methodInfo, out TState state, out string eventN int index = name.IndexOf('_'); //Ignore functions without an underscore - if (index < 0) - { + if(index < 0) { return false; } string stateName = name.Substring(0, index); eventName = name.Substring(index + 1); - try - { - state = (TState) Enum.Parse(typeof(TState), stateName); + try { + state = (TStates)Enum.Parse(typeof(TStates), stateName); } - catch (ArgumentException) - { + catch(ArgumentException) { //Not an method as listed in the state enum return false; } @@ -252,177 +291,151 @@ static bool ParseName(MethodInfo methodInfo, out TState state, out string eventN return true; } - static void BindEvents(TDriver driver, Component component, TState state, int stateInt, MethodInfo stateTargetDef, FieldInfo driverEvtDef) - { + static void BindEvents(TDriver driver, Component component, Enum state, int stateInt, MethodInfo stateTargetDef, FieldInfo driverEvtDef) { var genericTypes = driverEvtDef.FieldType.GetGenericArguments(); //get T1,T2,...TN from StateEvent var actionType = GetActionType(genericTypes); //typeof(Action) - + //evt.AddListener(State_Method); var obj = driverEvtDef.GetValue(driver); //driver.Foo var addMethodInfo = driverEvtDef.FieldType.GetMethod("AddListener", bindingFlags); // driver.Foo.AddListener Delegate del = null; - try - { + try { del = Delegate.CreateDelegate(actionType, component, stateTargetDef); } - catch (ArgumentException) - { + catch(ArgumentException) { throw new ArgumentException(string.Format("State ({0}_{1}) requires a callback of type: {2}, type found: {3}", state, driverEvtDef.Name, actionType, stateTargetDef)); } - - addMethodInfo.Invoke(obj, new object[] {stateInt, del}); //driver.Foo.AddListener(stateInt, component.State_Event); + addMethodInfo.Invoke(obj, new object[] { stateInt, del }); //driver.Foo.AddListener(stateInt, component.State_Event); } - static void BindEventsInternal(StateMapping targetState, Component component, MethodInfo method, string evtName) - { - switch (evtName) - { - case "Enter": - if (method.ReturnType == typeof(IEnumerator)) - { - targetState.hasEnterRoutine = true; - targetState.EnterRoutine = CreateDelegate>(method, component); - } - else - { - targetState.hasEnterRoutine = false; - targetState.EnterCall = CreateDelegate(method, component); - } + static void BindEventsInternal(StateMapping targetState, Component component, MethodInfo method, string evtName) { + switch(evtName) { + case "Enter": + if(method.ReturnType == typeof(IEnumerator)) { + targetState.hasEnterRoutine = true; + targetState.EnterRoutine = CreateDelegate>(method, component); + } + else { + targetState.hasEnterRoutine = false; + targetState.EnterCall = CreateDelegate(method, component); + } - break; - case "Exit": - if (method.ReturnType == typeof(IEnumerator)) - { - targetState.hasExitRoutine = true; - targetState.ExitRoutine = CreateDelegate>(method, component); - } - else - { - targetState.hasExitRoutine = false; - targetState.ExitCall = CreateDelegate(method, component); - } + break; + case "Exit": + if(method.ReturnType == typeof(IEnumerator)) { + targetState.hasExitRoutine = true; + targetState.ExitRoutine = CreateDelegate>(method, component); + } + else { + targetState.hasExitRoutine = false; + targetState.ExitCall = CreateDelegate(method, component); + } - break; - case "Finally": - targetState.Finally = CreateDelegate(method, component); - break; + break; + case "Finally": + targetState.Finally = CreateDelegate(method, component); + break; } } - static V CreateDelegate(MethodInfo method, Object target) where V : class - { + static V CreateDelegate(MethodInfo method, Object target) where V : class { var ret = (Delegate.CreateDelegate(typeof(V), target, method) as V); - if (ret == null) - { + if(ret == null) { throw new ArgumentException("Unable to create delegate for method called " + method.Name); } return ret; } - static Type GetActionType(Type[] genericArgs) - { - switch (genericArgs.Length) - { - case 0: - return typeof(Action); - case 1: - return typeof(Action<>).MakeGenericType(genericArgs); - case 2: - return typeof(Action<,>).MakeGenericType(genericArgs); - default: - throw new ArgumentOutOfRangeException(string.Format("Cannot create Action Type with {0} type arguments", genericArgs.Length)); + static Type GetActionType(Type[] genericArgs) { + switch(genericArgs.Length) { + case 0: + return typeof(Action); + case 1: + return typeof(Action<>).MakeGenericType(genericArgs); + case 2: + return typeof(Action<,>).MakeGenericType(genericArgs); + default: + throw new ArgumentOutOfRangeException(string.Format("Cannot create Action Type with {0} type arguments", genericArgs.Length)); } } -#endregion + #endregion -#region ChangeStates + #region ChangeStates - public void ChangeState(TState newState) - { + public void ChangeState(Enum newState) { ChangeState(newState, StateTransition.Safe); } - public void ChangeState(TState newState, StateTransition transition) - { - if (stateLookup == null) - { + public void ChangeState(Enum newState, StateTransition transition) { + if(stateLookup == null) { throw new Exception("States have not been configured, please call initialized before trying to set state"); } - if (!stateLookup.ContainsKey(newState)) - { + if(!stateLookup.ContainsKey(newState)) { throw new Exception("No state with the name " + newState.ToString() + " can be found. Please make sure you are called the correct type the statemachine was initialized with"); } var nextState = stateLookup[newState]; - if (!reenter && currentState == nextState) - { + if(!reenter && currentState == nextState) { return; } //Cancel any queued changes. - if (queuedChange != null) - { + if(queuedChange != null) { component.StopCoroutine(queuedChange); queuedChange = null; } - switch (transition) - { - //case StateMachineTransition.Blend: - //Do nothing - allows the state transitions to overlap each other. This is a dumb idea, as previous state might trigger new changes. - //A better way would be to start the two couroutines at the same time. IE don't wait for exit before starting start. - //How does this work in terms of overwrite? - //Is there a way to make this safe, I don't think so? - //break; - case StateTransition.Safe: - if (isInTransition) + switch(transition) { + //case StateMachineTransition.Blend: + //Do nothing - allows the state transitions to overlap each other. This is a dumb idea, as previous state might trigger new changes. + //A better way would be to start the two couroutines at the same time. IE don't wait for exit before starting start. + //How does this work in terms of overwrite? + //Is there a way to make this safe, I don't think so? + //break; + case StateTransition.Safe: + if(isInTransition) { + if(exitRoutine != null) //We are already exiting current state on our way to our previous target state { - if (exitRoutine != null) //We are already exiting current state on our way to our previous target state - { - //Overwrite with our new target - destinationState = nextState; - return; - } - - if (enterRoutine != null) //We are already entering our previous target state. Need to wait for that to finish and call the exit routine. - { - //Damn, I need to test this hard - queuedChange = WaitForPreviousTransition(nextState); - component.StartCoroutine(queuedChange); - return; - } + //Overwrite with our new target + destinationState = nextState; + return; } - break; - case StateTransition.Overwrite: - if (currentTransition != null) + if(enterRoutine != null) //We are already entering our previous target state. Need to wait for that to finish and call the exit routine. { - component.StopCoroutine(currentTransition); + //Damn, I need to test this hard + queuedChange = WaitForPreviousTransition(nextState); + component.StartCoroutine(queuedChange); + return; } + } - if (exitRoutine != null) - { - component.StopCoroutine(exitRoutine); - } + break; + case StateTransition.Overwrite: + if(currentTransition != null) { + component.StopCoroutine(currentTransition); + } - if (enterRoutine != null) - { - component.StopCoroutine(enterRoutine); - } + if(exitRoutine != null) { + component.StopCoroutine(exitRoutine); + } - //Note: if we are currently in an EnterRoutine and Exit is also a routine, this will be skipped in ChangeToNewStateRoutine() - break; + if(enterRoutine != null) { + component.StopCoroutine(enterRoutine); + } + + //Note: if we are currently in an EnterRoutine and Exit is also a routine, this will be skipped in ChangeToNewStateRoutine() + break; } - if ((currentState != null && currentState.hasExitRoutine) || nextState.hasEnterRoutine) - { + if((currentState != null && currentState.hasExitRoutine) || nextState.hasEnterRoutine) { isInTransition = true; currentTransition = ChangeToNewStateRoutine(nextState, transition); component.StartCoroutine(currentTransition); @@ -430,21 +443,18 @@ public void ChangeState(TState newState, StateTransition transition) else //Same frame transition, no coroutines are present { destinationState = nextState; //Assign here so Exit() has a valid reference - - if (currentState != null) - { + + if(currentState != null) { currentState.ExitCall(); currentState.Finally(); } lastState = currentState; currentState = destinationState; - if (currentState != null) - { + if(currentState != null) { currentState.EnterCall(); - if (Changed != null) - { - Changed((TState) currentState.state); + if(Changed != null) { + Changed(currentState.state); } } @@ -452,25 +462,21 @@ public void ChangeState(TState newState, StateTransition transition) } } - private IEnumerator ChangeToNewStateRoutine(StateMapping newState, StateTransition transition) - { + private IEnumerator ChangeToNewStateRoutine(StateMapping newState, StateTransition transition) { destinationState = newState; //Cache this so that we can overwrite it and hijack a transition - if (currentState != null) - { - if (currentState.hasExitRoutine) - { + if(currentState != null) { + if(currentState.hasExitRoutine) { exitRoutine = currentState.ExitRoutine(); - if (exitRoutine != null && transition != StateTransition.Overwrite) //Don't wait for exit if we are overwriting + if(exitRoutine != null && transition != StateTransition.Overwrite) //Don't wait for exit if we are overwriting { yield return component.StartCoroutine(exitRoutine); } exitRoutine = null; } - else - { + else { currentState.ExitCall(); } @@ -480,150 +486,125 @@ private IEnumerator ChangeToNewStateRoutine(StateMapping newSta lastState = currentState; currentState = destinationState; - if (currentState != null) - { - if (currentState.hasEnterRoutine) - { + if(currentState != null) { + if(currentState.hasEnterRoutine) { enterRoutine = currentState.EnterRoutine(); - if (enterRoutine != null) - { + if(enterRoutine != null) { yield return component.StartCoroutine(enterRoutine); } enterRoutine = null; } - else - { + else { currentState.EnterCall(); } //Broadcast change only after enter transition has begun. - if (Changed != null) - { - Changed((TState) currentState.state); + if(Changed != null) { + Changed(currentState.state); } } isInTransition = false; } - IEnumerator WaitForPreviousTransition(StateMapping nextState) - { + IEnumerator WaitForPreviousTransition(StateMapping nextState) { queuedState = nextState; //Cache this so fsm.NextState is accurate; - - while (isInTransition) - { + + while(isInTransition) { yield return null; } queuedState = null; - ChangeState((TState) nextState.state); + ChangeState(nextState.state); } -#endregion + #endregion -#region Properties & Helpers + #region Properties & Helpers - public bool LastStateExists - { + public bool LastStateExists { get { return lastState != null; } } - public TState LastState - { - get - { - if (lastState == null) - { + public Enum LastState { + get { + if(lastState == null) { throw new NullReferenceException("LastState cannot be accessed before ChangeState() has been called at least twice"); } - return (TState) lastState.state; + return (Enum)lastState.state; } } - public TState NextState - { - get - { - if (queuedState != null) //In safe mode sometimes we need to wait for the destination state to complete, and will be stored in queued state + public Enum NextState { + get { + if(queuedState != null) //In safe mode sometimes we need to wait for the destination state to complete, and will be stored in queued state { - return (TState) queuedState.state; + return (Enum)queuedState.state; } - if (destinationState == null) - { + if(destinationState == null) { return State; } - return (TState) destinationState.state; + return (Enum)destinationState.state; } } - public TState State - { - get - { - if (currentState == null) - { + public Enum State { + get { + if(currentState == null) { throw new NullReferenceException("State cannot be accessed before ChangeState() has been called at least once"); } - return (TState) currentState.state; + return (Enum)currentState.state; } } - public bool IsInTransition - { + public bool IsInTransition { get { return isInTransition; } } - public TDriver Driver - { + public TDriver Driver { get { return rootDriver; } } - public MonoBehaviour Component - { + public MonoBehaviour Component { get { return component; } } //format as method so can be passed as Func - private TState GetState() - { + private Enum GetState() { return State; } - private int GetStateInt() - { - return enumConverter(State); + private int GetStateInt() { + return (int)(object)State; + //return enumConverter(State); } - + //Compiler shenanigans to get ints from generic enums - private static int Identity(int x) - { + private static int Identity(int x) { return x; } - private bool IsDispatchAllowed() - { - if (currentState == null) - { + private bool IsDispatchAllowed() { + if(currentState == null) { return false; } - if (IsInTransition) - { + if(IsInTransition) { return false; } return true; } -#endregion + #endregion -#region Static API + #region Static API //Static Methods @@ -632,10 +613,21 @@ private bool IsDispatchAllowed() /// /// The component with defined state methods /// A valid stateMachine instance to manage MonoBehaviour state transitions - public static StateMachine Initialize(MonoBehaviour component) - { + public static StateMachine Initialize(MonoBehaviour component) { + var engine = component.GetComponent(); + if(engine == null) engine = component.gameObject.AddComponent(); + + return engine.Initialize(component); + } + + /// + /// Inspects a MonoBehaviour for state methods as defined by the supplied Enum, and returns a stateMachine instance used to transition states. + /// + /// The component with defined state methods + /// A valid stateMachine instance to manage MonoBehaviour state transitions + public static StateMachine Initialize(MonoBehaviour component) { var engine = component.GetComponent(); - if (engine == null) engine = component.gameObject.AddComponent(); + if(engine == null) engine = component.gameObject.AddComponent(); return engine.Initialize(component); } @@ -646,14 +638,13 @@ public static StateMachine Initialize(MonoBehaviour component) /// The component with defined state methods /// The default starting state /// A valid stateMachine instance to manage MonoBehaviour state transitions - public static StateMachine Initialize(MonoBehaviour component, TState startState) - { + public static StateMachine Initialize(MonoBehaviour component, TState startState) { var engine = component.GetComponent(); - if (engine == null) engine = component.gameObject.AddComponent(); + if(engine == null) engine = component.gameObject.AddComponent(); return engine.Initialize(component, startState); } -#endregion + #endregion } } \ No newline at end of file diff --git a/StateMachine/Assets/MonsterLove/Runtime/StateMachineRunner.cs b/StateMachine/Assets/MonsterLove/Runtime/StateMachineRunner.cs index 2fa9997..6cb2965 100644 --- a/StateMachine/Assets/MonsterLove/Runtime/StateMachineRunner.cs +++ b/StateMachine/Assets/MonsterLove/Runtime/StateMachineRunner.cs @@ -25,10 +25,8 @@ using System.Collections.Generic; using UnityEngine; -namespace MonsterLove.StateMachine -{ - public class StateMachineRunner : MonoBehaviour - { +namespace MonsterLove.StateMachine { + public class StateMachineRunner : MonoBehaviour { private List> stateMachineList = new List>(); /// @@ -37,8 +35,7 @@ public class StateMachineRunner : MonoBehaviour /// An Enum listing different state transitions /// The component whose state will be managed /// - public StateMachine Initialize(MonoBehaviour component) where TState : struct, IConvertible, IComparable - { + public StateMachine Initialize(MonoBehaviour component) where TState : struct, IConvertible, IComparable { var fsm = new StateMachine(component); stateMachineList.Add(fsm); @@ -53,8 +50,7 @@ public StateMachine Initialize(MonoBehaviour component) where TS /// The component whose state will be managed /// The default start state /// - public StateMachine Initialize(MonoBehaviour component, TState startState) where TState : struct, IConvertible, IComparable - { + public StateMachine Initialize(MonoBehaviour component, TState startState) where TState : struct, IConvertible, IComparable { var fsm = Initialize(component); fsm.ChangeState(startState); @@ -62,48 +58,37 @@ public StateMachine Initialize(MonoBehaviour component, TState s return fsm; } - void FixedUpdate() - { - for (int i = 0; i < stateMachineList.Count; i++) - { + void FixedUpdate() { + for(int i = 0; i < stateMachineList.Count; i++) { var fsm = stateMachineList[i]; - if (!fsm.IsInTransition && fsm.Component.enabled) - { - fsm.Driver.FixedUpdate.Invoke(); + if(!fsm.IsInTransition && fsm.Component.enabled) { + fsm.Driver.FixedUpdate.Invoke(); } } } - void Update() - { - for (int i = 0; i < stateMachineList.Count; i++) - { + void Update() { + for(int i = 0; i < stateMachineList.Count; i++) { var fsm = stateMachineList[i]; - if (!fsm.IsInTransition && fsm.Component.enabled) - { + if(!fsm.IsInTransition && fsm.Component.enabled) { fsm.Driver.Update.Invoke(); } } } - void LateUpdate() - { - for (int i = 0; i < stateMachineList.Count; i++) - { + void LateUpdate() { + for(int i = 0; i < stateMachineList.Count; i++) { var fsm = stateMachineList[i]; - if (!fsm.IsInTransition && fsm.Component.enabled) - { + if(!fsm.IsInTransition && fsm.Component.enabled) { fsm.Driver.LateUpdate.Invoke(); } } } - public static void DoNothing() - { + public static void DoNothing() { } - public static IEnumerator DoNothingCoroutine() - { + public static IEnumerator DoNothingCoroutine() { yield break; } } diff --git a/StateMachine/Assets/MonsterLove/Runtime/StateMapping.cs b/StateMachine/Assets/MonsterLove/Runtime/StateMapping.cs index 45ed989..6a255f0 100644 --- a/StateMachine/Assets/MonsterLove/Runtime/StateMapping.cs +++ b/StateMachine/Assets/MonsterLove/Runtime/StateMapping.cs @@ -25,11 +25,9 @@ using System.Collections.Generic; using UnityEngine; -namespace MonsterLove.StateMachine -{ - internal class StateMapping where TState : struct, IConvertible, IComparable - where TDriver : class, new() - { +namespace MonsterLove.StateMachine { + internal class StateMapping where TState : IConvertible, IComparable + where TDriver : class, new() { public TState state; public bool hasEnterRoutine; @@ -43,10 +41,9 @@ internal class StateMapping where TState : struct, IConvertible public Action Finally = StateMachineRunner.DoNothing; private Func stateProviderCallback; - private StateMachine fsm; + private StateMachine fsm; - public StateMapping(StateMachine fsm, TState state, Func stateProvider) - { + public StateMapping(StateMachine fsm, TState state, Func stateProvider) { this.fsm = fsm; this.state = state; stateProviderCallback = stateProvider;