Skip to content

Instantly share code, notes, and snippets.

@justonia
Created November 17, 2016 19:23
Show Gist options
  • Save justonia/d500e5ce0f8d28cb6713a78491587d92 to your computer and use it in GitHub Desktop.
Save justonia/d500e5ce0f8d28cb6713a78491587d92 to your computer and use it in GitHub Desktop.
State utility
using UnityEngine;
using System;
/// <summary>
/// The State class is a specialized StateMachineBehaviour that provides an event interface for
/// the messages that Unity calls. Perhaps more importantly, States communicate with the next state
/// (when transitioning into something else) or the previous state (when transitioning from
/// something else). This allows the end-programmer to utilize transition times without losing the
/// reliability of events that can signal changes in control contexts (otherwise there are overlaps
/// when 2 control contexts are simultaneously valid).
/// </summary>
public abstract class State : StateMachineBehaviour
{
public string Name = "";
[NonSerialized] public StateMachineEvent StateEnter = new StateMachineEvent();
[NonSerialized] public StateMachineEvent StateUpdate = new StateMachineEvent();
[NonSerialized] public StateMachineEvent StateExit = new StateMachineEvent();
[NonSerialized] public StateMachineEvent ControlEnter = new StateMachineEvent();
[NonSerialized] public StateMachineEvent ControlUpdate = new StateMachineEvent();
[NonSerialized] public StateMachineEvent ControlExit = new StateMachineEvent();
public StateMachineBase StateMachine { get; private set; }
public bool IsActive { get { return StateMachine.CurrentState == this; }}
protected abstract StateMachineBase GetOrCreateStateMachine(Animator animator, int layerIndex);
/// <summary>
/// Called when the state begins.
/// </summary>
/// <param name="animator"></param>
/// <param name="stateInfo"></param>
/// <param name="layerIndex"></param>
public override void OnStateEnter( Animator animator, AnimatorStateInfo stateInfo, int layerIndex )
{
// Get a ref to the StateMachine if one isn't set already.
if ( StateMachine == null )
{
StateMachine = GetOrCreateStateMachine(animator, layerIndex);
}
if (StateMachine == null)
{
// If StateMachine is still null then we have a problem.
LogStateError("OnStateEnter", "State requires a StateMachine at layer index {0} to support control context callbacks! Control contest events will not be emitted and OnControlEnter(), OnControlUpdate() and OnControlExit() will not be called until a StateMachine is available.", layerIndex );
return;
}
if (StateMachine.IsDebugLoggingMechanimStates)
{
LogStateDebug("OnStateEnter", null);
}
// Emit an event to signal this state enter to other objects.
StateEnter.Invoke( this, animator, stateInfo, layerIndex );
// This state is now entering.
StateMachine.SetEnteringState( this );
// Notify the previous state that the next state (this one) has entered.
if (StateMachine.DeferredStateExit != null)
{
StateMachine.DeferredStateExit.OnControlExit( animator, stateInfo, layerIndex);
OnControlEnter( animator, stateInfo, layerIndex );
StateMachine.SetDeferredStateExit( null );
StateMachine.SetExitingState(null);
}
else if ( StateMachine.CurrentState != null )
{
StateMachine.CurrentState.OnControlExit( animator, stateInfo, layerIndex );
}
else if ( !StateMachine.IsStarted )
{
// If CurrentState is null, then the StateMachine just started up and we need to
// set it to the current state and start the control context handlers firing.
StateMachine.SetIsStarted();
StateMachine.SetCurrentState( this );
OnControlEnter( animator, stateInfo, layerIndex );
}
}
/// <summary>
/// Called when the transition from the previous state has ended. Use this to set up control
/// event listeners or otherwise configure your control context for this state.
/// </summary>
/// <param name="animator"></param>
/// <param name="stateInfo"></param>
/// <param name="layerIndex"></param>
public virtual void OnControlEnter( Animator animator, AnimatorStateInfo stateInfo, int layerIndex )
{
// TODO: MISSING exiting state
// This method shouldn't run if no StateMachine is available.
if ( StateMachine == null )
{
return;
}
// This state is no longer entering -- it is now the current state.
StateMachine.SetEnteringState( null );
StateMachine.SetCurrentState( this );
LogStateDebug("OnControlEnter", null);
// Emit an event to signal this state enter to other objects.
ControlEnter.Invoke( this, animator, stateInfo, layerIndex );
}
/// <summary>
/// Called each frame while the state is active.
/// </summary>
/// <param name="animator"></param>
/// <param name="stateInfo"></param>
/// <param name="layerIndex"></param>
public override void OnStateUpdate( Animator animator, AnimatorStateInfo stateInfo, int layerIndex )
{
if ( StateMachine.IsDebugLoggingUpdate && StateMachine.IsDebugLoggingMechanimStates)
{
LogStateDebug("OnStateUpdate", null);
}
// Emit an event to signal this state update to other objects.
StateUpdate.Invoke( this, animator, stateInfo, layerIndex );
// Drive the special update function below. It only executes when this state is the only
// active state. As soon as any transition begins it no longer runs.
if ( StateMachine != null )
{
if ( StateMachine.CurrentState == this )
{
OnControlUpdate( animator, stateInfo, layerIndex );
}
}
}
/// <summary>
/// Called each frame while the state is the only active state. Use this to execute control
/// polling logic.
/// </remarks>
/// <param name="animator"></param>
/// <param name="stateInfo"></param>
/// <param name="layerIndex"></param>
public virtual void OnControlUpdate( Animator animator, AnimatorStateInfo stateInfo, int layerIndex )
{
// This method shouldn't run if no StateMachine is available.
if ( StateMachine == null )
{
return;
}
if ( StateMachine.IsDebugLoggingUpdate)
{
LogStateDebug("OnControlUpdate", null);
}
// Emit an event to signal this state update to other objects.
ControlUpdate.Invoke( this, animator, stateInfo, layerIndex );
}
/// <summary>
/// Called when the state ends.
/// </summary>
/// <param name="animator"></param>
/// <param name="stateInfo"></param>
/// <param name="layerIndex"></param>
public override void OnStateExit( Animator animator, AnimatorStateInfo stateInfo, int layerIndex )
{
// Emit an event to signal this state exit to other objects.
StateExit.Invoke( this, animator, stateInfo, layerIndex );
if ( StateMachine != null )
{
if ( StateMachine.EnteringState == null )
{
if (StateMachine.IsDebugLoggingMechanimStates)
{
LogStateDebug("OnStateExit", "scheduling deferring handling");
}
StateMachine.SetCurrentState( null );
StateMachine.SetExitingState( this );
StateMachine.SetDeferredStateExit( this );
}
else
{
if (StateMachine.IsDebugLoggingMechanimStates)
{
LogStateDebug("OnStateExit", null);
}
// Notify the next state that the previous state (this one) has exited.
if ( StateMachine.EnteringState != null )
{
StateMachine.EnteringState.OnControlEnter( animator, stateInfo, layerIndex );
}
// This state has now finished exiting.
StateMachine.SetExitingState( null );
}
}
}
/// <summary>
/// Called when the transition from the next state has begins. Use this to tear down control
/// event listeners or otherwise clean up your control context for this state.
/// </summary>
/// <param name="animator"></param>
/// <param name="stateInfo"></param>
/// <param name="layerIndex"></param>
public virtual void OnControlExit( Animator animator, AnimatorStateInfo stateInfo, int layerIndex )
{
// This method shouldn't run if no StateMachine is available.
if ( StateMachine == null )
{
return;
}
// This state is no longer the current state -- it has begun exiting.
StateMachine.SetCurrentState( null );
StateMachine.SetExitingState( this );
LogStateDebug("OnControlExit", null);
// Emit an event to signal this state exit to other objects.
ControlExit.Invoke( this, animator, stateInfo, layerIndex );
}
protected void LogStateDebug(string method, string fmt, params object[] args)
{
if (StateMachine.IsDebugLogging)
{
Debug.LogFormat("({2}, f:{0}, l:{1}] {3}.{4}: {5} -- exiting: {6}, entering: {7}",
Time.frameCount,
StateMachine.layerIndex,
StateMachine.gameObject.name,
Name,
method,
string.IsNullOrEmpty(fmt) ? "" : string.Format(fmt, args),
StateMachine.ExitingState != null ? StateMachine.ExitingState.Name : "<null>",
StateMachine.EnteringState != null ? StateMachine.EnteringState.Name : "<null>");
}
}
protected void LogStateError(string method, string fmt, params object[] args)
{
if (StateMachine.IsDebugLogging)
{
Debug.LogErrorFormat("({2}, f:{0}, l:{1}) {3}.{4}: {5} -- exiting: {6}, entering: {7}",
Time.frameCount,
StateMachine.layerIndex,
StateMachine.gameObject.name,
Name,
method,
string.IsNullOrEmpty(fmt) ? "" : string.Format(fmt, args),
StateMachine.ExitingState != null ? StateMachine.ExitingState.Name : "<null>",
StateMachine.EnteringState != null ? StateMachine.EnteringState.Name : "<null>");
}
}
}
using System;
using System.Linq;
using UnityEngine;
using UnityEngine.Events;
/// <summary>
/// An event used to pass information about the state machine and the state in question.
/// </summary>
[Serializable] public class StateMachineEvent : UnityEvent<State, Animator, AnimatorStateInfo, int> { }
/// <summary>
/// The StateMachine class is used by States to communicate to one another and supports the action
/// of other game objects in response to events signaling state changes.
/// </summary>
public class StateMachine<T> : StateMachineBase
where T : State
{
public virtual void Awake()
{
Animator = GetComponent<Animator>();
if ( Animator == null )
{
Debug.LogError( "StateMachine requires an animator component!" );
return;
}
Animator
.GetBehaviours<T>()
.ToList()
.ForEach( state => {
state.ControlEnter.AddListener( ControlEnter.Invoke );
state.ControlUpdate.AddListener( ControlUpdate.Invoke );
state.ControlExit.AddListener( ControlExit.Invoke );
state.StateEnter.AddListener( StateEnter.Invoke );
state.StateUpdate.AddListener( StateUpdate.Invoke );
state.StateExit.AddListener( StateExit.Invoke );
} );
}
public virtual void OnDestroy()
{
if ( Animator == null )
{
return;
}
Animator
.GetBehaviours<T>()
.ToList()
.ForEach( state => {
state.ControlEnter.RemoveListener( ControlEnter.Invoke );
state.ControlUpdate.RemoveListener( ControlUpdate.Invoke );
state.ControlExit.RemoveListener( ControlExit.Invoke );
state.StateEnter.RemoveListener( StateEnter.Invoke );
state.StateUpdate.RemoveListener( StateUpdate.Invoke );
state.StateExit.RemoveListener( StateExit.Invoke );
} );
}
}
using UnityEngine;
using System.Collections;
public abstract class StateMachineBase : MonoBehaviour
{
public bool IsDebugLogging = false;
public bool IsDebugLoggingMechanimStates = false;
public bool IsDebugLoggingUpdate = false;
public int layerIndex = 0;
public Animator Animator { get; protected set; }
public State CurrentState { get; protected set; }
public State EnteringState { get; protected set; }
public State ExitingState { get; protected set; }
public State DeferredStateExit { get; protected set; }
public bool IsStarted { get; protected set; }
public bool IsTransitioning { get; protected set; }
public int LayerIndex { get { return layerIndex; }}
public StateMachineEvent ControlEnter = new StateMachineEvent();
public StateMachineEvent ControlUpdate = new StateMachineEvent();
public StateMachineEvent ControlExit = new StateMachineEvent();
public StateMachineEvent StateEnter = new StateMachineEvent();
public StateMachineEvent StateUpdate = new StateMachineEvent();
public StateMachineEvent StateExit = new StateMachineEvent();
public void SetCurrentState( State state )
{
CurrentState = state;
IsTransitioning = true;
}
public void SetEnteringState( State state )
{
EnteringState = state;
}
public void SetExitingState( State state )
{
ExitingState = state;
IsTransitioning = false;
}
public void SetIsStarted()
{
IsStarted = true;
}
public void SetDeferredStateExit( State state )
{
DeferredStateExit = state;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment