You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
384 lines
14 KiB
384 lines
14 KiB
using System;
|
|
using System.Collections;
|
|
using System.Collections.Generic;
|
|
using System.Reflection;
|
|
using System.Runtime.CompilerServices;
|
|
using UnityEngine;
|
|
using Object = System.Object;
|
|
namespace LzFramework.FSM
|
|
{
|
|
public enum StateTransition
|
|
{
|
|
Safe,
|
|
Overwrite,
|
|
}
|
|
|
|
public interface IStateMachine
|
|
{
|
|
MonoBehaviour Component { get; }
|
|
StateMapping CurrentStateMap { get; }
|
|
bool IsInTransition { get; }
|
|
}
|
|
|
|
public class StateMachine<T> : IStateMachine where T : struct, IConvertible, IComparable
|
|
{
|
|
public event Action<T> Changed;
|
|
|
|
private StateMachineRunner engine;
|
|
private MonoBehaviour component;
|
|
|
|
private StateMapping lastState;
|
|
private StateMapping currentState;
|
|
private StateMapping destinationState;
|
|
|
|
private Dictionary<object, StateMapping> stateLookup;
|
|
|
|
private readonly string[] ignoredNames = new[] { "add", "remove", "get", "set" };
|
|
|
|
private bool isInTransition = false;
|
|
private IEnumerator currentTransition;
|
|
private IEnumerator exitRoutine;
|
|
private IEnumerator enterRoutine;
|
|
private IEnumerator queuedChange;
|
|
|
|
public StateMachine(StateMachineRunner engine, MonoBehaviour component)
|
|
{
|
|
this.engine = engine;
|
|
this.component = component;
|
|
|
|
//Define States
|
|
var values = Enum.GetValues(typeof(T));
|
|
if (values.Length < 1) { throw new ArgumentException("Enum provided to Initialize must have at least 1 visible definition"); }
|
|
|
|
stateLookup = new Dictionary<object, StateMapping>();
|
|
for (int i = 0; i < values.Length; i++)
|
|
{
|
|
var mapping = new StateMapping((Enum)values.GetValue(i));
|
|
stateLookup.Add(mapping.state, mapping);
|
|
}
|
|
|
|
//Reflect methods
|
|
var methods = component.GetType().GetMethods(BindingFlags.Instance | BindingFlags.DeclaredOnly | BindingFlags.Public |
|
|
BindingFlags.NonPublic);
|
|
|
|
//Bind methods to states
|
|
var separator = "_".ToCharArray();
|
|
for (int i = 0; i < methods.Length; i++)
|
|
{
|
|
if (methods[i].GetCustomAttributes(typeof(CompilerGeneratedAttribute), true).Length != 0)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
var names = methods[i].Name.Split(separator);
|
|
|
|
//Ignore functions without an underscore
|
|
if (names.Length <= 1)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
Enum key;
|
|
try
|
|
{
|
|
key = (Enum)Enum.Parse(typeof(T), names[0]);
|
|
}
|
|
catch (ArgumentException)
|
|
{
|
|
//Not an method as listed in the state enum
|
|
continue;
|
|
}
|
|
|
|
var targetState = stateLookup[key];
|
|
|
|
switch (names[1])
|
|
{
|
|
case "Enter":
|
|
if (methods[i].ReturnType == typeof(IEnumerator))
|
|
{
|
|
targetState.hasEnterRoutine = true;
|
|
targetState.EnterRoutine = CreateDelegate<Func<IEnumerator>>(methods[i], component);
|
|
}
|
|
else
|
|
{
|
|
targetState.hasEnterRoutine = false;
|
|
targetState.EnterCall = CreateDelegate<Action>(methods[i], component);
|
|
}
|
|
break;
|
|
case "Exit":
|
|
if (methods[i].ReturnType == typeof(IEnumerator))
|
|
{
|
|
targetState.hasExitRoutine = true;
|
|
targetState.ExitRoutine = CreateDelegate<Func<IEnumerator>>(methods[i], component);
|
|
}
|
|
else
|
|
{
|
|
targetState.hasExitRoutine = false;
|
|
targetState.ExitCall = CreateDelegate<Action>(methods[i], component);
|
|
}
|
|
break;
|
|
case "Finally":
|
|
targetState.Finally = CreateDelegate<Action>(methods[i], component);
|
|
break;
|
|
case "Update":
|
|
targetState.Update = CreateDelegate<Action>(methods[i], component);
|
|
break;
|
|
case "LateUpdate":
|
|
targetState.LateUpdate = CreateDelegate<Action>(methods[i], component);
|
|
break;
|
|
case "FixedUpdate":
|
|
targetState.FixedUpdate = CreateDelegate<Action>(methods[i], component);
|
|
break;
|
|
case "OnCollisionEnter":
|
|
targetState.OnCollisionEnter = CreateDelegate<Action<Collision>>(methods[i], component);
|
|
break;
|
|
}
|
|
}
|
|
|
|
//Create nil state mapping
|
|
currentState = new StateMapping(null);
|
|
}
|
|
|
|
private V CreateDelegate<V>(MethodInfo method, Object target) where V : class
|
|
{
|
|
var ret = (Delegate.CreateDelegate(typeof(V), target, method) as V);
|
|
|
|
if (ret == null)
|
|
{
|
|
throw new ArgumentException("Unabled to create delegate for method called " + method.Name);
|
|
}
|
|
return ret;
|
|
|
|
}
|
|
|
|
public void ChangeState(T newState)
|
|
{
|
|
ChangeState(newState, StateTransition.Safe);
|
|
}
|
|
|
|
public void ChangeState(T 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))
|
|
{
|
|
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 (currentState == nextState) return;
|
|
|
|
//Cancel any queued changes.
|
|
if (queuedChange != null)
|
|
{
|
|
engine.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)
|
|
{
|
|
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);
|
|
engine.StartCoroutine(queuedChange);
|
|
return;
|
|
}
|
|
}
|
|
break;
|
|
case StateTransition.Overwrite:
|
|
if (currentTransition != null)
|
|
{
|
|
engine.StopCoroutine(currentTransition);
|
|
}
|
|
if (exitRoutine != null)
|
|
{
|
|
engine.StopCoroutine(exitRoutine);
|
|
}
|
|
if (enterRoutine != null)
|
|
{
|
|
engine.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)
|
|
{
|
|
isInTransition = true;
|
|
currentTransition = ChangeToNewStateRoutine(nextState, transition);
|
|
engine.StartCoroutine(currentTransition);
|
|
}
|
|
else //Same frame transition, no coroutines are present
|
|
{
|
|
if (currentState != null)
|
|
{
|
|
currentState.ExitCall();
|
|
currentState.Finally();
|
|
}
|
|
|
|
lastState = currentState;
|
|
currentState = nextState;
|
|
if (currentState != null)
|
|
{
|
|
currentState.EnterCall();
|
|
if (Changed != null)
|
|
{
|
|
Changed((T)currentState.state);
|
|
}
|
|
}
|
|
isInTransition = false;
|
|
}
|
|
}
|
|
|
|
private IEnumerator ChangeToNewStateRoutine(StateMapping newState, StateTransition transition)
|
|
{
|
|
destinationState = newState; //Chache this so that we can overwrite it and hijack a transition
|
|
|
|
if (currentState != null)
|
|
{
|
|
if (currentState.hasExitRoutine)
|
|
{
|
|
exitRoutine = currentState.ExitRoutine();
|
|
|
|
if (exitRoutine != null && transition != StateTransition.Overwrite) //Don't wait for exit if we are overwriting
|
|
{
|
|
yield return engine.StartCoroutine(exitRoutine);
|
|
}
|
|
|
|
exitRoutine = null;
|
|
}
|
|
else
|
|
{
|
|
currentState.ExitCall();
|
|
}
|
|
|
|
currentState.Finally();
|
|
}
|
|
|
|
lastState = currentState;
|
|
currentState = destinationState;
|
|
|
|
if (currentState != null)
|
|
{
|
|
if (currentState.hasEnterRoutine)
|
|
{
|
|
enterRoutine = currentState.EnterRoutine();
|
|
|
|
if (enterRoutine != null)
|
|
{
|
|
yield return engine.StartCoroutine(enterRoutine);
|
|
}
|
|
|
|
enterRoutine = null;
|
|
}
|
|
else
|
|
{
|
|
currentState.EnterCall();
|
|
}
|
|
|
|
//Broadcast change only after enter transition has begun.
|
|
if (Changed != null)
|
|
{
|
|
Changed((T)currentState.state);
|
|
}
|
|
}
|
|
|
|
isInTransition = false;
|
|
}
|
|
|
|
IEnumerator WaitForPreviousTransition(StateMapping nextState)
|
|
{
|
|
while (isInTransition)
|
|
{
|
|
yield return null;
|
|
}
|
|
|
|
ChangeState((T)nextState.state);
|
|
}
|
|
|
|
public T LastState
|
|
{
|
|
get
|
|
{
|
|
if (lastState == null) return default(T);
|
|
|
|
return (T)lastState.state;
|
|
}
|
|
}
|
|
|
|
public T State
|
|
{
|
|
get { return (T)currentState.state; }
|
|
}
|
|
|
|
public bool IsInTransition
|
|
{
|
|
get { return isInTransition; }
|
|
}
|
|
|
|
public StateMapping CurrentStateMap
|
|
{
|
|
get { return currentState; }
|
|
}
|
|
|
|
public MonoBehaviour Component
|
|
{
|
|
get { return component; }
|
|
}
|
|
|
|
//Static Methods
|
|
|
|
/// <summary>
|
|
/// Inspects a MonoBehaviour for state methods as definied by the supplied Enum, and returns a stateMachine instance used to trasition states.
|
|
/// </summary>
|
|
/// <param name="component">The component with defined state methods</param>
|
|
/// <returns>A valid stateMachine instance to manage MonoBehaviour state transitions</returns>
|
|
public static StateMachine<T> Initialize(MonoBehaviour component)
|
|
{
|
|
var engine = component.GetComponent<StateMachineRunner>();
|
|
if (engine == null) engine = component.gameObject.AddComponent<StateMachineRunner>();
|
|
|
|
return engine.Initialize<T>(component);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Inspects a MonoBehaviour for state methods as definied by the supplied Enum, and returns a stateMachine instance used to trasition states.
|
|
/// </summary>
|
|
/// <param name="component">The component with defined state methods</param>
|
|
/// <param name="startState">The default starting state</param>
|
|
/// <returns>A valid stateMachine instance to manage MonoBehaviour state transitions</returns>
|
|
public static StateMachine<T> Initialize(MonoBehaviour component, T startState)
|
|
{
|
|
var engine = component.GetComponent<StateMachineRunner>();
|
|
if (engine == null) engine = component.gameObject.AddComponent<StateMachineRunner>();
|
|
|
|
return engine.Initialize<T>(component, startState);
|
|
}
|
|
|
|
}
|
|
}
|
|
|