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