// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2023 Kybernetik //
using System;
using UnityEngine;
namespace Animancer.FSM
{
/// A state that can be used in a .
///
/// The class contains various extension methods for this interface.
///
/// Documentation: Finite State Machines
///
/// https://kybernetik.com.au/animancer/api/Animancer.FSM/IState
///
public interface IState
{
/// Can this state be entered?
///
/// Checked by ,
/// and .
///
/// Not checked by .
///
bool CanEnterState { get; }
/// Can this state be exited?
///
/// Checked by ,
/// and .
///
/// Not checked by .
///
bool CanExitState { get; }
/// Called when this state is entered.
///
/// Called by ,
/// and .
///
void OnEnterState();
/// Called when this state is exited.
///
/// Called by ,
/// and .
///
void OnExitState();
}
/************************************************************************************************************************/
/// An that knows which it is used in.
///
/// The class contains various extension methods for this interface.
///
/// Documentation: Owned States
///
/// https://kybernetik.com.au/animancer/api/Animancer.FSM/IOwnedState_1
public interface IOwnedState : IState where TState : class, IState
{
/// The that this state is used in.
StateMachine OwnerStateMachine { get; }
}
/************************************************************************************************************************/
/// An empty that implements all the required methods as virtual.
///
/// Documentation: State Types
///
/// https://kybernetik.com.au/animancer/api/Animancer.FSM/State
///
public abstract class State : IState
{
/************************************************************************************************************************/
///
/// Returns true unless overridden.
public virtual bool CanEnterState => true;
///
/// Returns true unless overridden.
public virtual bool CanExitState => true;
///
public virtual void OnEnterState() { }
///
public virtual void OnExitState() { }
/************************************************************************************************************************/
}
/************************************************************************************************************************/
/// Various extension methods for and .
///
///
/// Documentation: Finite State Machines
///
///
///
/// public class Character : MonoBehaviour
/// {
/// public StateMachine<CharacterState> StateMachine { get; private set; }
/// }
///
/// public class CharacterState : StateBehaviour, IOwnedState<CharacterState>
/// {
/// [SerializeField]
/// private Character _Character;
/// public Character Character => _Character;
///
/// public StateMachine<CharacterState> OwnerStateMachine => _Character.StateMachine;
/// }
///
/// public class CharacterBrain : MonoBehaviour
/// {
/// [SerializeField] private Character _Character;
/// [SerializeField] private CharacterState _Jump;
///
/// private void Update()
/// {
/// if (Input.GetKeyDown(KeyCode.Space))
/// {
/// // Normally you would need to refer to both the state machine and the state:
/// _Character.StateMachine.TrySetState(_Jump);
///
/// // But since CharacterState implements IOwnedState you can use these extension methods:
/// _Jump.TryEnterState();
/// }
/// }
/// }
///
/// Inherited Types
/// Unfortunately, if the field type is not the same as the T in the IOwnedState<T>
/// implementation then attempting to use these extension methods without specifying the generic argument will
/// give the following error:
///
/// The type 'StateType' cannot be used as type parameter 'TState' in the generic type or method
/// 'StateExtensions.TryEnterState<TState>(TState)'. There is no implicit reference conversion from
/// 'StateType' to 'Animancer.FSM.IOwnedState<StateType>'.
///
/// For example, you might want to access members of a derived state class like this SetTarget method:
///
/// public class AttackState : CharacterState
/// {
/// public void SetTarget(Transform target) { }
/// }
///
/// public class CharacterBrain : MonoBehaviour
/// {
/// [SerializeField] private AttackState _Attack;
///
/// private void Update()
/// {
/// if (Input.GetMouseButtonDown(0))
/// {
/// _Attack.SetTarget(...)
/// // Can't do _Attack.TryEnterState();
/// _Attack.TryEnterState<CharacterState>();
/// }
/// }
/// }
///
/// Unlike the _Jump example, the _Attack field is an AttackState rather than the base
/// CharacterState so we can call _Attack.SetTarget(...) but that causes problems with these extension
/// methods.
///
/// Calling the method without specifying its generic argument automatically uses the variable's type as the
/// argument so both of the following calls do the same thing:
///
/// _Attack.TryEnterState();
/// _Attack.TryEnterState<AttackState>();
///
/// The problem is that AttackState inherits the implementation of IOwnedState from the base
/// CharacterState class. But since that implementation is IOwnedState<CharacterState>, rather
/// than IOwnedState<AttackState> that means TryEnterState<AttackState> does not satisfy
/// that method's generic constraints: where TState : class, IOwnedState<TState>
///
/// That is why you simply need to specify the base class which implements IOwnedState as the generic
/// argument to prevent it from inferring the wrong type:
///
/// _Attack.TryEnterState<CharacterState>();
///
/// https://kybernetik.com.au/animancer/api/Animancer.FSM/StateExtensions
[HelpURL(APIDocumentationURL + nameof(StateExtensions))]
public static class StateExtensions
{
/************************************************************************************************************************/
/// The URL of the API documentation for the system.
public const string APIDocumentationURL = "https://kybernetik.com.au/animancer/api/Animancer.FSM/";
/************************************************************************************************************************/
/// [Animancer Extension] Returns the .
public static TState GetPreviousState(this TState state)
where TState : class, IState
=> StateChange.PreviousState;
/// [Animancer Extension] Returns the .
public static TState GetNextState(this TState state)
where TState : class, IState
=> StateChange.NextState;
/************************************************************************************************************************/
/// [Animancer Extension]
/// Checks if the specified `state` is the in its
/// .
///
public static bool IsCurrentState(this TState state)
where TState : class, IOwnedState
=> state.OwnerStateMachine.CurrentState == state;
/************************************************************************************************************************/
/// [Animancer Extension]
/// 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 static bool TryEnterState(this TState state)
where TState : class, IOwnedState
=> state.OwnerStateMachine.TrySetState(state);
/************************************************************************************************************************/
/// [Animancer Extension]
/// 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 static bool TryReEnterState(this TState state)
where TState : class, IOwnedState
=> state.OwnerStateMachine.TryResetState(state);
/************************************************************************************************************************/
/// [Animancer Extension]
/// Calls on the then
/// changes to the specified `state` and calls on it.
///
/// This method does not check or
/// . To do that, you should use instead.
///
public static void ForceEnterState(this TState state)
where TState : class, IOwnedState
=> state.OwnerStateMachine.ForceSetState(state);
/************************************************************************************************************************/
#pragma warning disable IDE0079 // Remove unnecessary suppression.
#pragma warning disable CS1587 // XML comment is not placed on a valid language element.
#pragma warning restore IDE0079 // Remove unnecessary suppression.
// Copy this #region into a class which implements IOwnedState to give it the state extension methods as regular members.
// This will avoid any issues with the compiler inferring the wrong generic argument in the extension methods.
///************************************************************************************************************************/
//#region State Extensions
///************************************************************************************************************************/
/////
///// Checks if this state is the in its
///// .
/////
//public bool IsCurrentState() => OwnerStateMachine.CurrentState == this;
///************************************************************************************************************************/
/////
///// Calls on the
///// .
/////
//public bool TryEnterState() => OwnerStateMachine.TrySetState(this);
///************************************************************************************************************************/
/////
///// Calls on the
///// .
/////
//public bool TryReEnterState() => OwnerStateMachine.TryResetState(this);
///************************************************************************************************************************/
/////
///// Calls on the
///// .
/////
//public void ForceEnterState() => OwnerStateMachine.ForceSetState(this);
///************************************************************************************************************************/
//#endregion
///************************************************************************************************************************/
#if UNITY_ASSERTIONS
/// [Internal] Returns an error message explaining that the wrong type of change is being accessed.
internal static string GetChangeError(Type stateType, Type machineType, string changeType = "State")
{
Type previousType = null;
Type baseStateType = null;
System.Collections.Generic.HashSet activeChangeTypes = null;
var stackTrace = new System.Diagnostics.StackTrace(1, false).GetFrames();
for (int i = 0; i < stackTrace.Length; i++)
{
var type = stackTrace[i].GetMethod().DeclaringType;
if (type != previousType &&
type.IsGenericType &&
type.GetGenericTypeDefinition() == machineType)
{
var argument = type.GetGenericArguments()[0];
if (argument.IsAssignableFrom(stateType))
{
baseStateType = argument;
break;
}
else
{
if (activeChangeTypes == null)
activeChangeTypes = new System.Collections.Generic.HashSet();
if (!activeChangeTypes.Contains(argument))
activeChangeTypes.Add(argument);
}
}
previousType = type;
}
var text = new System.Text.StringBuilder()
.Append("Attempted to access ")
.Append(changeType)
.Append("Change<")
.Append(stateType.FullName)
.Append($"> but no {nameof(StateMachine)} of that type is currently changing its ")
.Append(changeType)
.AppendLine(".");
if (baseStateType != null)
{
text.Append(" - ")
.Append(changeType)
.Append(" changes must be accessed using the base ")
.Append(changeType)
.Append(" type, which is ")
.Append(changeType)
.Append("Change<")
.Append(baseStateType.FullName)
.AppendLine("> in this case.");
var caller = stackTrace[1].GetMethod();
if (caller.DeclaringType == typeof(StateExtensions))
{
var propertyName = stackTrace[0].GetMethod().Name;
propertyName = propertyName.Substring(4, propertyName.Length - 4);// Remove the "get_".
text.Append(" - This may be caused by the compiler incorrectly inferring the generic argument of the Get")
.Append(propertyName)
.Append(" method, in which case it must be manually specified like so: state.Get")
.Append(propertyName)
.Append('<')
.Append(baseStateType.FullName)
.AppendLine(">()");
}
}
else
{
if (activeChangeTypes == null)
{
text.Append(" - No other ")
.Append(changeType)
.AppendLine(" changes are currently occurring either.");
}
else
{
if (activeChangeTypes.Count == 1)
{
text.Append(" - There is 1 ")
.Append(changeType)
.AppendLine(" change currently occurring:");
}
else
{
text.Append(" - There are ")
.Append(activeChangeTypes.Count)
.Append(' ')
.Append(changeType)
.AppendLine(" changes currently occurring:");
}
foreach (var type in activeChangeTypes)
{
text.Append(" - ")
.AppendLine(type.FullName);
}
}
}
text.Append(" - ")
.Append(changeType)
.Append("Change<")
.Append(stateType.FullName)
.AppendLine($">.{nameof(StateChange.IsActive)} can be used to check if a change of that type is currently occurring.")
.AppendLine(" - See the documentation for more information: " +
"https://kybernetik.com.au/animancer/docs/manual/fsm/changing-states");
return text.ToString();
}
#endif
/************************************************************************************************************************/
}
}