chore: initial commit
This commit is contained in:
@@ -0,0 +1,68 @@
|
||||
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
|
||||
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Reflection;
|
||||
|
||||
[assembly: AssemblyTitle("Kybernetik.Animancer.FSM")]
|
||||
[assembly: AssemblyDescription("A Finite State Machine system for Unity.")]
|
||||
[assembly: AssemblyProduct("Animancer")]
|
||||
[assembly: AssemblyCompany("Kybernetik")]
|
||||
[assembly: AssemblyCopyright("Copyright © Kybernetik 2018-2026")]
|
||||
[assembly: AssemblyVersion("8.3.0.36")]
|
||||
|
||||
#if UNITY_EDITOR
|
||||
|
||||
[assembly: SuppressMessage("Style", "IDE0016:Use 'throw' expression",
|
||||
Justification = "Not supported by older Unity versions.")]
|
||||
[assembly: SuppressMessage("Style", "IDE0019:Use pattern matching",
|
||||
Justification = "Not supported by older Unity versions.")]
|
||||
[assembly: SuppressMessage("Style", "IDE0039:Use local function",
|
||||
Justification = "Not supported by older Unity versions.")]
|
||||
[assembly: SuppressMessage("Style", "IDE0044:Make field readonly",
|
||||
Justification = "Using the [SerializeField] attribute on a private field means Unity will set it from serialized data.")]
|
||||
[assembly: SuppressMessage("Code Quality", "IDE0051:Remove unused private members",
|
||||
Justification = "Unity messages can be private, but the IDE will not know that Unity can still call them.")]
|
||||
[assembly: SuppressMessage("Code Quality", "IDE0052:Remove unread private members",
|
||||
Justification = "Unity messages can be private and don't need to be called manually.")]
|
||||
[assembly: SuppressMessage("Style", "IDE0060:Remove unused parameter",
|
||||
Justification = "Unity messages sometimes need specific signatures, even if you don't use all the parameters.")]
|
||||
[assembly: SuppressMessage("Style", "IDE0063:Use simple 'using' statement",
|
||||
Justification = "Not supported by older Unity versions.")]
|
||||
[assembly: SuppressMessage("Style", "IDE0066:Convert switch statement to expression",
|
||||
Justification = "Not supported by older Unity versions.")]
|
||||
[assembly: SuppressMessage("Code Quality", "IDE0067:Dispose objects before losing scope",
|
||||
Justification = "Not always relevant.")]
|
||||
[assembly: SuppressMessage("Code Quality", "IDE0068:Use recommended dispose pattern",
|
||||
Justification = "Not always relevant.")]
|
||||
[assembly: SuppressMessage("Code Quality", "IDE0069:Disposable fields should be disposed",
|
||||
Justification = "Not always relevant.")]
|
||||
[assembly: SuppressMessage("Style", "IDE0083:Use pattern matching",
|
||||
Justification = "Not supported by older Unity versions")]
|
||||
[assembly: SuppressMessage("Style", "IDE0090:Use 'new(...)'",
|
||||
Justification = "Not supported by older Unity versions.")]
|
||||
[assembly: SuppressMessage("CodeQuality", "IDE0079:Remove unnecessary suppression",
|
||||
Justification = "Don't give code style advice in publically released code.")]
|
||||
[assembly: SuppressMessage("Style", "IDE1006:Naming Styles",
|
||||
Justification = "Don't give code style advice in publically released code.")]
|
||||
|
||||
[assembly: SuppressMessage("Correctness", "UNT0005:Suspicious Time.deltaTime usage",
|
||||
Justification = "Time.deltaTime is not suspicious in FixedUpdate, it has the same value as Time.fixedDeltaTime")]
|
||||
[assembly: SuppressMessage("Correctness", "UNT0029:Pattern matching with null on Unity objects",
|
||||
Justification = "Use a regular equality check if handling destroyed objects is necessary")]
|
||||
|
||||
[assembly: SuppressMessage("Code Quality", "CS0649:Field is never assigned to, and will always have its default value",
|
||||
Justification = "Using the [SerializeField] attribute on a private field means Unity will set it from serialized data.")]
|
||||
|
||||
[assembly: SuppressMessage("Microsoft.Design", "CA1001:TypesThatOwnDisposableFieldsShouldBeDisposable",
|
||||
Justification = "Having a field doesn't mean you are responsible for creating and destroying it.")]
|
||||
[assembly: SuppressMessage("Microsoft.Design", "CA1009:DeclareEventHandlersCorrectly",
|
||||
Justification = "Not all events need to care about the sender.")]
|
||||
[assembly: SuppressMessage("Microsoft.Design", "CA1033:InterfaceMethodsShouldBeCallableByChildTypes",
|
||||
Justification = "No need to pollute the member list of implementing types.")]
|
||||
[assembly: SuppressMessage("Microsoft.Design", "CA1063:ImplementIDisposableCorrectly",
|
||||
Justification = "No need to pollute the member list of implementing types.")]
|
||||
[assembly: SuppressMessage("Microsoft.Usage", "CA2235:MarkAllNonSerializableFields",
|
||||
Justification = "UnityEngine.Object is serializable by Unity.")]
|
||||
|
||||
#endif
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 94a6e52abdbcf8345be407d6740230a2
|
||||
labels:
|
||||
- FSM
|
||||
- FiniteStateMachine
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,52 @@
|
||||
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
|
||||
|
||||
using System;
|
||||
|
||||
namespace Animancer.FSM
|
||||
{
|
||||
/// <summary>An <see cref="IState"/> that uses delegates to define its behaviour.</summary>
|
||||
/// <remarks>
|
||||
/// <strong>Documentation:</strong>
|
||||
/// <see href="https://kybernetik.com.au/animancer/docs/manual/fsm/state-types">
|
||||
/// State Types</see>
|
||||
/// </remarks>
|
||||
/// https://kybernetik.com.au/animancer/api/Animancer.FSM/DelegateState
|
||||
///
|
||||
public class DelegateState : IState
|
||||
{
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Determines whether this state can be entered. Null is treated as returning true.</summary>
|
||||
public Func<bool> canEnter;
|
||||
|
||||
/// <summary>[<see cref="IState"/>] Calls <see cref="canEnter"/> to determine whether this state can be entered.</summary>
|
||||
public virtual bool CanEnterState => canEnter == null || canEnter();
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Determines whether this state can be exited. Null is treated as returning true.</summary>
|
||||
public Func<bool> canExit;
|
||||
|
||||
/// <summary>[<see cref="IState"/>] Calls <see cref="canExit"/> to determine whether this state can be exited.</summary>
|
||||
public virtual bool CanExitState => canExit == null || canExit();
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Called when this state is entered.</summary>
|
||||
public Action onEnter;
|
||||
|
||||
/// <summary>[<see cref="IState"/>] Calls <see cref="onEnter"/> when this state is entered.</summary>
|
||||
public virtual void OnEnterState() => onEnter?.Invoke();
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Called when this state is exited.</summary>
|
||||
public Action onExit;
|
||||
|
||||
/// <summary>[<see cref="IState"/>] Calls <see cref="onExit"/> when this state is exited.</summary>
|
||||
public virtual void OnExitState() => onExit?.Invoke();
|
||||
|
||||
/************************************************************************************************************************/
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 7a0b6fcf1d3471d4aa75f4c39f3c1c1e
|
||||
labels:
|
||||
- FSM
|
||||
- FiniteStateMachine
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,427 @@
|
||||
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 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>
|
||||
/// <strong>Documentation:</strong>
|
||||
/// <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>
|
||||
/// <strong>Documentation:</strong>
|
||||
/// <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>
|
||||
/// <strong>Documentation:</strong>
|
||||
/// <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>
|
||||
/// <strong>Documentation:</strong>
|
||||
/// <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
|
||||
{
|
||||
activeChangeTypes ??= new();
|
||||
|
||||
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[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 this: 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
|
||||
|
||||
/************************************************************************************************************************/
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 44bb48284153d6e498f900eb062fc584
|
||||
labels:
|
||||
- FSM
|
||||
- FiniteStateMachine
|
||||
- Interface
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,129 @@
|
||||
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
|
||||
|
||||
using System;
|
||||
|
||||
namespace Animancer.FSM
|
||||
{
|
||||
/// <summary>A static access point for the details of a key change in a <see cref="StateMachine{TKey, TState}"/>.</summary>
|
||||
/// <remarks>
|
||||
/// This system is thread-safe.
|
||||
/// <para></para>
|
||||
/// <strong>Documentation:</strong>
|
||||
/// <see href="https://kybernetik.com.au/animancer/docs/manual/fsm/changing-states">
|
||||
/// Changing States</see>
|
||||
/// </remarks>
|
||||
/// https://kybernetik.com.au/animancer/api/Animancer.FSM/KeyChange_1
|
||||
///
|
||||
public struct KeyChange<TKey> : IDisposable
|
||||
{
|
||||
/************************************************************************************************************************/
|
||||
|
||||
[ThreadStatic]
|
||||
private static KeyChange<TKey> _Current;
|
||||
|
||||
private IKeyedStateMachine<TKey> _StateMachine;
|
||||
private TKey _PreviousKey;
|
||||
private TKey _NextKey;
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Is a <see cref="KeyChange{TKey}"/> of this type currently occurring?</summary>
|
||||
public static bool IsActive => _Current._StateMachine != null;
|
||||
|
||||
/// <summary>The <see cref="KeyChange{TKey}"/> in which the current change is occurring.</summary>
|
||||
/// <remarks>This will be null if no change is currently occurring.</remarks>
|
||||
public static IKeyedStateMachine<TKey> StateMachine => _Current._StateMachine;
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>The key being changed from.</summary>
|
||||
/// <exception cref="InvalidOperationException">[Assert-Only]
|
||||
/// <see cref="IsActive"/> is false so this property is likely being accessed on the wrong generic type.
|
||||
/// </exception>
|
||||
public static TKey PreviousKey
|
||||
{
|
||||
get
|
||||
{
|
||||
#if UNITY_ASSERTIONS
|
||||
if (!IsActive)
|
||||
throw new InvalidOperationException(StateExtensions.GetChangeError(typeof(TKey), typeof(StateMachine<,>), "Key"));
|
||||
#endif
|
||||
return _Current._PreviousKey;
|
||||
}
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>The key being changed into.</summary>
|
||||
/// <exception cref="InvalidOperationException">[Assert-Only]
|
||||
/// <see cref="IsActive"/> is false so this property is likely being accessed on the wrong generic type.
|
||||
/// </exception>
|
||||
public static TKey NextKey
|
||||
{
|
||||
get
|
||||
{
|
||||
#if UNITY_ASSERTIONS
|
||||
if (!IsActive)
|
||||
throw new InvalidOperationException(StateExtensions.GetChangeError(typeof(TKey), typeof(StateMachine<,>), "Key"));
|
||||
#endif
|
||||
return _Current._NextKey;
|
||||
}
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>[Internal]
|
||||
/// Assigns the parameters as the details of the currently active change and creates a new
|
||||
/// <see cref="KeyChange{TKey}"/> containing the details of the previously active change so that disposing
|
||||
/// it will re-assign those previous details to be current again in case of recursive state changes.
|
||||
/// </summary>
|
||||
///
|
||||
/// <remarks>
|
||||
/// <strong>Example:</strong><code>
|
||||
/// using (new KeyChange<TState>(previousKey, nextKey))
|
||||
/// {
|
||||
/// // Do the actual key change.
|
||||
/// }
|
||||
/// </code></remarks>
|
||||
///
|
||||
internal KeyChange(IKeyedStateMachine<TKey> stateMachine, TKey previousKey, TKey nextKey)
|
||||
{
|
||||
this = _Current;
|
||||
|
||||
_Current._StateMachine = stateMachine;
|
||||
_Current._PreviousKey = previousKey;
|
||||
_Current._NextKey = nextKey;
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>[<see cref="IDisposable"/>]
|
||||
/// Re-assigns the values of this change (which were the previous values from when it was created) to be the
|
||||
/// currently active change. See the constructor for recommended usage.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Usually this will be returning to default values (nulls), but if one state change causes another then the
|
||||
/// second one ending will return to the first which will then return to the defaults.
|
||||
/// </remarks>
|
||||
public readonly void Dispose()
|
||||
{
|
||||
_Current = this;
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Returns a string describing the contents of this <see cref="KeyChange{TKey}"/>.</summary>
|
||||
public override readonly string ToString() => IsActive
|
||||
? $"{nameof(KeyChange<TKey>)}<{typeof(TKey).FullName}" +
|
||||
$">({nameof(PreviousKey)}={PreviousKey}" +
|
||||
$", {nameof(NextKey)}={NextKey})"
|
||||
: $"{nameof(KeyChange<TKey>)}<{typeof(TKey).FullName}(Not Currently Active)";
|
||||
|
||||
/// <summary>Returns a string describing the contents of the current <see cref="KeyChange{TKey}"/>.</summary>
|
||||
public static string CurrentToString()
|
||||
=> _Current.ToString();
|
||||
|
||||
/************************************************************************************************************************/
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
fileFormatVersion: 2
|
||||
guid: ba135b8e83a177043a5e505a7e9832e7
|
||||
labels:
|
||||
- FSM
|
||||
- FiniteStateMachine
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"name": "Kybernetik.Animancer.FSM",
|
||||
"rootNamespace": "",
|
||||
"references": [],
|
||||
"includePlatforms": [],
|
||||
"excludePlatforms": [],
|
||||
"allowUnsafeCode": false,
|
||||
"overrideReferences": false,
|
||||
"precompiledReferences": [],
|
||||
"autoReferenced": true,
|
||||
"defineConstraints": [],
|
||||
"versionDefines": [
|
||||
{
|
||||
"name": "com.unity.modules.imgui",
|
||||
"expression": "",
|
||||
"define": "UNITY_IMGUI"
|
||||
}
|
||||
],
|
||||
"noEngineReferences": false
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
fileFormatVersion: 2
|
||||
guid: b2665e54e4314ae429d34fdeafc9f3e0
|
||||
labels:
|
||||
- FSM
|
||||
- FiniteStateMachine
|
||||
AssemblyDefinitionImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,111 @@
|
||||
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
|
||||
|
||||
using UnityEngine;
|
||||
|
||||
namespace Animancer.FSM
|
||||
{
|
||||
/// <summary>Base class for <see cref="MonoBehaviour"/> states to be used in a <see cref="StateMachine{TState}"/>.</summary>
|
||||
/// <remarks>
|
||||
/// <strong>Documentation:</strong>
|
||||
/// <see href="https://kybernetik.com.au/animancer/docs/manual/fsm/state-types">
|
||||
/// State Types</see>
|
||||
/// </remarks>
|
||||
/// https://kybernetik.com.au/animancer/api/Animancer.FSM/StateBehaviour
|
||||
///
|
||||
// [HelpURL(StateExtensions.APIDocumentationURL + nameof(StateBehaviour))]
|
||||
public abstract class StateBehaviour : MonoBehaviour, IState
|
||||
{
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>[<see cref="IState.CanEnterState"/>]
|
||||
/// Determines whether the <see cref="StateMachine{TState}"/> can enter this state.
|
||||
/// Always returns true unless overridden.
|
||||
/// </summary>
|
||||
public virtual bool CanEnterState => true;
|
||||
|
||||
/// <summary>[<see cref="IState.CanExitState"/>]
|
||||
/// Determines whether the <see cref="StateMachine{TState}"/> can exit this state.
|
||||
/// Always returns true unless overridden.
|
||||
/// </summary>
|
||||
public virtual bool CanExitState => true;
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>[<see cref="IState.OnEnterState"/>]
|
||||
/// Asserts that this component isn't already enabled, then enables it.
|
||||
/// </summary>
|
||||
public virtual void OnEnterState()
|
||||
{
|
||||
AssertEnabledAndRepaintIfSelected(false, nameof(OnEnterState));
|
||||
|
||||
enabled = true;
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>[<see cref="IState.OnExitState"/>]
|
||||
/// Asserts that this component isn't already disabled, then disables it.
|
||||
/// </summary>
|
||||
public virtual void OnExitState()
|
||||
{
|
||||
if (this == null)
|
||||
return;
|
||||
|
||||
AssertEnabledAndRepaintIfSelected(true, nameof(OnExitState));
|
||||
|
||||
enabled = false;
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
#if UNITY_EDITOR
|
||||
/// <summary>[Editor-Only]
|
||||
/// Should the Inspector be repainted when a <see cref="StateBehaviour"/>
|
||||
/// is enabled or disabled while it is selected?
|
||||
/// </summary>
|
||||
/// <remarks>Default is true.</remarks>
|
||||
public static bool ForceRepaintOnEnableDisable { get; set; } = true;
|
||||
|
||||
private static double _LastRepaintTime;
|
||||
#endif
|
||||
|
||||
/// <summary>[Assert-Conditional]
|
||||
/// Asserts this <see cref="Behaviour.enabled"/>
|
||||
/// and instructs the Unity Editor to repaint if this object is selected so the Inspector updates properly.
|
||||
/// </summary>
|
||||
[System.Diagnostics.Conditional("UNITY_ASSERTIONS")]
|
||||
private void AssertEnabledAndRepaintIfSelected(bool expectEnabled, string callerName)
|
||||
{
|
||||
#if UNITY_ASSERTIONS
|
||||
if (enabled != expectEnabled)
|
||||
Debug.LogError(
|
||||
$"{nameof(StateBehaviour)} was already {(expectEnabled ? "disabled" : "enabled")}" +
|
||||
$" before {callerName}: {this}",
|
||||
this);
|
||||
#endif
|
||||
#if UNITY_EDITOR
|
||||
// Unity doesn't constantly repaint the Inspector if all the components are collapsed.
|
||||
// So we can simply force it here to ensure that it shows the correct state being enabled.
|
||||
if (ForceRepaintOnEnableDisable && UnityEditor.Selection.Contains(gameObject))
|
||||
UnityEditorInternal.InternalEditorUtility.RepaintAllViews();
|
||||
#endif
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
#if UNITY_EDITOR
|
||||
/// <summary>[Editor-Only] States start disabled and only the current state gets enabled at runtime.</summary>
|
||||
/// <remarks>Called in Edit Mode whenever this script is loaded or a value is changed in the Inspector.</remarks>
|
||||
protected virtual void OnValidate()
|
||||
{
|
||||
if (UnityEditor.EditorApplication.isPlayingOrWillChangePlaymode)
|
||||
return;
|
||||
|
||||
enabled = false;
|
||||
}
|
||||
#endif
|
||||
|
||||
/************************************************************************************************************************/
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 7d0ae395395b5d34da55dead806d6a08
|
||||
labels:
|
||||
- Component
|
||||
- FSM
|
||||
- FiniteStateMachine
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,131 @@
|
||||
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
|
||||
|
||||
using System;
|
||||
|
||||
namespace Animancer.FSM
|
||||
{
|
||||
/// <summary>A static access point for the details of a state change in a <see cref="StateMachine{TState}"/>.</summary>
|
||||
/// <remarks>
|
||||
/// This system is thread-safe.
|
||||
/// <para></para>
|
||||
/// <strong>Documentation:</strong>
|
||||
/// <see href="https://kybernetik.com.au/animancer/docs/manual/fsm/changing-states">
|
||||
/// Changing States</see>
|
||||
/// </remarks>
|
||||
/// https://kybernetik.com.au/animancer/api/Animancer.FSM/StateChange_1
|
||||
///
|
||||
public struct StateChange<TState> : IDisposable
|
||||
where TState : class, IState
|
||||
{
|
||||
/************************************************************************************************************************/
|
||||
|
||||
[ThreadStatic]
|
||||
private static StateChange<TState> _Current;
|
||||
|
||||
private StateMachine<TState> _StateMachine;
|
||||
private TState _PreviousState;
|
||||
private TState _NextState;
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Is a <see cref="StateChange{TState}"/> of this type currently occurring?</summary>
|
||||
public static bool IsActive
|
||||
=> _Current._StateMachine != null;
|
||||
|
||||
/// <summary>The <see cref="StateMachine{TState}"/> in which the current change is occurring.</summary>
|
||||
/// <remarks>This will be null if no change is currently occurring.</remarks>
|
||||
public static StateMachine<TState> StateMachine
|
||||
=> _Current._StateMachine;
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>The state currently being changed from.</summary>
|
||||
/// <exception cref="InvalidOperationException">[Assert-Only]
|
||||
/// <see cref="IsActive"/> is false so this property is likely being accessed on the wrong generic type.
|
||||
/// </exception>
|
||||
public static TState PreviousState
|
||||
{
|
||||
get
|
||||
{
|
||||
#if UNITY_ASSERTIONS
|
||||
if (!IsActive)
|
||||
throw new InvalidOperationException(
|
||||
StateExtensions.GetChangeError(typeof(TState), typeof(StateMachine<>)));
|
||||
#endif
|
||||
return _Current._PreviousState;
|
||||
}
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>The state being changed into.</summary>
|
||||
/// <exception cref="InvalidOperationException">[Assert-Only]
|
||||
/// <see cref="IsActive"/> is false so this property is likely being accessed on the wrong generic type.
|
||||
/// </exception>
|
||||
public static TState NextState
|
||||
{
|
||||
get
|
||||
{
|
||||
#if UNITY_ASSERTIONS
|
||||
if (!IsActive)
|
||||
throw new InvalidOperationException(
|
||||
StateExtensions.GetChangeError(typeof(TState), typeof(StateMachine<>)));
|
||||
#endif
|
||||
return _Current._NextState;
|
||||
}
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>[Internal]
|
||||
/// Assigns the parameters as the details of the currently active change and creates a new
|
||||
/// <see cref="StateChange{TState}"/> containing the details of the previously active change so that disposing
|
||||
/// it will re-assign those previous details to be current again in case of recursive state changes.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <strong>Example:</strong><code>
|
||||
/// using (new StateChange<TState>(stateMachine, previousState, nextState))
|
||||
/// {
|
||||
/// // Do the actual state change.
|
||||
/// }
|
||||
/// </code></remarks>
|
||||
internal StateChange(StateMachine<TState> stateMachine, TState previousState, TState nextState)
|
||||
{
|
||||
this = _Current;
|
||||
|
||||
_Current._StateMachine = stateMachine;
|
||||
_Current._PreviousState = previousState;
|
||||
_Current._NextState = nextState;
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>[<see cref="IDisposable"/>]
|
||||
/// Re-assigns the values of this change (which were the previous values from when it was created)
|
||||
/// to be the currently active change. See the constructor for recommended usage.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Usually this will be returning to default values (nulls), but if one state change causes another
|
||||
/// then the second one ending will return to the first which will then return to the defaults.
|
||||
/// </remarks>
|
||||
public readonly void Dispose()
|
||||
=> _Current = this;
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Returns a string describing the contents of this <see cref="StateChange{TState}"/>.</summary>
|
||||
public override readonly string ToString()
|
||||
=> IsActive
|
||||
? $"{nameof(StateChange<TState>)}<{typeof(TState).FullName}" +
|
||||
$">({nameof(PreviousState)}='{_PreviousState}'" +
|
||||
$", {nameof(NextState)}='{_NextState}')"
|
||||
: $"{nameof(StateChange<TState>)}<{typeof(TState).FullName}(Not Currently Active)";
|
||||
|
||||
/// <summary>Returns a string describing the contents of the current <see cref="StateChange{TState}"/>.</summary>
|
||||
public static string CurrentToString()
|
||||
=> _Current.ToString();
|
||||
|
||||
/************************************************************************************************************************/
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
fileFormatVersion: 2
|
||||
guid: d96becb371c86e241b44dea56e55385a
|
||||
labels:
|
||||
- FSM
|
||||
- FiniteStateMachine
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,212 @@
|
||||
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
|
||||
|
||||
using System;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Animancer.FSM
|
||||
{
|
||||
public partial class StateMachine<TState>
|
||||
{
|
||||
/// <summary>
|
||||
/// A simple system that can <see cref="InputBuffer{TStateMachine}.Buffer"/> a state then try to enter it every
|
||||
/// time <see cref="InputBuffer{TStateMachine}.Update(float)"/> is called until the
|
||||
/// <see cref="InputBuffer{TStateMachine}.TimeOut"/> expires.
|
||||
/// </summary>
|
||||
///
|
||||
/// <remarks>
|
||||
/// <strong>Documentation:</strong>
|
||||
/// <see href="https://kybernetik.com.au/animancer/docs/manual/fsm/utilities#input-buffers">
|
||||
/// Input Buffers</see>
|
||||
/// <para></para>
|
||||
/// See <see cref="StateMachine{TState}.InputBuffer{TStateMachine}"/> for example usage.
|
||||
/// </remarks>
|
||||
///
|
||||
/// https://kybernetik.com.au/animancer/api/Animancer.FSM/InputBuffer
|
||||
///
|
||||
public class InputBuffer : InputBuffer<StateMachine<TState>>
|
||||
{
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Creates a new <see cref="InputBuffer"/>.</summary>
|
||||
public InputBuffer() { }
|
||||
|
||||
/// <summary>Creates a new <see cref="InputBuffer"/> for the specified `stateMachine`.</summary>
|
||||
public InputBuffer(StateMachine<TState> stateMachine) : base(stateMachine) { }
|
||||
|
||||
/************************************************************************************************************************/
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A simple system that can <see cref="Buffer"/> a state then try to enter it every time
|
||||
/// <see cref="Update(float)"/> is called until the <see cref="TimeOut"/> expires.
|
||||
/// </summary>
|
||||
///
|
||||
/// <remarks>
|
||||
/// <strong>Documentation:</strong>
|
||||
/// <see href="https://kybernetik.com.au/animancer/docs/manual/fsm/utilities#input-buffers">
|
||||
/// Input Buffers</see>
|
||||
/// <para></para>
|
||||
/// <strong>Example:</strong><code>
|
||||
/// public StateMachine<CharacterState> stateMachine;// Initialized elsewhere.
|
||||
///
|
||||
/// [SerializeField] private CharacterState _Attack;
|
||||
/// [SerializeField] private float _AttackInputTimeOut = 0.5f;
|
||||
///
|
||||
/// private StateMachine<CharacterState>.InputBuffer _InputBuffer;
|
||||
///
|
||||
/// private void Awake()
|
||||
/// {
|
||||
/// // Initialize the buffer.
|
||||
/// _InputBuffer = new StateMachine<CharacterState>.InputBuffer(stateMachine);
|
||||
/// }
|
||||
///
|
||||
/// private void Update()
|
||||
/// {
|
||||
/// // When input is detected, buffer the desired state.
|
||||
/// if (Input.GetButtonDown("Fire1"))// Left Click by default.
|
||||
/// {
|
||||
/// _InputBuffer.Buffer(_Attack, _AttackInputTimeOut);
|
||||
/// }
|
||||
///
|
||||
/// // At the end of the frame, Update the buffer so it tries to enter the buffered state.
|
||||
/// // After the time out, it will clear itself so Update does nothing until something else is buffered.
|
||||
/// _InputBuffer.Update();
|
||||
/// }
|
||||
/// </code></remarks>
|
||||
///
|
||||
/// https://kybernetik.com.au/animancer/api/Animancer.FSM/InputBuffer_1
|
||||
///
|
||||
public class InputBuffer<TStateMachine> where TStateMachine : StateMachine<TState>
|
||||
{
|
||||
/************************************************************************************************************************/
|
||||
|
||||
private TStateMachine _StateMachine;
|
||||
private Action _ForceDefaultState;
|
||||
|
||||
/// <summary>The <see cref="StateMachine{TState}"/> this buffer is feeding input to.</summary>
|
||||
public TStateMachine StateMachine
|
||||
{
|
||||
get => _StateMachine;
|
||||
set
|
||||
{
|
||||
if (_StateMachine is WithDefault withDefault)
|
||||
withDefault.ForceSetDefaultState = _ForceDefaultState;
|
||||
|
||||
_StateMachine = value;
|
||||
|
||||
TryRegisterForceSetDefaultState();
|
||||
Clear();
|
||||
}
|
||||
}
|
||||
|
||||
private void TryRegisterForceSetDefaultState()
|
||||
{
|
||||
if (_StateMachine is WithDefault withDefault)
|
||||
{
|
||||
_ForceDefaultState = withDefault.ForceSetDefaultState;
|
||||
withDefault.ForceSetDefaultState = TryEnterStateOrForceDefault;
|
||||
}
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>The <typeparamref name="TState"/> this buffer is currently attempting to enter.</summary>
|
||||
public TState State { get; set; }
|
||||
|
||||
/// <summary>The amount of time left before the <see cref="State"/> is cleared.</summary>
|
||||
public float TimeOut { get; set; }
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Is this buffer currently trying to enter a <see cref="State"/>?</summary>
|
||||
public bool IsActive => State != null;
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Creates a new <see cref="InputBuffer{TStateMachine}"/>.</summary>
|
||||
public InputBuffer() { }
|
||||
|
||||
/// <summary>Creates a new <see cref="InputBuffer{TStateMachine}"/> for the specified `stateMachine`.</summary>
|
||||
public InputBuffer(TStateMachine stateMachine)
|
||||
{
|
||||
_StateMachine = stateMachine;
|
||||
TryRegisterForceSetDefaultState();
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Sets the <see cref="State"/> and <see cref="TimeOut"/>.</summary>
|
||||
/// <remarks>Doesn't actually attempt to enter the state until <see cref="Update(float)"/> is called.</remarks>
|
||||
public void Buffer(TState state, float timeOut)
|
||||
{
|
||||
State = state;
|
||||
TimeOut = timeOut;
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Attempts to enter the <see cref="State"/> and returns true if successful.</summary>
|
||||
protected virtual bool TryEnterState()
|
||||
=> StateMachine.TryResetState(State);
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>
|
||||
/// Calls <see cref="TryEnterState"/>. If it fails, then <see cref="WithDefault.ForceSetDefaultState"/>.
|
||||
/// </summary>
|
||||
public void TryEnterStateOrForceDefault()
|
||||
{
|
||||
if (IsActive &&
|
||||
TryEnterState())
|
||||
return;
|
||||
|
||||
_ForceDefaultState();
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Calls <see cref="Update(float)"/> using <see cref="Time.deltaTime"/>.</summary>
|
||||
/// <remarks>This method should be called at the end of a frame after any calls to <see cref="Buffer"/>.</remarks>
|
||||
public bool Update()
|
||||
=> Update(Time.deltaTime);
|
||||
|
||||
/// <summary>
|
||||
/// Attempts to enter the <see cref="State"/> if there is one and returns true if successful. Otherwise the
|
||||
/// <see cref="TimeOut"/> is decreased by `deltaTime` and <see cref="Clear"/> is called if it reaches 0.
|
||||
/// </summary>
|
||||
/// <remarks>This method should be called at the end of a frame after any calls to <see cref="Buffer"/>.</remarks>
|
||||
public bool Update(float deltaTime)
|
||||
{
|
||||
if (IsActive)
|
||||
{
|
||||
if (TryEnterState())
|
||||
{
|
||||
Clear();
|
||||
return true;
|
||||
}
|
||||
else
|
||||
{
|
||||
TimeOut -= deltaTime;
|
||||
|
||||
if (TimeOut < 0)
|
||||
Clear();
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Clears this buffer so it stops trying to enter the <see cref="State"/>.</summary>
|
||||
public virtual void Clear()
|
||||
{
|
||||
State = null;
|
||||
TimeOut = default;
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 3f0140c1027c5254882ca6415a514880
|
||||
labels:
|
||||
- FSM
|
||||
- FiniteStateMachine
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,83 @@
|
||||
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
|
||||
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Animancer.FSM
|
||||
{
|
||||
/// <summary>An object with a <see cref="Priority"/>.</summary>
|
||||
/// <remarks>
|
||||
/// <strong>Documentation:</strong>
|
||||
/// <see href="https://kybernetik.com.au/animancer/docs/manual/fsm/utilities#state-selectors">
|
||||
/// State Selectors</see>
|
||||
/// </remarks>
|
||||
/// https://kybernetik.com.au/animancer/api/Animancer.FSM/IPrioritizable
|
||||
///
|
||||
public interface IPrioritizable : IState
|
||||
{
|
||||
float Priority { get; }
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
public partial class StateMachine<TState>
|
||||
{
|
||||
/// <summary>A prioritised list of potential states for a <see cref="StateMachine{TState}"/> to enter.</summary>
|
||||
///
|
||||
/// <remarks>
|
||||
/// <strong>Documentation:</strong>
|
||||
/// <see href="https://kybernetik.com.au/animancer/docs/manual/fsm#state-selectors">
|
||||
/// State Selectors</see>
|
||||
/// <para></para>
|
||||
/// <strong>Example:</strong><code>
|
||||
/// public StateMachine<CharacterState> stateMachine;
|
||||
/// public CharacterState run;
|
||||
/// public CharacterState idle;
|
||||
///
|
||||
/// private readonly StateMachine<CharacterState>.StateSelector
|
||||
/// Selector = new();
|
||||
///
|
||||
/// private void Awake()
|
||||
/// {
|
||||
/// Selector.Add(1, run);
|
||||
/// Selector.Add(0, idle);
|
||||
/// }
|
||||
///
|
||||
/// public void RunOrIdle()
|
||||
/// {
|
||||
/// stateMachine.TrySetState(Selector.Values);
|
||||
/// // The "run" state has the highest priority so this will enter it if "run.CanEnterState" returns true.
|
||||
/// // Otherwise if "idle.CanEnterState" returns true it will enter that state instead.
|
||||
/// // If neither allows the transition, nothing happens and "stateMachine.TrySetState" returns false.
|
||||
/// }
|
||||
/// </code></remarks>
|
||||
///
|
||||
/// https://kybernetik.com.au/animancer/api/Animancer.FSM/StateSelector
|
||||
///
|
||||
public class StateSelector : SortedList<float, TState>
|
||||
{
|
||||
public StateSelector() : base(ReverseComparer<float>.Instance) { }
|
||||
|
||||
/// <summary>Adds the `state` to this selector with its <see cref="IPrioritizable.Priority"/>.</summary>
|
||||
public void Add<TPrioritizable>(TPrioritizable state)
|
||||
where TPrioritizable : TState, IPrioritizable
|
||||
=> Add(state.Priority, state);
|
||||
}
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>An <see cref="IComparer{T}"/> which reverses the default comparison.</summary>
|
||||
/// https://kybernetik.com.au/animancer/api/Animancer.FSM/ReverseComparer_1
|
||||
public class ReverseComparer<T> : IComparer<T>
|
||||
{
|
||||
/// <summary>The singleton instance.</summary>
|
||||
public static readonly ReverseComparer<T> Instance = new();
|
||||
|
||||
/// <summary>No need to let users create other instances.</summary>
|
||||
private ReverseComparer() { }
|
||||
|
||||
/// <summary>Uses <see cref="Comparer{T}.Default"/> with the parameters swapped.</summary>
|
||||
public int Compare(T x, T y) => Comparer<T>.Default.Compare(y, x);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 0f882cb524ddbc8419c5beba63a1939f
|
||||
labels:
|
||||
- FSM
|
||||
- FiniteStateMachine
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,145 @@
|
||||
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
|
||||
|
||||
using System;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Animancer.FSM
|
||||
{
|
||||
/// https://kybernetik.com.au/animancer/api/Animancer.FSM/StateMachine_1
|
||||
partial class StateMachine<TState>
|
||||
{
|
||||
/// <summary>A <see cref="StateMachine{TState}"/> with a <see cref="DefaultState"/>.</summary>
|
||||
/// <remarks>
|
||||
/// See <see cref="InitializeAfterDeserialize"/> if using this class in a serialized field.
|
||||
/// <para></para>
|
||||
/// <strong>Documentation:</strong>
|
||||
/// <see href="https://kybernetik.com.au/animancer/docs/manual/fsm/changing-states#default-states">
|
||||
/// Default States</see>
|
||||
/// </remarks>
|
||||
/// https://kybernetik.com.au/animancer/api/Animancer.FSM/WithDefault
|
||||
///
|
||||
[Serializable]
|
||||
public class WithDefault : StateMachine<TState>
|
||||
{
|
||||
/************************************************************************************************************************/
|
||||
|
||||
[SerializeField]
|
||||
private TState _DefaultState;
|
||||
|
||||
/// <summary>The starting state and main state to return to when nothing else is active.</summary>
|
||||
/// <remarks>
|
||||
/// If the <see cref="CurrentState"/> is <c>null</c> when setting this value, it calls
|
||||
/// <see cref="ForceSetState(TState)"/> to enter the specified state immediately.
|
||||
/// <para></para>
|
||||
/// For a character, this would typically be their <em>Idle</em> state.
|
||||
/// </remarks>
|
||||
public TState DefaultState
|
||||
{
|
||||
get => _DefaultState;
|
||||
set
|
||||
{
|
||||
_DefaultState = value;
|
||||
if (_CurrentState == null && value != null)
|
||||
ForceSetState(value);
|
||||
}
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Calls <see cref="ForceSetState(TState)"/> with the <see cref="DefaultState"/>.</summary>
|
||||
/// <remarks>This delegate is cached to avoid allocating garbage when used in Animancer Events.</remarks>
|
||||
public Action ForceSetDefaultState;
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Creates a new <see cref="WithDefault"/>.</summary>
|
||||
public WithDefault()
|
||||
{
|
||||
// Silly C# doesn't allow instance delegates to be assigned using field initializers.
|
||||
ForceSetDefaultState = () => ForceSetState(_DefaultState);
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Creates a new <see cref="WithDefault"/> and sets the <see cref="DefaultState"/>.</summary>
|
||||
public WithDefault(TState defaultState)
|
||||
: this()
|
||||
{
|
||||
_DefaultState = defaultState;
|
||||
ForceSetState(defaultState);
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override void InitializeAfterDeserialize()
|
||||
{
|
||||
if (_CurrentState != null)
|
||||
{
|
||||
using (new StateChange<TState>(this, null, _CurrentState))
|
||||
_CurrentState.OnEnterState();
|
||||
}
|
||||
else if (_DefaultState != null)
|
||||
{
|
||||
using (new StateChange<TState>(this, null, CurrentState))
|
||||
{
|
||||
_CurrentState = _DefaultState;
|
||||
_CurrentState.OnEnterState();
|
||||
}
|
||||
}
|
||||
|
||||
// Don't call the base method.
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Attempts to enter the <see cref="DefaultState"/> and returns true if successful.</summary>
|
||||
/// <remarks>
|
||||
/// This method returns true immediately if the specified <see cref="DefaultState"/> is already the
|
||||
/// <see cref="CurrentState"/>. To allow directly re-entering the same state, use
|
||||
/// <see cref="TryResetDefaultState"/> instead.
|
||||
/// </remarks>
|
||||
public bool TrySetDefaultState() => TrySetState(DefaultState);
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Attempts to enter the <see cref="DefaultState"/> and returns true if successful.</summary>
|
||||
/// <remarks>
|
||||
/// This method does not check if the <see cref="DefaultState"/> is already the <see cref="CurrentState"/>.
|
||||
/// To do so, use <see cref="TrySetDefaultState"/> instead.
|
||||
/// </remarks>
|
||||
public bool TryResetDefaultState() => TryResetState(DefaultState);
|
||||
|
||||
/************************************************************************************************************************/
|
||||
#if UNITY_EDITOR && UNITY_IMGUI
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override int GUILineCount => 2;
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override void DoGUI(ref Rect area)
|
||||
{
|
||||
area.height = UnityEditor.EditorGUIUtility.singleLineHeight;
|
||||
|
||||
UnityEditor.EditorGUI.BeginChangeCheck();
|
||||
|
||||
var state = StateMachineUtilities.DoGenericField(area, "Default State", DefaultState);
|
||||
|
||||
if (UnityEditor.EditorGUI.EndChangeCheck())
|
||||
DefaultState = state;
|
||||
|
||||
StateMachineUtilities.NextVerticalArea(ref area);
|
||||
|
||||
base.DoGUI(ref area);
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
#endif
|
||||
/************************************************************************************************************************/
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
fileFormatVersion: 2
|
||||
guid: eb5d8db5c4119fd47a2f652836d193f1
|
||||
labels:
|
||||
- FSM
|
||||
- FiniteStateMachine
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,471 @@
|
||||
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
|
||||
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Animancer.FSM
|
||||
{
|
||||
/// <summary>A simple keyless Finite State Machine system.</summary>
|
||||
/// <remarks>
|
||||
/// <strong>Documentation:</strong>
|
||||
/// <see href="https://kybernetik.com.au/animancer/docs/manual/fsm">
|
||||
/// Finite State Machines</see>
|
||||
/// </remarks>
|
||||
/// https://kybernetik.com.au/animancer/api/Animancer.FSM/IStateMachine
|
||||
///
|
||||
public interface IStateMachine
|
||||
{
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>The currently active state.</summary>
|
||||
object CurrentState { get; }
|
||||
|
||||
/// <summary>The <see cref="StateChange{TState}.PreviousState"/>.</summary>
|
||||
object PreviousState { get; }
|
||||
|
||||
/// <summary>The <see cref="StateChange{TState}.NextState"/>.</summary>
|
||||
object NextState { get; }
|
||||
|
||||
/// <summary>Is it currently possible to enter the specified `state`?</summary>
|
||||
/// <remarks>
|
||||
/// This requires <see cref="IState.CanExitState"/> on the <see cref="CurrentState"/> and
|
||||
/// <see cref="IState.CanEnterState"/> on the specified `state` to both return true.
|
||||
/// </remarks>
|
||||
bool CanSetState(object state);
|
||||
|
||||
/// <summary>Returns the first of the `states` which can currently be entered.</summary>
|
||||
object CanSetState(IList states);
|
||||
|
||||
/// <summary>Attempts to enter the specified `state` and returns true if successful.</summary>
|
||||
/// <remarks>
|
||||
/// This method returns true immediately if the specified `state` is already the <see cref="CurrentState"/>.
|
||||
/// To allow directly re-entering the same state, use <see cref="TryResetState(object)"/> instead.
|
||||
/// </remarks>
|
||||
bool TrySetState(object state);
|
||||
|
||||
/// <summary>Attempts to enter any of the specified `states` and returns true if successful.</summary>
|
||||
/// <remarks>
|
||||
/// This method returns true and does nothing else if the <see cref="CurrentState"/> is in the list.
|
||||
/// To allow directly re-entering the same state, use <see cref="TryResetState(IList)"/> instead.
|
||||
/// <para></para>
|
||||
/// States are checked in ascending order (i.e. from <c>[0]</c> to <c>[states.Count - 1]</c>).
|
||||
/// </remarks>
|
||||
bool TrySetState(IList states);
|
||||
|
||||
/// <summary>Attempts to enter the specified `state` and returns true if successful.</summary>
|
||||
/// <remarks>
|
||||
/// This method does not check if the `state` is already the <see cref="CurrentState"/>. To do so, use
|
||||
/// <see cref="TrySetState(object)"/> instead.
|
||||
/// </remarks>
|
||||
bool TryResetState(object state);
|
||||
|
||||
/// <summary>Attempts to enter any of the specified `states` and returns true if successful.</summary>
|
||||
/// <remarks>
|
||||
/// This method does not check if the `state` is already the <see cref="CurrentState"/>. To do so, use
|
||||
/// <see cref="TrySetState(IList)"/> instead.
|
||||
/// <para></para>
|
||||
/// States are checked in ascending order (i.e. from <c>[0]</c> to <c>[states.Count - 1]</c>).
|
||||
/// </remarks>
|
||||
bool TryResetState(IList states);
|
||||
|
||||
/// <summary>
|
||||
/// Calls <see cref="IState.OnExitState"/> on the <see cref="CurrentState"/> then changes it to the
|
||||
/// specified `state` and calls <see cref="IState.OnEnterState"/> on it.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This method does not check <see cref="IState.CanExitState"/> or
|
||||
/// <see cref="IState.CanEnterState"/>. To do that, you should use <see cref="TrySetState"/> instead.
|
||||
/// </remarks>
|
||||
void ForceSetState(object state);
|
||||
|
||||
#if UNITY_ASSERTIONS
|
||||
/// <summary>[Assert-Only] Should the <see cref="CurrentState"/> be allowed to be set to null? Default is false.</summary>
|
||||
/// <remarks>Can be set by <see cref="SetAllowNullStates"/>.</remarks>
|
||||
bool AllowNullStates { get; }
|
||||
#endif
|
||||
|
||||
/// <summary>[Assert-Conditional] Sets <see cref="AllowNullStates"/>.</summary>
|
||||
void SetAllowNullStates(bool allow = true);
|
||||
|
||||
/************************************************************************************************************************/
|
||||
#if UNITY_EDITOR && UNITY_IMGUI
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>[Editor-Only] The number of standard size lines that <see cref="DoGUI"/> will use.</summary>
|
||||
int GUILineCount { get; }
|
||||
|
||||
/// <summary>[Editor-Only] Draws GUI fields to display the status of this state machine.</summary>
|
||||
void DoGUI();
|
||||
|
||||
/// <summary>[Editor-Only] Draws GUI fields to display the status of this state machine in the given `area`.</summary>
|
||||
void DoGUI(ref Rect area);
|
||||
|
||||
/************************************************************************************************************************/
|
||||
#endif
|
||||
/************************************************************************************************************************/
|
||||
}
|
||||
|
||||
/// <summary>A simple keyless Finite State Machine system.</summary>
|
||||
/// <remarks>
|
||||
/// This class doesn't keep track of any states other than the currently active one.
|
||||
/// See <see cref="StateMachine{TKey, TState}"/> for a system that allows
|
||||
/// states to be pre-registered and accessed using a separate key.
|
||||
/// <para></para>
|
||||
/// See <see cref="InitializeAfterDeserialize"/> if using this class in a serialized field.
|
||||
/// <para></para>
|
||||
/// <strong>Documentation:</strong>
|
||||
/// <see href="https://kybernetik.com.au/animancer/docs/manual/fsm">
|
||||
/// Finite State Machines</see>
|
||||
/// </remarks>
|
||||
/// https://kybernetik.com.au/animancer/api/Animancer.FSM/StateMachine_1
|
||||
///
|
||||
[HelpURL(StateExtensions.APIDocumentationURL + nameof(StateMachine<TState>) + "_1")]
|
||||
[Serializable]
|
||||
public partial class StateMachine<TState> : IStateMachine
|
||||
where TState : class, IState
|
||||
{
|
||||
/************************************************************************************************************************/
|
||||
|
||||
[SerializeField]
|
||||
private TState _CurrentState;
|
||||
|
||||
/// <summary>[<see cref="SerializeField"/>] The currently active state.</summary>
|
||||
public TState CurrentState => _CurrentState;
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>The <see cref="StateChange{TState}.PreviousState"/>.</summary>
|
||||
public TState PreviousState => StateChange<TState>.PreviousState;
|
||||
|
||||
/// <summary>The <see cref="StateChange{TState}.NextState"/>.</summary>
|
||||
public TState NextState => StateChange<TState>.NextState;
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Creates a new <see cref="StateMachine{TState}"/>, leaving the <see cref="CurrentState"/> null.</summary>
|
||||
public StateMachine() { }
|
||||
|
||||
/// <summary>Creates a new <see cref="StateMachine{TState}"/> and immediately enters the `state`.</summary>
|
||||
/// <remarks>This calls <see cref="IState.OnEnterState"/> but not <see cref="IState.CanEnterState"/>.</remarks>
|
||||
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<TState>(this, null, state))
|
||||
{
|
||||
_CurrentState = state;
|
||||
state.OnEnterState();
|
||||
}
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Call this after deserializing to properly initialize the <see cref="CurrentState"/>.</summary>
|
||||
/// <remarks>
|
||||
/// Unfortunately, <see cref="ISerializationCallbackReceiver"/> can't be used to automate this
|
||||
/// because many Unity functions aren't available during serialization such as getting or setting a
|
||||
/// <see cref="Behaviour.enabled"/> like <see cref="StateBehaviour.OnEnterState"/> does.
|
||||
/// <para></para>
|
||||
/// <strong>Example:</strong><code>
|
||||
/// public class MyComponent : MonoBehaviour
|
||||
/// {
|
||||
/// [SerializeField]
|
||||
/// private StateMachine<MyState> _StateMachine;
|
||||
///
|
||||
/// protected virtual void Awake()
|
||||
/// {
|
||||
/// _StateMachine.InitializeAfterDeserialize();
|
||||
/// }
|
||||
/// }
|
||||
/// </code></remarks>
|
||||
public virtual void InitializeAfterDeserialize()
|
||||
{
|
||||
if (_CurrentState != null)
|
||||
using (new StateChange<TState>(this, null, _CurrentState))
|
||||
_CurrentState.OnEnterState();
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Is it currently possible to enter the specified `state`?</summary>
|
||||
/// <remarks>
|
||||
/// This requires <see cref="IState.CanExitState"/> on the <see cref="CurrentState"/> and
|
||||
/// <see cref="IState.CanEnterState"/> on the specified `state` to both return true.
|
||||
/// </remarks>
|
||||
public bool CanSetState(TState state)
|
||||
{
|
||||
#if UNITY_ASSERTIONS
|
||||
if (state == null && !AllowNullStates)
|
||||
throw new ArgumentNullException(nameof(state), NullNotAllowed);
|
||||
#endif
|
||||
|
||||
using (new StateChange<TState>(this, _CurrentState, state))
|
||||
{
|
||||
if (_CurrentState != null && !_CurrentState.CanExitState)
|
||||
return false;
|
||||
|
||||
if (state != null && !state.CanEnterState)
|
||||
return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Returns the first of the `states` which can currently be entered.</summary>
|
||||
/// <remarks>
|
||||
/// This requires <see cref="IState.CanExitState"/> on the <see cref="CurrentState"/> and
|
||||
/// <see cref="IState.CanEnterState"/> on one of the `states` to both return true.
|
||||
/// <para></para>
|
||||
/// States are checked in ascending order (i.e. from <c>[0]</c> to <c>[states.Count - 1]</c>).
|
||||
/// </remarks>
|
||||
public TState CanSetState(IList<TState> 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;
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Attempts to enter the specified `state` and returns true if successful.</summary>
|
||||
/// <remarks>
|
||||
/// This method returns true immediately if the specified `state` is already the <see cref="CurrentState"/>.
|
||||
/// To allow directly re-entering the same state, use <see cref="TryResetState(TState)"/> instead.
|
||||
/// </remarks>
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>Attempts to enter any of the specified `states` and returns true if successful.</summary>
|
||||
/// <remarks>
|
||||
/// This method returns true and does nothing else if the <see cref="CurrentState"/> is in the list.
|
||||
/// To allow directly re-entering the same state, use <see cref="TryResetState(IList{TState})"/> instead.
|
||||
/// <para></para>
|
||||
/// States are checked in ascending order (i.e. from <c>[0]</c> to <c>[states.Count - 1]</c>).
|
||||
/// </remarks>
|
||||
public bool TrySetState(IList<TState> states)
|
||||
{
|
||||
var count = states.Count;
|
||||
for (int i = 0; i < count; i++)
|
||||
if (TrySetState(states[i]))
|
||||
return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Attempts to enter the specified `state` and returns true if successful.</summary>
|
||||
/// <remarks>
|
||||
/// This method does not check if the `state` is already the <see cref="CurrentState"/>. To do so, use
|
||||
/// <see cref="TrySetState(TState)"/> instead.
|
||||
/// </remarks>
|
||||
public bool TryResetState(TState state)
|
||||
{
|
||||
if (!CanSetState(state))
|
||||
return false;
|
||||
|
||||
ForceSetState(state);
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>Attempts to enter any of the specified `states` and returns true if successful.</summary>
|
||||
/// <remarks>
|
||||
/// This method does not check if the `state` is already the <see cref="CurrentState"/>. To do so, use
|
||||
/// <see cref="TrySetState(IList{TState})"/> instead.
|
||||
/// <para></para>
|
||||
/// States are checked in ascending order (i.e. from <c>[0]</c> to <c>[states.Count - 1]</c>).
|
||||
/// </remarks>
|
||||
public bool TryResetState(IList<TState> states)
|
||||
{
|
||||
var count = states.Count;
|
||||
for (int i = 0; i < count; i++)
|
||||
if (TryResetState(states[i]))
|
||||
return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>
|
||||
/// Calls <see cref="IState.OnExitState"/> on the <see cref="CurrentState"/> then changes it to the
|
||||
/// specified `state` and calls <see cref="IState.OnEnterState"/> on it.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This method does not check <see cref="IState.CanExitState"/> or
|
||||
/// <see cref="IState.CanEnterState"/>. To do that, you should use <see cref="TrySetState"/> instead.
|
||||
/// </remarks>
|
||||
public void ForceSetState(TState state)
|
||||
{
|
||||
#if UNITY_ASSERTIONS
|
||||
if (state == null)
|
||||
{
|
||||
if (!AllowNullStates)
|
||||
throw new ArgumentNullException(nameof(state), NullNotAllowed);
|
||||
}
|
||||
else if (state is IOwnedState<TState> 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<TState>(this, _CurrentState, state))
|
||||
{
|
||||
_CurrentState?.OnExitState();
|
||||
|
||||
_CurrentState = state;
|
||||
|
||||
state?.OnEnterState();
|
||||
}
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Returns a string describing the type of this state machine and its <see cref="CurrentState"/>.</summary>
|
||||
public override string ToString() => $"{GetType().Name} -> {_CurrentState}";
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
#if UNITY_ASSERTIONS
|
||||
/// <summary>[Assert-Only] Should the <see cref="CurrentState"/> be allowed to be set to null? Default is false.</summary>
|
||||
/// <remarks>Can be set by <see cref="SetAllowNullStates"/>.</remarks>
|
||||
public bool AllowNullStates { get; private set; }
|
||||
|
||||
/// <summary>[Assert-Only] The error given when attempting to set the <see cref="CurrentState"/> to null.</summary>
|
||||
private const string NullNotAllowed =
|
||||
"This " + nameof(StateMachine<TState>) + " does not allow its state to be set to null." +
|
||||
" Use " + nameof(SetAllowNullStates) + " to allow it if this is intentional.";
|
||||
#endif
|
||||
|
||||
/// <summary>[Assert-Conditional] Sets <see cref="AllowNullStates"/>.</summary>
|
||||
[System.Diagnostics.Conditional("UNITY_ASSERTIONS")]
|
||||
public void SetAllowNullStates(bool allow = true)
|
||||
{
|
||||
#if UNITY_ASSERTIONS
|
||||
AllowNullStates = allow;
|
||||
#endif
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
#region GUI
|
||||
/************************************************************************************************************************/
|
||||
#if UNITY_EDITOR && UNITY_IMGUI
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>[Editor-Only] The number of standard size lines that <see cref="DoGUI"/> will use.</summary>
|
||||
public virtual int GUILineCount => 1;
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>[Editor-Only] Draws GUI fields to display the status of this state machine.</summary>
|
||||
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);
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>[Editor-Only] Draws GUI fields to display the status of this state machine in the given `area`.</summary>
|
||||
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
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <inheritdoc/>
|
||||
object IStateMachine.CurrentState => _CurrentState;
|
||||
|
||||
/// <inheritdoc/>
|
||||
object IStateMachine.PreviousState => PreviousState;
|
||||
|
||||
/// <inheritdoc/>
|
||||
object IStateMachine.NextState => NextState;
|
||||
|
||||
/// <inheritdoc/>
|
||||
object IStateMachine.CanSetState(IList states) => CanSetState((List<TState>)states);
|
||||
|
||||
/// <inheritdoc/>
|
||||
bool IStateMachine.CanSetState(object state) => CanSetState((TState)state);
|
||||
|
||||
/// <inheritdoc/>
|
||||
void IStateMachine.ForceSetState(object state) => ForceSetState((TState)state);
|
||||
|
||||
/// <inheritdoc/>
|
||||
bool IStateMachine.TryResetState(IList states) => TryResetState((List<TState>)states);
|
||||
|
||||
/// <inheritdoc/>
|
||||
bool IStateMachine.TryResetState(object state) => TryResetState((TState)state);
|
||||
|
||||
/// <inheritdoc/>
|
||||
bool IStateMachine.TrySetState(IList states) => TrySetState((List<TState>)states);
|
||||
|
||||
/// <inheritdoc/>
|
||||
bool IStateMachine.TrySetState(object state) => TrySetState((TState)state);
|
||||
|
||||
/// <inheritdoc/>
|
||||
void IStateMachine.SetAllowNullStates(bool allow) => SetAllowNullStates(allow);
|
||||
|
||||
/************************************************************************************************************************/
|
||||
#endregion
|
||||
/************************************************************************************************************************/
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 87a6d066d8da93b4bbdc228c47509675
|
||||
labels:
|
||||
- FSM
|
||||
- FiniteStateMachine
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,88 @@
|
||||
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
|
||||
|
||||
namespace Animancer.FSM
|
||||
{
|
||||
public partial class StateMachine<TKey, TState>
|
||||
{
|
||||
/// <summary>
|
||||
/// A simple system that can <see cref="StateMachine{TState}.InputBuffer{TStateMachine}.State"/> a state then
|
||||
/// try to enter it every time <see cref="StateMachine{TState}.InputBuffer{TStateMachine}.Update(float)"/> is
|
||||
/// called until the <see cref="StateMachine{TState}.InputBuffer{TStateMachine}.TimeOut"/> expires.
|
||||
/// </summary>
|
||||
///
|
||||
/// <remarks>
|
||||
/// <para></para>
|
||||
/// <strong>Documentation:</strong>
|
||||
/// <see href="https://kybernetik.com.au/animancer/docs/manual/fsm/utilities#input-buffers">
|
||||
/// Input Buffers</see>
|
||||
/// <para></para>
|
||||
/// See <see cref="StateMachine{TState}.InputBuffer{TStateMachine}"/> for example usage.
|
||||
/// </remarks>
|
||||
///
|
||||
/// https://kybernetik.com.au/animancer/api/Animancer.FSM/InputBuffer
|
||||
///
|
||||
public new class InputBuffer : InputBuffer<StateMachine<TKey, TState>>
|
||||
{
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>The <typeparamref name="TKey"/> of the state this buffer is currently attempting to enter.</summary>
|
||||
public TKey Key { get; set; }
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Creates a new <see cref="InputBuffer"/>.</summary>
|
||||
public InputBuffer() { }
|
||||
|
||||
/// <summary>Creates a new <see cref="InputBuffer"/> for the specified `stateMachine`.</summary>
|
||||
public InputBuffer(StateMachine<TKey, TState> stateMachine)
|
||||
: base(stateMachine)
|
||||
{ }
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>
|
||||
/// If a state is registered with the `key`, this method calls <see cref="Buffer(TKey, TState, float)"/>
|
||||
/// and returns true. Otherwise it returns false.
|
||||
/// </summary>
|
||||
/// <remarks>Doesn't actually attempt to enter the state until <see cref="Update(float)"/> is called.</remarks>
|
||||
public bool Buffer(TKey key, float timeOut)
|
||||
{
|
||||
if (StateMachine.TryGetValue(key, out var state))
|
||||
{
|
||||
Buffer(key, state, timeOut);
|
||||
return true;
|
||||
}
|
||||
else return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the <see cref="Key"/>, <see cref="StateMachine{TState}.InputBuffer.State"/>, and
|
||||
/// <see cref="TimeOut"/>.
|
||||
/// </summary>
|
||||
/// <remarks>Doesn't actually attempt to enter the state until <see cref="Update(float)"/> is called.</remarks>
|
||||
public void Buffer(TKey key, TState state, float timeOut)
|
||||
{
|
||||
Key = key;
|
||||
Buffer(state, timeOut);
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override bool TryEnterState()
|
||||
=> StateMachine.TryResetState(Key, State);
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override void Clear()
|
||||
{
|
||||
base.Clear();
|
||||
Key = default;
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 2a348c9a4a87c294e960ae27c06c12f1
|
||||
labels:
|
||||
- FSM
|
||||
- FiniteStateMachine
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,142 @@
|
||||
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
|
||||
|
||||
using System;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Animancer.FSM
|
||||
{
|
||||
/// https://kybernetik.com.au/animancer/api/Animancer.FSM/StateMachine_2
|
||||
partial class StateMachine<TKey, TState>
|
||||
{
|
||||
/// <summary>A <see cref="StateMachine{TKey, TState}"/> with a <see cref="DefaultKey"/>.</summary>
|
||||
/// <remarks>
|
||||
/// See <see cref="InitializeAfterDeserialize"/> if using this class in a serialized field.
|
||||
/// <para></para>
|
||||
/// <strong>Documentation:</strong>
|
||||
/// <see href="https://kybernetik.com.au/animancer/docs/manual/fsm/changing-states#default-states">
|
||||
/// Default States</see>
|
||||
/// </remarks>
|
||||
/// https://kybernetik.com.au/animancer/api/Animancer.FSM/WithDefault
|
||||
///
|
||||
[Serializable]
|
||||
public new class WithDefault : StateMachine<TKey, TState>
|
||||
{
|
||||
/************************************************************************************************************************/
|
||||
|
||||
[SerializeField]
|
||||
private TKey _DefaultKey;
|
||||
|
||||
/// <summary>The starting state and main state to return to when nothing else is active.</summary>
|
||||
/// <remarks>
|
||||
/// If the <see cref="CurrentState"/> is <c>null</c> when setting this value, it calls
|
||||
/// <see cref="ForceSetState(TKey)"/> to enter the specified state immediately.
|
||||
/// <para></para>
|
||||
/// For a character, this would typically be their <em>Idle</em> state.
|
||||
/// </remarks>
|
||||
public TKey DefaultKey
|
||||
{
|
||||
get => _DefaultKey;
|
||||
set
|
||||
{
|
||||
_DefaultKey = value;
|
||||
if (CurrentState == null && value != null)
|
||||
ForceSetState(value);
|
||||
}
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Calls <see cref="ForceSetState(TKey)"/> with the <see cref="DefaultKey"/>.</summary>
|
||||
/// <remarks>This delegate is cached to avoid allocating garbage when used in Animancer Events.</remarks>
|
||||
public readonly Action ForceSetDefaultState;
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Creates a new <see cref="WithDefault"/>.</summary>
|
||||
public WithDefault()
|
||||
{
|
||||
// Silly C# doesn't allow instance delegates to be assigned using field initializers.
|
||||
ForceSetDefaultState = () => ForceSetState(_DefaultKey);
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Creates a new <see cref="WithDefault"/> and sets the <see cref="DefaultKey"/>.</summary>
|
||||
public WithDefault(TKey defaultKey)
|
||||
: this()
|
||||
{
|
||||
_DefaultKey = defaultKey;
|
||||
ForceSetState(defaultKey);
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override void InitializeAfterDeserialize()
|
||||
{
|
||||
if (CurrentState != null)
|
||||
{
|
||||
using (new KeyChange<TKey>(this, default, _DefaultKey))
|
||||
using (new StateChange<TState>(this, null, CurrentState))
|
||||
CurrentState.OnEnterState();
|
||||
}
|
||||
else
|
||||
{
|
||||
ForceSetState(_DefaultKey);
|
||||
}
|
||||
|
||||
// Don't call the base method.
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Attempts to enter the <see cref="DefaultKey"/> and returns true if successful.</summary>
|
||||
/// <remarks>
|
||||
/// This method returns true immediately if the specified <see cref="DefaultKey"/> is already the
|
||||
/// <see cref="CurrentKey"/>. To allow directly re-entering the same state, use
|
||||
/// <see cref="TryResetDefaultState"/> instead.
|
||||
/// </remarks>
|
||||
public TState TrySetDefaultState() => TrySetState(_DefaultKey);
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Attempts to enter the <see cref="DefaultKey"/> and returns true if successful.</summary>
|
||||
/// <remarks>
|
||||
/// This method does not check if the <see cref="DefaultKey"/> is already the <see cref="CurrentKey"/>.
|
||||
/// To do so, use <see cref="TrySetDefaultState"/> instead.
|
||||
/// </remarks>
|
||||
public TState TryResetDefaultState() => TryResetState(_DefaultKey);
|
||||
|
||||
/************************************************************************************************************************/
|
||||
#if UNITY_EDITOR && UNITY_IMGUI
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override int GUILineCount => 2;
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override void DoGUI(ref Rect area)
|
||||
{
|
||||
area.height = UnityEditor.EditorGUIUtility.singleLineHeight;
|
||||
|
||||
UnityEditor.EditorGUI.BeginChangeCheck();
|
||||
|
||||
var state = StateMachineUtilities.DoGenericField(area, "Default Key", DefaultKey);
|
||||
|
||||
if (UnityEditor.EditorGUI.EndChangeCheck())
|
||||
DefaultKey = state;
|
||||
|
||||
StateMachineUtilities.NextVerticalArea(ref area);
|
||||
|
||||
base.DoGUI(ref area);
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
#endif
|
||||
/************************************************************************************************************************/
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
fileFormatVersion: 2
|
||||
guid: a4aaa753173eb1c45bd98cd956086a93
|
||||
labels:
|
||||
- FSM
|
||||
- FiniteStateMachine
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,404 @@
|
||||
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
|
||||
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Animancer.FSM
|
||||
{
|
||||
/// <summary>Interface for accessing <see cref="StateMachine{TKey, TState}"/> without the <c>TState</c>.</summary>
|
||||
/// <remarks>
|
||||
/// <strong>Documentation:</strong>
|
||||
/// <see href="https://kybernetik.com.au/animancer/docs/manual/fsm/keys">
|
||||
/// Keyed State Machines</see>
|
||||
/// </remarks>
|
||||
/// https://kybernetik.com.au/animancer/api/Animancer.FSM/IKeyedStateMachine_1
|
||||
///
|
||||
public interface IKeyedStateMachine<TKey>
|
||||
{
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>The key which identifies the <see cref="StateMachine{TState}.CurrentState"/>.</summary>
|
||||
TKey CurrentKey { get; }
|
||||
|
||||
/// <summary>The <see cref="KeyChange{TKey}.PreviousKey"/>.</summary>
|
||||
TKey PreviousKey { get; }
|
||||
|
||||
/// <summary>The <see cref="KeyChange{TKey}.NextKey"/>.</summary>
|
||||
TKey NextKey { get; }
|
||||
|
||||
/// <summary>Attempts to enter the state registered with the specified `key` and returns it if successful.</summary>
|
||||
/// <remarks>
|
||||
/// This method returns true immediately if the specified `key` is already the <see cref="CurrentKey"/>. To
|
||||
/// allow directly re-entering the same state, use <see cref="TryResetState(TKey)"/> instead.
|
||||
/// </remarks>
|
||||
object TrySetState(TKey key);
|
||||
|
||||
/// <summary>Attempts to enter the state registered with the specified `key` and returns it if successful.</summary>
|
||||
/// <remarks>
|
||||
/// This method does not check if the `key` is already the <see cref="CurrentKey"/>. To do so, use
|
||||
/// <see cref="TrySetState(TKey)"/> instead.
|
||||
/// </remarks>
|
||||
object TryResetState(TKey key);
|
||||
|
||||
/// <summary>
|
||||
/// Uses <see cref="StateMachine{TKey, TState}.ForceSetState(TKey, TState)"/> to change to the state registered
|
||||
/// with the `key`. If nothing is registered, it changes to <c>default(TState)</c>.
|
||||
/// </summary>
|
||||
object ForceSetState(TKey key);
|
||||
|
||||
/************************************************************************************************************************/
|
||||
}
|
||||
|
||||
/// <summary>A simple Finite State Machine system that registers each state with a particular key.</summary>
|
||||
/// <remarks>
|
||||
/// This class allows states to be registered with a particular key upfront and then accessed later using that key.
|
||||
/// See <see cref="StateMachine{TState}"/> for a system that does not bother keeping track of any states other than
|
||||
/// the active one.
|
||||
/// <para></para>
|
||||
/// See <see cref="InitializeAfterDeserialize"/> if using this class in a serialized field.
|
||||
/// <para></para>
|
||||
/// <strong>Documentation:</strong>
|
||||
/// <see href="https://kybernetik.com.au/animancer/docs/manual/fsm/keys">
|
||||
/// Keyed State Machines</see>
|
||||
/// </remarks>
|
||||
/// https://kybernetik.com.au/animancer/api/Animancer.FSM/StateMachine_2
|
||||
///
|
||||
[HelpURL(StateExtensions.APIDocumentationURL + nameof(StateMachine<TState>) + "_2")]
|
||||
[Serializable]
|
||||
public partial class StateMachine<TKey, TState> : StateMachine<TState>, IKeyedStateMachine<TKey>, IDictionary<TKey, TState>
|
||||
where TState : class, IState
|
||||
{
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>The collection of states mapped to a particular key.</summary>
|
||||
public IDictionary<TKey, TState> Dictionary { get; set; }
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
[SerializeField]
|
||||
private TKey _CurrentKey;
|
||||
|
||||
/// <summary>The key which identifies the <see cref="StateMachine{TState}.CurrentState"/>.</summary>
|
||||
public TKey CurrentKey => _CurrentKey;
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>The <see cref="KeyChange{TKey}.PreviousKey"/>.</summary>
|
||||
public TKey PreviousKey => KeyChange<TKey>.PreviousKey;
|
||||
|
||||
/// <summary>The <see cref="KeyChange{TKey}.NextKey"/>.</summary>
|
||||
public TKey NextKey => KeyChange<TKey>.NextKey;
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new <see cref="StateMachine{TKey, TState}"/> with a new <see cref="Dictionary"/>, leaving the
|
||||
/// <see cref="CurrentState"/> null.
|
||||
/// </summary>
|
||||
public StateMachine()
|
||||
{
|
||||
Dictionary = new Dictionary<TKey, TState>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new <see cref="StateMachine{TKey, TState}"/> which uses the specified `dictionary`, leaving the
|
||||
/// <see cref="CurrentState"/> null.
|
||||
/// </summary>
|
||||
public StateMachine(IDictionary<TKey, TState> dictionary)
|
||||
{
|
||||
Dictionary = dictionary;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Constructs a new <see cref="StateMachine{TKey, TState}"/> with a new <see cref="Dictionary"/> and
|
||||
/// immediately uses the `defaultKey` to enter the `defaultState`.
|
||||
/// </summary>
|
||||
/// <remarks>This calls <see cref="IState.OnEnterState"/> but not <see cref="IState.CanEnterState"/>.</remarks>
|
||||
public StateMachine(TKey defaultKey, TState defaultState)
|
||||
{
|
||||
Dictionary = new Dictionary<TKey, TState>
|
||||
{
|
||||
{ defaultKey, defaultState }
|
||||
};
|
||||
ForceSetState(defaultKey, defaultState);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Constructs a new <see cref="StateMachine{TKey, TState}"/> which uses the specified `dictionary` and
|
||||
/// immediately uses the `defaultKey` to enter the `defaultState`.
|
||||
/// </summary>
|
||||
/// <remarks>This calls <see cref="IState.OnEnterState"/> but not <see cref="IState.CanEnterState"/>.</remarks>
|
||||
public StateMachine(IDictionary<TKey, TState> dictionary, TKey defaultKey, TState defaultState)
|
||||
{
|
||||
Dictionary = dictionary;
|
||||
dictionary.Add(defaultKey, defaultState);
|
||||
ForceSetState(defaultKey, defaultState);
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override void InitializeAfterDeserialize()
|
||||
{
|
||||
if (CurrentState != null)
|
||||
{
|
||||
using (new KeyChange<TKey>(this, default, _CurrentKey))
|
||||
using (new StateChange<TState>(this, null, CurrentState))
|
||||
CurrentState.OnEnterState();
|
||||
}
|
||||
else if (Dictionary.TryGetValue(_CurrentKey, out var state))
|
||||
{
|
||||
ForceSetState(_CurrentKey, state);
|
||||
}
|
||||
|
||||
// Don't call the base method.
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Attempts to enter the specified `state` and returns true if successful.</summary>
|
||||
/// <remarks>
|
||||
/// 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="TryResetState(TKey, TState)"/> instead.
|
||||
/// </remarks>
|
||||
public bool TrySetState(TKey key, TState state)
|
||||
{
|
||||
if (CurrentState == state)
|
||||
return true;
|
||||
else
|
||||
return TryResetState(key, state);
|
||||
}
|
||||
|
||||
/// <summary>Attempts to enter the state registered with the specified `key` and returns it if successful.</summary>
|
||||
/// <remarks>
|
||||
/// This method returns true immediately if the specified `key` is already the <see cref="CurrentKey"/>. To
|
||||
/// allow directly re-entering the same state, use <see cref="TryResetState(TKey)"/> instead.
|
||||
/// </remarks>
|
||||
public TState TrySetState(TKey key)
|
||||
{
|
||||
if (EqualityComparer<TKey>.Default.Equals(_CurrentKey, key))
|
||||
return CurrentState;
|
||||
else
|
||||
return TryResetState(key);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
object IKeyedStateMachine<TKey>.TrySetState(TKey key) => TrySetState(key);
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Attempts to enter the specified `state` and returns true if successful.</summary>
|
||||
/// <remarks>
|
||||
/// This method does not check if the `state` is already the <see cref="StateMachine{TState}.CurrentState"/>.
|
||||
/// To do so, use <see cref="TrySetState(TKey, TState)"/> instead.
|
||||
/// </remarks>
|
||||
public bool TryResetState(TKey key, TState state)
|
||||
{
|
||||
using (new KeyChange<TKey>(this, _CurrentKey, key))
|
||||
{
|
||||
if (!CanSetState(state))
|
||||
return false;
|
||||
|
||||
_CurrentKey = key;
|
||||
ForceSetState(state);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Attempts to enter the state registered with the specified `key` and returns it if successful.</summary>
|
||||
/// <remarks>
|
||||
/// This method does not check if the `key` is already the <see cref="CurrentKey"/>. To do so, use
|
||||
/// <see cref="TrySetState(TKey)"/> instead.
|
||||
/// </remarks>
|
||||
public TState TryResetState(TKey key)
|
||||
{
|
||||
if (Dictionary.TryGetValue(key, out var state) &&
|
||||
TryResetState(key, state))
|
||||
return state;
|
||||
else
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
object IKeyedStateMachine<TKey>.TryResetState(TKey key) => TryResetState(key);
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>
|
||||
/// Calls <see cref="IState.OnExitState"/> on the <see cref="StateMachine{TState}.CurrentState"/> then changes
|
||||
/// to the specified `key` and `state` and calls <see cref="IState.OnEnterState"/> on it.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This method does not check <see cref="IState.CanExitState"/> or <see cref="IState.CanEnterState"/>. To do
|
||||
/// that, you should use <see cref="TrySetState(TKey, TState)"/> instead.
|
||||
/// </remarks>
|
||||
public void ForceSetState(TKey key, TState state)
|
||||
{
|
||||
using (new KeyChange<TKey>(this, _CurrentKey, key))
|
||||
{
|
||||
_CurrentKey = key;
|
||||
ForceSetState(state);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Uses <see cref="ForceSetState(TKey, TState)"/> to change to the state registered with the `key`. If nothing
|
||||
/// is registered, it use <c>null</c> and will throw an exception unless
|
||||
/// <see cref="StateMachine{TState}.AllowNullStates"/> is enabled.
|
||||
/// </summary>
|
||||
public TState ForceSetState(TKey key)
|
||||
{
|
||||
Dictionary.TryGetValue(key, out var state);
|
||||
ForceSetState(key, state);
|
||||
return state;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
object IKeyedStateMachine<TKey>.ForceSetState(TKey key) => ForceSetState(key);
|
||||
|
||||
/************************************************************************************************************************/
|
||||
#region Dictionary Wrappers
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>The state registered with the `key` in the <see cref="Dictionary"/>.</summary>
|
||||
public TState this[TKey key] { get => Dictionary[key]; set => Dictionary[key] = value; }
|
||||
|
||||
/// <summary>Gets the state registered with the specified `key` in the <see cref="Dictionary"/>.</summary>
|
||||
public bool TryGetValue(TKey key, out TState state) => Dictionary.TryGetValue(key, out state);
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Gets an <see cref="ICollection{T}"/> containing the keys of the <see cref="Dictionary"/>.</summary>
|
||||
public ICollection<TKey> Keys => Dictionary.Keys;
|
||||
|
||||
/// <summary>Gets an <see cref="ICollection{T}"/> containing the state of the <see cref="Dictionary"/>.</summary>
|
||||
public ICollection<TState> Values => Dictionary.Values;
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Gets the number of states contained in the <see cref="Dictionary"/>.</summary>
|
||||
public int Count => Dictionary.Count;
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Adds a state to the <see cref="Dictionary"/>.</summary>
|
||||
public void Add(TKey key, TState state) => Dictionary.Add(key, state);
|
||||
|
||||
/// <summary>Adds a state to the <see cref="Dictionary"/>.</summary>
|
||||
public void Add(KeyValuePair<TKey, TState> item) => Dictionary.Add(item);
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Removes a state from the <see cref="Dictionary"/>.</summary>
|
||||
public bool Remove(TKey key) => Dictionary.Remove(key);
|
||||
|
||||
/// <summary>Removes a state from the <see cref="Dictionary"/>.</summary>
|
||||
public bool Remove(KeyValuePair<TKey, TState> item) => Dictionary.Remove(item);
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Removes all state from the <see cref="Dictionary"/>.</summary>
|
||||
public void Clear() => Dictionary.Clear();
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Determines whether the <see cref="Dictionary"/> contains a specific value.</summary>
|
||||
public bool Contains(KeyValuePair<TKey, TState> item) => Dictionary.Contains(item);
|
||||
|
||||
/// <summary>Determines whether the <see cref="Dictionary"/> contains a state with the specified `key`.</summary>
|
||||
public bool ContainsKey(TKey key) => Dictionary.ContainsKey(key);
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Returns an enumerator that iterates through the <see cref="Dictionary"/>.</summary>
|
||||
public IEnumerator<KeyValuePair<TKey, TState>> GetEnumerator() => Dictionary.GetEnumerator();
|
||||
|
||||
/// <summary>Returns an enumerator that iterates through the <see cref="Dictionary"/>.</summary>
|
||||
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Copies the contents of the <see cref="Dictionary"/> to the `array` starting at the `arrayIndex`.</summary>
|
||||
public void CopyTo(KeyValuePair<TKey, TState>[] array, int arrayIndex) => Dictionary.CopyTo(array, arrayIndex);
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Indicates whether the <see cref="Dictionary"/> is read-only.</summary>
|
||||
bool ICollection<KeyValuePair<TKey, TState>>.IsReadOnly => Dictionary.IsReadOnly;
|
||||
|
||||
/************************************************************************************************************************/
|
||||
#endregion
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Returns the state registered with the specified `key`, or null if none is present.</summary>
|
||||
public TState GetState(TKey key)
|
||||
{
|
||||
TryGetValue(key, out var state);
|
||||
return state;
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Adds the specified `keys` and `states`. Both arrays must be the same size.</summary>
|
||||
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]);
|
||||
}
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>
|
||||
/// Sets the <see cref="CurrentKey"/> without changing the <see cref="StateMachine{TState}.CurrentState"/>.
|
||||
/// </summary>
|
||||
public void SetFakeKey(TKey key) => _CurrentKey = key;
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>
|
||||
/// Returns a string describing the type of this state machine and its <see cref="CurrentKey"/> and
|
||||
/// <see cref="StateMachine{TState}.CurrentState"/>.
|
||||
/// </summary>
|
||||
public override string ToString()
|
||||
=> $"{GetType().FullName} -> {_CurrentKey} -> {(CurrentState != null ? CurrentState.ToString() : "null")}";
|
||||
|
||||
/************************************************************************************************************************/
|
||||
#if UNITY_EDITOR && UNITY_IMGUI
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override int GUILineCount => 2;
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <inheritdoc/>
|
||||
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
|
||||
/************************************************************************************************************************/
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 9d7e35072ba28604d95afbcda2209721
|
||||
labels:
|
||||
- FSM
|
||||
- FiniteStateMachine
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,58 @@
|
||||
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
|
||||
|
||||
#if UNITY_EDITOR && UNITY_IMGUI
|
||||
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
using Object = UnityEngine.Object;
|
||||
|
||||
namespace Animancer.FSM
|
||||
{
|
||||
/// <summary>[Editor-Only] Utilities used by the <see cref="FSM"/> system.</summary>
|
||||
/// <remarks>
|
||||
/// <strong>Documentation:</strong>
|
||||
/// <see href="https://kybernetik.com.au/animancer/docs/manual/fsm">
|
||||
/// Finite State Machines</see>
|
||||
/// </remarks>
|
||||
/// https://kybernetik.com.au/animancer/api/Animancer.FSM/StateMachineUtilities
|
||||
///
|
||||
public static class StateMachineUtilities
|
||||
{
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Draws a GUI field for the `value`.</summary>
|
||||
public static T DoGenericField<T>(Rect area, string label, T value)
|
||||
{
|
||||
if (typeof(Object).IsAssignableFrom(typeof(T)))
|
||||
{
|
||||
return (T)(object)EditorGUI.ObjectField(
|
||||
area,
|
||||
label,
|
||||
value as Object,
|
||||
typeof(T),
|
||||
true);
|
||||
}
|
||||
|
||||
var stateName = value != null ? value.ToString() : "Null";
|
||||
EditorGUI.LabelField(area, label, stateName);
|
||||
return value;
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>
|
||||
/// If the <see cref="Rect.height"/> is positive, this method moves the <see cref="Rect.y"/> by that amount and
|
||||
/// adds the <see cref="EditorGUIUtility.standardVerticalSpacing"/>.
|
||||
/// </summary>
|
||||
public static void NextVerticalArea(ref Rect area)
|
||||
{
|
||||
if (area.height > 0)
|
||||
area.y += area.height + EditorGUIUtility.standardVerticalSpacing;
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 58fae748392df8546bad814552de49af
|
||||
labels:
|
||||
- FSM
|
||||
- FiniteStateMachine
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
Reference in New Issue
Block a user