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,8 @@
fileFormatVersion: 2
guid: d43a4ac3938779641b8b1769d3e689a6
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 91114f4897040cf4c9645e6f413f4e93
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,102 @@
fileFormatVersion: 2
guid: c8f836b9e2ef7eb41ad944e21ec66ce4
PluginImporter:
externalObjects: {}
serializedVersion: 2
iconMap: {}
executionOrder: {}
defineConstraints: []
isPreloaded: 0
isOverridable: 0
isExplicitlyReferenced: 0
validateReferences: 1
platformData:
- first:
: Any
second:
enabled: 0
settings:
Exclude Android: 1
Exclude Editor: 1
Exclude Linux: 1
Exclude Linux64: 1
Exclude LinuxUniversal: 1
Exclude OSXUniversal: 1
Exclude Win: 1
Exclude Win64: 1
- first:
Android: Android
second:
enabled: 0
settings:
CPU: ARMv7
- first:
Any:
second:
enabled: 0
settings: {}
- first:
Editor: Editor
second:
enabled: 0
settings:
CPU: AnyCPU
DefaultValueInitialized: true
OS: AnyOS
- first:
Facebook: Win
second:
enabled: 0
settings:
CPU: AnyCPU
- first:
Facebook: Win64
second:
enabled: 0
settings:
CPU: AnyCPU
- first:
Standalone: Linux
second:
enabled: 0
settings:
CPU: x86
- first:
Standalone: Linux64
second:
enabled: 0
settings:
CPU: AnyCPU
- first:
Standalone: LinuxUniversal
second:
enabled: 0
settings:
CPU: None
- first:
Standalone: OSXUniversal
second:
enabled: 0
settings:
CPU: AnyCPU
- first:
Standalone: Win
second:
enabled: 0
settings:
CPU: AnyCPU
- first:
Standalone: Win64
second:
enabled: 0
settings:
CPU: AnyCPU
- first:
Windows Store Apps: WindowsStoreApps
second:
enabled: 0
settings:
CPU: AnyCPU
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,8 @@
<?xml version="1.0"?>
<doc>
<assembly>
<name>Animancer.Lite</name>
</assembly>
<members>
</members>
</doc>

View File

@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 4bf92cc1f409c88418660c79554fcb48
TextScriptImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,7 @@
[InternetShortcut]
URL=https://kybernetik.com.au/animancer/docs/source/dlls/
IDList=
HotKey=0
IconIndex=0
[{000214A0-0000-0000-C000-000000000046}]
Prop3=19,11

View File

