// 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
/************************************************************************************************************************/
}
}