2020-03-09 18:51:14 -04:00
using System ;
2020-03-08 07:32:41 -04:00
using UnityEngine ;
2020-03-15 18:18:29 -04:00
using UnityEngine.Events ;
using UnityEngine.SceneManagement ;
2020-03-08 07:32:41 -04:00
using UnityAtoms.BaseAtoms ;
2020-03-11 20:30:25 -04:00
using UnityAtoms ;
2020-03-08 07:32:41 -04:00
namespace UnityAtoms.FSM
{
2020-03-08 15:41:22 -04:00
[EditorIcon("atom-icon-delicate")]
[CreateAssetMenu(menuName = "Unity Atoms/FSM/Finite State Machine", fileName = "FiniteStateMachine")]
2020-03-08 07:32:41 -04:00
public class FiniteStateMachine : StringVariable
{
2020-03-11 16:11:27 -04:00
/// <summary>
/// Get or set current value of this FSM. If a sub FSM is having the current state, then its state will be returned. Using the setter is the same thing as calling `Dispatch`.
/// </summary>
/// <value>The command to issue.</value>
2020-03-09 18:51:14 -04:00
public override string Value
{
get
{
var state = GetState ( _value ) ;
2020-03-11 20:30:25 -04:00
return state ! = null & & state . SubMachine ! = null ? state . SubMachine . Value : _value ;
2020-03-09 18:51:14 -04:00
}
set = > Dispatch ( value ) ;
}
2020-03-08 08:24:46 -04:00
public FSMTransitionDataEvent TransitionStarted { get = > _transitionStarted ; set = > _transitionStarted = value ; }
public BoolEvent CompleteCurrentTransition { get = > _completeCurrentTransition ; set = > _completeCurrentTransition = value ; }
2020-03-11 20:30:25 -04:00
public override string InitialValue { get = > _states ? . List ! = null & & _states . List . Count > 0 ? _states . List [ 0 ] . Id : "" ; }
2020-03-08 07:32:41 -04:00
/// <summary>
/// Gets a boolean value indicating if the state machine is currently transitioning.
/// </summary>
public bool IsTransitioning { get = > _currentTransition ! = null ; }
[SerializeField]
2020-03-08 08:24:46 -04:00
private FSMTransitionDataEvent _transitionStarted = default ( FSMTransitionDataEvent ) ;
2020-03-08 07:32:41 -04:00
[SerializeField]
private BoolEvent _completeCurrentTransition = default ( BoolEvent ) ;
2020-03-09 18:51:14 -04:00
[SerializeField]
2020-03-11 20:30:25 -04:00
[AtomList]
private FSMStateListWrapper _states ;
2020-03-08 07:32:41 -04:00
[SerializeField]
2020-03-11 20:30:25 -04:00
[AtomList]
private TransitionListWrapper _transitions ;
2020-03-08 07:32:41 -04:00
private bool _isUpdatingState = false ;
private Transition _currentTransition = null ;
private bool _resetOnNextTransitionCompleted = false ;
2020-03-11 16:11:27 -04:00
private bool _triggerEventsOnNextReset = false ;
2020-03-09 18:51:14 -04:00
private event Action < float , string > _onUpdate ;
2020-03-11 16:11:27 -04:00
private event Action < float , string > _onFixedUpdate ;
2020-03-09 18:51:14 -04:00
private event Action < string > _dispatchWhen ;
2020-03-11 16:11:27 -04:00
private event Action < string > _onStateCooldown ;
2020-03-09 18:51:14 -04:00
/// <summary>
/// The value in this state machine, disregarding a "deeper" values in a sub machine.
/// </summary>
private string _currentFlatValue ;
2020-03-15 18:18:29 -04:00
private void OnEnable ( )
2020-03-11 16:11:27 -04:00
{
2020-03-11 16:42:59 -04:00
if ( CompleteCurrentTransition ! = null & & CompleteCurrentTransition . ReplayBufferSize > 0 )
{
Debug . LogWarning ( "The Complete Current Transition event had a replay buffer size great than 0, which would cause unwanted behaviour. Setting it to 0 in order to avoid unexpected behaviour." ) ;
CompleteCurrentTransition . ReplayBufferSize = 0 ;
}
2020-03-15 18:18:29 -04:00
_isUpdatingState = false ;
_currentTransition = null ;
_resetOnNextTransitionCompleted = false ;
_triggerEventsOnNextReset = false ;
2020-03-11 16:11:27 -04:00
2020-03-15 18:18:29 -04:00
// Make sure application is playing
if ( Application . isPlaying )
{
FiniteStateMachineMonoHook . GetInstance ( createIfNotExist : true ) . OnUpdate - = OnUpdate ;
FiniteStateMachineMonoHook . GetInstance ( ) . OnUpdate + = OnUpdate ;
FiniteStateMachineMonoHook . GetInstance ( ) . OnStart - = OnStart ;
FiniteStateMachineMonoHook . GetInstance ( ) . OnStart + = OnStart ;
}
else
{
UnityAction < Scene , LoadSceneMode > handler = null ;
handler = ( scene , mode ) = >
{
SceneManager . sceneLoaded - = handler ;
FiniteStateMachineMonoHook . GetInstance ( createIfNotExist : true ) . OnUpdate - = OnUpdate ;
FiniteStateMachineMonoHook . GetInstance ( ) . OnUpdate + = OnUpdate ;
Reset ( ) ;
} ;
SceneManager . sceneLoaded + = handler ;
}
2020-03-11 16:11:27 -04:00
}
2020-03-09 18:51:14 -04:00
private void OnDisable ( )
{
2020-03-11 16:11:27 -04:00
if ( FiniteStateMachineMonoHook . GetInstance ( ) ! = null )
2020-03-09 18:51:14 -04:00
{
2020-03-11 16:11:27 -04:00
FiniteStateMachineMonoHook . GetInstance ( ) . OnUpdate - = OnUpdate ;
FiniteStateMachineMonoHook . GetInstance ( ) . OnStart - = OnStart ;
2020-03-09 18:51:14 -04:00
}
_onUpdate = null ;
2020-03-11 16:11:27 -04:00
_onFixedUpdate = null ;
2020-03-09 18:51:14 -04:00
_dispatchWhen = null ;
2020-03-11 16:11:27 -04:00
_onStateCooldown = null ;
2020-03-09 18:51:14 -04:00
}
2020-03-11 16:11:27 -04:00
public void OnUpdate ( Action < float , string > handler , GameObject gameObject )
2020-03-09 18:51:14 -04:00
{
2020-03-11 16:11:27 -04:00
Action < float , string > extendedHandler = null ;
extendedHandler = ( deltaTime , state ) = >
{
// Unregister created handler if original handler doesn't or if the GameObject has been destroyed
if ( handler = = null | | gameObject = = null | | ! gameObject . scene . IsValid ( ) )
{
_onUpdate - = extendedHandler ;
return ;
}
handler ( deltaTime , state ) ;
} ;
_onUpdate + = extendedHandler ;
2020-03-09 18:51:14 -04:00
}
2020-03-11 16:11:27 -04:00
public void OnFixedUpdate ( Action < float , string > handler , GameObject gameObject )
2020-03-09 18:51:14 -04:00
{
2020-03-11 16:11:27 -04:00
Action < float , string > extendedHandler = null ;
extendedHandler = ( deltaTime , state ) = >
2020-03-09 18:51:14 -04:00
{
2020-03-11 16:11:27 -04:00
// Unregister created handler if original handler doesn't or if the GameObject has been destroyed
if ( handler = = null | | gameObject = = null | | ! gameObject . scene . IsValid ( ) )
{
_onFixedUpdate - = extendedHandler ;
return ;
}
handler ( deltaTime , state ) ;
} ;
_onFixedUpdate + = extendedHandler ;
}
public void DispatchWhen ( string command , Func < string , bool > func , GameObject gameObject )
{
Action < string > extendedHandler = null ;
extendedHandler = ( value ) = >
{
// Unregister created handler if original handler doesn't or if the GameObject has been destroyed
if ( func = = null | | gameObject = = null | | ! gameObject . scene . IsValid ( ) )
{
_dispatchWhen - = extendedHandler ;
return ;
}
2020-03-09 18:51:14 -04:00
if ( func ( value ) )
{
Dispatch ( command ) ;
}
} ;
2020-03-11 16:11:27 -04:00
_dispatchWhen + = extendedHandler ;
2020-03-09 18:51:14 -04:00
}
2020-03-08 07:32:41 -04:00
2020-03-11 16:11:27 -04:00
public void OnStateCooldown ( string state , Action < string > handler , GameObject gameObject )
2020-03-08 07:32:41 -04:00
{
2020-03-11 16:11:27 -04:00
Action < string > extendedHandler = null ;
extendedHandler = ( value ) = >
{
// Unregister created handler if original handler doesn't or if the GameObject has been destroyed
if ( handler = = null | | gameObject = = null | | ! gameObject . scene . IsValid ( ) )
{
_onStateCooldown - = extendedHandler ;
return ;
}
if ( value = = state )
{
handler ( value ) ;
}
} ;
_onStateCooldown + = extendedHandler ;
}
public override void Reset ( bool shouldTriggerEvents = false )
{
2020-03-11 20:30:25 -04:00
Validate ( ) ;
2020-03-11 16:11:27 -04:00
// Set all timers to the same as the cooldown
2020-03-11 20:30:25 -04:00
for ( var i = 0 ; i < _states . List . Count ; + + i )
2020-03-11 16:11:27 -04:00
{
2020-03-11 20:30:25 -04:00
_states . List [ i ] . Timer = _states . List [ i ] . Cooldown ;
2020-03-11 16:11:27 -04:00
}
2020-03-09 18:51:14 -04:00
2020-03-08 07:32:41 -04:00
if ( ! _resetOnNextTransitionCompleted & & ! IsTransitioning )
{
2020-03-08 15:41:22 -04:00
_resetOnNextTransitionCompleted = false ;
2020-03-09 18:51:14 -04:00
ResetAllSubMachines ( ) ;
2020-03-11 16:11:27 -04:00
base . Reset ( shouldTriggerEvents ) ;
2020-03-09 18:51:14 -04:00
_currentFlatValue = _value ;
2020-03-08 07:32:41 -04:00
}
else
{
2020-03-11 16:11:27 -04:00
_triggerEventsOnNextReset = shouldTriggerEvents ;
2020-03-08 07:32:41 -04:00
_resetOnNextTransitionCompleted = true ;
}
}
/// <summary>
/// Dispatches a new command to the FiniteStateMachine, invoking any necessary transitions.
/// </summary>
/// <param name="command"></param>
public void Dispatch ( string command )
{
// Commands dispatched during transition will be ignored
if ( IsTransitioning )
return ;
var transition = FindTransitionForCurrentState ( command ) ;
if ( transition ! = null )
{
// Commands should not be dispatched in events listening to state changes of this machine
if ( _isUpdatingState )
{
Debug . LogWarning ( "Do not call Dispatch from handlers listening to Changed and ChangedWithHistory events." ) ;
return ;
}
if ( transition . TestCondition ( ) )
{
_currentTransition = transition ;
if ( _transitionStarted ! = null )
{
_transitionStarted . Raise (
2020-03-08 08:24:46 -04:00
new FSMTransitionData ( )
2020-03-08 07:32:41 -04:00
{
FromState = transition . FromState ,
ToState = transition . ToState ,
Command = transition . Command ,
CompleteTransition = _completeCurrentTransition ,
}
) ;
}
2020-03-11 16:11:27 -04:00
transition . Begin ( this , EndCurrentTransition ) ;
2020-03-08 07:32:41 -04:00
}
}
2020-03-09 18:51:14 -04:00
else
{
2020-03-11 16:11:27 -04:00
// State doesn't exist in this FSM, propagate down to all sub FSMs.
2020-03-09 18:51:14 -04:00
var state = GetState ( _currentFlatValue ) ;
2020-03-15 18:18:29 -04:00
state ? . SubMachine ? . Dispatch ( command ) ;
2020-03-09 18:51:14 -04:00
}
2020-03-08 07:32:41 -04:00
}
2020-03-11 16:11:27 -04:00
protected override bool ValueEquals ( string other ) = > false ; // Always trigger events even if changing to the same state as previous
2020-03-09 18:51:14 -04:00
2020-03-11 20:30:25 -04:00
private void Validate ( )
{
for ( var i = 0 ; i < _transitions . List . Count ; + + i )
{
var transition = _transitions . List [ i ] ;
if ( ! _states . List . Exists ( ( s ) = > s . Id = = transition . FromState ) )
{
Debug . LogError ( $"Transition with From State {transition.FromState} can't be found in the defined states." ) ;
}
if ( ! _states . List . Exists ( ( s ) = > s . Id = = transition . ToState ) )
{
Debug . LogError ( $"Transition with To State {transition.ToState} can't be found in the defined states." ) ;
}
}
}
2020-03-11 16:11:27 -04:00
private void EndCurrentTransition ( )
2020-03-08 07:32:41 -04:00
{
if ( _resetOnNextTransitionCompleted )
{
2020-03-08 15:41:22 -04:00
_resetOnNextTransitionCompleted = false ;
2020-03-11 16:11:27 -04:00
Reset ( _triggerEventsOnNextReset ) ;
2020-03-08 07:32:41 -04:00
return ;
}
_isUpdatingState = true ;
2020-03-09 18:51:14 -04:00
var toState = GetState ( _currentTransition . ToState ) ;
if ( toState . SubMachine ! = null )
{
// Reset sub machines in to state
2020-03-11 16:11:27 -04:00
toState . SubMachine . Reset ( ) ;
2020-03-09 18:51:14 -04:00
base . Value = toState . SubMachine . Value ;
}
else
{
base . Value = _currentTransition . ToState ;
}
_currentFlatValue = _currentTransition . ToState ;
2020-03-08 15:41:22 -04:00
_currentTransition = null ;
2020-03-09 18:51:14 -04:00
2020-03-08 07:32:41 -04:00
_isUpdatingState = false ;
}
2020-03-11 16:11:27 -04:00
private void ResetAllSubMachines ( )
2020-03-09 18:51:14 -04:00
{
2020-03-11 20:30:25 -04:00
for ( var i = 0 ; i < _states . List . Count ; + + i )
2020-03-09 18:51:14 -04:00
{
2020-03-11 20:30:25 -04:00
if ( _states . List [ i ] . SubMachine ! = null )
2020-03-09 18:51:14 -04:00
{
2020-03-11 20:30:25 -04:00
_states . List [ i ] . SubMachine . Reset ( ) ;
2020-03-09 18:51:14 -04:00
}
}
}
2020-03-11 16:11:27 -04:00
private void OnStart ( )
2020-03-09 18:51:14 -04:00
{
2020-03-11 16:11:27 -04:00
Reset ( ) ;
}
private void OnUpdate ( float deltaTime )
{
// Update timers and call OnStateCooldown handlers if applicable
2020-03-09 18:51:14 -04:00
var currentValue = Value ;
2020-03-11 20:30:25 -04:00
for ( var i = 0 ; i < _states . List . Count ; + + i )
2020-03-09 18:51:14 -04:00
{
2020-03-11 20:30:25 -04:00
var state = _states . List [ i ] ;
2020-03-11 16:11:27 -04:00
if ( state . Cooldown > 0f )
2020-03-09 18:51:14 -04:00
{
var isCurrent = state . Id = = currentValue ;
var isOngoing = ! isCurrent & & state . Timer > 0f ;
if ( isCurrent | | isOngoing )
{
2020-03-11 16:11:27 -04:00
if ( isCurrent & & state . Timer < = 0f & & _onStateCooldown ! = null )
{
_onStateCooldown . Invoke ( currentValue ) ;
}
2020-03-09 18:51:14 -04:00
state . Timer + = deltaTime ;
2020-03-11 16:11:27 -04:00
if ( state . Timer > = state . Cooldown )
2020-03-09 18:51:14 -04:00
{
state . Timer = 0f ;
}
}
}
}
2020-03-11 16:11:27 -04:00
// Call OnUpdate handlers
2020-03-09 18:51:14 -04:00
if ( _onUpdate ! = null )
{
_onUpdate . Invoke ( deltaTime , currentValue ) ;
}
2020-03-11 16:11:27 -04:00
// Call DispatchWhen handlers
2020-03-09 18:51:14 -04:00
if ( _dispatchWhen ! = null )
{
_dispatchWhen . Invoke ( currentValue ) ;
}
}
2020-03-11 16:11:27 -04:00
private void OnFixedUpdate ( float deltaTime )
{
// Call OnFixedUpdate handlers
if ( _onFixedUpdate ! = null )
{
_onFixedUpdate . Invoke ( deltaTime , Value ) ;
}
}
2020-03-08 07:32:41 -04:00
private Transition FindTransitionForCurrentState ( string command )
{
Transition ret = null ;
2020-03-11 20:30:25 -04:00
for ( var i = 0 ; i < _transitions . List . Count ; + + i )
2020-03-08 07:32:41 -04:00
{
2020-03-11 20:30:25 -04:00
var transition = _transitions . List [ i ] ;
2020-03-09 18:51:14 -04:00
if ( command = = transition . Command & & _currentFlatValue = = transition . FromState )
2020-03-08 07:32:41 -04:00
{
2020-03-08 15:41:22 -04:00
return transition ;
2020-03-08 07:32:41 -04:00
}
}
return ret ;
}
2020-03-09 18:51:14 -04:00
private FSMState GetState ( string id )
{
2020-03-11 20:30:25 -04:00
for ( var i = 0 ; i < _states . List . Count ; + + i )
2020-03-09 18:51:14 -04:00
{
2020-03-11 20:30:25 -04:00
if ( _states . List [ i ] . Id = = id )
2020-03-09 18:51:14 -04:00
{
2020-03-11 20:30:25 -04:00
return _states . List [ i ] ;
2020-03-09 18:51:14 -04:00
}
}
2020-03-11 20:30:25 -04:00
return null ;
2020-03-09 18:51:14 -04:00
}
2020-03-08 07:32:41 -04:00
}
}