123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418 |
- // Animancer // https://kybernetik.com.au/animancer // Copyright 2022 Kybernetik //
- using System;
- using UnityEngine;
- namespace Animancer.FSM
- {
- /// <summary>A state that can be used in a <see cref="StateMachine{TState}"/>.</summary>
- /// <remarks>
- /// The <see cref="StateExtensions"/> class contains various extension methods for this interface.
- /// <para></para>
- /// Documentation: <see href="https://kybernetik.com.au/animancer/docs/manual/fsm">Finite State Machines</see>
- /// </remarks>
- /// https://kybernetik.com.au/animancer/api/Animancer.FSM/IState
- ///
- public interface IState
- {
- /// <summary>Can this state be entered?</summary>
- /// <remarks>
- /// Checked by <see cref="StateMachine{TState}.CanSetState"/>, <see cref="StateMachine{TState}.TrySetState"/>
- /// and <see cref="StateMachine{TState}.TryResetState"/>.
- /// <para></para>
- /// Not checked by <see cref="StateMachine{TState}.ForceSetState"/>.
- /// </remarks>
- bool CanEnterState { get; }
- /// <summary>Can this state be exited?</summary>
- /// <remarks>
- /// Checked by <see cref="StateMachine{TState}.CanSetState"/>, <see cref="StateMachine{TState}.TrySetState"/>
- /// and <see cref="StateMachine{TState}.TryResetState"/>.
- /// <para></para>
- /// Not checked by <see cref="StateMachine{TState}.ForceSetState"/>.
- /// </remarks>
- bool CanExitState { get; }
- /// <summary>Called when this state is entered.</summary>
- /// <remarks>
- /// Called by <see cref="StateMachine{TState}.TrySetState"/>, <see cref="StateMachine{TState}.TryResetState"/>
- /// and <see cref="StateMachine{TState}.ForceSetState"/>.
- /// </remarks>
- void OnEnterState();
- /// <summary>Called when this state is exited.</summary>
- /// <remarks>
- /// Called by <see cref="StateMachine{TState}.TrySetState"/>, <see cref="StateMachine{TState}.TryResetState"/>
- /// and <see cref="StateMachine{TState}.ForceSetState"/>.
- /// </remarks>
- void OnExitState();
- }
- /************************************************************************************************************************/
- /// <summary>An <see cref="IState"/> that knows which <see cref="StateMachine{TState}"/> it is used in.</summary>
- /// <remarks>
- /// The <see cref="StateExtensions"/> class contains various extension methods for this interface.
- /// <para></para>
- /// Documentation: <see href="https://kybernetik.com.au/animancer/docs/manual/fsm/state-types#owned-states">Owned States</see>
- /// </remarks>
- /// https://kybernetik.com.au/animancer/api/Animancer.FSM/IOwnedState_1
- public interface IOwnedState<TState> : IState where TState : class, IState
- {
- /// <summary>The <see cref="StateMachine{TState}"/> that this state is used in.</summary>
- StateMachine<TState> OwnerStateMachine { get; }
- }
- /************************************************************************************************************************/
- /// <summary>An empty <see cref="IState"/> that implements all the required methods as <c>virtual</c>.</summary>
- /// <remarks>
- /// Documentation: <see href="https://kybernetik.com.au/animancer/docs/manual/fsm/state-types">State Types</see>
- /// </remarks>
- /// https://kybernetik.com.au/animancer/api/Animancer.FSM/State
- ///
- public abstract class State : IState
- {
- /************************************************************************************************************************/
- /// <summary><see cref="IState.CanEnterState"/></summary>
- /// <remarks>Returns true unless overridden.</remarks>
- public virtual bool CanEnterState => true;
- /// <summary><see cref="IState.CanExitState"/></summary>
- /// <remarks>Returns true unless overridden.</remarks>
- public virtual bool CanExitState => true;
- /// <summary><see cref="IState.OnEnterState"/></summary>
- public virtual void OnEnterState() { }
- /// <summary><see cref="IState.OnExitState"/></summary>
- public virtual void OnExitState() { }
- /************************************************************************************************************************/
- }
- /************************************************************************************************************************/
- /// <summary>Various extension methods for <see cref="IState"/> and <see cref="IOwnedState{TState}"/>.</summary>
- ///
- /// <remarks>
- /// Documentation: <see href="https://kybernetik.com.au/animancer/docs/manual/fsm">Finite State Machines</see>
- /// </remarks>
- ///
- /// <example><code>
- /// 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();
- /// }
- /// }
- /// }
- /// </code>
- /// <h2>Inherited Types</h2>
- /// Unfortunately, if the field type is not the same as the <c>T</c> in the <c>IOwnedState<T></c>
- /// implementation then attempting to use these extension methods without specifying the generic argument will
- /// give the following error:
- /// <para></para>
- /// <em>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>'.</em>
- /// <para></para>
- /// For example, you might want to access members of a derived state class like this <c>SetTarget</c> method:
- /// <para></para><code>
- /// 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>();
- /// }
- /// }
- /// }
- /// </code>
- /// Unlike the <c>_Jump</c> example, the <c>_Attack</c> field is an <c>AttackState</c> rather than the base
- /// <c>CharacterState</c> so we can call <c>_Attack.SetTarget(...)</c> but that causes problems with these extension
- /// methods.
- /// <para></para>
- /// 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:
- /// <para></para><code>
- /// _Attack.TryEnterState();
- /// _Attack.TryEnterState<AttackState>();
- /// </code>
- /// The problem is that <c>AttackState</c> inherits the implementation of <c>IOwnedState</c> from the base
- /// <c>CharacterState</c> class. But since that implementation is <c>IOwnedState<CharacterState></c>, rather
- /// than <c>IOwnedState<AttackState></c> that means <c>TryEnterState<AttackState></c> does not satisfy
- /// that method's generic constraints: <c>where TState : class, IOwnedState<TState></c>
- /// <para></para>
- /// That is why you simply need to specify the base class which implements <c>IOwnedState</c> as the generic
- /// argument to prevent it from inferring the wrong type:
- /// <para></para><code>
- /// _Attack.TryEnterState<CharacterState>();
- /// </code></example>
- /// https://kybernetik.com.au/animancer/api/Animancer.FSM/StateExtensions
- [HelpURL(APIDocumentationURL + nameof(StateExtensions))]
- public static class StateExtensions
- {
- /************************************************************************************************************************/
- /// <summary>The URL of the API documentation for the <see cref="FSM"/> system.</summary>
- public const string APIDocumentationURL = "https://kybernetik.com.au/animancer/api/Animancer.FSM/";
- /************************************************************************************************************************/
- /// <summary>[Animancer Extension] Returns the <see cref="StateChange{TState}.PreviousState"/>.</summary>
- public static TState GetPreviousState<TState>(this TState state)
- where TState : class, IState
- => StateChange<TState>.PreviousState;
- /// <summary>[Animancer Extension] Returns the <see cref="StateChange{TState}.NextState"/>.</summary>
- public static TState GetNextState<TState>(this TState state)
- where TState : class, IState
- => StateChange<TState>.NextState;
- /************************************************************************************************************************/
- /// <summary>[Animancer Extension]
- /// Checks if the specified `state` is the <see cref="StateMachine{TState}.CurrentState"/> in its
- /// <see cref="IOwnedState{TState}.OwnerStateMachine"/>.
- /// </summary>
- public static bool IsCurrentState<TState>(this TState state)
- where TState : class, IOwnedState<TState>
- => state.OwnerStateMachine.CurrentState == state;
- /************************************************************************************************************************/
- /// <summary>[Animancer Extension]
- /// Attempts to enter the specified `state` and returns true if successful.
- /// <para></para>
- /// 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="TryReEnterState"/> instead.
- /// </summary>
- public static bool TryEnterState<TState>(this TState state)
- where TState : class, IOwnedState<TState>
- => state.OwnerStateMachine.TrySetState(state);
- /************************************************************************************************************************/
- /// <summary>[Animancer Extension]
- /// Attempts to enter the specified `state` and returns true if successful.
- /// <para></para>
- /// This method does not check if the `state` is already the <see cref="StateMachine{TState}.CurrentState"/>.
- /// To do so, use <see cref="TryEnterState"/> instead.
- /// </summary>
- public static bool TryReEnterState<TState>(this TState state)
- where TState : class, IOwnedState<TState>
- => state.OwnerStateMachine.TryResetState(state);
- /************************************************************************************************************************/
- /// <summary>[Animancer Extension]
- /// Calls <see cref="IState.OnExitState"/> on the <see cref="StateMachine{TState}.CurrentState"/> then
- /// changes to the specified `state` and calls <see cref="IState.OnEnterState"/> on it.
- /// <para></para>
- /// This method does not check <see cref="IState.CanExitState"/> or
- /// <see cref="IState.CanEnterState"/>. To do that, you should use <see cref="TrySetState"/> instead.
- /// </summary>
- public static void ForceEnterState<TState>(this TState state)
- where TState : class, IOwnedState<TState>
- => 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
- ///************************************************************************************************************************/
- ///// <summary>
- ///// Checks if this state is the <see cref="StateMachine{TState}.CurrentState"/> in its
- ///// <see cref="IOwnedState{TState}.OwnerStateMachine"/>.
- ///// </summary>
- //public bool IsCurrentState() => OwnerStateMachine.CurrentState == this;
- ///************************************************************************************************************************/
- ///// <summary>
- ///// Calls <see cref="StateMachine{TState}.TrySetState(TState)"/> on the
- ///// <see cref="IOwnedState{TState}.OwnerStateMachine"/>.
- ///// </summary>
- //public bool TryEnterState() => OwnerStateMachine.TrySetState(this);
- ///************************************************************************************************************************/
- ///// <summary>
- ///// Calls <see cref="StateMachine{TState}.TryResetState(TState)"/> on the
- ///// <see cref="IOwnedState{TState}.OwnerStateMachine"/>.
- ///// </summary>
- //public bool TryReEnterState() => OwnerStateMachine.TryResetState(this);
- ///************************************************************************************************************************/
- ///// <summary>
- ///// Calls <see cref="StateMachine{TState}.ForceSetState(TState)"/> on the
- ///// <see cref="IOwnedState{TState}.OwnerStateMachine"/>.
- ///// </summary>
- //public void ForceEnterState() => OwnerStateMachine.ForceSetState(this);
- ///************************************************************************************************************************/
- //#endregion
- ///************************************************************************************************************************/
- #if UNITY_ASSERTIONS
- /// <summary>[Internal] Returns an error message explaining that the wrong type of change is being accessed.</summary>
- internal static string GetChangeError(Type stateType, Type machineType, string changeType = "State")
- {
- Type previousType = null;
- Type baseStateType = null;
- System.Collections.Generic.HashSet<Type> 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<Type>();
- 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<IState>)} 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<IState>.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
- /************************************************************************************************************************/
- }
- }
|