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