// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2023 Kybernetik // using System; using System.Collections; using System.Collections.Generic; using UnityEngine; namespace Animancer.FSM { /// A simple keyless Finite State Machine system. /// /// Documentation: Finite State Machines /// /// https://kybernetik.com.au/animancer/api/Animancer.FSM/IStateMachine /// public interface IStateMachine { /************************************************************************************************************************/ /// The currently active state. object CurrentState { get; } /// The . object PreviousState { get; } /// The . object NextState { get; } /// Is it currently possible to enter the specified `state`? /// /// This requires on the and /// on the specified `state` to both return true. /// bool CanSetState(object state); /// Returns the first of the `states` which can currently be entered. object CanSetState(IList states); /// 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. /// bool TrySetState(object 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]). /// bool TrySetState(IList states); /// 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. /// bool TryResetState(object state); /// 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]). /// bool TryResetState(IList states); /// /// Calls on the then changes it to the /// specified `state` and calls on it. /// /// /// This method does not check or /// . To do that, you should use instead. /// void ForceSetState(object state); #if UNITY_ASSERTIONS /// [Assert-Only] Should the be allowed to be set to null? Default is false. /// Can be set by . bool AllowNullStates { get; } #endif /// [Assert-Conditional] Sets . void SetAllowNullStates(bool allow = true); /************************************************************************************************************************/ #if UNITY_EDITOR /************************************************************************************************************************/ /// [Editor-Only] The number of standard size lines that will use. int GUILineCount { get; } /// [Editor-Only] Draws GUI fields to display the status of this state machine. void DoGUI(); /// [Editor-Only] Draws GUI fields to display the status of this state machine in the given `area`. void DoGUI(ref Rect area); /************************************************************************************************************************/ #endif /************************************************************************************************************************/ } /// 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. /// /// See if using this class in a serialized field. /// /// Documentation: Finite State Machines /// /// https://kybernetik.com.au/animancer/api/Animancer.FSM/StateMachine_1 /// [HelpURL(StateExtensions.APIDocumentationURL + nameof(StateMachine) + "_1")] [Serializable] public partial class StateMachine : IStateMachine where TState : class, IState { /************************************************************************************************************************/ [SerializeField] private TState _CurrentState; /// [] The currently active state. public TState CurrentState => _CurrentState; /************************************************************************************************************************/ /// 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(); } } /************************************************************************************************************************/ /// Call this after deserializing to properly initialize the . /// /// public class MyComponent : MonoBehaviour /// { /// [SerializeField] /// private CharacterState.StateMachine _StateMachine; /// /// protected virtual void Awake() /// { /// _StateMachine.InitializeAfterDeserialize(); /// } /// } /// /// /// Unfortunately, can't be used to automate this because many /// Unity functions aren't available during serialization such as getting or setting a /// like does. /// public virtual void InitializeAfterDeserialize() { if (_CurrentState != null) using (new StateChange(this, null, _CurrentState)) _CurrentState.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 it 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 } /************************************************************************************************************************/ #region GUI /************************************************************************************************************************/ #if UNITY_EDITOR /************************************************************************************************************************/ /// [Editor-Only] The number of standard size lines that will use. public virtual int GUILineCount => 1; /************************************************************************************************************************/ /// [Editor-Only] Draws GUI fields to display the status of this state machine. public void DoGUI() { var spacing = UnityEditor.EditorGUIUtility.standardVerticalSpacing; var lines = GUILineCount; var height = UnityEditor.EditorGUIUtility.singleLineHeight * lines + spacing * (lines - 1); var area = GUILayoutUtility.GetRect(0, height); area.height -= spacing; DoGUI(ref area); } /************************************************************************************************************************/ /// [Editor-Only] Draws GUI fields to display the status of this state machine in the given `area`. public virtual void DoGUI(ref Rect area) { area.height = UnityEditor.EditorGUIUtility.singleLineHeight; UnityEditor.EditorGUI.BeginChangeCheck(); var state = StateMachineUtilities.DoGenericField(area, "Current State", _CurrentState); if (UnityEditor.EditorGUI.EndChangeCheck()) { if (Event.current.control) ForceSetState(state); else TrySetState(state); } StateMachineUtilities.NextVerticalArea(ref area); } /************************************************************************************************************************/ #endif #endregion /************************************************************************************************************************/ #region IStateMachine /************************************************************************************************************************/ /// object IStateMachine.CurrentState => _CurrentState; /// object IStateMachine.PreviousState => PreviousState; /// object IStateMachine.NextState => NextState; /// object IStateMachine.CanSetState(IList states) => CanSetState((List)states); /// bool IStateMachine.CanSetState(object state) => CanSetState((TState)state); /// void IStateMachine.ForceSetState(object state) => ForceSetState((TState)state); /// bool IStateMachine.TryResetState(IList states) => TryResetState((List)states); /// bool IStateMachine.TryResetState(object state) => TryResetState((TState)state); /// bool IStateMachine.TrySetState(IList states) => TrySetState((List)states); /// bool IStateMachine.TrySetState(object state) => TrySetState((TState)state); /// void IStateMachine.SetAllowNullStates(bool allow) => SetAllowNullStates(allow); /************************************************************************************************************************/ #endregion /************************************************************************************************************************/ } }