// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2023 Kybernetik // using System; using System.Collections; using System.Collections.Generic; using UnityEngine; namespace Animancer.FSM { /// Interface for accessing without the TState. /// /// Documentation: Keyed State Machines /// /// https://kybernetik.com.au/animancer/api/Animancer.FSM/IKeyedStateMachine_1 /// public interface IKeyedStateMachine { /************************************************************************************************************************/ /// The key which identifies the . TKey CurrentKey { get; } /// The . TKey PreviousKey { get; } /// The . TKey NextKey { get; } /// Attempts to enter the state registered with the specified `key` and returns it if successful. /// /// This method returns true immediately if the specified `key` is already the . To /// allow directly re-entering the same state, use instead. /// object TrySetState(TKey key); /// Attempts to enter the state registered with the specified `key` and returns it if successful. /// /// This method does not check if the `key` is already the . To do so, use /// instead. /// object TryResetState(TKey key); /// /// Uses to change to the state registered /// with the `key`. If nothing is registered, it changes to default(TState). /// object ForceSetState(TKey key); /************************************************************************************************************************/ } /// A simple Finite State Machine system that registers each state with a particular key. /// /// This class allows states to be registered with a particular key upfront and then accessed later using that key. /// See for a system that does not bother keeping track of any states other than /// the active one. /// /// See if using this class in a serialized field. /// /// Documentation: Keyed State Machines /// /// https://kybernetik.com.au/animancer/api/Animancer.FSM/StateMachine_2 /// [HelpURL(StateExtensions.APIDocumentationURL + nameof(StateMachine) + "_2")] [Serializable] public partial class StateMachine : StateMachine, IKeyedStateMachine, IDictionary where TState : class, IState { /************************************************************************************************************************/ /// The collection of states mapped to a particular key. public IDictionary Dictionary { get; set; } /************************************************************************************************************************/ [SerializeField] private TKey _CurrentKey; /// The key which identifies the . public TKey CurrentKey => _CurrentKey; /************************************************************************************************************************/ /// The . public TKey PreviousKey => KeyChange.PreviousKey; /// The . public TKey NextKey => KeyChange.NextKey; /************************************************************************************************************************/ /// /// Creates a new with a new , leaving the /// null. /// public StateMachine() { Dictionary = new Dictionary(); } /// /// Creates a new which uses the specified `dictionary`, leaving the /// null. /// public StateMachine(IDictionary dictionary) { Dictionary = dictionary; } /// /// Constructs a new with a new and /// immediately uses the `defaultKey` to enter the `defaultState`. /// /// This calls but not . public StateMachine(TKey defaultKey, TState defaultState) { Dictionary = new Dictionary { { defaultKey, defaultState } }; ForceSetState(defaultKey, defaultState); } /// /// Constructs a new which uses the specified `dictionary` and /// immediately uses the `defaultKey` to enter the `defaultState`. /// /// This calls but not . public StateMachine(IDictionary dictionary, TKey defaultKey, TState defaultState) { Dictionary = dictionary; dictionary.Add(defaultKey, defaultState); ForceSetState(defaultKey, defaultState); } /************************************************************************************************************************/ /// public override void InitializeAfterDeserialize() { if (CurrentState != null) { using (new KeyChange(this, default, _CurrentKey)) using (new StateChange(this, null, CurrentState)) CurrentState.OnEnterState(); } else if (Dictionary.TryGetValue(_CurrentKey, out var state)) { ForceSetState(_CurrentKey, state); } // Don't call the base method. } /************************************************************************************************************************/ /// 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(TKey key, TState state) { if (CurrentState == state) return true; else return TryResetState(key, state); } /// Attempts to enter the state registered with the specified `key` and returns it if successful. /// /// This method returns true immediately if the specified `key` is already the . To /// allow directly re-entering the same state, use instead. /// public TState TrySetState(TKey key) { if (EqualityComparer.Default.Equals(_CurrentKey, key)) return CurrentState; else return TryResetState(key); } /// object IKeyedStateMachine.TrySetState(TKey key) => TrySetState(key); /************************************************************************************************************************/ /// 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(TKey key, TState state) { using (new KeyChange(this, _CurrentKey, key)) { if (!CanSetState(state)) return false; _CurrentKey = key; ForceSetState(state); return true; } } /// Attempts to enter the state registered with the specified `key` and returns it if successful. /// /// This method does not check if the `key` is already the . To do so, use /// instead. /// public TState TryResetState(TKey key) { if (Dictionary.TryGetValue(key, out var state) && TryResetState(key, state)) return state; else return null; } /// object IKeyedStateMachine.TryResetState(TKey key) => TryResetState(key); /************************************************************************************************************************/ /// /// Calls on the then changes /// to the specified `key` and `state` and calls on it. /// /// /// This method does not check or . To do /// that, you should use instead. /// public void ForceSetState(TKey key, TState state) { using (new KeyChange(this, _CurrentKey, key)) { _CurrentKey = key; ForceSetState(state); } } /// /// Uses to change to the state registered with the `key`. If nothing /// is registered, it use null and will throw an exception unless /// is enabled. /// public TState ForceSetState(TKey key) { Dictionary.TryGetValue(key, out var state); ForceSetState(key, state); return state; } /// object IKeyedStateMachine.ForceSetState(TKey key) => ForceSetState(key); /************************************************************************************************************************/ #region Dictionary Wrappers /************************************************************************************************************************/ /// The state registered with the `key` in the . public TState this[TKey key] { get => Dictionary[key]; set => Dictionary[key] = value; } /// Gets the state registered with the specified `key` in the . public bool TryGetValue(TKey key, out TState state) => Dictionary.TryGetValue(key, out state); /************************************************************************************************************************/ /// Gets an containing the keys of the . public ICollection Keys => Dictionary.Keys; /// Gets an containing the state of the . public ICollection Values => Dictionary.Values; /************************************************************************************************************************/ /// Gets the number of states contained in the . public int Count => Dictionary.Count; /************************************************************************************************************************/ /// Adds a state to the . public void Add(TKey key, TState state) => Dictionary.Add(key, state); /// Adds a state to the . public void Add(KeyValuePair item) => Dictionary.Add(item); /************************************************************************************************************************/ /// Removes a state from the . public bool Remove(TKey key) => Dictionary.Remove(key); /// Removes a state from the . public bool Remove(KeyValuePair item) => Dictionary.Remove(item); /************************************************************************************************************************/ /// Removes all state from the . public void Clear() => Dictionary.Clear(); /************************************************************************************************************************/ /// Determines whether the contains a specific value. public bool Contains(KeyValuePair item) => Dictionary.Contains(item); /// Determines whether the contains a state with the specified `key`. public bool ContainsKey(TKey key) => Dictionary.ContainsKey(key); /************************************************************************************************************************/ /// Returns an enumerator that iterates through the . public IEnumerator> GetEnumerator() => Dictionary.GetEnumerator(); /// Returns an enumerator that iterates through the . IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); /************************************************************************************************************************/ /// Copies the contents of the to the `array` starting at the `arrayIndex`. public void CopyTo(KeyValuePair[] array, int arrayIndex) => Dictionary.CopyTo(array, arrayIndex); /************************************************************************************************************************/ /// Indicates whether the is read-only. bool ICollection>.IsReadOnly => Dictionary.IsReadOnly; /************************************************************************************************************************/ #endregion /************************************************************************************************************************/ /// Returns the state registered with the specified `key`, or null if none is present. public TState GetState(TKey key) { TryGetValue(key, out var state); return state; } /************************************************************************************************************************/ /// Adds the specified `keys` and `states`. Both arrays must be the same size. public void AddRange(TKey[] keys, TState[] states) { Debug.Assert(keys.Length == states.Length, $"The '{nameof(keys)}' and '{nameof(states)}' arrays must be the same size."); for (int i = 0; i < keys.Length; i++) { Dictionary.Add(keys[i], states[i]); } } /************************************************************************************************************************/ /// /// Sets the without changing the . /// public void SetFakeKey(TKey key) => _CurrentKey = key; /************************************************************************************************************************/ /// /// Returns a string describing the type of this state machine and its and /// . /// public override string ToString() => $"{GetType().FullName} -> {_CurrentKey} -> {(CurrentState != null ? CurrentState.ToString() : "null")}"; /************************************************************************************************************************/ #if UNITY_EDITOR /************************************************************************************************************************/ /// public override int GUILineCount => 2; /************************************************************************************************************************/ /// public override void DoGUI(ref Rect area) { area.height = UnityEditor.EditorGUIUtility.singleLineHeight; UnityEditor.EditorGUI.BeginChangeCheck(); var key = StateMachineUtilities.DoGenericField(area, "Current Key", _CurrentKey); if (UnityEditor.EditorGUI.EndChangeCheck()) SetFakeKey(key); StateMachineUtilities.NextVerticalArea(ref area); base.DoGUI(ref area); } /************************************************************************************************************************/ #endif /************************************************************************************************************************/ } }