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#

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
}
}