@@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 448a88aa0eed28d44b692c71d983d042
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,102 @@
fileFormatVersion: 2
guid: f930d3de443bbeb41a6e84b706c9a2b1
PluginImporter:
externalObjects: {}
serializedVersion: 2
iconMap: {}
executionOrder: {}
defineConstraints: []
isPreloaded: 0
isOverridable: 0
isExplicitlyReferenced: 0
validateReferences: 1
platformData:
- first:
: Any
second:
enabled: 0
settings:
Exclude Android: 1
Exclude Editor: 1
Exclude Linux: 1
Exclude Linux64: 1
Exclude LinuxUniversal: 1
Exclude OSXUniversal: 1
Exclude Win: 1
Exclude Win64: 1
- first:
Android: Android
second:
enabled: 0
settings:
CPU: ARMv7
- first:
Any:
second:
enabled: 0
settings: {}
- first:
Editor: Editor
second:
enabled: 0
settings:
CPU: AnyCPU
DefaultValueInitialized: true
OS: AnyOS
- first:
Facebook: Win
second:
enabled: 0
settings:
CPU: AnyCPU
- first:
Facebook: Win64
second:
enabled: 0
settings:
CPU: AnyCPU
- first:
Standalone: Linux
second:
enabled: 0
settings:
CPU: x86
- first:
Standalone: Linux64
second:
enabled: 0
settings:
CPU: AnyCPU
- first:
Standalone: LinuxUniversal
second:
enabled: 0
settings:
CPU: None
- first:
Standalone: OSXUniversal
second:
enabled: 0
settings:
CPU: AnyCPU
- first:
Standalone: Win
second:
enabled: 0
settings:
CPU: AnyCPU
- first:
Standalone: Win64
second:
enabled: 0
settings:
CPU: AnyCPU
- first:
Windows Store Apps: WindowsStoreApps
second:
enabled: 0
settings:
CPU: AnyCPU
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 131880f34c4a28d41910e4cfba9b5e6b
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,54 @@
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
using UnityEngine.Animations;
namespace Animancer
{
/// <summary>[Pro-Only]
/// A base class that allows Animation Jobs to be easily inserted into an Animancer graph.
/// </summary>
///
/// <remarks>
/// <strong>Documentation:</strong>
/// <see href="https://kybernetik.com.au/animancer/docs/manual/ik#animated-properties">
/// Animated Properties</see>
/// </remarks>
///
/// https://kybernetik.com.au/animancer/api/Animancer/AnimancerJob_1
///
public abstract class AnimancerJob<T> where T : struct, IAnimationJob
{
/************************************************************************************************************************/
/// <summary>The <see cref="IAnimationJob"/>.</summary>
protected T _Job;
/// <summary>The <see cref="AnimationScriptPlayable"/> running the job.</summary>
protected AnimationScriptPlayable _Playable;
/************************************************************************************************************************/
/// <summary>Creates the <see cref="_Playable"/> and inserts it between the root and the graph output.</summary>
protected void CreatePlayable(AnimancerGraph animancer)
{
_Playable = animancer.InsertOutputJob(_Job);
}
/************************************************************************************************************************/
/// <summary>
/// Destroys the <see cref="_Playable"/> and restores the graph connection it was intercepting.
/// </summary>
/// <remarks>
/// This method is NOT called automatically, so if you need to guarantee that things will get cleaned up you
/// should use <see cref="AnimancerGraph.Disposables"/>.
/// </remarks>
public virtual void Destroy()
{
AnimancerUtilities.RemovePlayable(_Playable);
}
/************************************************************************************************************************/
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 724cb07f283f36649bdaa5081690d13b
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,72 @@
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
using UnityEngine.Animations;
using Unity.Collections;
namespace Animancer
{
/// <summary>[Pro-Only]
/// A wrapper which allows access to the value of <see cref="bool"/> properties that are controlled by animations.
/// </summary>
///
/// <remarks>
/// <strong>Documentation:</strong>
/// <see href="https://kybernetik.com.au/animancer/docs/manual/ik#animated-properties">
/// Animated Properties</see>
/// </remarks>
///
/// https://kybernetik.com.au/animancer/api/Animancer/AnimatedBool
///
public class AnimatedBool : AnimatedProperty<AnimatedBool.Job, bool>
{
/************************************************************************************************************************/
/// <summary>
/// Allocates room for a specified number of properties to be filled by
/// <see cref="InitializeProperty(int, Transform, Type, string)"/>.
/// </summary>
public AnimatedBool(IAnimancerComponent animancer, int propertyCount,
NativeArrayOptions options = NativeArrayOptions.ClearMemory)
: base(animancer, propertyCount, options)
{ }
/// <summary>Initializes a single property.</summary>
public AnimatedBool(IAnimancerComponent animancer, string propertyName)
: base(animancer, propertyName)
{ }
/// <summary>Initializes a group of properties.</summary>
public AnimatedBool(IAnimancerComponent animancer, params string[] propertyNames)
: base(animancer, propertyNames)
{ }
/************************************************************************************************************************/
protected override void CreateJob()
{
_Job = new() { properties = _Properties, values = _Values };
}
/************************************************************************************************************************/
/// <summary>An <see cref="IAnimationJob"/> which reads an array of <see cref="bool"/> values.</summary>
/// https://kybernetik.com.au/animancer/api/Animancer/Job
///
public struct Job : IAnimationJob
{
public NativeArray<PropertyStreamHandle> properties;
public NativeArray<bool> values;
public void ProcessRootMotion(AnimationStream stream) { }
public void ProcessAnimation(AnimationStream stream)
{
for (int i = properties.Length - 1; i >= 0; i--)
values[i] = properties[i].GetBool(stream);
}
}
/************************************************************************************************************************/
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 264f1b6d2ea80b94d9d05a9cdf24e5e1
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,72 @@
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
using UnityEngine.Animations;
using Unity.Collections;
namespace Animancer
{
/// <summary>[Pro-Only]
/// A wrapper which allows access to the value of <see cref="float"/> properties that are controlled by animations.
/// </summary>
///
/// <remarks>
/// <strong>Documentation:</strong>
/// <see href="https://kybernetik.com.au/animancer/docs/manual/ik#animated-properties">
/// Animated Properties</see>
/// </remarks>
///
/// https://kybernetik.com.au/animancer/api/Animancer/AnimatedFloat
///
public class AnimatedFloat : AnimatedProperty<AnimatedFloat.Job, float>
{
/************************************************************************************************************************/
/// <summary>
/// Allocates room for a specified number of properties to be filled by
/// <see cref="InitializeProperty(int, Transform, Type, string)"/>.
/// </summary>
public AnimatedFloat(IAnimancerComponent animancer, int propertyCount,
NativeArrayOptions options = NativeArrayOptions.ClearMemory)
: base(animancer, propertyCount, options)
{ }
/// <summary>Initializes a single property.</summary>
public AnimatedFloat(IAnimancerComponent animancer, string propertyName)
: base(animancer, propertyName)
{ }
/// <summary>Initializes a group of properties.</summary>
public AnimatedFloat(IAnimancerComponent animancer, params string[] propertyNames)
: base(animancer, propertyNames)
{ }
/************************************************************************************************************************/
protected override void CreateJob()
{
_Job = new() { properties = _Properties, values = _Values };
}
/************************************************************************************************************************/
/// <summary>An <see cref="IAnimationJob"/> which reads an array of <see cref="float"/> values.</summary>
/// https://kybernetik.com.au/animancer/api/Animancer/Job
///
public struct Job : IAnimationJob
{
public NativeArray<PropertyStreamHandle> properties;
public NativeArray<float> values;
public void ProcessRootMotion(AnimationStream stream) { }
public void ProcessAnimation(AnimationStream stream)
{
for (int i = properties.Length - 1; i >= 0; i--)
values[i] = properties[i].GetFloat(stream);
}
}
/************************************************************************************************************************/
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 777209ec6fe423944acd4b2cd65e1e9f
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,72 @@
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
using UnityEngine.Animations;
using Unity.Collections;
namespace Animancer
{
/// <summary>[Pro-Only]
/// A wrapper which allows access to the value of <see cref="int"/> properties that are controlled by animations.
/// </summary>
///
/// <remarks>
/// <strong>Documentation:</strong>
/// <see href="https://kybernetik.com.au/animancer/docs/manual/ik#animated-properties">
/// Animated Properties</see>
/// </remarks>
///
/// https://kybernetik.com.au/animancer/api/Animancer/AnimatedInt
///
public class AnimatedInt : AnimatedProperty<AnimatedInt.Job, int>
{
/************************************************************************************************************************/
/// <summary>
/// Allocates room for a specified number of properties to be filled by
/// <see cref="InitializeProperty(int, Transform, Type, string)"/>.
/// </summary>
public AnimatedInt(IAnimancerComponent animancer, int propertyCount,
NativeArrayOptions options = NativeArrayOptions.ClearMemory)
: base(animancer, propertyCount, options)
{ }
/// <summary>Initializes a single property.</summary>
public AnimatedInt(IAnimancerComponent animancer, string propertyName)
: base(animancer, propertyName)
{ }
/// <summary>Initializes a group of properties.</summary>
public AnimatedInt(IAnimancerComponent animancer, params string[] propertyNames)
: base(animancer, propertyNames)
{ }
/************************************************************************************************************************/
protected override void CreateJob()
{
_Job = new() { properties = _Properties, values = _Values };
}
/************************************************************************************************************************/
/// <summary>An <see cref="IAnimationJob"/> which reads an array of <see cref="int"/> values.</summary>
/// https://kybernetik.com.au/animancer/api/Animancer/Job
///
public struct Job : IAnimationJob
{
public NativeArray<PropertyStreamHandle> properties;
public NativeArray<int> values;
public readonly void ProcessRootMotion(AnimationStream stream) { }
public void ProcessAnimation(AnimationStream stream)
{
for (int i = properties.Length - 1; i >= 0; i--)
values[i] = properties[i].GetInt(stream);
}
}
/************************************************************************************************************************/
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 0b40cf978cf9c2940bf4113e0a689ebd
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,162 @@
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
using System;
using UnityEngine;
using UnityEngine.Animations;
using Unity.Collections;
namespace Animancer
{
/// <summary>[Pro-Only]
/// A base wrapper which allows access to the value of properties that are controlled by animations.
/// </summary>
///
/// <remarks>
/// <strong>Documentation:</strong>
/// <see href="https://kybernetik.com.au/animancer/docs/manual/ik#animated-properties">
/// Animated Properties</see>
/// </remarks>
///
/// https://kybernetik.com.au/animancer/api/Animancer/AnimatedProperty_2
///
public abstract class AnimatedProperty<TJob, TValue> : AnimancerJob<TJob>, IDisposable
where TJob : struct, IAnimationJob
where TValue : struct
{
/************************************************************************************************************************/
/// <summary>The properties wrapped by this object.</summary>
protected NativeArray<PropertyStreamHandle> _Properties;
/// <summary>The value of each of the <see cref="_Properties"/> from the most recent update.</summary>
protected NativeArray<TValue> _Values;
/************************************************************************************************************************/
#region Initialization
/************************************************************************************************************************/
/// <summary>
/// Allocates room for a specified number of properties to be filled by
/// <see cref="InitializeProperty(int, Transform, Type, string)"/>.
/// </summary>
public AnimatedProperty(
IAnimancerComponent animancer,
int propertyCount,
NativeArrayOptions options = NativeArrayOptions.ClearMemory)
{
_Properties = new(propertyCount, Allocator.Persistent, options);
_Values = new(propertyCount, Allocator.Persistent);
CreateJob();
var playable = animancer.Graph;
CreatePlayable(playable);
playable.Disposables.Add(this);
}
/// <summary>Initializes a single property.</summary>
public AnimatedProperty(IAnimancerComponent animancer, string propertyName)
: this(animancer, 1, NativeArrayOptions.UninitializedMemory)
{
var animator = animancer.Animator;
_Properties[0] = animator.BindStreamProperty(animator.transform, typeof(Animator), propertyName);
}
/// <summary>Initializes a group of properties.</summary>
public AnimatedProperty(IAnimancerComponent animancer, params string[] propertyNames)
: this(animancer, propertyNames.Length, NativeArrayOptions.UninitializedMemory)
{
var count = propertyNames.Length;
var animator = animancer.Animator;
var transform = animator.transform;
for (int i = 0; i < count; i++)
InitializeProperty(animator, i, transform, typeof(Animator), propertyNames[i]);
}
/************************************************************************************************************************/
/// <summary>Initializes a property on the target <see cref="Animator"/>.</summary>
public void InitializeProperty(Animator animator, int index, string name)
=> InitializeProperty(animator, index, animator.transform, typeof(Animator), name);
/// <summary>Initializes the specified `index` to read a property with the specified `name`.</summary>
public void InitializeProperty(Animator animator, int index, Transform transform, Type type, string name)
=> _Properties[index] = animator.BindStreamProperty(transform, type, name);
/************************************************************************************************************************/
/// <summary>Creates and assigns the <see cref="AnimancerJob._Job"/>.</summary>
protected abstract void CreateJob();
/************************************************************************************************************************/
#endregion
/************************************************************************************************************************/
#region Accessors
/************************************************************************************************************************/
/// <summary>Returns the value of the first property.</summary>
public TValue Value
=> this[0];
/// <summary>Returns the value of the first property.</summary>
public static implicit operator TValue(AnimatedProperty<TJob, TValue> properties)
=> properties[0];
/************************************************************************************************************************/
/// <summary>Returns the value of the property at the specified `index`.</summary>
/// <remarks>This method is identical to <see cref="this[int]"/>.</remarks>
public TValue GetValue(int index)
=> _Values[index];
/// <summary>Returns the value of the property at the specified `index`.</summary>
/// <remarks>This indexer is identical to <see cref="GetValue(int)"/>.</remarks>
public TValue this[int index]
=> _Values[index];
/************************************************************************************************************************/
/// <summary>Resizes the `values` if necessary and copies the value of each property into it.</summary>
public void GetValues(ref TValue[] values)
{
AnimancerUtilities.SetLength(ref values, _Values.Length);
_Values.CopyTo(values);
}
/// <summary>Returns a new array containing the values of all properties.</summary>
/// <remarks>Use <see cref="GetValues(ref TValue[])"/> to avoid allocating a new array every call.</remarks>
public TValue[] GetValues()
{
var values = new TValue[_Values.Length];
_Values.CopyTo(values);
return values;
}
/************************************************************************************************************************/
#endregion
/************************************************************************************************************************/
void IDisposable.Dispose() => Dispose();
/// <summary>Cleans up the <see cref="NativeArray{T}"/>s.</summary>
/// <remarks>Called by <see cref="AnimancerGraph.OnPlayableDestroy"/>.</remarks>
protected virtual void Dispose()
{
if (_Properties.IsCreated)
{
_Properties.Dispose();
_Values.Dispose();
}
}
/// <summary>Destroys the <see cref="_Playable"/> and restores the graph connection it was intercepting.</summary>
public override void Destroy()
{
Dispose();
base.Destroy();
}
/************************************************************************************************************************/
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 007ec75f71aafa545afbe26b3c2fabe9
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,68 @@
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
using System;
using System.Collections.Generic;
using UnityEngine;
namespace Animancer
{
/// <summary>
/// A system which protects the <see cref="Editor.AnimationGatherer"/>
/// from checking the same object multiple times.
/// </summary>
/// https://kybernetik.com.au/animancer/api/Animancer/AnimationGathererRecursionGuard
///
public struct AnimationGathererRecursionGuard : IDisposable
{
/************************************************************************************************************************/
/// <summary>The maximum number of recursive fields to check through before stopping.</summary>
public static int MaxFieldDepth = 7;
/************************************************************************************************************************/
/// <summary>Types which will be skipped when attempting to gather animations from an unknown object type.</summary>
public static readonly HashSet<Type> DontGatherFrom = new();
/************************************************************************************************************************/
private static readonly HashSet<object>
ObjectsChecked = new();
private static int _GuardCount;
/************************************************************************************************************************/
/// <summary>Call this with a <c>using</c> statement before calling <see cref="HasCheckedObject"/>.</summary>
public static AnimationGathererRecursionGuard Begin()
{
_GuardCount++;
return default;
}
/************************************************************************************************************************/
/// <summary>Ends a block started by <see cref="Begin"/>.</summary>
public readonly void Dispose()
{
_GuardCount--;
if (_GuardCount == 0)
ObjectsChecked.Clear();
}
/************************************************************************************************************************/
/// <summary>Stores the specified object and returns true if it wasn't already stored.</summary>
public static bool HasCheckedObject(object obj)
{
if (_GuardCount <= 0)
Debug.LogError(
$"{nameof(AnimationGathererRecursionGuard)} is being used without {nameof(Begin)} being caled");
return !ObjectsChecked.Add(obj);
}
/************************************************************************************************************************/
}
}

View File

@@ -0,0 +1,13 @@
fileFormatVersion: 2
guid: 5dadff2dec9768843b6238a2cbe0ac9b
timeCreated: 1516751545
licenseType: Store
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,67 @@
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
using System.Diagnostics.CodeAnalysis;
using System.Reflection;
[assembly: AssemblyTitle("Kybernetik.Animancer")]
[assembly: AssemblyDescription("An animation system for Unity which is based on the Playables API.")]
[assembly: AssemblyProduct("Animancer Pro")]
[assembly: AssemblyCompany("Kybernetik")]
[assembly: AssemblyCopyright("Copyright © Kybernetik 2018-2026")]
[assembly: AssemblyVersion("8.3.0.36")]
#if UNITY_EDITOR
[assembly: System.Runtime.CompilerServices.InternalsVisibleTo("Kybernetik.Animancer.Editor")]
[assembly: SuppressMessage("Style", "IDE0039:Use local function",
Justification = "Locals create a new delegate with each use which is less efficient and can break code.")]
[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", "IDE0062:Make local function 'static'",
Justification = "Not supported by Unity")]
[assembly: SuppressMessage("Style", "IDE0063:Use simple 'using' statement",
Justification = "Not always good for implying intent.")]
[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("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", "UNT0008:Null propagation on Unity objects",
Justification = "Use a regular equality check if handling destroyed objects is necessary")]
[assembly: SuppressMessage("Type Safety", "UNT0014:Invalid type for call to GetComponent",
Justification = "Doesn't account for generic constraints.")]
[assembly: SuppressMessage("Correctness", "UNT0023:Coalescing assignment on Unity objects",
Justification = "Use a regular equality check if handling destroyed objects is necessary")]
[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,11 @@
fileFormatVersion: 2
guid: d4631c27e82e1e64680850290bf7dc8e
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: d891b80605a89af41940677d892e0418
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,78 @@
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
using UnityEngine;
namespace Animancer
{
/// <summary>An <see cref="IUpdatable"/> that cancels any fades and logs warnings when they occur.</summary>
///
/// <remarks>
/// This is useful for <see cref="Sprite"/> based characters since fading does nothing for them.
/// <para></para>
/// You can also set the <see cref="AnimancerGraph.DefaultFadeDuration"/> to 0 so that you don't need to set it
/// manually on all your transitions.
/// <para></para>
/// <strong>Example:</strong>
/// <code>
/// [SerializeField] private AnimancerComponent _Animancer;
///
/// protected virtual void Awake()
/// {
/// // To only apply it only in the Unity Editor and Development Builds:
/// DontAllowFade.Assert(_Animancer);
///
/// // Or to apply it at all times:
/// _Animancer.Graph.RequireUpdate(new DontAllowFade());
/// }
/// </code></remarks>
///
/// https://kybernetik.com.au/animancer/api/Animancer/DontAllowFade
///
public class DontAllowFade : Updatable
{
/************************************************************************************************************************/
/// <summary>[Assert-Conditional] Applies a <see cref="DontAllowFade"/> to `animancer`.</summary>
[System.Diagnostics.Conditional(Strings.Assertions)]
public static void Assert(AnimancerGraph animancer)
{
#if UNITY_ASSERTIONS
animancer.RequirePreUpdate(new DontAllowFade());
#endif
}
/************************************************************************************************************************/
/// <summary>If the `node` is fading, this methods logs a warning (Assert-Only) and cancels the fade.</summary>
private static void Validate(AnimancerNode node)
{
if (node != null && node.FadeSpeed != 0)
{
#if UNITY_ASSERTIONS
Debug.LogWarning($"The following {node.GetType().Name} is fading even though " +
$"{nameof(DontAllowFade)} is active: {node.GetDescription()}",
node.Graph.Component as Object);
#endif
node.Weight = node.TargetWeight;
}
}
/************************************************************************************************************************/
/// <summary>Calls <see cref="Validate"/> on all layers and their <see cref="AnimancerLayer.CurrentState"/>.</summary>
public override void Update()
{
var layers = AnimancerGraph.Current.Layers;
for (int i = layers.Count - 1; i >= 0; i--)
{
var layer = layers[i];
Validate(layer);
Validate(layer.CurrentState);
}
}
/************************************************************************************************************************/
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 8847753c6eace0b4c84c323e465a428f
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: cb0b29d765f534f4dbda52acb782341d
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,77 @@
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
using System;
namespace Animancer
{
/// <summary>Extension methods for <see cref="FadeGroup"/>.</summary>
/// https://kybernetik.com.au/animancer/api/Animancer/FadeGroupExtensions
public static class FadeGroupExtensions
{
/************************************************************************************************************************/
/// <summary>[Pro-Only]
/// Assigns the `function` as the <see cref="FadeGroup.Easing"/> if the `fade` isn't <c>null</c>.
/// </summary>
/// <remarks>
/// <em>Animancer Lite ignores this feature in runtime builds.</em>
/// <para></para>
/// <strong>Example:</strong><code>
/// void EasingExample(AnimancerComponent animancer, AnimationClip clip)
/// {
/// // Start fading the animation normally.
/// AnimancerState state = animancer.Play(clip, 0.25f);
///
/// // Then a custom Easing delegate to modify it.
/// state.FadeGroup.SetEasing(t => t * t);// Square the 0-1 value to start slow and end fast.
///
/// // The Easing class has lots of standard mathematical curve functions.
/// state.FadeGroup.SetEasing(Easing.Sine.InOut);
///
/// // Or you can use the Easing.Function enum.
/// state.FadeGroup.SetEasing(Easing.Function.SineInOut);
/// }
/// </code>
/// </remarks>
public static void SetEasing(this FadeGroup fade, Func<float, float> function)
{
if (fade != null)
fade.Easing = function;
}
/************************************************************************************************************************/
/// <summary>[Pro-Only]
/// Assigns the <see cref="Easing.GetDelegate(Easing.Function)"/> as the
/// <see cref="FadeGroup.Easing"/> if the `fade` isn't <c>null</c>.
/// </summary>
/// <remarks>
/// <em>Animancer Lite ignores this feature in runtime builds.</em>
/// <para></para>
/// <strong>Example:</strong><code>
/// void EasingExample(AnimancerComponent animancer, AnimationClip clip)
/// {
/// // Start fading the animation normally.
/// AnimancerState state = animancer.Play(clip, 0.25f);
///
/// // Then a custom Easing delegate to modify it.
/// state.FadeGroup.SetEasing(t => t * t);// Square the 0-1 value to start slow and end fast.
///
/// // The Easing class has lots of standard mathematical curve functions.
/// state.FadeGroup.SetEasing(Easing.Sine.InOut);
///
/// // Or you can use the Easing.Function enum.
/// state.FadeGroup.SetEasing(Easing.Function.SineInOut);
/// }
/// </code>
/// </remarks>
public static void SetEasing(this FadeGroup fade, Easing.Function function)
{
if (fade != null)
fade.Easing = function.GetDelegate();
}
/************************************************************************************************************************/
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 0141a1bdad0bc044dbef84c772ab5188
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,78 @@
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
using System;
using System.Collections.Generic;
namespace Animancer
{
/// <summary>
/// Fades the child weights of a <see cref="MixerState{TParameter}"/>
/// to a new parameter value instead of fading the actual parameter.
/// </summary>
///
/// <remarks>
/// <para></para>
/// <strong>Documentation:</strong>
/// <see href="https://kybernetik.com.au/animancer/docs/manual/parameters#smoothing">
/// Smoothing</see>
/// <para></para>
/// <strong>Example:</strong>
/// Imagine a Linear Mixer with thresholds 0, 1, 2 and child states A, B, C.
/// If you fade its Parameter from 0 to 2 the states would go from A to B to C.
/// But if you use this system instead, the states would go directly from A to C.
/// <h2>Usage</h2>
/// <code>
/// [SerializeField] private AnimancerComponent _Animancer;
/// [SerializeField] private LinearMixerTransition _Mixer;
///
/// public void FadeMixerTo(float parameter, float fadeDuration)
/// {
/// _Mixer.State.FadeChildWeights(parameter, fadeDuration);
/// }
/// </code></remarks>
///
/// https://kybernetik.com.au/animancer/api/Animancer/MixerChildFade
///
public static class MixerChildFade
{
/************************************************************************************************************************/
private static readonly List<float>
ChildWeights = new();
/************************************************************************************************************************/
/// <summary>
/// Fades the child weights of a <see cref="MixerState{TParameter}"/>
/// to a new parameter value instead of fading the actual parameter.
/// </summary>
/// <remarks>See <see cref="MixerChildFade"/> for a usage example.</remarks>
public static void FadeChildWeights<TParameter>(
this MixerState<TParameter> mixer,
TParameter parameter,
float fadeDuration)
{
ChildWeights.Clear();
var childCount = mixer.ChildCount;
for (int i = 0; i < childCount; i++)
ChildWeights.Add(mixer.GetChild(i).Weight);
mixer.Parameter = parameter;
if (!mixer.RecalculateWeights())
return;
var mixerPlayable = mixer.Playable;
for (int i = 0; i < childCount; i++)
{
var child = mixer.GetChild(i);
mixerPlayable.SetChildWeight(child, ChildWeights[i]);
child.StartFade(Math.Max(child.TargetWeight, float.Epsilon), fadeDuration);
}
}
/************************************************************************************************************************/
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 9ce7cd50d8d836f4ba0a3481884aa46b
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: c5f172a996b077843bb4cbb443d6a1fa
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,32 @@
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2022 Kybernetik //
using System.Collections.Generic;
using UnityEngine;
namespace Animancer
{
/// <summary>A set of up/right/down/left animations.</summary>
/// <remarks>
/// <strong>Documentation:</strong>
/// <see href="https://kybernetik.com.au/animancer/docs/manual/playing/directional-sets">
/// Directional Animation Sets</see>
/// </remarks>
/// https://kybernetik.com.au/animancer/api/Animancer/DirectionalAnimationSet2
///
[CreateAssetMenu(
menuName = Strings.MenuPrefix + "Directional Animation Set/2 Directions",
order = Strings.AssetMenuOrder + 4)]
[AnimancerHelpUrl(typeof(DirectionalAnimationSet2))]
public class DirectionalAnimationSet2 : DirectionalSet2<AnimationClip>,
IAnimationClipSource
{
/************************************************************************************************************************/
/// <summary>[<see cref="IAnimationClipSource"/>] Adds all animations from this set to the `clips`.</summary>
void IAnimationClipSource.GetAnimationClips(List<AnimationClip> clips)
=> AddTo(clips);
/************************************************************************************************************************/
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: d9673a15309479c4087f3fcb1cda880e
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,36 @@
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
using System.Collections.Generic;
using UnityEngine;
namespace Animancer
{
/// <summary>A set of up/right/down/left animations.</summary>
/// <remarks>
/// Consider using <c>DirectionalSet&lt;AnimationClip&gt;</c> in code instead of this class
/// to allow <see cref="DirectionalAnimationSet4"/> and <see cref="DirectionalAnimationSet8"/>
/// to be used interchangeably.
/// <para></para>
/// <strong>Documentation:</strong>
/// <see href="https://kybernetik.com.au/animancer/docs/manual/playing/directional-sets">
/// Directional Animation Sets</see>
/// </remarks>
/// https://kybernetik.com.au/animancer/api/Animancer/DirectionalAnimationSet4
///
[CreateAssetMenu(
menuName = Strings.MenuPrefix + "Directional Animation Set/4 Directions",
order = Strings.AssetMenuOrder + 5)]
[AnimancerHelpUrl(typeof(DirectionalAnimationSet4))]
public class DirectionalAnimationSet4 : DirectionalSet4<AnimationClip>,
IAnimationClipSource
{
/************************************************************************************************************************/
/// <summary>[<see cref="IAnimationClipSource"/>] Adds all animations from this set to the `clips`.</summary>
void IAnimationClipSource.GetAnimationClips(List<AnimationClip> clips)
=> AddTo(clips);
/************************************************************************************************************************/
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 6eceeb59a892d074db28203df8e4cd3a
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,36 @@
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
using System.Collections.Generic;
using UnityEngine;
namespace Animancer
{
/// <summary>A set of up/right/down/left animations with diagonals as well.</summary>
/// <remarks>
/// Consider using <c>DirectionalSet&lt;AnimationClip&gt;</c> in code instead of this class
/// to allow <see cref="DirectionalAnimationSet4"/> and <see cref="DirectionalAnimationSet8"/>
/// to be used interchangeably.
/// <para></para>
/// <strong>Documentation:</strong>
/// <see href="https://kybernetik.com.au/animancer/docs/manual/playing/directional-sets">
/// Directional Animation Sets</see>
/// </remarks>
/// https://kybernetik.com.au/animancer/api/Animancer/DirectionalAnimationSet8
///
[CreateAssetMenu(
menuName = Strings.MenuPrefix + "Directional Animation Set/8 Directions",
order = Strings.AssetMenuOrder + 6)]
[AnimancerHelpUrl(typeof(DirectionalAnimationSet8))]
public class DirectionalAnimationSet8 : DirectionalSet8<AnimationClip>,
IAnimationClipSource
{
/************************************************************************************************************************/
/// <summary>[<see cref="IAnimationClipSource"/>] Adds all animations from this set to the `clips`.</summary>
void IAnimationClipSource.GetAnimationClips(List<AnimationClip> clips)
=> AddTo(clips);
/************************************************************************************************************************/
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 8b5b6ac0c7ebd7b41b307d920db7b245
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,402 @@
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
using UnityEngine;
namespace Animancer
{
/// <summary>A <see cref="DirectionalAnimations3D{T}"/> using <see cref="int"/> as the group type.</summary>
///
/// <remarks>
/// <strong>Sample:</strong>
/// <see href="https://kybernetik.com.au/animancer/docs/samples/sprites/character-3d">
/// Directional Character 3D</see>
/// </remarks>
///
/// https://kybernetik.com.au/animancer/api/Animancer/DirectionalAnimations3D
///
[AddComponentMenu(Strings.MenuPrefix + "Directional Animations 3D")]
[AnimancerHelpUrl(typeof(DirectionalAnimations3D))]
public class DirectionalAnimations3D : DirectionalAnimations3D<int> { }
/************************************************************************************************************************/
/// <summary>
/// A component which manages a screen-facing billboard and plays animations from a
/// <see cref="DirectionalSet4{T}"/> to make it look like a <see cref="Sprite"/>
/// based character is facing a particular direction in 3D space.
/// </summary>
///
/// <remarks>
/// <strong>Sample:</strong>
/// <see href="https://kybernetik.com.au/animancer/docs/samples/sprites/character-3d">
/// Directional Character 3D</see>
/// </remarks>
///
/// https://kybernetik.com.au/animancer/api/Animancer/DirectionalAnimations3D_1
///
[AnimancerHelpUrl(typeof(DirectionalAnimations3D<>))]
public class DirectionalAnimations3D<TGroup> : MonoBehaviour
{
/************************************************************************************************************************/
#region Fields and Properties
/************************************************************************************************************************/
[SerializeField]
[Tooltip("The object to rotate according to the " + nameof(Mode))]
private Transform _Transform;
/// <summary>[<see cref="SerializeField"/>]
/// The object to rotate according to the <see cref="Mode"/>.
/// </summary>
/// <remarks>Uses this <see cref="Component.transform"/> by default.</remarks>
public ref Transform Transform
=> ref _Transform;
/************************************************************************************************************************/
[SerializeField]
[Tooltip("The " + nameof(UnityEngine.Camera) + " to make the " + nameof(Transform) + " face towards" +
"\n\nLeave this null to automatically use the Main Camera")]
private Transform _Camera;
/// <summary>[<see cref="SerializeField"/>]
/// The <see cref="UnityEngine.Camera"/> to make the <see cref="Transform"/> face towards.
/// </summary>
/// <remarks>
/// Leave this <c>null</c> to automatically use the <see cref="Camera.main"/>.
/// </remarks>
public Transform Camera
{
get
{
if (_Camera == null)
{
var camera = UnityEngine.Camera.main;
if (camera != null)
_Camera = camera.transform;
}
return _Camera;
}
set => _Camera = value;
}
/************************************************************************************************************************/
[SerializeField]
[Tooltip("The " + nameof(AnimancerComponent) + " to play animations on")]
private AnimancerComponent _Animancer;
/// <summary>[<see cref="SerializeField"/>]
/// The <see cref="AnimancerComponent"/> to play animations on.
/// </summary>
public ref AnimancerComponent Animancer
=> ref _Animancer;
/************************************************************************************************************************/
[SerializeField]
[Tooltip("The " + nameof(DirectionalAnimationSet4) + " or " + nameof(DirectionalAnimationSet8) +
" to play animations from (Forwards in 3D space corresponds to the Up animation)")]
private DirectionalSet<AnimationClip> _Animations;
/// <summary>[<see cref="SerializeField"/>]
/// The animations to choose between based on the <see cref="Forward"/> direction.
/// </summary>
/// <remarks>Forwards in 3D space corresponds to the Up animation.</remarks>
public ref DirectionalSet<AnimationClip> Animations
=> ref _Animations;
/************************************************************************************************************************/
[SerializeField]
[Tooltip("The World-Space direction this character is facing used to select which animation to play")]
private Vector3 _Forward = Vector3.forward;
/// <summary>[<see cref="SerializeField"/>]
/// The World-Space direction this character is facing used to select which animation to play.
/// </summary>
public Vector3 Forward
{
get => _Forward;
set
{
_Forward = value;
if (!enabled)
PlayCurrentAnimation(TimeSynchronizer.CurrentGroup);
}
}
/************************************************************************************************************************/
/// <summary>Functions used to face the <see cref="Transform"/> towards the <see cref="Camera"/>.</summary>
public enum BillboardMode
{
/// <summary>Don't control the <see cref="Transform"/>.</summary>
None,
/// <summary>Copy the <see cref="Camera"/> 's rotation.</summary>
MatchRotation,
/// <summary>Face the <see cref="Camera"/>'s position.</summary>
FacePosition,
/// <summary>As <see cref="MatchRotation"/>, but only rotate around the Y axis.</summary>
UprightMatchRotation,
/// <summary>As <see cref="FacePosition"/>, but only rotate around the Y axis.</summary>
UprightFacePosition,
/// <summary>
/// As <see cref="UprightMatchRotation"/>,
/// and also scale on the Y axis to maintain the same screen size
/// regardless of the <see cref="Camera"/>'s Euler X Angle.</summary>
/// <remarks>Only use this mode with an Orthographic Camera</remarks>
UprightMatchRotationStretched,
/// <summary>
/// As <see cref="UprightFacePosition"/>,
/// and also scale on the Y axis to maintain the same screen size
/// regardless of the <see cref="Camera"/>'s Euler X Angle.</summary>
/// <remarks>Only use this mode with an Orthographic Camera</remarks>
UprightFacePositionStretched,
}
[SerializeField]
[Tooltip("The function used to face the " + nameof(Transform) + " towards the " + nameof(Camera) + ":" +
"\n• None - Don't control the " + nameof(Transform) +
"\n• Match Rotation - Copy the " + nameof(Camera) + "'s rotation" +
"\n• Face Position - Face the " + nameof(Camera) + "'s position" +
"\n• Upright - As above, but only rotate around the Y axis" +
"\n• Stretched - As above, and also scale on the Y axis to maintain the same screen size" +
" regardless of the " + nameof(Camera) + "'s Euler X Angle (only use with an Orthographic Camera)")]
private BillboardMode _Mode = BillboardMode.UprightMatchRotation;
/// <summary>[<see cref="SerializeField"/>]
/// The function used to face the <see cref="Transform"/> towards the <see cref="Camera"/>.
/// </summary>
public BillboardMode Mode
{
get => _Mode;
set
{
_Mode = value;
ResetScaleIfNotStretched();
}
}
/************************************************************************************************************************/
/// <summary>
/// Maintains the <see cref="AnimancerState.NormalizedTime"/> when swapping between animations.
/// </summary>
public readonly TimeSynchronizer<TGroup>
TimeSynchronizer = new(default, true);
/************************************************************************************************************************/
#endregion
/************************************************************************************************************************/
#region Methods
/************************************************************************************************************************/
/// <summary>
/// Finds missing references,
/// samples the current animation,
/// and resets the scale to 1 if not using a stretched mode.
/// </summary>
protected virtual void OnValidate()
{
gameObject.GetComponentInParentOrChildren(ref _Transform);
gameObject.GetComponentInParentOrChildren(ref _Animancer);
if (TryGetCurrentAnimation(out var animation))
AnimancerUtilities.EditModeSampleAnimation(animation, _Animancer);
ResetScaleIfNotStretched();
}
/************************************************************************************************************************/
/// <summary>
/// Finds missing references,
/// samples the current animation,
/// and resets the scale to 1 if not using a stretched mode.
/// </summary>
protected virtual void OnDrawGizmosSelected()
{
if (TryGetCurrentAnimation(out var animation))
AnimancerUtilities.EditModeSampleAnimation(animation, _Animancer);
if (_Transform == null)
return;
var position = _Transform.position;
var length = 1f;
var renderer = GetComponentInChildren<Renderer>();
if (renderer != null)
{
var bounds = renderer.bounds;
position.y += bounds.extents.y;
length = bounds.extents.magnitude;
}
Gizmos.color = new(0.75f, 0.75f, 1, 1);
Gizmos.DrawRay(position, Forward.normalized * length);
}
/************************************************************************************************************************/
/// <summary>
/// Applies the <see cref="Mode"/> then plays the appropriate animation
/// based on the current rotation and <see cref="Forward"/> direction.
/// </summary>
protected virtual void Update()
{
UpdateTransform();
PlayCurrentAnimation(TimeSynchronizer.CurrentGroup);
}
/************************************************************************************************************************/
/// <summary>Applies the <see cref="Mode"/>.</summary>
public void UpdateTransform()
{
switch (_Mode)
{
default:
case BillboardMode.None:
break;
case BillboardMode.MatchRotation:
_Transform.rotation = Camera.rotation;
break;
case BillboardMode.FacePosition:
_Transform.rotation = Quaternion.LookRotation(_Transform.position - Camera.position);
break;
case BillboardMode.UprightMatchRotation:
_Transform.eulerAngles = new(0, Camera.eulerAngles.y, 0);
break;
case BillboardMode.UprightFacePosition:
var direction = _Transform.position - Camera.position;
_Transform.eulerAngles = new(
0,
Mathf.Atan2(direction.x, direction.z) * Mathf.Rad2Deg,
0);
break;
case BillboardMode.UprightMatchRotationStretched:
var eulerAngles = Camera.eulerAngles;
_Transform.eulerAngles = new(0, eulerAngles.y, 0);
StretchHeight(eulerAngles.x);
break;
case BillboardMode.UprightFacePositionStretched:
StretchHeight(Camera.eulerAngles.x);
goto case BillboardMode.UprightFacePosition;
}
}
/************************************************************************************************************************/
/// <summary>
/// Scales the <see cref="Transform"/> on the Y axis to maintain the same screen size
/// regardless of the <see cref="Camera"/>'s Euler X Angle.
/// </summary>
/// <remarks>This calculation only makes sense with an orthographic camera.</remarks>
private void StretchHeight(float eulerX)
{
if (eulerX > 180)
eulerX -= 360;
else if (eulerX < -180)
eulerX += 360;
_Transform.localScale = new(
1,
1 / Mathf.Cos(eulerX * Mathf.Deg2Rad),
1);
}
/// <summary>
/// Resets the <see cref="Transform.localScale"/> to 1 if not using a stretched <see cref="Mode"/>.
/// </summary>
private void ResetScaleIfNotStretched()
{
if (_Transform == null)
return;
switch (_Mode)
{
case BillboardMode.UprightMatchRotationStretched:
case BillboardMode.UprightFacePositionStretched:
break;
default:
_Transform.localScale = Vector3.one;
break;
}
}
/************************************************************************************************************************/
/// <summary>
/// Sets the <see cref="Animations"/> and plays the appropriate animation
/// based on the current rotation and <see cref="Forward"/> direction.
/// </summary>
public void SetAnimations(DirectionalSet<AnimationClip> animations, TGroup group = default)
{
_Animations = animations;
PlayCurrentAnimation(group);
}
/************************************************************************************************************************/
/// <summary>
/// Plays the appropriate animation based on the current rotation and <see cref="Forward"/> direction.
/// </summary>
/// <remarks>
/// If the `group` is the same as the previous, the new animation will be given the same
/// <see cref="AnimancerState.NormalizedTime"/> as the previous.
/// </remarks>
public void PlayCurrentAnimation(TGroup group)
{
if (TryGetCurrentAnimation(out var animation))
{
TimeSynchronizer.StoreTime(_Animancer);
_Animancer.Play(animation);
TimeSynchronizer.SyncTime(_Animancer, group);
}
}
/************************************************************************************************************************/
/// <summary>
/// Tries to get an appropriate animation based on the current rotation and <see cref="Forward"/> direction.
/// </summary>
private bool TryGetCurrentAnimation(out AnimationClip animation)
{
if (_Animations == null ||
_Forward == default)
{
animation = null;
return false;
}
var localForward = _Transform.InverseTransformDirection(_Forward);
var horizontalForward = new Vector2(localForward.x, localForward.z);
animation = _Animations.Get(horizontalForward);
return true;
}
/************************************************************************************************************************/
#endregion
/************************************************************************************************************************/
}
}

View File

@@ -0,0 +1,13 @@
fileFormatVersion: 2
guid: 39d4b0d1540fd634c9ad4c6a16a999c5
labels:
- Example
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,101 @@
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
using System;
using System.Collections.Generic;
using UnityEngine;
namespace Animancer
{
/// <summary>
/// A <see cref="ClipTransition"/> which gets its clip from a
/// <see cref="DirectionalSet{T}"/> of <see cref="AnimationClip"/>s.
/// </summary>
///
/// <remarks>
/// <para></para>
/// <strong>Documentation:</strong>
/// <see href="https://kybernetik.com.au/animancer/docs/manual/playing/directional-sets">
/// Directional Animation Sets</see>
/// <para></para>
/// <strong>Example:</strong><code>
/// // Leave the Clip field empty in the Inspector and assign its AnimationSet instead.
/// [SerializeField] private DirectionalClipTransition _Transition;
///
/// ...
///
/// // Then you can just call SetDirection and Play it like any other transition.
/// // All of the transition's details like Fade Duration and Events will be applied to whichever clip is plays.
/// _Transition.SetDirection(Vector2.right);
/// _Animancer.Play(_Transition);
/// </code></remarks>
///
/// https://kybernetik.com.au/animancer/api/Animancer/DirectionalClipTransition
///
[Serializable]
public class DirectionalClipTransition : ClipTransition,
ICopyable<DirectionalClipTransition>
{
/************************************************************************************************************************/
[SerializeField]
[Tooltip("The animations used to determine the " + nameof(Clip))]
private DirectionalSet<AnimationClip> _AnimationSet;
/// <summary>[<see cref="SerializeField"/>]
/// The <see cref="DirectionalSet{T}"/> used to determine the <see cref="ClipTransition.Clip"/>.
/// </summary>
public ref DirectionalSet<AnimationClip> AnimationSet
=> ref _AnimationSet;
/// <summary>The name of the serialized backing field of <see cref="AnimationSet"/>.</summary>
public const string AnimationSetField = nameof(_AnimationSet);
/************************************************************************************************************************/
/// <summary>Sets the <see cref="ClipTransition.Clip"/> from the <see cref="AnimationSet"/>.</summary>
public void SetDirection(Vector2 direction)
=> Clip = _AnimationSet.Get(direction);
/// <summary>Sets the <see cref="ClipTransition.Clip"/> from the <see cref="AnimationSet"/>.</summary>
public void SetDirection(int direction)
=> Clip = _AnimationSet.Get(direction);
/// <summary>Sets the <see cref="ClipTransition.Clip"/> from the <see cref="AnimationSet"/>.</summary>
public void SetDirection(Direction4 direction)
=> SetDirection((int)direction);
/// <summary>Sets the <see cref="ClipTransition.Clip"/> from the <see cref="AnimationSet"/>.</summary>
public void SetDirection(Direction8 direction)
=> SetDirection((int)direction);
/************************************************************************************************************************/
/// <inheritdoc/>
public override void GatherAnimationClips(ICollection<AnimationClip> clips)
{
base.GatherAnimationClips(clips);
clips.GatherFromSource(_AnimationSet);
}
/************************************************************************************************************************/
/// <inheritdoc/>
public override Transition<ClipState> Clone(CloneContext context)
{
var clone = new DirectionalClipTransition();
clone.CopyFrom(this, context);
return clone;
}
/// <inheritdoc/>
public virtual void CopyFrom(DirectionalClipTransition copyFrom, CloneContext context)
{
base.CopyFrom(copyFrom, context);
_AnimationSet = copyFrom._AnimationSet;
}
/************************************************************************************************************************/
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 5125aedb0106a4443a684797d8c73344
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,172 @@
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
using System;
using System.Collections.Generic;
using UnityEngine;
using Object = UnityEngine.Object;
namespace Animancer
{
/// <summary>A generic set of objects corresponding to up/right/down/left.</summary>
/// <remarks>
/// <strong>Documentation:</strong>
/// <see href="https://kybernetik.com.au/animancer/docs/manual/playing/directional-sets">
/// Directional Animation Sets</see>
/// </remarks>
/// https://kybernetik.com.au/animancer/api/Animancer/DirectionalSet_1
///
[AnimancerHelpUrl(typeof(DirectionalSet<>))]
public abstract class DirectionalSet<T> : ScriptableObject
{
/************************************************************************************************************************/
#region Read-Only
/************************************************************************************************************************/
#if UNITY_ASSERTIONS
private bool _AllowChanges;
#endif
/// <summary>[Assert-Only] Sets a debug flag to enable or disable the ability to modify this set.</summary>
[System.Diagnostics.Conditional(Strings.Assertions)]
public void AllowChanges(bool allow = true)
{
#if UNITY_ASSERTIONS
_AllowChanges = allow;
#endif
}
/// <summary>[Assert-Only]
/// Throws an <see cref="InvalidOperationException"/> if <see cref="AllowChanges"/> wasn't called.
/// </summary>
[System.Diagnostics.Conditional(Strings.Assertions)]
public void AssertAllowChanges()
{
#if UNITY_ASSERTIONS
if (!_AllowChanges)
throw new InvalidOperationException(
$"Unable to modify {this} because it is read-only." +
$" Call {nameof(AllowChanges)}() before making any changes.");
#endif
}
/************************************************************************************************************************/
#endregion
/************************************************************************************************************************/
#region Directions
/************************************************************************************************************************/
/// <summary>The number of directions in this set.</summary>
public abstract int DirectionCount { get; }
/************************************************************************************************************************/
/// <summary>Returns the name of the specified `direction`.</summary>
protected abstract string GetDirectionName(int direction);
/************************************************************************************************************************/
/// <summary>Returns the object associated with the specified `direction`.</summary>
public abstract T Get(int direction);
/************************************************************************************************************************/
/// <summary>Returns the object closest to the specified `direction`.</summary>
public abstract T Get(Vector2 direction);
/************************************************************************************************************************/
/// <summary>Sets the object associated with the specified `direction`.</summary>
public abstract void Set(int direction, T value);
/************************************************************************************************************************/
/// <summary>[Editor-Only]
/// Attempts to assign the `value` to one of this set's fields based on its `name` and
/// returns the direction index of that field (or -1 if it was unable to determine the direction).
/// </summary>
public int SetByName(string name, T value)
{
var direction = GetDirection(name);
if (direction >= 0)
Set(direction, value);
return direction;
}
/// <summary>[Editor-Only]
/// Attempts to assign the `value` to one of this set's fields based on its name and
/// returns the direction index of that field (or -1 if it was unable to determine the direction).
/// </summary>
public int SetByName<U>(U value)
where U : Object, T
=> SetByName(value.name, value);
/************************************************************************************************************************/
#region Conversion
/************************************************************************************************************************/
/// <summary>Returns a vector representing the specified `direction`.</summary>
public abstract Vector2 GetDirection(int direction);
/************************************************************************************************************************/
/// <summary>Returns the index of the direction which best matches the given `name`.</summary>
public abstract int GetDirection(string name);
/************************************************************************************************************************/
/// <summary>Returns a copy of the `vector` pointing in the closest direction this set has an object for.</summary>
public abstract Vector2 Snap(Vector2 vector);
/************************************************************************************************************************/
#endregion
/************************************************************************************************************************/
#region Gathering
/************************************************************************************************************************/
/// <summary>Adds all objects from this set to the `values`, starting from the specified `index`.</summary>
public void AddTo(T[] values, int index)
{
var count = DirectionCount;
for (int i = 0; i < count; i++)
values[index + i] = Get(i);
}
/// <summary>Adds all objects from this set to the `values`.</summary>
public void AddTo(List<T> values)
{
var count = DirectionCount;
for (int i = 0; i < count; i++)
values.Add(Get(i));
}
/************************************************************************************************************************/
/// <summary>
/// Adds unit vectors corresponding to each of the objects in this set to the `directions`,
/// starting from the specified `index`.
/// </summary>
public void AddTo(Vector2[] directions, int index)
{
var count = DirectionCount;
for (int i = 0; i < count; i++)
directions[index + i] = GetDirection(i);
}
/************************************************************************************************************************/
/// <summary>Calls <see cref="AddTo"/> and <see cref="AddTo"/>.</summary>
public void AddTo(T[] values, Vector2[] directions, int index)
{
AddTo(values, index);
AddTo(directions, index);
}
/************************************************************************************************************************/
#endregion
/************************************************************************************************************************/
#endregion
/************************************************************************************************************************/
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: c0d9246f866eb6740a90936cf5d67836
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,150 @@
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2022 Kybernetik //
using System;
using UnityEngine;
namespace Animancer
{
/// <summary>A generic set of objects corresponding to left/right.</summary>
/// <remarks>
/// <strong>Documentation:</strong>
/// <see href="https://kybernetik.com.au/animancer/docs/manual/playing/directional-sets">
/// Directional Animation Sets</see>
/// </remarks>
/// https://kybernetik.com.au/animancer/api/Animancer/DirectionalSet2_1
///
[AnimancerHelpUrl(typeof(DirectionalSet2<>))]
public class DirectionalSet2<T> : DirectionalSet<T>
{
/************************************************************************************************************************/
#region Fields and Properties
/************************************************************************************************************************/
[SerializeField]
private T _Left;
/// <summary>[<see cref="SerializeField"/>] The object for <see cref="Direction2.Left"/>.</summary>
/// <exception cref="ArgumentException"><see cref="AllowChanges"/> was not called before setting this value.</exception>
public T Left
{
get => _Left;
set
{
AssertAllowChanges();
_Left = value;
AnimancerUtilities.SetDirty(this);
}
}
/************************************************************************************************************************/
[SerializeField]
private T _Right;
/// <summary>[<see cref="SerializeField"/>] The object for <see cref="Direction2.Right"/>.</summary>
/// <exception cref="ArgumentException"><see cref="AllowChanges"/> was not called before setting this value.</exception>
public T Right
{
get => _Right;
set
{
AssertAllowChanges();
_Right = value;
AnimancerUtilities.SetDirty(this);
}
}
/************************************************************************************************************************/
#endregion
/************************************************************************************************************************/
#region Directions
/************************************************************************************************************************/
/// <inheritdoc/>
public override int DirectionCount
=> 2;
/************************************************************************************************************************/
/// <inheritdoc/>
protected override string GetDirectionName(int direction)
=> ((Direction2)direction).ToString();
/************************************************************************************************************************/
/// <summary>Returns the object associated with the specified `direction`.</summary>
public T Get(Direction2 direction)
=> direction switch
{
Direction2.Left => _Left,
Direction2.Right => _Right,
_ => throw AnimancerUtilities.CreateUnsupportedArgumentException(direction),
};
/// <inheritdoc/>
public override T Get(int direction)
=> Get((Direction2)direction);
/************************************************************************************************************************/
/// <inheritdoc/>
public override T Get(Vector2 direction)
=> Get(direction.x);
/// <summary>Negative `direction` returns <see cref="Left"/>, 0 or positive returns <see cref="Right"/>.</summary>
public T Get(float direction)
=> direction < 0 ? _Left : _Right;
/************************************************************************************************************************/
/// <summary>Sets the object associated with the specified `direction`.</summary>
public void Set(Direction2 direction, T value)
{
switch (direction)
{
case Direction2.Left: Left = value; break;
case Direction2.Right: Right = value; break;
default: throw AnimancerUtilities.CreateUnsupportedArgumentException(direction);
}
}
/// <inheritdoc/>
public override void Set(int direction, T value)
=> Set((Direction2)direction, value);
/************************************************************************************************************************/
/// <inheritdoc/>
public override Vector2 GetDirection(int direction)
=> ((Direction2)direction).ToVector2();
/************************************************************************************************************************/
/// <inheritdoc/>
public override int GetDirection(string name)
{
var direction = AnimancerUtilities.GetDirection(name);
if (direction.x == 0)
direction.x = direction.y;
return direction.x switch
{
> 0 => (int)Direction2.Right,
< 0 => (int)Direction2.Left,
_ => -1
};
}
/************************************************************************************************************************/
/// <inheritdoc/>
public override Vector2 Snap(Vector2 vector)
=> Directions.SnapToDirection2(vector);
/************************************************************************************************************************/
#endregion
/************************************************************************************************************************/
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 39fd354c6deee7842849491706a1da26
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,210 @@
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
using System;
using UnityEngine;
namespace Animancer
{
/// <summary>A generic set of objects corresponding to up/right/down/left.</summary>
/// <remarks>
/// <strong>Documentation:</strong>
/// <see href="https://kybernetik.com.au/animancer/docs/manual/playing/directional-sets">
/// Directional Animation Sets</see>
/// </remarks>
/// https://kybernetik.com.au/animancer/api/Animancer/DirectionalSet4_1
///
[AnimancerHelpUrl(typeof(DirectionalSet4<>))]
public class DirectionalSet4<T> : DirectionalSet<T>
{
/************************************************************************************************************************/
#region Fields and Properties
/************************************************************************************************************************/
[SerializeField]
private T _Up;
/// <summary>[<see cref="SerializeField"/>] The object for <see cref="Direction4.Up"/>.</summary>
/// <exception cref="ArgumentException"><see cref="AllowChanges"/> was not called before setting this value.</exception>
public T Up
{
get => _Up;
set
{
AssertAllowChanges();
_Up = value;
AnimancerUtilities.SetDirty(this);
}
}
/************************************************************************************************************************/
[SerializeField]
private T _Right;
/// <summary>[<see cref="SerializeField"/>] The object for <see cref="Direction4.Right"/>.</summary>
/// <exception cref="ArgumentException"><see cref="AllowChanges"/> was not called before setting this value.</exception>
public T Right
{
get => _Right;
set
{
AssertAllowChanges();
_Right = value;
AnimancerUtilities.SetDirty(this);
}
}
/************************************************************************************************************************/
[SerializeField]
private T _Down;
/// <summary>[<see cref="SerializeField"/>] The object for <see cref="Direction4.Down"/>.</summary>
/// <exception cref="ArgumentException"><see cref="AllowChanges"/> was not called before setting this value.</exception>
public T Down
{
get => _Down;
set
{
AssertAllowChanges();
_Down = value;
AnimancerUtilities.SetDirty(this);
}
}
/************************************************************************************************************************/
[SerializeField]
private T _Left;
/// <summary>[<see cref="SerializeField"/>] The object for <see cref="Direction4.Left"/>.</summary>
/// <exception cref="ArgumentException"><see cref="AllowChanges"/> was not called before setting this value.</exception>
public T Left
{
get => _Left;
set
{
AssertAllowChanges();
_Left = value;
AnimancerUtilities.SetDirty(this);
}
}
/************************************************************************************************************************/
#endregion
/************************************************************************************************************************/
#region Directions
/************************************************************************************************************************/
/// <inheritdoc/>
public override int DirectionCount
=> 4;
/************************************************************************************************************************/
/// <inheritdoc/>
protected override string GetDirectionName(int direction)
=> ((Direction4)direction).ToString();
/************************************************************************************************************************/
/// <summary>Returns the object associated with the specified `direction`.</summary>
public T Get(Direction4 direction)
=> direction switch
{
Direction4.Up => _Up,
Direction4.Right => _Right,
Direction4.Down => _Down,
Direction4.Left => _Left,
_ => throw AnimancerUtilities.CreateUnsupportedArgumentException(direction),
};
/// <inheritdoc/>
public override T Get(int direction)
=> Get((Direction4)direction);
/************************************************************************************************************************/
/// <inheritdoc/>
public override T Get(Vector2 direction)
{
if (direction.x >= 0)
{
if (direction.y >= 0)
return direction.x > direction.y ? _Right : _Up;
else
return direction.x > -direction.y ? _Right : _Down;
}
else
{
if (direction.y >= 0)
return direction.x < -direction.y ? _Left : _Up;
else
return direction.x < direction.y ? _Left : _Down;
}
}
/************************************************************************************************************************/
/// <summary>Sets the object associated with the specified `direction`.</summary>
public void Set(Direction4 direction, T value)
{
switch (direction)
{
case Direction4.Up: Up = value; break;
case Direction4.Right: Right = value; break;
case Direction4.Down: Down = value; break;
case Direction4.Left: Left = value; break;
default: throw AnimancerUtilities.CreateUnsupportedArgumentException(direction);
}
}
/// <inheritdoc/>
public override void Set(int direction, T value)
=> Set((Direction4)direction, value);
/************************************************************************************************************************/
/// <inheritdoc/>
public override Vector2 GetDirection(int direction)
=> ((Direction4)direction).ToVector2();
/************************************************************************************************************************/
/// <inheritdoc/>
public override int GetDirection(string name)
{
var direction = AnimancerUtilities.GetDirection(name);
if (direction.x == 0)
{
if (direction.y > 0)
return (int)Direction4.Up;
else if (direction.y < 0)
return (int)Direction4.Down;
else
return -1;
}
else
{
if (direction.x > 0)
return (int)Direction4.Right;
else if (direction.x < 0)
return (int)Direction4.Left;
else
return -1;
}
}
/************************************************************************************************************************/
/// <inheritdoc/>
public override Vector2 Snap(Vector2 vector)
=> Directions.SnapToDirection4(vector);
/************************************************************************************************************************/
#endregion
/************************************************************************************************************************/
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 36b7e15ac4728544a958431a6e72d911
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,227 @@
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
using System;
using UnityEngine;
namespace Animancer
{
/// <summary>A generic set of objects corresponding to up/right/down/left with diagonals as well.</summary>
/// <remarks>
/// <strong>Documentation:</strong>
/// <see href="https://kybernetik.com.au/animancer/docs/manual/playing/directional-sets">
/// Directional Animation Sets</see>
/// </remarks>
/// https://kybernetik.com.au/animancer/api/Animancer/DirectionalSet8_1
///
[AnimancerHelpUrl(typeof(DirectionalSet8<>))]
public class DirectionalSet8<T> : DirectionalSet4<T>
{
/************************************************************************************************************************/
#region Fields and Properties
/************************************************************************************************************************/
[SerializeField]
private T _UpRight;
/// <summary>[<see cref="SerializeField"/>] The object for <see cref="Direction8.UpRight"/>.</summary>
/// <exception cref="ArgumentException"><see cref="AllowChanges"/> was not called before setting this value.</exception>
public T UpRight
{
get => _UpRight;
set
{
AssertAllowChanges();
_UpRight = value;
AnimancerUtilities.SetDirty(this);
}
}
/************************************************************************************************************************/
[SerializeField]
private T _DownRight;
/// <summary>[<see cref="SerializeField"/>] The object for <see cref="Direction8.DownRight"/>.</summary>
/// <exception cref="ArgumentException"><see cref="AllowChanges"/> was not called before setting this value.</exception>
public T DownRight
{
get => _DownRight;
set
{
AssertAllowChanges();
_DownRight = value;
AnimancerUtilities.SetDirty(this);
}
}
/************************************************************************************************************************/
[SerializeField]
private T _DownLeft;
/// <summary>[<see cref="SerializeField"/>] The object for <see cref="Direction8.DownLeft"/>.</summary>
/// <exception cref="ArgumentException"><see cref="AllowChanges"/> was not called before setting this value.</exception>
public T DownLeft
{
get => _DownLeft;
set
{
AssertAllowChanges();
_DownLeft = value;
AnimancerUtilities.SetDirty(this);
}
}
/************************************************************************************************************************/
[SerializeField]
private T _UpLeft;
/// <summary>[<see cref="SerializeField"/>] The object for <see cref="Direction8.UpLeft"/>.</summary>
/// <exception cref="ArgumentException"><see cref="AllowChanges"/> was not called before setting this value.</exception>
public T UpLeft
{
get => _UpLeft;
set
{
AssertAllowChanges();
_UpLeft = value;
AnimancerUtilities.SetDirty(this);
}
}
/************************************************************************************************************************/
#endregion
/************************************************************************************************************************/
#region Directions
/************************************************************************************************************************/
/// <inheritdoc/>
public override int DirectionCount
=> 8;
/************************************************************************************************************************/
/// <inheritdoc/>
protected override string GetDirectionName(int direction)
=> ((Direction8)direction).ToString();
/************************************************************************************************************************/
/// <summary>Returns the object associated with the specified `direction`.</summary>
public T Get(Direction8 direction)
=> direction switch
{
Direction8.Up => Up,
Direction8.Right => Right,
Direction8.Down => Down,
Direction8.Left => Left,
Direction8.UpRight => _UpRight,
Direction8.DownRight => _DownRight,
Direction8.DownLeft => _DownLeft,
Direction8.UpLeft => _UpLeft,
_ => throw AnimancerUtilities.CreateUnsupportedArgumentException(direction),
};
/// <inheritdoc/>
public override T Get(int direction)
=> Get((Direction8)direction);
/************************************************************************************************************************/
/// <inheritdoc/>
public override T Get(Vector2 direction)
{
var angle = Mathf.Atan2(direction.y, direction.x);
var octant = Mathf.RoundToInt(8 * angle / (2 * Mathf.PI) + 8) % 8;
return octant switch
{
0 => Right,
1 => _UpRight,
2 => Up,
3 => _UpLeft,
4 => Left,
5 => _DownLeft,
6 => Down,
7 => _DownRight,
_ => throw new ArgumentOutOfRangeException("Invalid octant"),
};
}
/************************************************************************************************************************/
/// <summary>Sets the object associated with the specified `direction`.</summary>
public void Set(Direction8 direction, T value)
{
switch (direction)
{
case Direction8.Up: Up = value; break;
case Direction8.Right: Right = value; break;
case Direction8.Down: Down = value; break;
case Direction8.Left: Left = value; break;
case Direction8.UpRight: UpRight = value; break;
case Direction8.DownRight: DownRight = value; break;
case Direction8.DownLeft: DownLeft = value; break;
case Direction8.UpLeft: UpLeft = value; break;
default: throw AnimancerUtilities.CreateUnsupportedArgumentException(direction);
}
}
/// <inheritdoc/>
public override void Set(int direction, T value)
=> Set((Direction8)direction, value);
/************************************************************************************************************************/
/// <inheritdoc/>
public override Vector2 GetDirection(int direction)
=> ((Direction8)direction).ToVector2();
/************************************************************************************************************************/
/// <inheritdoc/>
public override int GetDirection(string name)
{
var direction = AnimancerUtilities.GetDirection(name);
if (direction.x == 0)
{
if (direction.y > 0)
return (int)Direction8.Up;
else if (direction.y < 0)
return (int)Direction8.Down;
else
return -1;
}
else if (direction.x > 0)
{
if (direction.y > 0)
return (int)Direction8.UpRight;
else if (direction.y < 0)
return (int)Direction8.DownRight;
else
return (int)Direction8.Right;
}
else
{
if (direction.y > 0)
return (int)Direction8.UpLeft;
else if (direction.y < 0)
return (int)Direction8.DownLeft;
else
return (int)Direction8.Left;
}
}
/************************************************************************************************************************/
/// <inheritdoc/>
public override Vector2 Snap(Vector2 vector)
=> Directions.SnapToDirection8(vector);
/************************************************************************************************************************/
#endregion
/************************************************************************************************************************/
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: f3cebe3f939a8954395cc5920c892519
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,283 @@
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
using System;
using UnityEngine;
namespace Animancer
{
/// <summary>Left or Right.</summary>
/// <remarks>
/// <strong>Documentation:</strong>
/// <see href="https://kybernetik.com.au/animancer/docs/manual/playing/directional-sets">
/// Directional Animation Sets</see>
/// </remarks>
/// https://kybernetik.com.au/animancer/api/Animancer/Direction2
///
public enum Direction2
{
/************************************************************************************************************************/
/// <summary><see cref="Vector2.left"/>.</summary>
Left,
/// <summary><see cref="Vector2.right"/>.</summary>
Right,
/************************************************************************************************************************/
}
/************************************************************************************************************************/
/// <summary>Up, Right, Down, or Left.</summary>
/// <remarks>
/// <strong>Documentation:</strong>
/// <see href="https://kybernetik.com.au/animancer/docs/manual/playing/directional-sets">
/// Directional Animation Sets</see>
/// </remarks>
/// https://kybernetik.com.au/animancer/api/Animancer/Direction4
///
public enum Direction4
{
/************************************************************************************************************************/
/// <summary><see cref="Vector2.up"/>.</summary>
Up,
/// <summary><see cref="Vector2.right"/>.</summary>
Right,
/// <summary><see cref="Vector2.down"/>.</summary>
Down,
/// <summary><see cref="Vector2.left"/>.</summary>
Left,
/************************************************************************************************************************/
}
/************************************************************************************************************************/
/// <summary>Up, Right, Down, Left, or their diagonals.</summary>
/// <remarks>
/// <strong>Documentation:</strong>
/// <see href="https://kybernetik.com.au/animancer/docs/manual/playing/directional-sets">
/// Directional Animation Sets</see>
/// </remarks>
/// https://kybernetik.com.au/animancer/api/Animancer/Direction8
///
public enum Direction8
{
/// <summary><see cref="Vector2.up"/>.</summary>
Up,
/// <summary><see cref="Vector2.right"/>.</summary>
Right,
/// <summary><see cref="Vector2.down"/>.</summary>
Down,
/// <summary><see cref="Vector2.left"/>.</summary>
Left,
/// <summary><see cref="Vector2"/>(0.7..., 0.7...).</summary>
UpRight,
/// <summary><see cref="Vector2"/>(0.7..., -0.7...).</summary>
DownRight,
/// <summary><see cref="Vector2"/>(-0.7..., -0.7...).</summary>
DownLeft,
/// <summary><see cref="Vector2"/>(-0.7..., 0.7...).</summary>
UpLeft,
}
/************************************************************************************************************************/
/// <summary>
/// Utilities relating to <see cref="Direction2"/>, <see cref="Direction4"/> and <see cref="Direction8"/>.
/// </summary>
/// <remarks>
/// <strong>Documentation:</strong>
/// <see href="https://kybernetik.com.au/animancer/docs/manual/playing/directional-sets">
/// Directional Animation Sets</see>
/// </remarks>
/// https://kybernetik.com.au/animancer/api/Animancer/Directions
///
public static class Directions
{
/************************************************************************************************************************/
/// <summary>1 / (Square Root of 2).</summary>
public const float InverseSqrt2 = 0.70710678118f;
/// <summary>A vector with a magnitude of 1 pointing up to the right.</summary>
/// <remarks>The value is approximately (0.7, 0.7).</remarks>
public static Vector2 UpRight => new(InverseSqrt2, InverseSqrt2);
/// <summary>A vector with a magnitude of 1 pointing down to the right.</summary>
/// <remarks>The value is approximately (0.7, -0.7).</remarks>
public static Vector2 DownRight => new(InverseSqrt2, -InverseSqrt2);
/// <summary>A vector with a magnitude of 1 pointing down to the left.</summary>
/// <remarks>The value is approximately (-0.7, -0.7).</remarks>
public static Vector2 DownLeft => new(-InverseSqrt2, -InverseSqrt2);
/// <summary>A vector with a magnitude of 1 pointing up to the left.</summary>
/// <remarks>The value is approximately (-0.707, 0.707).</remarks>
public static Vector2 UpLeft => new(-InverseSqrt2, InverseSqrt2);
/************************************************************************************************************************/
/// <summary>Returns the opposite of the given `direction`.</summary>
public static Direction2 GetOppositeDirection(this Direction2 direction)
=> (Direction2)(1 - direction);
/// <summary>Returns the opposite of the given `direction`.</summary>
public static Direction4 GetOppositeDirection(this Direction4 direction)
=> (Direction4)((int)(direction + 2) % 4);
/// <summary>Returns the opposite of the given `direction`.</summary>
public static Direction8 GetOppositeDirection(this Direction8 direction)
=> (Direction8)((int)(direction + 4) % 8);
/************************************************************************************************************************/
/// <summary>Returns a vector representing the specified `direction`.</summary>
public static Vector2 ToVector2(this Direction2 direction)
=> direction switch
{
Direction2.Left => Vector2.left,
Direction2.Right => Vector2.right,
_ => throw AnimancerUtilities.CreateUnsupportedArgumentException(direction),
};
/// <summary>Returns a vector representing the specified `direction`.</summary>
public static Vector2 ToVector2(this Direction4 direction)
=> direction switch
{
Direction4.Up => Vector2.up,
Direction4.Right => Vector2.right,
Direction4.Down => Vector2.down,
Direction4.Left => Vector2.left,
_ => throw AnimancerUtilities.CreateUnsupportedArgumentException(direction),
};
/// <summary>Returns a vector representing the specified `direction`.</summary>
public static Vector2 ToVector2(this Direction8 direction)
=> direction switch
{
Direction8.Up => Vector2.up,
Direction8.Right => Vector2.right,
Direction8.Down => Vector2.down,
Direction8.Left => Vector2.left,
Direction8.UpRight => UpRight,
Direction8.DownRight => DownRight,
Direction8.DownLeft => DownLeft,
Direction8.UpLeft => UpLeft,
_ => throw AnimancerUtilities.CreateUnsupportedArgumentException(direction),
};
/************************************************************************************************************************/
/// <summary>Returns the direction closest to the specified `vector.x`.</summary>
/// <remarks>Negative `direction` returns <see cref="Left"/>, 0 or positive returns <see cref="Right"/>.</remarks>
public static Direction2 ToDirection2(Vector2 vector)
=> ToDirection2(vector.x);
/// <summary>Returns the direction closest to the specified `direction`.</summary>
/// <remarks>Negative `direction` returns <see cref="Left"/>, 0 or positive returns <see cref="Right"/>.</remarks>
public static Direction2 ToDirection2(float direction)
=> direction < 0 ? Direction2.Left : Direction2.Right;
/************************************************************************************************************************/
/// <summary>Returns the direction closest to the specified `vector`.</summary>
public static Direction4 ToDirection4(Vector2 vector)
{
if (vector.x >= 0)
{
if (vector.y >= 0)
return vector.x > vector.y ? Direction4.Right : Direction4.Up;
else
return vector.x > -vector.y ? Direction4.Right : Direction4.Down;
}
else
{
if (vector.y >= 0)
return vector.x < -vector.y ? Direction4.Left : Direction4.Up;
else
return vector.x < vector.y ? Direction4.Left : Direction4.Down;
}
}
/// <summary>Returns the direction closest to the specified `vector`.</summary>
public static Direction4 ToDirection4(Vector2Int vector)
{
if (vector.x >= 0)
{
if (vector.y >= 0)
return vector.x > vector.y ? Direction4.Right : Direction4.Up;
else
return vector.x > -vector.y ? Direction4.Right : Direction4.Down;
}
else
{
if (vector.y >= 0)
return vector.x < -vector.y ? Direction4.Left : Direction4.Up;
else
return vector.x < vector.y ? Direction4.Left : Direction4.Down;
}
}
/************************************************************************************************************************/
/// <summary>Returns the direction closest to the specified `vector`.</summary>
public static Direction8 ToDirection8(Vector2 vector)
{
var angle = Mathf.Atan2(vector.y, vector.x);
var octant = Mathf.RoundToInt(8 * angle / (2 * Mathf.PI) + 8) % 8;
return octant switch
{
0 => Direction8.Right,
1 => Direction8.UpRight,
2 => Direction8.Up,
3 => Direction8.UpLeft,
4 => Direction8.Left,
5 => Direction8.DownLeft,
6 => Direction8.Down,
7 => Direction8.DownRight,
_ => throw new ArgumentOutOfRangeException("Invalid octant"),
};
}
/************************************************************************************************************************/
/// <summary>Returns a copy of the `vector` pointing in the closest <see cref="Direction2"/>.</summary>
public static Vector2 SnapToDirection2(Vector2 vector)
=> vector.x < 0
? new(-vector.magnitude, 0)
: new(vector.magnitude, 0);
/// <summary>Returns a copy of the `vector` pointing in the closest <see cref="Direction4"/>.</summary>
public static Vector2 SnapToDirection4(Vector2 vector)
{
var magnitude = vector.magnitude;
var direction = ToDirection4(vector);
vector = direction.ToVector2() * magnitude;
return vector;
}
/// <summary>Returns a copy of the `vector` pointing in the closest <see cref="Direction8"/>.</summary>
public static Vector2 SnapToDirection8(Vector2 vector)
{
var magnitude = vector.magnitude;
var direction = ToDirection8(vector);
vector = direction.ToVector2() * magnitude;
return vector;
}
/************************************************************************************************************************/
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: fee4d3d52e50f824aa0e74d1291ec365
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,166 @@
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
using System;
namespace Animancer
{
/// <summary>[Pro-Only]
/// A callback to be triggered after an <see cref="AnimancerNode"/>
/// either starts or finishes fading out to 0 <see cref="AnimancerNode.EffectiveWeight"/>.
/// </summary>
///
/// <remarks>
/// The <see cref="AnimancerNode.EffectiveWeight"/> is only checked at the end of the animation update
/// so if it's set multiple times in the same frame then the callback might not be triggered.
/// <para></para>
/// Most <see href="https://kybernetik.com.au/animancer/docs/manual/fsm">Finite State Machine</see>
/// systems already have their own mechanism for notifying your code when a state is exited
/// so this system is generally only useful when something like that is not already available.
/// <para></para>
/// <strong>Example:</strong> see the <see cref="ExitEvent(AnimancerNode, Action, bool)"/> constructor.
/// </remarks>
///
/// https://kybernetik.com.au/animancer/api/Animancer/ExitEvent
///
public class ExitEvent : Updatable
{
/************************************************************************************************************************/
private Action _Callback;
/// <summary>The method to invoke when this event is triggered.</summary>
public Action Callback
{
get => _Callback;
set
{
_Callback = value;
if (_Callback != null)
EnableIfActive();
else
Disable();
}
}
/************************************************************************************************************************/
private AnimancerNode _Node;
/// <summary>The target node which determines when to trigger this event.</summary>
public AnimancerNode Node
{
get => _Node;
set
{
_Node = value;
if (_Node != null)
EnableIfActive();
else
Disable();
}
}
/************************************************************************************************************************/
/// <summary>
/// Should the <see cref="Callback"/> be invoked when the <see cref="Node"/> starts fading out?
/// Otherwise, it will be invoked after the <see cref="AnimancerNode.EffectiveWeight"/> reaches 0.
/// Default is <c>false</c>.
/// </summary>
public bool InvokeOnStartExiting { get; set; }
/************************************************************************************************************************/
/// <summary>Creates a new <see cref="ExitEvent"/>.</summary>
///
/// <remarks>
/// <strong>Example:</strong><code>
/// private ExitEvent _OnStateExited;
///
/// void ExitEventExample(AnimancerComponent animancer, AnimationClip clip)
/// {
/// var state = animancer.Play(clip);
///
/// // One line initialization:
/// (_OnStateExited ??= new(state, OnStateExited)).Enable();
///
/// // Or two lines:
/// _OnStateExited ??= new(state, OnStateExited);
/// _OnStateExited.Enable();
/// }
///
/// private void OnStateExited()
/// {
/// Debug.Log(_OnStateExited.State + " Exited");
/// }
/// </code></remarks>
///
public ExitEvent(
AnimancerNode node,
Action callback,
bool invokeOnStartExiting = false)
{
_Node = node;
_Callback = callback;
InvokeOnStartExiting = invokeOnStartExiting;
}
/************************************************************************************************************************/
/// <summary>Registers this event to start receiving updates.</summary>
public void Enable()
{
if (_Callback != null)
_Node?.Graph?.RequirePostUpdate(this);
}
/// <summary>
/// Registers this event to start receiving updates if the
/// <see cref="AnimancerNode.TargetWeight"/> is above 0 (i.e. it's not fading out).
/// </summary>
public void EnableIfActive()
{
if (_Callback != null &&
_Node != null &&
_Node.Graph != null &&
_Node.TargetWeight > 0)
_Node.Graph.RequirePostUpdate(this);
}
/************************************************************************************************************************/
/// <summary>Cancels this event to stop receiving updates.</summary>
public void Disable()
{
_Node?.Graph?.CancelPostUpdate(this);
}
/************************************************************************************************************************/
/// <inheritdoc/>
public override void Update()
{
if (!_Node.IsValid())
return;
if (InvokeOnStartExiting)
{
if (_Node.TargetWeight != 0)
return;
}
else
{
if (_Node.EffectiveWeight > 0)
return;
}
_Callback();
Disable();
}
/************************************************************************************************************************/
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 8b86ef1d8772ec040b70676e2c90852c
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,66 @@
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
#if UNITY_PLAYABLE_DIRECTOR
using UnityEngine;
using UnityEngine.Playables;
namespace Animancer
{
/// <summary>Sets a <see cref="PlayableDirector"/> as Animancer's <see cref="IExposedPropertyTable"/>.</summary>
/// <remarks>
/// This class allows Control Tracks to work properly when played in a <see cref="PlayableAssetState"/>.
/// <para></para>
/// <strong>Documentation:</strong>
/// <see href="https://kybernetik.com.au/animancer/docs/manual/timeline#exposed-references">
/// Exposed References</see>
/// </remarks>
/// https://kybernetik.com.au/animancer/api/Animancer/ExposedPropertyTable
///
[AddComponentMenu(Strings.MenuPrefix + "Exposed Property Table")]
[AnimancerHelpUrl(typeof(ExposedPropertyTable))]
[DefaultExecutionOrder(-10000)]// Initialize before anything else might need to use the table.
public class ExposedPropertyTable : MonoBehaviour
{
/************************************************************************************************************************/
[SerializeField] private AnimancerComponent _Animancer;
[SerializeField] private PlayableDirector _Director;
/************************************************************************************************************************/
/// <summary>Calls <see cref="OnValidate"/> and if no <see cref="PlayableDirector"/> was found it adds one.</summary>
protected virtual void Reset()
{
OnValidate();
if (_Director == null)
_Director = gameObject.AddComponent<PlayableDirector>();
_Director.enabled = false;
_Director.playOnAwake = false;
}
/************************************************************************************************************************/
/// <summary>Tries to automatically find any missing references.</summary>
protected virtual void OnValidate()
{
gameObject.GetComponentInParentOrChildren(ref _Animancer);
gameObject.GetComponentInParentOrChildren(ref _Director);
}
/************************************************************************************************************************/
/// <summary>Sets the <see cref="PlayableDirector"/> as Animancer's <see cref="IExposedPropertyTable"/>.</summary>
protected virtual void Awake()
{
_Animancer.Graph.PlayableGraph.SetResolver(_Director);
}
/************************************************************************************************************************/
}
}
#endif

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 0efb115a78218d549a4abaeaf617a2b0
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 8f96d1a421960344082423975948ee06
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

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:

View File

@@ -0,0 +1,87 @@
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
using UnityEngine.Playables;
namespace Animancer
{
/// <summary>A utility for re-assigning Animancer's <see cref="PlayableOutput"/>.</summary>
///
/// <remarks>
/// This should be totally useless, but for some reason it seems to fix an issue with Unity's
/// Animation Rigging package. Normally, all of the Rig's parameters get reset to their
/// starting values any time a playable is connected or disconnected (which Animancer does frequently),
/// but using this utility effectively re-captures the starting values
/// so any subsequent resets retain the values you set.
/// <para></para>
/// <strong>Example:</strong>
/// <para></para><code>
/// public class PlayableOutputRefresherExample : MonoBehaviour
/// {
/// [SerializeField] private AnimancerComponent _Animancer;
/// [SerializeField] private Rig _Rig;
///
/// // A field to store it in.
/// private PlayableOutputRefresher _OutputRefresher;
///
/// protected virtual void OnEnable()
/// {
/// // Initialize on startup.
/// _OutputRefresher = new(_Animancer);
/// }
///
/// public void SetWeight(float weight)
/// {
/// // Change something that would be reset.
/// _Rig.weight = weight;
///
/// // Then call this afterwards.
/// _OutputRefresher.Refresh();
/// }
/// }
/// </code></remarks>
///
/// https://kybernetik.com.au/animancer/api/Animancer/PlayableOutputRefresher
///
public struct PlayableOutputRefresher
{
/************************************************************************************************************************/
/// <summary>The <see cref="PlayableOutput"/> of Animancer's <see cref="PlayableGraph"/>.</summary>
public PlayableOutput Output { get; set; }
/// <summary>The root <see cref="Playable"/> of Animancer's <see cref="PlayableGraph"/>.</summary>
public Playable Root { get; set; }
/************************************************************************************************************************/
/// <summary>Creates a new <see cref="PlayableOutputRefresher"/>.</summary>
public PlayableOutputRefresher(PlayableOutput output)
{
Output = output;
Root = Output.GetSourcePlayable();
}
/************************************************************************************************************************/
/// <summary>Creates a new <see cref="PlayableOutputRefresher"/>.</summary>
public PlayableOutputRefresher(AnimancerGraph animancer)
: this(animancer.Output)
{ }
/************************************************************************************************************************/
/// <summary>Re-assigns the <see cref="Root"/> as the source playable of the <see cref="Output"/>.</summary>
public readonly void Refresh()
=> Output.SetSourcePlayable(Root);
/************************************************************************************************************************/
/// <summary>Re-acquires the <see cref="Root"/> from the <see cref="Output"/>.</summary>
/// <remarks>Call this after <see cref="AnimancerGraph.InsertOutputPlayable"/>.</remarks>
public void OnSourcePlayableChanged()
=> Root = Output.GetSourcePlayable();
/************************************************************************************************************************/
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 22624bd4cfa616c4f96ef8534fdbdd8e
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 2dfb135533444044db8f6fc06502a12e
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,120 @@
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
using UnityEngine;
namespace Animancer
{
/// <summary>
/// A component which takes the root motion from an <see cref="UnityEngine.Animator"/>
/// and applies it to a different object.
/// </summary>
///
/// <remarks>
/// This can be useful if the character's <see cref="Rigidbody"/> or <see cref="CharacterController"/> is on a
/// parent of the <see cref="UnityEngine.Animator"/> to keep the model separate from the logical components.
/// <para></para>
/// <strong>Documentation:</strong>
/// <see href="https://kybernetik.com.au/animancer/docs/manual/other/root-motion#redirecting-root-motion">
/// Redirecting Root Motion</see>
/// </remarks>
///
/// https://kybernetik.com.au/animancer/api/Animancer/RedirectRootMotion
///
[HelpURL("https://kybernetik.com.au/animancer/api/Animancer/" + nameof(RedirectRootMotion))]
[RequireComponent(typeof(Animator))]
public abstract class RedirectRootMotion : MonoBehaviour
{
/************************************************************************************************************************/
[SerializeField]
[Tooltip("The Animator which provides the root motion")]
private Animator _Animator;
/// <summary>The <see cref="UnityEngine.Animator"/> which provides the root motion.</summary>
public ref Animator Animator => ref _Animator;
/************************************************************************************************************************/
/// <summary>The current position of the target.</summary>
public abstract Vector3 Position { get; set; }
/// <summary>The current rotation of the target.</summary>
public abstract Quaternion Rotation { get; set; }
/************************************************************************************************************************/
/// <summary>Is <see cref="Animator.applyRootMotion"/> enabled?</summary>
public virtual bool ApplyRootMotion
=> Animator != null;
/************************************************************************************************************************/
/// <summary>Automatically finds the <see cref="Animator"/>.</summary>
protected virtual void OnValidate()
{
TryGetComponent(out _Animator);
}
/************************************************************************************************************************/
/// <summary>Applies the root motion from the <see cref="Animator"/> to the <see cref="Target"/>.</summary>
protected virtual void OnAnimatorMove()
{
if (!ApplyRootMotion)
return;
Position += Animator.deltaPosition;
Rotation *= Animator.deltaRotation;
}
/************************************************************************************************************************/
}
/// <summary>A <see cref="RedirectRootMotion"/> with a generic <see cref="Target"/>.</remarks>
/// https://kybernetik.com.au/animancer/api/Animancer/RedirectRootMotion_1
///
[HelpURL("https://kybernetik.com.au/animancer/api/Animancer/" + nameof(RedirectRootMotion<T>) + "_1")]
public abstract class RedirectRootMotion<T> : RedirectRootMotion
where T : Object
{
/************************************************************************************************************************/
[SerializeField]
[Tooltip("The object which the root motion will be applied to")]
private T _Target;
/// <summary>The object which the root motion will be applied to.</summary>
public ref T Target => ref _Target;
/************************************************************************************************************************/
/// <summary>
/// Returns true if the <see cref="Target"/> and <see cref="RedirectRootMotion.Animator"/> are set and
/// <see cref="Animator.applyRootMotion"/> is enabled.
/// </summary>
public override bool ApplyRootMotion
=> Target != null
&& base.ApplyRootMotion;
/************************************************************************************************************************/
/// <summary>Automatically finds the <see cref="RedirectRootMotion.Animator"/> and <see cref="Target"/>.</summary>
protected override void OnValidate()
{
base.OnValidate();
if (_Target == null)
{
var parent = transform.parent;
if (parent != null)
_Target = parent.GetComponentInParent<T>();
if (_Target == null)
TryGetComponent(out _Target);
}
}
/************************************************************************************************************************/
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 5d3350b34d54d9e4b885e1a21932ae98
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,57 @@
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
#if UNITY_PHYSICS_3D
using UnityEngine;
namespace Animancer
{
/// <summary>
/// A component which takes the root motion from an <see cref="Animator"/> and applies it to a
/// <see cref="CharacterController"/>.
/// </summary>
///
/// <remarks>
/// <strong>Documentation:</strong>
/// <see href="https://kybernetik.com.au/animancer/docs/manual/other/root-motion#redirecting-root-motion">
/// Redirecting Root Motion</see>
/// </remarks>
///
/// https://kybernetik.com.au/animancer/api/Animancer/RedirectRootMotionToCharacterController
///
[AddComponentMenu("Animancer/Redirect Root Motion To Character Controller")]
[HelpURL("https://kybernetik.com.au/animancer/api/Animancer/" + nameof(RedirectRootMotionToCharacterController))]
public class RedirectRootMotionToCharacterController : RedirectRootMotion<CharacterController>
{
/************************************************************************************************************************/
/// <inheritdoc/>
public override Vector3 Position
{
get => Target.transform.position;
set => Target.Move(value - Position);
}
/// <inheritdoc/>
public override Quaternion Rotation
{
get => Target.transform.rotation;
set => Target.transform.rotation = value;
}
/// <inheritdoc/>
protected override void OnAnimatorMove()
{
if (!ApplyRootMotion)
return;
Target.Move(Animator.deltaPosition);
Target.transform.rotation *= Animator.deltaRotation;
}
/************************************************************************************************************************/
}
}
#endif

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: d90d197b5f47af544aff6260d9dfadf2
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,47 @@
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
#if UNITY_PHYSICS_3D
using UnityEngine;
namespace Animancer
{
/// <summary>
/// A component which takes the root motion from an <see cref="Animator"/> and applies it to a
/// <see cref="Rigidbody"/>.
/// </summary>
///
/// <remarks>
/// <strong>Documentation:</strong>
/// <see href="https://kybernetik.com.au/animancer/docs/manual/other/root-motion#redirecting-root-motion">
/// Redirecting Root Motion</see>
/// </remarks>
///
/// https://kybernetik.com.au/animancer/api/Animancer/RedirectRootMotionToRigidbody
///
[AddComponentMenu("Animancer/Redirect Root Motion To Rigidbody")]
[HelpURL("https://kybernetik.com.au/animancer/api/Animancer/" + nameof(RedirectRootMotionToRigidbody))]
public class RedirectRootMotionToRigidbody : RedirectRootMotion<Rigidbody>
{
/************************************************************************************************************************/
/// <inheritdoc/>
public override Vector3 Position
{
get => Target.position;
set => Target.MovePosition(value);
}
/// <inheritdoc/>
public override Quaternion Rotation
{
get => Target.rotation;
set => Target.MoveRotation(value);
}
/************************************************************************************************************************/
}
}
#endif

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 8bb9852087f39824f9647d710481937e
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,43 @@
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
using UnityEngine;
namespace Animancer
{
/// <summary>
/// A component which takes the root motion from an <see cref="Animator"/> and applies it to a
/// <see cref="Transform"/>.
/// </summary>
///
/// <remarks>
/// <strong>Documentation:</strong>
/// <see href="https://kybernetik.com.au/animancer/docs/manual/other/root-motion#redirecting-root-motion">
/// Redirecting Root Motion</see>
/// </remarks>
///
/// https://kybernetik.com.au/animancer/api/Animancer/RedirectRootMotionToTransform
///
[AddComponentMenu("Animancer/Redirect Root Motion To Transform")]
[HelpURL("https://kybernetik.com.au/animancer/api/Animancer/" + nameof(RedirectRootMotionToTransform))]
public class RedirectRootMotionToTransform : RedirectRootMotion<Transform>
{
/************************************************************************************************************************/
/// <inheritdoc/>
public override Vector3 Position
{
get => Target.position;
set => Target.position = value;
}
/// <inheritdoc/>
public override Quaternion Rotation
{
get => Target.rotation;
set => Target.rotation = value;
}
/************************************************************************************************************************/
}
}

Some files were not shown because too many files have changed in this diff Show More