IndieGame/client/Assets/Plugins/Animancer/Utilities/FSM/StateMachine2.cs

400 lines
19 KiB
C#
Raw Normal View History

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