chore: initial commit

This commit is contained in:
2026-05-08 11:04:00 +08:00
commit f55d2a57c3
6278 changed files with 866081 additions and 0 deletions

View File

@@ -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

View File

@@ -0,0 +1,14 @@
fileFormatVersion: 2
guid: 94a6e52abdbcf8345be407d6740230a2
labels:
- FSM
- FiniteStateMachine
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -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();
/************************************************************************************************************************/
}
}

View File

@@ -0,0 +1,14 @@
fileFormatVersion: 2
guid: 7a0b6fcf1d3471d4aa75f4c39f3c1c1e
labels:
- FSM
- FiniteStateMachine
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -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&lt;CharacterState&gt; StateMachine { get; private set; }
/// }
///
/// public class CharacterState : StateBehaviour, IOwnedState&lt;CharacterState&gt;
/// {
/// [SerializeField]
/// private Character _Character;
/// public Character Character =&gt; _Character;
///
/// public StateMachine&lt;CharacterState&gt; OwnerStateMachine =&gt; _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&lt;T&gt;</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&lt;TState&gt;(TState)'. There is no implicit reference conversion from
/// 'StateType' to 'Animancer.FSM.IOwnedState&lt;StateType&gt;'.</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&lt;CharacterState&gt;();
/// }
/// }
/// }
/// </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&lt;AttackState&gt;();
/// </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&lt;CharacterState&gt;</c>, rather
/// than <c>IOwnedState&lt;AttackState&gt;</c> that means <c>TryEnterState&lt;AttackState&gt;</c> does not satisfy
/// that method's generic constraints: <c>where TState : class, IOwnedState&lt;TState&gt;</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&lt;CharacterState&gt;();
/// </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
/************************************************************************************************************************/
}
}

View File

@@ -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:

View File

@@ -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&lt;TState&gt;(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();
/************************************************************************************************************************/
}
}

View File

@@ -0,0 +1,14 @@
fileFormatVersion: 2
guid: ba135b8e83a177043a5e505a7e9832e7
labels:
- FSM
- FiniteStateMachine
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -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
}

View File

@@ -0,0 +1,10 @@
fileFormatVersion: 2
guid: b2665e54e4314ae429d34fdeafc9f3e0
labels:
- FSM
- FiniteStateMachine
AssemblyDefinitionImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

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

View File

@@ -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:

View File

@@ -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&lt;TState&gt;(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();
/************************************************************************************************************************/
}
}

View File

@@ -0,0 +1,14 @@
fileFormatVersion: 2
guid: d96becb371c86e241b44dea56e55385a
labels:
- FSM
- FiniteStateMachine
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -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&lt;CharacterState&gt; stateMachine;// Initialized elsewhere.
///
/// [SerializeField] private CharacterState _Attack;
/// [SerializeField] private float _AttackInputTimeOut = 0.5f;
///
/// private StateMachine&lt;CharacterState&gt;.InputBuffer _InputBuffer;
///
/// private void Awake()
/// {
/// // Initialize the buffer.
/// _InputBuffer = new StateMachine&lt;CharacterState&gt;.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;
}
/************************************************************************************************************************/
}
}
}

View File

@@ -0,0 +1,14 @@
fileFormatVersion: 2
guid: 3f0140c1027c5254882ca6415a514880
labels:
- FSM
- FiniteStateMachine
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -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&lt;CharacterState&gt; stateMachine;
/// public CharacterState run;
/// public CharacterState idle;
///
/// private readonly StateMachine&lt;CharacterState&gt;.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);
}
}

View File

@@ -0,0 +1,14 @@
fileFormatVersion: 2
guid: 0f882cb524ddbc8419c5beba63a1939f
labels:
- FSM
- FiniteStateMachine
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

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

View File

@@ -0,0 +1,14 @@
fileFormatVersion: 2
guid: eb5d8db5c4119fd47a2f652836d193f1
labels:
- FSM
- FiniteStateMachine
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -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&lt;MyState&gt; _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
/************************************************************************************************************************/
}
}

View File

@@ -0,0 +1,14 @@
fileFormatVersion: 2
guid: 87a6d066d8da93b4bbdc228c47509675
labels:
- FSM
- FiniteStateMachine
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

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

View File

@@ -0,0 +1,14 @@
fileFormatVersion: 2
guid: 2a348c9a4a87c294e960ae27c06c12f1
labels:
- FSM
- FiniteStateMachine
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

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

View File

@@ -0,0 +1,14 @@
fileFormatVersion: 2
guid: a4aaa753173eb1c45bd98cd956086a93
labels:
- FSM
- FiniteStateMachine
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

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

View File

@@ -0,0 +1,14 @@
fileFormatVersion: 2
guid: 9d7e35072ba28604d95afbcda2209721
labels:
- FSM
- FiniteStateMachine
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -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

View File

@@ -0,0 +1,14 @@
fileFormatVersion: 2
guid: 58fae748392df8546bad814552de49af
labels:
- FSM
- FiniteStateMachine
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant: