Skip to content

Instantly share code, notes, and snippets.

@p3nGu1nZz
Created May 30, 2022 23:10
Show Gist options
  • Save p3nGu1nZz/95424845f585f0f7a0bb94c54c1b2fd1 to your computer and use it in GitHub Desktop.
Save p3nGu1nZz/95424845f585f0f7a0bb94c54c1b2fd1 to your computer and use it in GitHub Desktop.
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