Created
May 30, 2022 23:10
-
-
Save p3nGu1nZz/95424845f585f0f7a0bb94c54c1b2fd1 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using System; | |
using System.Collections; | |
using System.Collections.Generic; | |
using UnityEngine; | |
namespace HFSM | |
{ | |
/// <summary> | |
/// Our General Purspose Hieracrhial State Machine Engine. This class allows | |
/// sub states to bind addition nodes of states to create a tree structure. We also | |
/// pass in a Type reference to the context (Some type of controller class) bound to | |
/// this state machine. This allows us to pass the binding context into the callbacks. | |
/// </summary> | |
/// <typeparam name="T"></typeparam> | |
public abstract class StateMachine<T> | |
{ | |
T context; | |
StateMachine<T> currentState; | |
StateMachine<T> defaultState; | |
StateMachine<T> parentState; | |
public StateMachine<T> ParentState { get { return parentState; } } | |
public StateMachine<T> CurrentState { get { return currentState; } } | |
Dictionary<Type, StateMachine<T>> states = new Dictionary<Type, StateMachine<T>>(); | |
Dictionary<int, StateMachine<T>> transitions = new Dictionary<int, StateMachine<T>>(); | |
public Dictionary<Type, StateMachine<T>> States { get { return states; } } | |
public Dictionary<int, StateMachine<T>> Transitions { get { return transitions; } } | |
/// <summary> | |
/// Our base type class that will contain our transition triggers | |
/// </summary> | |
public readonly struct Triggers { } | |
/// <summary> | |
/// Our load function which handles binding our context to our state machine | |
/// </summary> | |
/// <param name="context"></param> | |
public void Bind(T context) | |
{ | |
this.context = context; | |
OnLoad(context); | |
} | |
/// <summary> | |
/// Called when we start our scene once. | |
/// </summary> | |
public void Start() | |
{ | |
OnStart(context); | |
} | |
/// <summary> | |
/// Called when we enter the state from another. | |
/// </summary> | |
public void Enter() | |
{ | |
OnEnter(context); | |
if (currentState == null && defaultState != null) | |
{ | |
currentState = defaultState; | |
} | |
currentState?.Enter(); | |
} | |
/// <summary> | |
/// Called by our context's monobehavior | |
/// </summary> | |
public void FixedUpdate() | |
{ | |
OnFixedUpdate(context); | |
currentState?.FixedUpdate(); | |
} | |
/// <summary> | |
/// Called by our context's monobehavior | |
/// </summary> | |
public void Update() | |
{ | |
OnUpdate(context); | |
currentState?.Update(); | |
} | |
/// <summary> | |
/// Called by our context's monobehavior | |
/// </summary> | |
public void LateUpdate() | |
{ | |
OnLateUpdate(context); | |
currentState?.LateUpdate(); | |
} | |
public void AnimUpdate() | |
{ | |
OnAnimUpdate(context); | |
currentState?.AnimUpdate(); | |
} | |
/// <summary> | |
/// Called by our context's monobehavior | |
/// </summary> | |
public void CollisionEnter(Collision collision) | |
{ | |
OnCollisionEnter(context, collision); | |
currentState?.CollisionEnter(collision); | |
} | |
/// <summary> | |
/// Called by our context's monobehavior | |
/// </summary> | |
public void CollisionExit(Collision collision) | |
{ | |
OnCollisionExit(context, collision); | |
currentState?.CollisionExit(collision); | |
} | |
/// <summary> | |
/// Called right before transition to another state. | |
/// </summary> | |
public void Exit() | |
{ | |
currentState?.Exit(); | |
OnExit(context); | |
} | |
/// <summary> | |
/// callback override for loading our state | |
/// </summary> | |
/// <param name="context"></param> | |
protected virtual void OnLoad(T context) { } | |
/// <summary> | |
/// callback override for starting our start in the scene | |
/// </summary> | |
/// <param name="context"></param> | |
protected virtual void OnStart(T context) { } | |
/// <summary> | |
/// Callback for when we enter the State | |
/// </summary> | |
/// <param name="context"></param> | |
protected virtual void OnEnter(T context) { } | |
/// <summary> | |
/// Callback for our monobehavior | |
/// </summary> | |
/// <param name="context"></param> | |
protected virtual void OnFixedUpdate(T context) { } | |
/// <summary> | |
/// Callback for our monobehavior | |
/// </summary> | |
/// <param name="context"></param> | |
protected virtual void OnUpdate(T context) { } | |
/// <summary> | |
/// Callback for our monobehavior | |
/// </summary> | |
/// <param name="context"></param> | |
protected virtual void OnLateUpdate(T context) { } | |
/// <summary> | |
/// Callback for updating our animation controller by a specific state | |
/// </summary> | |
/// <param name="context"></param> | |
protected virtual void OnAnimUpdate(T context) { } | |
/// <summary> | |
/// Callback for our monobehavior | |
/// </summary> | |
/// <param name="context"></param> | |
protected virtual void OnCollisionEnter(T context, Collision collision) { } | |
/// <summary> | |
/// Callback for our monobehavior | |
/// </summary> | |
/// <param name="context"></param> | |
protected virtual void OnCollisionExit(T context, Collision collision) { } | |
/// <summary> | |
/// Callback for when we leave this state for another | |
/// </summary> | |
/// <param name="context"></param> | |
protected virtual void OnExit(T context) { } | |
/// <summary> | |
/// Loads a given state into our dicitionary of Substates | |
/// </summary> | |
/// <param name="state"></param> | |
public void LoadState(StateMachine<T> state) | |
{ | |
if (states.Count == 0) | |
{ | |
defaultState = state; | |
} | |
state.parentState = this; | |
if (context != null) | |
{ | |
state.Bind(context); | |
} | |
try | |
{ | |
states.Add(state.GetType(), state); | |
} | |
catch (ArgumentException) | |
{ | |
throw new DuplicateStateException($"State {GetType()} already contains a substate of a type {state.GetType()}"); | |
} | |
} | |
/// <summary> | |
/// Creates a transition between two states. The transition is stored on the from state, and | |
/// only allows one unique trigger per from state. | |
/// </summary> | |
/// <param name="from">the state we are transitioning from</param> | |
/// <param name="to">the state we are transitioning to</param> | |
/// <param name="trigger">the enum trigger we are dispatching</param> | |
public void AddTransition(StateMachine<T> from, StateMachine<T> to, int trigger) | |
{ | |
if (!states.TryGetValue(from.GetType(), out _)) | |
{ | |
throw new InvalidTransitionException($"State {GetType()} does not have a substate of type {from.GetType()} to transition from"); | |
} | |
if (!states.TryGetValue(to.GetType(), out _)) | |
{ | |
throw new InvalidTransitionException($"State {GetType()} does not have a substate of type {to.GetType()} to transition into"); | |
} | |
try | |
{ | |
from.transitions.Add(trigger, to); | |
} | |
catch (ArgumentException) | |
{ | |
throw new DuplicateTransitionException($"State {from.GetType()} already has a transition defined for trigger {trigger}"); | |
} | |
} | |
public void AddTransitionToChild(StateMachine<T> from, StateMachine<T> to, int trigger) | |
{ | |
if (!states.TryGetValue(from.GetType(), out _)) | |
{ | |
throw new InvalidTransitionException($"State {GetType()} does not have a substate of type {from.GetType()} to transition from"); | |
} | |
if (!to.parentState.states.TryGetValue(to.GetType(), out _)) | |
{ | |
throw new InvalidTransitionException($"State {to.parentState.GetType()} does not have a child substate of type {to.GetType()} to transition into"); | |
} | |
try | |
{ | |
from.transitions.Add(trigger, to); | |
} | |
catch (ArgumentException) | |
{ | |
throw new DuplicateTransitionException($"State {from.GetType()} already has a transition defined for trigger {trigger}"); | |
} | |
} | |
/// <summary> | |
/// Notifies our parent state about the state transition we triggered. The trigger is looked up | |
/// on our parent state not the substate | |
/// </summary> | |
/// <param name="trigger"></param> | |
public void SendTrigger(int trigger) | |
{ | |
var root = this; | |
while (root?.parentState != null) | |
{ | |
root = root.parentState; | |
} | |
while (root != null) | |
{ | |
if (root.transitions.TryGetValue(trigger, out StateMachine<T> toState)) | |
{ | |
root.parentState?.ChangeState(toState); | |
return; | |
} | |
root = root.currentState; | |
} | |
#if DEVELOPMENT_BUILD | |
Debug.LogWarning($"Trigger {trigger} was not consumed by any transition"); | |
#elif UNITY_EDITOR | |
throw new NeglectedTriggerException($"Trigger {trigger} was not consumed by any transition"); | |
#endif | |
} | |
/// <summary> | |
/// handles changing our current state into a new state. This function also checks the parent substates for | |
/// transitioning into child states. | |
/// </summary> | |
/// <param name="state"></param> | |
private void ChangeState(StateMachine<T> state) | |
{ | |
currentState?.Exit(); | |
//Check our current state first | |
if (states.TryGetValue(state.GetType(), out _)) | |
{ | |
currentState = states[state.GetType()]; | |
currentState.Enter(); | |
return; | |
} | |
//Also check parent of our transitioning to state | |
if (state.parentState.states.TryGetValue(state.GetType(), out _)) | |
{ | |
currentState.parentState.currentState = state.parentState; | |
state.parentState.currentState = state.parentState.states[state.GetType()]; | |
state.parentState.currentState.parentState.Enter(); | |
return; | |
} | |
#if DEVELOPMENT_BUILD | |
Debug.LogWarning($"State {state.GetType()} does not exist in {state.parentState.GetType()}"); | |
#else | |
throw new InvalidStateException($"State {state.GetType()} does not exist in {state.parentState.GetType()}"); | |
#endif | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment