You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
468 lines
16 KiB
C#
468 lines
16 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Reflection;
|
|
|
|
namespace Otter {
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
/// <example>
|
|
/// 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.
|
|
/// </example>
|
|
/// <typeparam name="TState">An enum of states.</typeparam>
|
|
public class StateMachine<TState> : Component {
|
|
|
|
#region Private Fields
|
|
|
|
Dictionary<TState, State> states = new Dictionary<TState, State>();
|
|
List<TState> stateStack = new List<TState>();
|
|
List<TState> pushQueue = new List<TState>();
|
|
List<bool> pushPopBuffer = new List<bool>(); // keep track of the order of commands
|
|
List<float> timers = new List<float>();
|
|
|
|
Dictionary<TState, Dictionary<TState, Action>> transitions = new Dictionary<TState, Dictionary<TState, Action>>();
|
|
|
|
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
|
|
|
|
/// <summary>
|
|
/// Determines if the StateMachine will autopopulate its states based off of the values of the Enum.
|
|
/// </summary>
|
|
public bool AutoPopulate = true;
|
|
|
|
#endregion
|
|
|
|
#region Public Properties
|
|
|
|
/// <summary>
|
|
/// The current state.
|
|
/// </summary>
|
|
public TState CurrentState { get; private set; }
|
|
|
|
#endregion
|
|
|
|
#region Constructors
|
|
|
|
/// <summary>
|
|
/// Create a new StateMachine.
|
|
/// </summary>
|
|
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
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
/// <param name="e">The Entity to get methods from.</param>
|
|
public void PopulateMethodsFromEntity(Entity e) {
|
|
if (typeof(TState).IsEnum) {
|
|
foreach (TState value in Enum.GetValues(typeof(TState))) {
|
|
AddState(value, e);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
public void PopulateMethodsFromEntity() {
|
|
PopulateMethodsFromEntity(Entity);
|
|
}
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
/// <param name="state">The state to change to.</param>
|
|
public void ChangeState(TState state) {
|
|
pushQueue.Clear();
|
|
stateStack.Clear();
|
|
|
|
changeTo = state;
|
|
change = true;
|
|
|
|
if (updating) {
|
|
|
|
}
|
|
else {
|
|
ChangeState();
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Push a state onto a stack of states. The state machine will always run the top of the stack.
|
|
/// </summary>
|
|
/// <param name="state">The state to push.</param>
|
|
public void PushState(TState state) {
|
|
if (updating) {
|
|
pushQueue.Add(state);
|
|
pushPopBuffer.Add(true); //true means push
|
|
}
|
|
else {
|
|
PushStateImmediate(state);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Pop the top state on the stack (if there is a stack.)
|
|
/// </summary>
|
|
public void PopState() {
|
|
if (updating) {
|
|
pushPopBuffer.Add(false); //false means pop
|
|
}
|
|
else {
|
|
PopStateImmediate();
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Update the State Machine.
|
|
/// </summary>
|
|
public override void Update() {
|
|
base.Update();
|
|
if (states.ContainsKey(CurrentState) && !noState) {
|
|
updating = true;
|
|
s.Update();
|
|
updating = false;
|
|
|
|
ChangeState();
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Add a transition callback for when going from one state to another.
|
|
/// </summary>
|
|
/// <param name="fromState">The State that is ending.</param>
|
|
/// <param name="toState">The State that is starting.</param>
|
|
/// <param name="function">The Action to run when the machine goes from the fromState to the toState.</param>
|
|
public void AddTransition(TState fromState, TState toState, Action function) {
|
|
if (!transitions.ContainsKey(fromState)) {
|
|
transitions.Add(fromState, new Dictionary<TState, Action>());
|
|
}
|
|
transitions[fromState].Add(toState, function);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Add a state with three Actions.
|
|
/// </summary>
|
|
/// <param name="key">The key to reference the State with.</param>
|
|
/// <param name="onEnter">The method to call when entering this state.</param>
|
|
/// <param name="onUpdate">The method to call when updating this state.</param>
|
|
/// <param name="onExit">The method to call when exiting this state.</param>
|
|
public void AddState(TState key, Action onEnter, Action onUpdate, Action onExit) {
|
|
states.Add(key, new State(onEnter, onUpdate, onExit));
|
|
}
|
|
|
|
/// <summary>
|
|
/// Add a state with just an update Action.
|
|
/// </summary>
|
|
/// <param name="key">The key to reference the State with.</param>
|
|
/// <param name="onUpdate">The method to call when updating this state.</param>
|
|
public void AddState(TState key, Action onUpdate) {
|
|
states.Add(key, new State(onUpdate));
|
|
}
|
|
|
|
/// <summary>
|
|
/// Add a state.
|
|
/// </summary>
|
|
/// <param name="key">The key to reference the State with.</param>
|
|
/// <param name="value">The State to add.</param>
|
|
public void AddState(TState key, State value) {
|
|
states.Add(key, value);
|
|
}
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
/// <param name="key">The key to reference the State with.</param>
|
|
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
|
|
|
|
}
|
|
|
|
/// <summary>
|
|
/// Used in StateMachine. Contains functions for enter, update, and exit.
|
|
/// </summary>
|
|
public class State {
|
|
|
|
#region Public Fields
|
|
|
|
/// <summary>
|
|
/// The method to call when entering this state.
|
|
/// </summary>
|
|
public Action OnEnter = delegate { };
|
|
|
|
/// <summary>
|
|
/// The method to call when updating this state.
|
|
/// </summary>
|
|
public Action OnUpdate = delegate { };
|
|
|
|
/// <summary>
|
|
/// The method to call when exiting this state.
|
|
/// </summary>
|
|
public Action OnExit = delegate { };
|
|
|
|
#endregion
|
|
|
|
#region Public Properties
|
|
|
|
/// <summary>
|
|
/// The Id that this state has been assigned.
|
|
/// </summary>
|
|
public int Id { get; internal set; }
|
|
|
|
#endregion
|
|
|
|
#region Constructors
|
|
|
|
/// <summary>
|
|
/// Create a new State with three Actions.
|
|
/// </summary>
|
|
/// <param name="onEnter">The method to call when entering this state.</param>
|
|
/// <param name="onUpdate">The method to call when updating this state.</param>
|
|
/// <param name="onExit">The method to call when exiting this state.</param>
|
|
public State(Action onEnter = null, Action onUpdate = null, Action onExit = null) {
|
|
Functions(onEnter, onUpdate, onExit);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Create a new State with just an update Action.
|
|
/// </summary>
|
|
/// <param name="onUpdate">The method to call when updating this state.</param>
|
|
public State(Action onUpdate) : this(null, onUpdate) { }
|
|
|
|
#endregion
|
|
|
|
#region Public Methods
|
|
|
|
/// <summary>
|
|
/// Set all three of the methods for enter, update, and exit.
|
|
/// </summary>
|
|
/// <param name="onEnter">The method to call when entering this state.</param>
|
|
/// <param name="onUpdate">The method to call when updating this state.</param>
|
|
/// <param name="onExit">The method to call when exiting this state.</param>
|
|
public void Functions(Action onEnter, Action onUpdate, Action onExit) {
|
|
if (onEnter != null) {
|
|
OnEnter = onEnter;
|
|
}
|
|
if (onUpdate != null) {
|
|
OnUpdate = onUpdate;
|
|
}
|
|
if (onExit != null) {
|
|
OnExit = onExit;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Call OnUpdate.
|
|
/// </summary>
|
|
public void Update() {
|
|
OnUpdate();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Call OnEnter.
|
|
/// </summary>
|
|
public void Enter() {
|
|
OnEnter();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Call OnExit.
|
|
/// </summary>
|
|
public void Exit() {
|
|
OnExit();
|
|
}
|
|
|
|
#endregion
|
|
|
|
}
|
|
}
|