// Animancer // https://kybernetik.com.au/animancer // Copyright 2021 Kybernetik // using System; using System.Collections.Generic; using UnityEngine; namespace Animancer.FSM { /// A simple keyless Finite State Machine system. /// /// This class doesn't keep track of any states other than the currently active one. /// See for a system that allows states to be pre-registered and accessed /// using a separate key. /// /// Documentation: Finite State Machines /// /// https://kybernetik.com.au/animancer/api/Animancer.FSM/StateMachine_1 /// [HelpURL(StateExtensions.APIDocumentationURL + nameof(StateMachine) + "_1")] public partial class StateMachine where TState : class, IState { /************************************************************************************************************************/ /// The currently active state. public TState CurrentState { get; private set; } /************************************************************************************************************************/ /// The . public TState PreviousState => StateChange.PreviousState; /// The . public TState NextState => StateChange.NextState; /************************************************************************************************************************/ /// Creates a new , leaving the null. public StateMachine() { } /// Creates a new and immediately enters the `state`. /// This calls but not . public StateMachine(TState state) { #if UNITY_ASSERTIONS if (state == null)// AllowNullStates won't be true yet since this is the constructor. throw new ArgumentNullException(nameof(state), NullNotAllowed); #endif using (new StateChange(this, null, state)) { CurrentState = state; state.OnEnterState(); } } /************************************************************************************************************************/ /// Is it currently possible to enter the specified `state`? /// /// This requires on the and /// on the specified `state` to both return true. /// public bool CanSetState(TState state) { #if UNITY_ASSERTIONS if (state == null && !AllowNullStates) throw new ArgumentNullException(nameof(state), NullNotAllowed); #endif using (new StateChange(this, CurrentState, state)) { if (CurrentState != null && !CurrentState.CanExitState) return false; if (state != null && !state.CanEnterState) return false; return true; } } /// Returns the first of the `states` which can currently be entered. /// /// This requires on the and /// on one of the `states` to both return true. /// /// States are checked in ascending order (i.e. from [0] to [states.Count - 1]). /// public TState CanSetState(IList states) { // We call CanSetState so that it will check CanExitState for each individual pair in case it does // something based on the next state. var count = states.Count; for (int i = 0; i < count; i++) { var state = states[i]; if (CanSetState(state)) return state; } return null; } /************************************************************************************************************************/ /// /// Attempts to enter the specified `state` and returns true if successful. /// /// This method returns true immediately if the specified `state` is already the . /// To allow directly re-entering the same state, use instead. /// public bool TrySetState(TState state) { if (CurrentState == state) { #if UNITY_ASSERTIONS if (state == null && !AllowNullStates) throw new ArgumentNullException(nameof(state), NullNotAllowed); #endif return true; } return TryResetState(state); } /// /// Attempts to enter any of the specified `states` and returns true if successful. /// /// This method returns true and does nothing else if the is in the list. /// To allow directly re-entering the same state, use instead. /// /// States are checked in ascending order (i.e. from [0] to [states.Count - 1]). public bool TrySetState(IList states) { var count = states.Count; for (int i = 0; i < count; i++) if (TrySetState(states[i])) return true; return false; } /************************************************************************************************************************/ /// /// Attempts to enter the specified `state` and returns true if successful. /// /// This method does not check if the `state` is already the . To do so, use /// instead. /// public bool TryResetState(TState state) { if (!CanSetState(state)) return false; ForceSetState(state); return true; } /// /// Attempts to enter any of the specified `states` and returns true if successful. /// /// This method does not check if the `state` is already the . To do so, use /// instead. /// /// States are checked in ascending order (i.e. from [0] to [states.Count - 1]). public bool TryResetState(IList states) { var count = states.Count; for (int i = 0; i < count; i++) if (TryResetState(states[i])) return true; return false; } /************************************************************************************************************************/ /// /// Calls on the then changes to the /// specified `state` and calls on it. /// /// This method does not check or /// . To do that, you should use instead. /// public void ForceSetState(TState state) { #if UNITY_ASSERTIONS if (state == null) { if (!AllowNullStates) throw new ArgumentNullException(nameof(state), NullNotAllowed); } else if (state is IOwnedState owned && owned.OwnerStateMachine != this) { throw new InvalidOperationException( $"Attempted to use a state in a machine that is not its owner." + $"\n State: {state}" + $"\n Machine: {this}"); } #endif using (new StateChange(this, CurrentState, state)) { CurrentState?.OnExitState(); CurrentState = state; state?.OnEnterState(); } } /************************************************************************************************************************/ /// Returns a string describing the type of this state machine and its . public override string ToString() => $"{GetType().Name} -> {CurrentState}"; /************************************************************************************************************************/ #if UNITY_ASSERTIONS /// [Assert-Only] Should the be allowed to be set to null? Default is false. /// Can be set by . public bool AllowNullStates { get; private set; } /// [Assert-Only] The error given when attempting to set the to null. private const string NullNotAllowed = "This " + nameof(StateMachine) + " does not allow its state to be set to null." + " Use " + nameof(SetAllowNullStates) + " to allow it if this is intentional."; #endif /// [Assert-Conditional] Sets . [System.Diagnostics.Conditional("UNITY_ASSERTIONS")] public void SetAllowNullStates(bool allow = true) { #if UNITY_ASSERTIONS AllowNullStates = allow; #endif } /************************************************************************************************************************/ #if UNITY_EDITOR /// [Editor-Only] Draws an Inspector field for the . public virtual void OnInspectorGUI() { if (typeof(UnityEngine.Object).IsAssignableFrom(typeof(TState))) { UnityEditor.EditorGUI.BeginChangeCheck(); var state = UnityEditor.EditorGUILayout.ObjectField( "Current State", CurrentState as UnityEngine.Object, typeof(TState), true); if (UnityEditor.EditorGUI.EndChangeCheck()) TrySetState(state as TState); } } #endif /************************************************************************************************************************/ } }