init
This commit is contained in:
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: d43a4ac3938779641b8b1769d3e689a6
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 91114f4897040cf4c9645e6f413f4e93
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
BIN
Binary file not shown.
+102
@@ -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:
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0"?>
|
||||
<doc>
|
||||
<assembly>
|
||||
<name>Animancer.Lite</name>
|
||||
</assembly>
|
||||
<members>
|
||||
</members>
|
||||
</doc>
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 4bf92cc1f409c88418660c79554fcb48
|
||||
TextScriptImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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
|
||||
@@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 448a88aa0eed28d44b692c71d983d042
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
BIN
Binary file not shown.
+102
@@ -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:
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 131880f34c4a28d41910e4cfba9b5e6b
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,54 @@
|
||||
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2024 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);
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 724cb07f283f36649bdaa5081690d13b
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,72 @@
|
||||
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2024 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);
|
||||
}
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 264f1b6d2ea80b94d9d05a9cdf24e5e1
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,72 @@
|
||||
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2024 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);
|
||||
}
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 777209ec6fe423944acd4b2cd65e1e9f
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,72 @@
|
||||
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2024 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);
|
||||
}
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 0b40cf978cf9c2940bf4113e0a689ebd
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,156 @@
|
||||
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2024 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();
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 007ec75f71aafa545afbe26b3c2fabe9
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,67 @@
|
||||
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2024 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-2024")]
|
||||
[assembly: AssemblyVersion("8.0.2.29")]
|
||||
|
||||
#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
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: d4631c27e82e1e64680850290bf7dc8e
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: d891b80605a89af41940677d892e0418
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,78 @@
|
||||
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2024 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);
|
||||
}
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: cb0b29d765f534f4dbda52acb782341d
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,77 @@
|
||||
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2024 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();
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 0141a1bdad0bc044dbef84c772ab5188
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,78 @@
|
||||
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2024 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/blending/mixers/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 1 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);
|
||||
}
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 9ce7cd50d8d836f4ba0a3481884aa46b
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c5f172a996b077843bb4cbb443d6a1fa
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
+358
@@ -0,0 +1,358 @@
|
||||
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2024 Kybernetik //
|
||||
|
||||
using System;
|
||||
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/DirectionalAnimationSet
|
||||
///
|
||||
[CreateAssetMenu(
|
||||
menuName = Strings.MenuPrefix + "Directional Animation Set/4 Directions",
|
||||
order = Strings.AssetMenuOrder + 3)]
|
||||
[AnimancerHelpUrl(typeof(DirectionalAnimationSet))]
|
||||
public class DirectionalAnimationSet : ScriptableObject,
|
||||
IAnimationClipSource
|
||||
{
|
||||
/************************************************************************************************************************/
|
||||
|
||||
[SerializeField]
|
||||
private AnimationClip _Up;
|
||||
|
||||
/// <summary>[<see cref="SerializeField"/>] The animation facing up (0, 1).</summary>
|
||||
/// <exception cref="ArgumentException"><see cref="AllowSetClips"/> was not called before setting this value.</exception>
|
||||
public AnimationClip Up
|
||||
{
|
||||
get => _Up;
|
||||
set
|
||||
{
|
||||
AssertCanSetClips();
|
||||
_Up = value;
|
||||
AnimancerUtilities.SetDirty(this);
|
||||
}
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
[SerializeField]
|
||||
private AnimationClip _Right;
|
||||
|
||||
/// <summary>[<see cref="SerializeField"/>] The animation facing right (1, 0).</summary>
|
||||
/// <exception cref="ArgumentException"><see cref="AllowSetClips"/> was not called before setting this value.</exception>
|
||||
public AnimationClip Right
|
||||
{
|
||||
get => _Right;
|
||||
set
|
||||
{
|
||||
AssertCanSetClips();
|
||||
_Right = value;
|
||||
AnimancerUtilities.SetDirty(this);
|
||||
}
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
[SerializeField]
|
||||
private AnimationClip _Down;
|
||||
|
||||
/// <summary>[<see cref="SerializeField"/>] The animation facing down (0, -1).</summary>
|
||||
/// <exception cref="ArgumentException"><see cref="AllowSetClips"/> was not called before setting this value.</exception>
|
||||
public AnimationClip Down
|
||||
{
|
||||
get => _Down;
|
||||
set
|
||||
{
|
||||
AssertCanSetClips();
|
||||
_Down = value;
|
||||
AnimancerUtilities.SetDirty(this);
|
||||
}
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
[SerializeField]
|
||||
private AnimationClip _Left;
|
||||
|
||||
/// <summary>[<see cref="SerializeField"/>] The animation facing left (-1, 0).</summary>
|
||||
/// <exception cref="ArgumentException"><see cref="AllowSetClips"/> was not called before setting this value.</exception>
|
||||
public AnimationClip Left
|
||||
{
|
||||
get => _Left;
|
||||
set
|
||||
{
|
||||
AssertCanSetClips();
|
||||
_Left = value;
|
||||
AnimancerUtilities.SetDirty(this);
|
||||
}
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
#if UNITY_ASSERTIONS
|
||||
private bool _AllowSetClips;
|
||||
#endif
|
||||
|
||||
/// <summary>[Assert-Only]
|
||||
/// Determines whether the <see cref="AnimationClip"/> properties are allowed to be set.
|
||||
/// </summary>
|
||||
[System.Diagnostics.Conditional(Strings.Assertions)]
|
||||
public void AllowSetClips(bool allow = true)
|
||||
{
|
||||
#if UNITY_ASSERTIONS
|
||||
_AllowSetClips = allow;
|
||||
#endif
|
||||
}
|
||||
|
||||
/// <summary>[Assert-Only]
|
||||
/// Throws an <see cref="ArgumentException"/> if <see cref="AllowSetClips"/> wasn't called.
|
||||
/// </summary>
|
||||
[System.Diagnostics.Conditional(Strings.Assertions)]
|
||||
public void AssertCanSetClips()
|
||||
{
|
||||
#if UNITY_ASSERTIONS
|
||||
AnimancerUtilities.Assert(_AllowSetClips,
|
||||
$"{nameof(AllowSetClips)}() must be called before attempting to set any of" +
|
||||
$" the animations in a {nameof(DirectionalAnimationSet)}" +
|
||||
$" to ensure that they are not changed accidentally.");
|
||||
#endif
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Returns the animation closest to the specified `direction`.</summary>
|
||||
public virtual AnimationClip GetClip(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;
|
||||
}
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
#region Directions
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>The number of animations in this set.</summary>
|
||||
public virtual int ClipCount
|
||||
=> 4;
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <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/Direction
|
||||
///
|
||||
public enum Direction
|
||||
{
|
||||
/// <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>Returns the name of the specified `direction`.</summary>
|
||||
protected virtual string GetDirectionName(int direction)
|
||||
=> ((Direction)direction).ToString();
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Returns the animation associated with the specified `direction`.</summary>
|
||||
public AnimationClip GetClip(Direction direction)
|
||||
=> direction switch
|
||||
{
|
||||
Direction.Up => _Up,
|
||||
Direction.Right => _Right,
|
||||
Direction.Down => _Down,
|
||||
Direction.Left => _Left,
|
||||
_ => throw AnimancerUtilities.CreateUnsupportedArgumentException(direction),
|
||||
};
|
||||
|
||||
/// <summary>Returns the animation associated with the specified `direction`.</summary>
|
||||
public virtual AnimationClip GetClip(int direction)
|
||||
=> GetClip((Direction)direction);
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Sets the animation associated with the specified `direction`.</summary>
|
||||
public void SetClip(Direction direction, AnimationClip clip)
|
||||
{
|
||||
switch (direction)
|
||||
{
|
||||
case Direction.Up: Up = clip; break;
|
||||
case Direction.Right: Right = clip; break;
|
||||
case Direction.Down: Down = clip; break;
|
||||
case Direction.Left: Left = clip; break;
|
||||
default: throw AnimancerUtilities.CreateUnsupportedArgumentException(direction);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Sets the animation associated with the specified `direction`.</summary>
|
||||
public virtual void SetClip(int direction, AnimationClip clip) => SetClip((Direction)direction, clip);
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>[Editor-Only]
|
||||
/// Attempts to assign the `clip` 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 virtual int SetClipByName(AnimationClip clip)
|
||||
{
|
||||
var name = clip.name;
|
||||
|
||||
int bestDirection = -1;
|
||||
int bestDirectionIndex = -1;
|
||||
|
||||
var directionCount = ClipCount;
|
||||
for (int i = 0; i < directionCount; i++)
|
||||
{
|
||||
var index = name.LastIndexOf(GetDirectionName(i));
|
||||
if (bestDirectionIndex < index)
|
||||
{
|
||||
bestDirectionIndex = index;
|
||||
bestDirection = i;
|
||||
}
|
||||
}
|
||||
|
||||
if (bestDirection >= 0)
|
||||
SetClip(bestDirection, clip);
|
||||
|
||||
return bestDirection;
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
#region Conversion
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Returns a vector representing the specified `direction`.</summary>
|
||||
public static Vector2 DirectionToVector(Direction direction)
|
||||
=> direction switch
|
||||
{
|
||||
Direction.Up => Vector2.up,
|
||||
Direction.Right => Vector2.right,
|
||||
Direction.Down => Vector2.down,
|
||||
Direction.Left => Vector2.left,
|
||||
_ => throw AnimancerUtilities.CreateUnsupportedArgumentException(direction),
|
||||
};
|
||||
|
||||
/// <summary>Returns a vector representing the specified `direction`.</summary>
|
||||
public virtual Vector2 GetDirection(int direction)
|
||||
=> DirectionToVector((Direction)direction);
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Returns the direction closest to the specified `vector`.</summary>
|
||||
public static Direction VectorToDirection(Vector2 vector)
|
||||
{
|
||||
if (vector.x >= 0)
|
||||
{
|
||||
if (vector.y >= 0)
|
||||
return vector.x > vector.y ? Direction.Right : Direction.Up;
|
||||
else
|
||||
return vector.x > -vector.y ? Direction.Right : Direction.Down;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (vector.y >= 0)
|
||||
return vector.x < -vector.y ? Direction.Left : Direction.Up;
|
||||
else
|
||||
return vector.x < vector.y ? Direction.Left : Direction.Down;
|
||||
}
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Returns a copy of the `vector` pointing in the closest direction this set type has an animation for.</summary>
|
||||
public static Vector2 SnapVectorToDirection(Vector2 vector)
|
||||
{
|
||||
var magnitude = vector.magnitude;
|
||||
var direction = VectorToDirection(vector);
|
||||
vector = DirectionToVector(direction) * magnitude;
|
||||
return vector;
|
||||
}
|
||||
|
||||
/// <summary>Returns a copy of the `vector` pointing in the closest direction this set has an animation for.</summary>
|
||||
public virtual Vector2 Snap(Vector2 vector)
|
||||
=> SnapVectorToDirection(vector);
|
||||
|
||||
/************************************************************************************************************************/
|
||||
#endregion
|
||||
/************************************************************************************************************************/
|
||||
#region Collections
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Adds all animations from this set to the `clips`, starting from the specified `index`.</summary>
|
||||
public void AddClips(AnimationClip[] clips, int index)
|
||||
{
|
||||
var count = ClipCount;
|
||||
for (int i = 0; i < count; i++)
|
||||
clips[index + i] = GetClip(i);
|
||||
}
|
||||
|
||||
/// <summary>[<see cref="IAnimationClipSource"/>] Adds all animations from this set to the `clips`.</summary>
|
||||
public void GetAnimationClips(List<AnimationClip> clips)
|
||||
{
|
||||
var count = ClipCount;
|
||||
for (int i = 0; i < count; i++)
|
||||
clips.Add(GetClip(i));
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>
|
||||
/// Adds unit vectors corresponding to each of the animations in this set to the `directions`, starting from
|
||||
/// the specified `index`.
|
||||
/// </summary>
|
||||
public void AddDirections(Vector2[] directions, int index)
|
||||
{
|
||||
var count = ClipCount;
|
||||
for (int i = 0; i < count; i++)
|
||||
directions[index + i] = GetDirection(i);
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Calls <see cref="AddClips"/> and <see cref="AddDirections"/>.</summary>
|
||||
public void AddClipsAndDirections(AnimationClip[] clips, Vector2[] directions, int index)
|
||||
{
|
||||
AddClips(clips, index);
|
||||
AddDirections(directions, index);
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
#endregion
|
||||
/************************************************************************************************************************/
|
||||
#endregion
|
||||
/************************************************************************************************************************/
|
||||
}
|
||||
}
|
||||
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 6eceeb59a892d074db28203df8e4cd3a
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
+326
@@ -0,0 +1,326 @@
|
||||
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2024 Kybernetik //
|
||||
|
||||
using System;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Animancer
|
||||
{
|
||||
/// <summary>A set of up/right/down/left animations 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/DirectionalAnimationSet8
|
||||
///
|
||||
[CreateAssetMenu(
|
||||
menuName = Strings.MenuPrefix + "Directional Animation Set/8 Directions",
|
||||
order = Strings.AssetMenuOrder + 4)]
|
||||
[AnimancerHelpUrl(typeof(DirectionalAnimationSet8))]
|
||||
public class DirectionalAnimationSet8 : DirectionalAnimationSet
|
||||
{
|
||||
/************************************************************************************************************************/
|
||||
|
||||
[SerializeField]
|
||||
private AnimationClip _UpRight;
|
||||
|
||||
/// <summary>[<see cref="SerializeField"/>] The animation facing diagonally up-right ~(0.7, 0.7).</summary>
|
||||
/// <exception cref="ArgumentException"><see cref="AllowSetClips"/> was not called before setting this value.</exception>
|
||||
public AnimationClip UpRight
|
||||
{
|
||||
get => _UpRight;
|
||||
set
|
||||
{
|
||||
AssertCanSetClips();
|
||||
_UpRight = value;
|
||||
AnimancerUtilities.SetDirty(this);
|
||||
}
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
[SerializeField]
|
||||
private AnimationClip _DownRight;
|
||||
|
||||
/// <summary>[<see cref="SerializeField"/>] The animation facing diagonally down-right ~(0.7, -0.7).</summary>
|
||||
/// <exception cref="ArgumentException"><see cref="AllowSetClips"/> was not called before setting this value.</exception>
|
||||
public AnimationClip DownRight
|
||||
{
|
||||
get => _DownRight;
|
||||
set
|
||||
{
|
||||
AssertCanSetClips();
|
||||
_DownRight = value;
|
||||
AnimancerUtilities.SetDirty(this);
|
||||
}
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
[SerializeField]
|
||||
private AnimationClip _DownLeft;
|
||||
|
||||
/// <summary>[<see cref="SerializeField"/>] The animation facing diagonally down-left ~(-0.7, -0.7).</summary>
|
||||
/// <exception cref="ArgumentException"><see cref="AllowSetClips"/> was not called before setting this value.</exception>
|
||||
public AnimationClip DownLeft
|
||||
{
|
||||
get => _DownLeft;
|
||||
set
|
||||
{
|
||||
AssertCanSetClips();
|
||||
_DownLeft = value;
|
||||
AnimancerUtilities.SetDirty(this);
|
||||
}
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
[SerializeField]
|
||||
private AnimationClip _UpLeft;
|
||||
|
||||
/// <summary>[<see cref="SerializeField"/>] The animation facing diagonally up-left ~(-0.7, 0.7).</summary>
|
||||
/// <exception cref="ArgumentException"><see cref="AllowSetClips"/> was not called before setting this value.</exception>
|
||||
public AnimationClip UpLeft
|
||||
{
|
||||
get => _UpLeft;
|
||||
set
|
||||
{
|
||||
AssertCanSetClips();
|
||||
_UpLeft = value;
|
||||
AnimancerUtilities.SetDirty(this);
|
||||
}
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Returns the animation closest to the specified `direction`.</summary>
|
||||
public override AnimationClip GetClip(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"),
|
||||
};
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
#region Directions
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Constants for each of the diagonal directions.</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/Diagonals
|
||||
///
|
||||
public static class Diagonals
|
||||
{
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>1 / (Square Root of 2).</summary>
|
||||
public const float OneOverSqrt2 = 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(OneOverSqrt2, OneOverSqrt2);
|
||||
|
||||
/// <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(OneOverSqrt2, -OneOverSqrt2);
|
||||
|
||||
/// <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(-OneOverSqrt2, -OneOverSqrt2);
|
||||
|
||||
/// <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(-OneOverSqrt2, OneOverSqrt2);
|
||||
|
||||
/************************************************************************************************************************/
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override int ClipCount
|
||||
=> 8;
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <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/Direction
|
||||
///
|
||||
public new enum Direction
|
||||
{
|
||||
/// <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,
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
protected override string GetDirectionName(int direction)
|
||||
=> ((Direction)direction).ToString();
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Returns the animation associated with the specified `direction`.</summary>
|
||||
public AnimationClip GetClip(Direction direction)
|
||||
=> direction switch
|
||||
{
|
||||
Direction.Up => Up,
|
||||
Direction.Right => Right,
|
||||
Direction.Down => Down,
|
||||
Direction.Left => Left,
|
||||
Direction.UpRight => _UpRight,
|
||||
Direction.DownRight => _DownRight,
|
||||
Direction.DownLeft => _DownLeft,
|
||||
Direction.UpLeft => _UpLeft,
|
||||
_ => throw AnimancerUtilities.CreateUnsupportedArgumentException(direction),
|
||||
};
|
||||
|
||||
public override AnimationClip GetClip(int direction)
|
||||
=> GetClip((Direction)direction);
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Sets the animation associated with the specified `direction`.</summary>
|
||||
public void SetClip(Direction direction, AnimationClip clip)
|
||||
{
|
||||
switch (direction)
|
||||
{
|
||||
case Direction.Up: Up = clip; break;
|
||||
case Direction.Right: Right = clip; break;
|
||||
case Direction.Down: Down = clip; break;
|
||||
case Direction.Left: Left = clip; break;
|
||||
case Direction.UpRight: UpRight = clip; break;
|
||||
case Direction.DownRight: DownRight = clip; break;
|
||||
case Direction.DownLeft: DownLeft = clip; break;
|
||||
case Direction.UpLeft: UpLeft = clip; break;
|
||||
default: throw AnimancerUtilities.CreateUnsupportedArgumentException(direction);
|
||||
}
|
||||
}
|
||||
|
||||
public override void SetClip(int direction, AnimationClip clip)
|
||||
=> SetClip((Direction)direction, clip);
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Returns a vector representing the specified `direction`.</summary>
|
||||
public static Vector2 DirectionToVector(Direction direction)
|
||||
=> direction switch
|
||||
{
|
||||
Direction.Up => Vector2.up,
|
||||
Direction.Right => Vector2.right,
|
||||
Direction.Down => Vector2.down,
|
||||
Direction.Left => Vector2.left,
|
||||
Direction.UpRight => Diagonals.UpRight,
|
||||
Direction.DownRight => Diagonals.DownRight,
|
||||
Direction.DownLeft => Diagonals.DownLeft,
|
||||
Direction.UpLeft => Diagonals.UpLeft,
|
||||
_ => throw AnimancerUtilities.CreateUnsupportedArgumentException(direction),
|
||||
};
|
||||
|
||||
public override Vector2 GetDirection(int direction)
|
||||
=> DirectionToVector((Direction)direction);
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Returns the direction closest to the specified `vector`.</summary>
|
||||
public new static Direction VectorToDirection(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 => Direction.Right,
|
||||
1 => Direction.UpRight,
|
||||
2 => Direction.Up,
|
||||
3 => Direction.UpLeft,
|
||||
4 => Direction.Left,
|
||||
5 => Direction.DownLeft,
|
||||
6 => Direction.Down,
|
||||
7 => Direction.DownRight,
|
||||
_ => throw new ArgumentOutOfRangeException("Invalid octant"),
|
||||
};
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>
|
||||
/// Returns a copy of the `vector` pointing in the closest direction
|
||||
/// which this set type has an animation for.
|
||||
/// </summary>
|
||||
public new static Vector2 SnapVectorToDirection(Vector2 vector)
|
||||
{
|
||||
var magnitude = vector.magnitude;
|
||||
var direction = VectorToDirection(vector);
|
||||
vector = DirectionToVector(direction) * magnitude;
|
||||
return vector;
|
||||
}
|
||||
|
||||
public override Vector2 Snap(Vector2 vector)
|
||||
=> SnapVectorToDirection(vector);
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override int SetClipByName(AnimationClip clip)
|
||||
{
|
||||
var name = clip.name;
|
||||
|
||||
var directionCount = ClipCount;
|
||||
for (int i = directionCount - 1; i >= 0; i--)
|
||||
{
|
||||
if (name.Contains(GetDirectionName(i)))
|
||||
{
|
||||
SetClip(i, clip);
|
||||
return i;
|
||||
}
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
#endregion
|
||||
/************************************************************************************************************************/
|
||||
}
|
||||
}
|
||||
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 8b5b6ac0c7ebd7b41b307d920db7b245
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
+402
@@ -0,0 +1,402 @@
|
||||
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2024 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="DirectionalAnimationSet"/> 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(DirectionalAnimationSet) + " to play animations from" +
|
||||
" (Forwards in 3D space corresponds to the Up animation)")]
|
||||
private DirectionalAnimationSet _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 DirectionalAnimationSet 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(DirectionalAnimationSet 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.GetClip(horizontalForward);
|
||||
return true;
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
#endregion
|
||||
/************************************************************************************************************************/
|
||||
}
|
||||
}
|
||||
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 39d4b0d1540fd634c9ad4c6a16a999c5
|
||||
labels:
|
||||
- Example
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
+100
@@ -0,0 +1,100 @@
|
||||
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2024 Kybernetik //
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Animancer
|
||||
{
|
||||
/// <summary>A <see cref="ClipTransition"/> which gets its clip from a <see cref="DirectionalAnimationSet"/>.</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 which used to determine the " + nameof(Clip))]
|
||||
private DirectionalAnimationSet _AnimationSet;
|
||||
|
||||
/// <summary>[<see cref="SerializeField"/>]
|
||||
/// The <see cref="DirectionalAnimationSet"/> used to determine the <see cref="ClipTransition.Clip"/>.
|
||||
/// </summary>
|
||||
public ref DirectionalAnimationSet AnimationSet
|
||||
=> ref _AnimationSet;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override UnityEngine.Object MainObject
|
||||
=> _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.GetClip(direction);
|
||||
|
||||
/// <summary>Sets the <see cref="ClipTransition.Clip"/> from the <see cref="AnimationSet"/>.</summary>
|
||||
public void SetDirection(int direction)
|
||||
=> Clip = _AnimationSet.GetClip(direction);
|
||||
|
||||
/// <summary>Sets the <see cref="ClipTransition.Clip"/> from the <see cref="AnimationSet"/>.</summary>
|
||||
public void SetDirection(DirectionalAnimationSet.Direction direction)
|
||||
=> Clip = _AnimationSet.GetClip(direction);
|
||||
|
||||
/// <summary>Sets the <see cref="ClipTransition.Clip"/> from the <see cref="AnimationSet"/>.</summary>
|
||||
public void SetDirection(DirectionalAnimationSet8.Direction direction)
|
||||
=> Clip = _AnimationSet.GetClip((int)direction);
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override void GatherAnimationClips(ICollection<AnimationClip> clips)
|
||||
{
|
||||
base.GatherAnimationClips(clips);
|
||||
clips.GatherFromSource(_AnimationSet);
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <inheritdoc/>
|
||||
public virtual void CopyFrom(DirectionalClipTransition copyFrom, CloneContext context)
|
||||
{
|
||||
base.CopyFrom(copyFrom, context);
|
||||
|
||||
if (copyFrom == null)
|
||||
{
|
||||
_AnimationSet = default;
|
||||
return;
|
||||
}
|
||||
|
||||
_AnimationSet = copyFrom._AnimationSet;
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
}
|
||||
}
|
||||
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 5125aedb0106a4443a684797d8c73344
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,166 @@
|
||||
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2024 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:
|
||||
/// (_OnClipExit ??= new(state, OnStateExited)).Enable();
|
||||
///
|
||||
/// // Or two lines:
|
||||
/// _OnClipExit ??= new(state, OnStateExited);
|
||||
/// _OnClipExit.Enable();
|
||||
/// }
|
||||
///
|
||||
/// private void OnStateExited()
|
||||
/// {
|
||||
/// Debug.Log(_OnClipExit.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();
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 8b86ef1d8772ec040b70676e2c90852c
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,66 @@
|
||||
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2024 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
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 0efb115a78218d549a4abaeaf617a2b0
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 8f96d1a421960344082423975948ee06
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,68 @@
|
||||
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2024 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-2024")]
|
||||
[assembly: AssemblyVersion("8.0.2.29")]
|
||||
|
||||
#if UNITY_EDITOR
|
||||
|
||||
[assembly: SuppressMessage("Style", "IDE0016:Use 'throw' expression",
|
||||
Justification = "Not supported by older Unity versions.")]
|
||||
[assembly: SuppressMessage("Style", "IDE0019:Use pattern matching",
|
||||
Justification = "Not supported by older Unity versions.")]
|
||||
[assembly: SuppressMessage("Style", "IDE0039:Use local function",
|
||||
Justification = "Not supported by older Unity versions.")]
|
||||
[assembly: SuppressMessage("Style", "IDE0044:Make field readonly",
|
||||
Justification = "Using the [SerializeField] attribute on a private field means Unity will set it from serialized data.")]
|
||||
[assembly: SuppressMessage("Code Quality", "IDE0051:Remove unused private members",
|
||||
Justification = "Unity messages can be private, but the IDE will not know that Unity can still call them.")]
|
||||
[assembly: SuppressMessage("Code Quality", "IDE0052:Remove unread private members",
|
||||
Justification = "Unity messages can be private and don't need to be called manually.")]
|
||||
[assembly: SuppressMessage("Style", "IDE0060:Remove unused parameter",
|
||||
Justification = "Unity messages sometimes need specific signatures, even if you don't use all the parameters.")]
|
||||
[assembly: SuppressMessage("Style", "IDE0063:Use simple 'using' statement",
|
||||
Justification = "Not supported by older Unity versions.")]
|
||||
[assembly: SuppressMessage("Style", "IDE0066:Convert switch statement to expression",
|
||||
Justification = "Not supported by older Unity versions.")]
|
||||
[assembly: SuppressMessage("Code Quality", "IDE0067:Dispose objects before losing scope",
|
||||
Justification = "Not always relevant.")]
|
||||
[assembly: SuppressMessage("Code Quality", "IDE0068:Use recommended dispose pattern",
|
||||
Justification = "Not always relevant.")]
|
||||
[assembly: SuppressMessage("Code Quality", "IDE0069:Disposable fields should be disposed",
|
||||
Justification = "Not always relevant.")]
|
||||
[assembly: SuppressMessage("Style", "IDE0083:Use pattern matching",
|
||||
Justification = "Not supported by older Unity versions")]
|
||||
[assembly: SuppressMessage("Style", "IDE0090:Use 'new(...)'",
|
||||
Justification = "Not supported by older Unity versions.")]
|
||||
[assembly: SuppressMessage("CodeQuality", "IDE0079:Remove unnecessary suppression",
|
||||
Justification = "Don't give code style advice in publically released code.")]
|
||||
[assembly: SuppressMessage("Style", "IDE1006:Naming Styles",
|
||||
Justification = "Don't give code style advice in publically released code.")]
|
||||
|
||||
[assembly: SuppressMessage("Correctness", "UNT0005:Suspicious Time.deltaTime usage",
|
||||
Justification = "Time.deltaTime is not suspicious in FixedUpdate, it has the same value as Time.fixedDeltaTime")]
|
||||
[assembly: SuppressMessage("Correctness", "UNT0029:Pattern matching with null on Unity objects",
|
||||
Justification = "Use a regular equality check if handling destroyed objects is necessary")]
|
||||
|
||||
[assembly: SuppressMessage("Code Quality", "CS0649:Field is never assigned to, and will always have its default value",
|
||||
Justification = "Using the [SerializeField] attribute on a private field means Unity will set it from serialized data.")]
|
||||
|
||||
[assembly: SuppressMessage("Microsoft.Design", "CA1001:TypesThatOwnDisposableFieldsShouldBeDisposable",
|
||||
Justification = "Having a field doesn't mean you are responsible for creating and destroying it.")]
|
||||
[assembly: SuppressMessage("Microsoft.Design", "CA1009:DeclareEventHandlersCorrectly",
|
||||
Justification = "Not all events need to care about the sender.")]
|
||||
[assembly: SuppressMessage("Microsoft.Design", "CA1033:InterfaceMethodsShouldBeCallableByChildTypes",
|
||||
Justification = "No need to pollute the member list of implementing types.")]
|
||||
[assembly: SuppressMessage("Microsoft.Design", "CA1063:ImplementIDisposableCorrectly",
|
||||
Justification = "No need to pollute the member list of implementing types.")]
|
||||
[assembly: SuppressMessage("Microsoft.Usage", "CA2235:MarkAllNonSerializableFields",
|
||||
Justification = "UnityEngine.Object is serializable by Unity.")]
|
||||
|
||||
#endif
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 94a6e52abdbcf8345be407d6740230a2
|
||||
labels:
|
||||
- FSM
|
||||
- FiniteStateMachine
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,52 @@
|
||||
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2024 Kybernetik //
|
||||
|
||||
using System;
|
||||
|
||||
namespace Animancer.FSM
|
||||
{
|
||||
/// <summary>An <see cref="IState"/> that uses delegates to define its behaviour.</summary>
|
||||
/// <remarks>
|
||||
/// <strong>Documentation:</strong>
|
||||
/// <see href="https://kybernetik.com.au/animancer/docs/manual/fsm/state-types">
|
||||
/// State Types</see>
|
||||
/// </remarks>
|
||||
/// https://kybernetik.com.au/animancer/api/Animancer.FSM/DelegateState
|
||||
///
|
||||
public class DelegateState : IState
|
||||
{
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Determines whether this state can be entered. Null is treated as returning true.</summary>
|
||||
public Func<bool> canEnter;
|
||||
|
||||
/// <summary>[<see cref="IState"/>] Calls <see cref="canEnter"/> to determine whether this state can be entered.</summary>
|
||||
public virtual bool CanEnterState => canEnter == null || canEnter();
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Determines whether this state can be exited. Null is treated as returning true.</summary>
|
||||
public Func<bool> canExit;
|
||||
|
||||
/// <summary>[<see cref="IState"/>] Calls <see cref="canExit"/> to determine whether this state can be exited.</summary>
|
||||
public virtual bool CanExitState => canExit == null || canExit();
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Called when this state is entered.</summary>
|
||||
public Action onEnter;
|
||||
|
||||
/// <summary>[<see cref="IState"/>] Calls <see cref="onEnter"/> when this state is entered.</summary>
|
||||
public virtual void OnEnterState() => onEnter?.Invoke();
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Called when this state is exited.</summary>
|
||||
public Action onExit;
|
||||
|
||||
/// <summary>[<see cref="IState"/>] Calls <see cref="onExit"/> when this state is exited.</summary>
|
||||
public virtual void OnExitState() => onExit?.Invoke();
|
||||
|
||||
/************************************************************************************************************************/
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 7a0b6fcf1d3471d4aa75f4c39f3c1c1e
|
||||
labels:
|
||||
- FSM
|
||||
- FiniteStateMachine
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,427 @@
|
||||
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2024 Kybernetik //
|
||||
|
||||
using System;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Animancer.FSM
|
||||
{
|
||||
/// <summary>A state that can be used in a <see cref="StateMachine{TState}"/>.</summary>
|
||||
/// <remarks>
|
||||
/// The <see cref="StateExtensions"/> class contains various extension methods for this interface.
|
||||
/// <para></para>
|
||||
/// <strong>Documentation:</strong>
|
||||
/// <see href="https://kybernetik.com.au/animancer/docs/manual/fsm">
|
||||
/// Finite State Machines</see>
|
||||
/// </remarks>
|
||||
/// https://kybernetik.com.au/animancer/api/Animancer.FSM/IState
|
||||
///
|
||||
public interface IState
|
||||
{
|
||||
/// <summary>Can this state be entered?</summary>
|
||||
/// <remarks>
|
||||
/// Checked by <see cref="StateMachine{TState}.CanSetState"/>, <see cref="StateMachine{TState}.TrySetState"/>
|
||||
/// and <see cref="StateMachine{TState}.TryResetState"/>.
|
||||
/// <para></para>
|
||||
/// Not checked by <see cref="StateMachine{TState}.ForceSetState"/>.
|
||||
/// </remarks>
|
||||
bool CanEnterState { get; }
|
||||
|
||||
/// <summary>Can this state be exited?</summary>
|
||||
/// <remarks>
|
||||
/// Checked by <see cref="StateMachine{TState}.CanSetState"/>, <see cref="StateMachine{TState}.TrySetState"/>
|
||||
/// and <see cref="StateMachine{TState}.TryResetState"/>.
|
||||
/// <para></para>
|
||||
/// Not checked by <see cref="StateMachine{TState}.ForceSetState"/>.
|
||||
/// </remarks>
|
||||
bool CanExitState { get; }
|
||||
|
||||
/// <summary>Called when this state is entered.</summary>
|
||||
/// <remarks>
|
||||
/// Called by <see cref="StateMachine{TState}.TrySetState"/>, <see cref="StateMachine{TState}.TryResetState"/>
|
||||
/// and <see cref="StateMachine{TState}.ForceSetState"/>.
|
||||
/// </remarks>
|
||||
void OnEnterState();
|
||||
|
||||
/// <summary>Called when this state is exited.</summary>
|
||||
/// <remarks>
|
||||
/// Called by <see cref="StateMachine{TState}.TrySetState"/>, <see cref="StateMachine{TState}.TryResetState"/>
|
||||
/// and <see cref="StateMachine{TState}.ForceSetState"/>.
|
||||
/// </remarks>
|
||||
void OnExitState();
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>An <see cref="IState"/> that knows which <see cref="StateMachine{TState}"/> it is used in.</summary>
|
||||
/// <remarks>
|
||||
/// The <see cref="StateExtensions"/> class contains various extension methods for this interface.
|
||||
/// <para></para>
|
||||
/// <strong>Documentation:</strong>
|
||||
/// <see href="https://kybernetik.com.au/animancer/docs/manual/fsm/state-types#owned-states">
|
||||
/// Owned States</see>
|
||||
/// </remarks>
|
||||
/// https://kybernetik.com.au/animancer/api/Animancer.FSM/IOwnedState_1
|
||||
public interface IOwnedState<TState> : IState
|
||||
where TState : class, IState
|
||||
{
|
||||
/// <summary>The <see cref="StateMachine{TState}"/> that this state is used in.</summary>
|
||||
StateMachine<TState> OwnerStateMachine { get; }
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>An empty <see cref="IState"/> that implements all the required methods as <c>virtual</c>.</summary>
|
||||
/// <remarks>
|
||||
/// <strong>Documentation:</strong>
|
||||
/// <see href="https://kybernetik.com.au/animancer/docs/manual/fsm/state-types">
|
||||
/// State Types</see>
|
||||
/// </remarks>
|
||||
/// https://kybernetik.com.au/animancer/api/Animancer.FSM/State
|
||||
///
|
||||
public abstract class State : IState
|
||||
{
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary><see cref="IState.CanEnterState"/></summary>
|
||||
/// <remarks>Returns true unless overridden.</remarks>
|
||||
public virtual bool CanEnterState => true;
|
||||
|
||||
/// <summary><see cref="IState.CanExitState"/></summary>
|
||||
/// <remarks>Returns true unless overridden.</remarks>
|
||||
public virtual bool CanExitState => true;
|
||||
|
||||
/// <summary><see cref="IState.OnEnterState"/></summary>
|
||||
public virtual void OnEnterState() { }
|
||||
|
||||
/// <summary><see cref="IState.OnExitState"/></summary>
|
||||
public virtual void OnExitState() { }
|
||||
|
||||
/************************************************************************************************************************/
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Various extension methods for <see cref="IState"/> and <see cref="IOwnedState{TState}"/>.</summary>
|
||||
///
|
||||
/// <remarks>
|
||||
/// <strong>Documentation:</strong>
|
||||
/// <see href="https://kybernetik.com.au/animancer/docs/manual/fsm">
|
||||
/// Finite State Machines</see>
|
||||
/// </remarks>
|
||||
///
|
||||
/// <example><code>
|
||||
/// public class Character : MonoBehaviour
|
||||
/// {
|
||||
/// public StateMachine<CharacterState> StateMachine { get; private set; }
|
||||
/// }
|
||||
///
|
||||
/// public class CharacterState : StateBehaviour, IOwnedState<CharacterState>
|
||||
/// {
|
||||
/// [SerializeField]
|
||||
/// private Character _Character;
|
||||
/// public Character Character => _Character;
|
||||
///
|
||||
/// public StateMachine<CharacterState> OwnerStateMachine => _Character.StateMachine;
|
||||
/// }
|
||||
///
|
||||
/// public class CharacterBrain : MonoBehaviour
|
||||
/// {
|
||||
/// [SerializeField] private Character _Character;
|
||||
/// [SerializeField] private CharacterState _Jump;
|
||||
///
|
||||
/// private void Update()
|
||||
/// {
|
||||
/// if (Input.GetKeyDown(KeyCode.Space))
|
||||
/// {
|
||||
/// // Normally you would need to refer to both the state machine and the state:
|
||||
/// _Character.StateMachine.TrySetState(_Jump);
|
||||
///
|
||||
/// // But since CharacterState implements IOwnedState you can use these extension methods:
|
||||
/// _Jump.TryEnterState();
|
||||
/// }
|
||||
/// }
|
||||
/// }
|
||||
/// </code>
|
||||
/// <h2>Inherited Types</h2>
|
||||
/// Unfortunately, if the field type is not the same as the <c>T</c> in the <c>IOwnedState<T></c>
|
||||
/// implementation then attempting to use these extension methods without specifying the generic argument will
|
||||
/// give the following error:
|
||||
/// <para></para>
|
||||
/// <em>The type 'StateType' cannot be used as type parameter 'TState' in the generic type or method
|
||||
/// 'StateExtensions.TryEnterState<TState>(TState)'. There is no implicit reference conversion from
|
||||
/// 'StateType' to 'Animancer.FSM.IOwnedState<StateType>'.</em>
|
||||
/// <para></para>
|
||||
/// For example, you might want to access members of a derived state class like this <c>SetTarget</c> method:
|
||||
/// <para></para><code>
|
||||
/// public class AttackState : CharacterState
|
||||
/// {
|
||||
/// public void SetTarget(Transform target) { }
|
||||
/// }
|
||||
///
|
||||
/// public class CharacterBrain : MonoBehaviour
|
||||
/// {
|
||||
/// [SerializeField] private AttackState _Attack;
|
||||
///
|
||||
/// private void Update()
|
||||
/// {
|
||||
/// if (Input.GetMouseButtonDown(0))
|
||||
/// {
|
||||
/// _Attack.SetTarget(...)
|
||||
/// // Can't do _Attack.TryEnterState();
|
||||
/// _Attack.TryEnterState<CharacterState>();
|
||||
/// }
|
||||
/// }
|
||||
/// }
|
||||
/// </code>
|
||||
/// Unlike the <c>_Jump</c> example, the <c>_Attack</c> field is an <c>AttackState</c> rather than the base
|
||||
/// <c>CharacterState</c> so we can call <c>_Attack.SetTarget(...)</c> but that causes problems with these extension
|
||||
/// methods.
|
||||
/// <para></para>
|
||||
/// Calling the method without specifying its generic argument automatically uses the variable's type as the
|
||||
/// argument so both of the following calls do the same thing:
|
||||
/// <para></para><code>
|
||||
/// _Attack.TryEnterState();
|
||||
/// _Attack.TryEnterState<AttackState>();
|
||||
/// </code>
|
||||
/// The problem is that <c>AttackState</c> inherits the implementation of <c>IOwnedState</c> from the base
|
||||
/// <c>CharacterState</c> class. But since that implementation is <c>IOwnedState<CharacterState></c>, rather
|
||||
/// than <c>IOwnedState<AttackState></c> that means <c>TryEnterState<AttackState></c> does not satisfy
|
||||
/// that method's generic constraints: <c>where TState : class, IOwnedState<TState></c>
|
||||
/// <para></para>
|
||||
/// That is why you simply need to specify the base class which implements <c>IOwnedState</c> as the generic
|
||||
/// argument to prevent it from inferring the wrong type:
|
||||
/// <para></para><code>
|
||||
/// _Attack.TryEnterState<CharacterState>();
|
||||
/// </code></example>
|
||||
/// https://kybernetik.com.au/animancer/api/Animancer.FSM/StateExtensions
|
||||
[HelpURL(APIDocumentationURL + nameof(StateExtensions))]
|
||||
public static class StateExtensions
|
||||
{
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>The URL of the API documentation for the <see cref="FSM"/> system.</summary>
|
||||
public const string APIDocumentationURL = "https://kybernetik.com.au/animancer/api/Animancer.FSM/";
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>[Animancer Extension] Returns the <see cref="StateChange{TState}.PreviousState"/>.</summary>
|
||||
public static TState GetPreviousState<TState>(this TState state)
|
||||
where TState : class, IState
|
||||
=> StateChange<TState>.PreviousState;
|
||||
|
||||
/// <summary>[Animancer Extension] Returns the <see cref="StateChange{TState}.NextState"/>.</summary>
|
||||
public static TState GetNextState<TState>(this TState state)
|
||||
where TState : class, IState
|
||||
=> StateChange<TState>.NextState;
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>[Animancer Extension]
|
||||
/// Checks if the specified `state` is the <see cref="StateMachine{TState}.CurrentState"/> in its
|
||||
/// <see cref="IOwnedState{TState}.OwnerStateMachine"/>.
|
||||
/// </summary>
|
||||
public static bool IsCurrentState<TState>(this TState state)
|
||||
where TState : class, IOwnedState<TState>
|
||||
=> state.OwnerStateMachine.CurrentState == state;
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>[Animancer Extension]
|
||||
/// Attempts to enter the specified `state` and returns true if successful.
|
||||
/// <para></para>
|
||||
/// This method returns true immediately if the specified `state` is already the
|
||||
/// <see cref="StateMachine{TState}.CurrentState"/>. To allow directly re-entering the same state, use
|
||||
/// <see cref="TryReEnterState"/> instead.
|
||||
/// </summary>
|
||||
public static bool TryEnterState<TState>(this TState state)
|
||||
where TState : class, IOwnedState<TState>
|
||||
=> state.OwnerStateMachine.TrySetState(state);
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>[Animancer Extension]
|
||||
/// Attempts to enter the specified `state` and returns true if successful.
|
||||
/// <para></para>
|
||||
/// This method does not check if the `state` is already the <see cref="StateMachine{TState}.CurrentState"/>.
|
||||
/// To do so, use <see cref="TryEnterState"/> instead.
|
||||
/// </summary>
|
||||
public static bool TryReEnterState<TState>(this TState state)
|
||||
where TState : class, IOwnedState<TState>
|
||||
=> state.OwnerStateMachine.TryResetState(state);
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>[Animancer Extension]
|
||||
/// Calls <see cref="IState.OnExitState"/> on the <see cref="StateMachine{TState}.CurrentState"/> then
|
||||
/// changes to the specified `state` and calls <see cref="IState.OnEnterState"/> on it.
|
||||
/// <para></para>
|
||||
/// This method does not check <see cref="IState.CanExitState"/> or
|
||||
/// <see cref="IState.CanEnterState"/>. To do that, you should use <see cref="TrySetState"/> instead.
|
||||
/// </summary>
|
||||
public static void ForceEnterState<TState>(this TState state)
|
||||
where TState : class, IOwnedState<TState>
|
||||
=> state.OwnerStateMachine.ForceSetState(state);
|
||||
|
||||
/************************************************************************************************************************/
|
||||
#pragma warning disable IDE0079 // Remove unnecessary suppression.
|
||||
#pragma warning disable CS1587 // XML comment is not placed on a valid language element.
|
||||
#pragma warning restore IDE0079 // Remove unnecessary suppression.
|
||||
// Copy this #region into a class which implements IOwnedState to give it the state extension methods as regular members.
|
||||
// This will avoid any issues with the compiler inferring the wrong generic argument in the extension methods.
|
||||
///************************************************************************************************************************/
|
||||
//#region State Extensions
|
||||
///************************************************************************************************************************/
|
||||
|
||||
///// <summary>
|
||||
///// Checks if this state is the <see cref="StateMachine{TState}.CurrentState"/> in its
|
||||
///// <see cref="IOwnedState{TState}.OwnerStateMachine"/>.
|
||||
///// </summary>
|
||||
//public bool IsCurrentState() => OwnerStateMachine.CurrentState == this;
|
||||
|
||||
///************************************************************************************************************************/
|
||||
|
||||
///// <summary>
|
||||
///// Calls <see cref="StateMachine{TState}.TrySetState(TState)"/> on the
|
||||
///// <see cref="IOwnedState{TState}.OwnerStateMachine"/>.
|
||||
///// </summary>
|
||||
//public bool TryEnterState() => OwnerStateMachine.TrySetState(this);
|
||||
|
||||
///************************************************************************************************************************/
|
||||
|
||||
///// <summary>
|
||||
///// Calls <see cref="StateMachine{TState}.TryResetState(TState)"/> on the
|
||||
///// <see cref="IOwnedState{TState}.OwnerStateMachine"/>.
|
||||
///// </summary>
|
||||
//public bool TryReEnterState() => OwnerStateMachine.TryResetState(this);
|
||||
|
||||
///************************************************************************************************************************/
|
||||
|
||||
///// <summary>
|
||||
///// Calls <see cref="StateMachine{TState}.ForceSetState(TState)"/> on the
|
||||
///// <see cref="IOwnedState{TState}.OwnerStateMachine"/>.
|
||||
///// </summary>
|
||||
//public void ForceEnterState() => OwnerStateMachine.ForceSetState(this);
|
||||
|
||||
///************************************************************************************************************************/
|
||||
//#endregion
|
||||
///************************************************************************************************************************/
|
||||
|
||||
#if UNITY_ASSERTIONS
|
||||
/// <summary>[Internal] Returns an error message explaining that the wrong type of change is being accessed.</summary>
|
||||
internal static string GetChangeError(Type stateType, Type machineType, string changeType = "State")
|
||||
{
|
||||
Type previousType = null;
|
||||
Type baseStateType = null;
|
||||
System.Collections.Generic.HashSet<Type> activeChangeTypes = null;
|
||||
|
||||
var stackTrace = new System.Diagnostics.StackTrace(1, false).GetFrames();
|
||||
for (int i = 0; i < stackTrace.Length; i++)
|
||||
{
|
||||
var type = stackTrace[i].GetMethod().DeclaringType;
|
||||
if (type != previousType &&
|
||||
type.IsGenericType &&
|
||||
type.GetGenericTypeDefinition() == machineType)
|
||||
{
|
||||
var argument = type.GetGenericArguments()[0];
|
||||
if (argument.IsAssignableFrom(stateType))
|
||||
{
|
||||
baseStateType = argument;
|
||||
break;
|
||||
}
|
||||
else
|
||||
{
|
||||
activeChangeTypes ??= new();
|
||||
|
||||
if (!activeChangeTypes.Contains(argument))
|
||||
activeChangeTypes.Add(argument);
|
||||
}
|
||||
}
|
||||
|
||||
previousType = type;
|
||||
}
|
||||
|
||||
var text = new System.Text.StringBuilder()
|
||||
.Append("Attempted to access ")
|
||||
.Append(changeType)
|
||||
.Append("Change<")
|
||||
.Append(stateType.FullName)
|
||||
.Append($"> but no {nameof(StateMachine<IState>)} of that type is currently changing its ")
|
||||
.Append(changeType)
|
||||
.AppendLine(".");
|
||||
|
||||
if (baseStateType != null)
|
||||
{
|
||||
text.Append(" - ")
|
||||
.Append(changeType)
|
||||
.Append(" changes must be accessed using the base ")
|
||||
.Append(changeType)
|
||||
.Append(" type, which is ")
|
||||
.Append(changeType)
|
||||
.Append("Change<")
|
||||
.Append(baseStateType.FullName)
|
||||
.AppendLine("> in this case.");
|
||||
|
||||
var caller = stackTrace[1].GetMethod();
|
||||
if (caller.DeclaringType == typeof(StateExtensions))
|
||||
{
|
||||
var propertyName = stackTrace[0].GetMethod().Name;
|
||||
propertyName = propertyName[4..];// Remove the "get_".
|
||||
|
||||
text.Append(" - This may be caused by the compiler incorrectly inferring the generic argument of the Get")
|
||||
.Append(propertyName)
|
||||
.Append(" method, in which case it must be manually specified like this: state.Get")
|
||||
.Append(propertyName)
|
||||
.Append('<')
|
||||
.Append(baseStateType.FullName)
|
||||
.AppendLine(">()");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (activeChangeTypes == null)
|
||||
{
|
||||
text.Append(" - No other ")
|
||||
.Append(changeType)
|
||||
.AppendLine(" changes are currently occurring either.");
|
||||
}
|
||||
else
|
||||
{
|
||||
if (activeChangeTypes.Count == 1)
|
||||
{
|
||||
text.Append(" - There is 1 ")
|
||||
.Append(changeType)
|
||||
.AppendLine(" change currently occurring:");
|
||||
}
|
||||
else
|
||||
{
|
||||
text.Append(" - There are ")
|
||||
.Append(activeChangeTypes.Count)
|
||||
.Append(' ')
|
||||
.Append(changeType)
|
||||
.AppendLine(" changes currently occurring:");
|
||||
}
|
||||
|
||||
foreach (var type in activeChangeTypes)
|
||||
{
|
||||
text.Append(" - ")
|
||||
.AppendLine(type.FullName);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
text.Append(" - ")
|
||||
.Append(changeType)
|
||||
.Append("Change<")
|
||||
.Append(stateType.FullName)
|
||||
.AppendLine($">.{nameof(StateChange<IState>.IsActive)} can be used to check if a change of that type is currently occurring.")
|
||||
.AppendLine(" - See the documentation for more information: " +
|
||||
"https://kybernetik.com.au/animancer/docs/manual/fsm/changing-states");
|
||||
|
||||
return text.ToString();
|
||||
}
|
||||
#endif
|
||||
|
||||
/************************************************************************************************************************/
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 44bb48284153d6e498f900eb062fc584
|
||||
labels:
|
||||
- FSM
|
||||
- FiniteStateMachine
|
||||
- Interface
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,129 @@
|
||||
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2024 Kybernetik //
|
||||
|
||||
using System;
|
||||
|
||||
namespace Animancer.FSM
|
||||
{
|
||||
/// <summary>A static access point for the details of a key change in a <see cref="StateMachine{TKey, TState}"/>.</summary>
|
||||
/// <remarks>
|
||||
/// This system is thread-safe.
|
||||
/// <para></para>
|
||||
/// <strong>Documentation:</strong>
|
||||
/// <see href="https://kybernetik.com.au/animancer/docs/manual/fsm/changing-states">
|
||||
/// Changing States</see>
|
||||
/// </remarks>
|
||||
/// https://kybernetik.com.au/animancer/api/Animancer.FSM/KeyChange_1
|
||||
///
|
||||
public struct KeyChange<TKey> : IDisposable
|
||||
{
|
||||
/************************************************************************************************************************/
|
||||
|
||||
[ThreadStatic]
|
||||
private static KeyChange<TKey> _Current;
|
||||
|
||||
private IKeyedStateMachine<TKey> _StateMachine;
|
||||
private TKey _PreviousKey;
|
||||
private TKey _NextKey;
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Is a <see cref="KeyChange{TKey}"/> of this type currently occurring?</summary>
|
||||
public static bool IsActive => _Current._StateMachine != null;
|
||||
|
||||
/// <summary>The <see cref="KeyChange{TKey}"/> in which the current change is occurring.</summary>
|
||||
/// <remarks>This will be null if no change is currently occurring.</remarks>
|
||||
public static IKeyedStateMachine<TKey> StateMachine => _Current._StateMachine;
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>The key being changed from.</summary>
|
||||
/// <exception cref="InvalidOperationException">[Assert-Only]
|
||||
/// <see cref="IsActive"/> is false so this property is likely being accessed on the wrong generic type.
|
||||
/// </exception>
|
||||
public static TKey PreviousKey
|
||||
{
|
||||
get
|
||||
{
|
||||
#if UNITY_ASSERTIONS
|
||||
if (!IsActive)
|
||||
throw new InvalidOperationException(StateExtensions.GetChangeError(typeof(TKey), typeof(StateMachine<,>), "Key"));
|
||||
#endif
|
||||
return _Current._PreviousKey;
|
||||
}
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>The key being changed into.</summary>
|
||||
/// <exception cref="InvalidOperationException">[Assert-Only]
|
||||
/// <see cref="IsActive"/> is false so this property is likely being accessed on the wrong generic type.
|
||||
/// </exception>
|
||||
public static TKey NextKey
|
||||
{
|
||||
get
|
||||
{
|
||||
#if UNITY_ASSERTIONS
|
||||
if (!IsActive)
|
||||
throw new InvalidOperationException(StateExtensions.GetChangeError(typeof(TKey), typeof(StateMachine<,>), "Key"));
|
||||
#endif
|
||||
return _Current._NextKey;
|
||||
}
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>[Internal]
|
||||
/// Assigns the parameters as the details of the currently active change and creates a new
|
||||
/// <see cref="KeyChange{TKey}"/> containing the details of the previously active change so that disposing
|
||||
/// it will re-assign those previous details to be current again in case of recursive state changes.
|
||||
/// </summary>
|
||||
///
|
||||
/// <remarks>
|
||||
/// <strong>Example:</strong><code>
|
||||
/// using (new KeyChange<TState>(previousKey, nextKey))
|
||||
/// {
|
||||
/// // Do the actual key change.
|
||||
/// }
|
||||
/// </code></remarks>
|
||||
///
|
||||
internal KeyChange(IKeyedStateMachine<TKey> stateMachine, TKey previousKey, TKey nextKey)
|
||||
{
|
||||
this = _Current;
|
||||
|
||||
_Current._StateMachine = stateMachine;
|
||||
_Current._PreviousKey = previousKey;
|
||||
_Current._NextKey = nextKey;
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>[<see cref="IDisposable"/>]
|
||||
/// Re-assigns the values of this change (which were the previous values from when it was created) to be the
|
||||
/// currently active change. See the constructor for recommended usage.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Usually this will be returning to default values (nulls), but if one state change causes another then the
|
||||
/// second one ending will return to the first which will then return to the defaults.
|
||||
/// </remarks>
|
||||
public readonly void Dispose()
|
||||
{
|
||||
_Current = this;
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Returns a string describing the contents of this <see cref="KeyChange{TKey}"/>.</summary>
|
||||
public override readonly string ToString() => IsActive
|
||||
? $"{nameof(KeyChange<TKey>)}<{typeof(TKey).FullName}" +
|
||||
$">({nameof(PreviousKey)}={PreviousKey}" +
|
||||
$", {nameof(NextKey)}={NextKey})"
|
||||
: $"{nameof(KeyChange<TKey>)}<{typeof(TKey).FullName}(Not Currently Active)";
|
||||
|
||||
/// <summary>Returns a string describing the contents of the current <see cref="KeyChange{TKey}"/>.</summary>
|
||||
public static string CurrentToString()
|
||||
=> _Current.ToString();
|
||||
|
||||
/************************************************************************************************************************/
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
fileFormatVersion: 2
|
||||
guid: ba135b8e83a177043a5e505a7e9832e7
|
||||
labels:
|
||||
- FSM
|
||||
- FiniteStateMachine
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"name": "Kybernetik.Animancer.FSM",
|
||||
"rootNamespace": "",
|
||||
"references": [],
|
||||
"includePlatforms": [],
|
||||
"excludePlatforms": [],
|
||||
"allowUnsafeCode": false,
|
||||
"overrideReferences": false,
|
||||
"precompiledReferences": [],
|
||||
"autoReferenced": true,
|
||||
"defineConstraints": [],
|
||||
"versionDefines": [
|
||||
{
|
||||
"name": "com.unity.modules.imgui",
|
||||
"expression": "",
|
||||
"define": "UNITY_IMGUI"
|
||||
}
|
||||
],
|
||||
"noEngineReferences": false
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
fileFormatVersion: 2
|
||||
guid: b2665e54e4314ae429d34fdeafc9f3e0
|
||||
labels:
|
||||
- FSM
|
||||
- FiniteStateMachine
|
||||
AssemblyDefinitionImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,92 @@
|
||||
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2024 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()
|
||||
{
|
||||
#if UNITY_ASSERTIONS
|
||||
if (enabled)
|
||||
Debug.LogError(
|
||||
$"{nameof(StateBehaviour)} was already enabled before {nameof(OnEnterState)}: {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.
|
||||
else
|
||||
UnityEditorInternal.InternalEditorUtility.RepaintAllViews();
|
||||
#endif
|
||||
|
||||
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;
|
||||
|
||||
#if UNITY_ASSERTIONS
|
||||
if (!enabled)
|
||||
Debug.LogError(
|
||||
$"{nameof(StateBehaviour)} was already disabled before {nameof(OnExitState)}: {this}",
|
||||
this);
|
||||
#endif
|
||||
|
||||
enabled = false;
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
#if UNITY_EDITOR
|
||||
/// <summary>[Editor-Only] States start disabled and only the current state gets enabled at runtime.</summary>
|
||||
/// <remarks>Called in Edit Mode whenever this script is loaded or a value is changed in the Inspector.</remarks>
|
||||
protected virtual void OnValidate()
|
||||
{
|
||||
if (UnityEditor.EditorApplication.isPlayingOrWillChangePlaymode)
|
||||
return;
|
||||
|
||||
enabled = false;
|
||||
}
|
||||
#endif
|
||||
|
||||
/************************************************************************************************************************/
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 7d0ae395395b5d34da55dead806d6a08
|
||||
labels:
|
||||
- Component
|
||||
- FSM
|
||||
- FiniteStateMachine
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,131 @@
|
||||
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2024 Kybernetik //
|
||||
|
||||
using System;
|
||||
|
||||
namespace Animancer.FSM
|
||||
{
|
||||
/// <summary>A static access point for the details of a state change in a <see cref="StateMachine{TState}"/>.</summary>
|
||||
/// <remarks>
|
||||
/// This system is thread-safe.
|
||||
/// <para></para>
|
||||
/// <strong>Documentation:</strong>
|
||||
/// <see href="https://kybernetik.com.au/animancer/docs/manual/fsm/changing-states">
|
||||
/// Changing States</see>
|
||||
/// </remarks>
|
||||
/// https://kybernetik.com.au/animancer/api/Animancer.FSM/StateChange_1
|
||||
///
|
||||
public struct StateChange<TState> : IDisposable
|
||||
where TState : class, IState
|
||||
{
|
||||
/************************************************************************************************************************/
|
||||
|
||||
[ThreadStatic]
|
||||
private static StateChange<TState> _Current;
|
||||
|
||||
private StateMachine<TState> _StateMachine;
|
||||
private TState _PreviousState;
|
||||
private TState _NextState;
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Is a <see cref="StateChange{TState}"/> of this type currently occurring?</summary>
|
||||
public static bool IsActive
|
||||
=> _Current._StateMachine != null;
|
||||
|
||||
/// <summary>The <see cref="StateMachine{TState}"/> in which the current change is occurring.</summary>
|
||||
/// <remarks>This will be null if no change is currently occurring.</remarks>
|
||||
public static StateMachine<TState> StateMachine
|
||||
=> _Current._StateMachine;
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>The state currently being changed from.</summary>
|
||||
/// <exception cref="InvalidOperationException">[Assert-Only]
|
||||
/// <see cref="IsActive"/> is false so this property is likely being accessed on the wrong generic type.
|
||||
/// </exception>
|
||||
public static TState PreviousState
|
||||
{
|
||||
get
|
||||
{
|
||||
#if UNITY_ASSERTIONS
|
||||
if (!IsActive)
|
||||
throw new InvalidOperationException(
|
||||
StateExtensions.GetChangeError(typeof(TState), typeof(StateMachine<>)));
|
||||
#endif
|
||||
return _Current._PreviousState;
|
||||
}
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>The state being changed into.</summary>
|
||||
/// <exception cref="InvalidOperationException">[Assert-Only]
|
||||
/// <see cref="IsActive"/> is false so this property is likely being accessed on the wrong generic type.
|
||||
/// </exception>
|
||||
public static TState NextState
|
||||
{
|
||||
get
|
||||
{
|
||||
#if UNITY_ASSERTIONS
|
||||
if (!IsActive)
|
||||
throw new InvalidOperationException(
|
||||
StateExtensions.GetChangeError(typeof(TState), typeof(StateMachine<>)));
|
||||
#endif
|
||||
return _Current._NextState;
|
||||
}
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>[Internal]
|
||||
/// Assigns the parameters as the details of the currently active change and creates a new
|
||||
/// <see cref="StateChange{TState}"/> containing the details of the previously active change so that disposing
|
||||
/// it will re-assign those previous details to be current again in case of recursive state changes.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <strong>Example:</strong><code>
|
||||
/// using (new StateChange<TState>(stateMachine, previousState, nextState))
|
||||
/// {
|
||||
/// // Do the actual state change.
|
||||
/// }
|
||||
/// </code></remarks>
|
||||
internal StateChange(StateMachine<TState> stateMachine, TState previousState, TState nextState)
|
||||
{
|
||||
this = _Current;
|
||||
|
||||
_Current._StateMachine = stateMachine;
|
||||
_Current._PreviousState = previousState;
|
||||
_Current._NextState = nextState;
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>[<see cref="IDisposable"/>]
|
||||
/// Re-assigns the values of this change (which were the previous values from when it was created)
|
||||
/// to be the currently active change. See the constructor for recommended usage.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Usually this will be returning to default values (nulls), but if one state change causes another
|
||||
/// then the second one ending will return to the first which will then return to the defaults.
|
||||
/// </remarks>
|
||||
public readonly void Dispose()
|
||||
=> _Current = this;
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Returns a string describing the contents of this <see cref="StateChange{TState}"/>.</summary>
|
||||
public override readonly string ToString()
|
||||
=> IsActive
|
||||
? $"{nameof(StateChange<TState>)}<{typeof(TState).FullName}" +
|
||||
$">({nameof(PreviousState)}='{_PreviousState}'" +
|
||||
$", {nameof(NextState)}='{_NextState}')"
|
||||
: $"{nameof(StateChange<TState>)}<{typeof(TState).FullName}(Not Currently Active)";
|
||||
|
||||
/// <summary>Returns a string describing the contents of the current <see cref="StateChange{TState}"/>.</summary>
|
||||
public static string CurrentToString()
|
||||
=> _Current.ToString();
|
||||
|
||||
/************************************************************************************************************************/
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
fileFormatVersion: 2
|
||||
guid: d96becb371c86e241b44dea56e55385a
|
||||
labels:
|
||||
- FSM
|
||||
- FiniteStateMachine
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,212 @@
|
||||
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2024 Kybernetik //
|
||||
|
||||
using System;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Animancer.FSM
|
||||
{
|
||||
public partial class StateMachine<TState>
|
||||
{
|
||||
/// <summary>
|
||||
/// A simple system that can <see cref="InputBuffer{TStateMachine}.Buffer"/> a state then try to enter it every
|
||||
/// time <see cref="InputBuffer{TStateMachine}.Update(float)"/> is called until the
|
||||
/// <see cref="InputBuffer{TStateMachine}.TimeOut"/> expires.
|
||||
/// </summary>
|
||||
///
|
||||
/// <remarks>
|
||||
/// <strong>Documentation:</strong>
|
||||
/// <see href="https://kybernetik.com.au/animancer/docs/manual/fsm/utilities#input-buffers">
|
||||
/// Input Buffers</see>
|
||||
/// <para></para>
|
||||
/// See <see cref="StateMachine{TState}.InputBuffer{TStateMachine}"/> for example usage.
|
||||
/// </remarks>
|
||||
///
|
||||
/// https://kybernetik.com.au/animancer/api/Animancer.FSM/InputBuffer
|
||||
///
|
||||
public class InputBuffer : InputBuffer<StateMachine<TState>>
|
||||
{
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Creates a new <see cref="InputBuffer"/>.</summary>
|
||||
public InputBuffer() { }
|
||||
|
||||
/// <summary>Creates a new <see cref="InputBuffer"/> for the specified `stateMachine`.</summary>
|
||||
public InputBuffer(StateMachine<TState> stateMachine) : base(stateMachine) { }
|
||||
|
||||
/************************************************************************************************************************/
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A simple system that can <see cref="Buffer"/> a state then try to enter it every time
|
||||
/// <see cref="Update(float)"/> is called until the <see cref="TimeOut"/> expires.
|
||||
/// </summary>
|
||||
///
|
||||
/// <remarks>
|
||||
/// <strong>Documentation:</strong>
|
||||
/// <see href="https://kybernetik.com.au/animancer/docs/manual/fsm/utilities#input-buffers">
|
||||
/// Input Buffers</see>
|
||||
/// <para></para>
|
||||
/// <strong>Example:</strong><code>
|
||||
/// public StateMachine<CharacterState> stateMachine;// Initialized elsewhere.
|
||||
///
|
||||
/// [SerializeField] private CharacterState _Attack;
|
||||
/// [SerializeField] private float _AttackInputTimeOut = 0.5f;
|
||||
///
|
||||
/// private StateMachine<CharacterState>.InputBuffer _InputBuffer;
|
||||
///
|
||||
/// private void Awake()
|
||||
/// {
|
||||
/// // Initialize the buffer.
|
||||
/// _InputBuffer = new StateMachine<CharacterState>.InputBuffer(stateMachine);
|
||||
/// }
|
||||
///
|
||||
/// private void Update()
|
||||
/// {
|
||||
/// // When input is detected, buffer the desired state.
|
||||
/// if (Input.GetButtonDown("Fire1"))// Left Click by default.
|
||||
/// {
|
||||
/// _InputBuffer.Buffer(_Attack, _AttackInputTimeOut);
|
||||
/// }
|
||||
///
|
||||
/// // At the end of the frame, Update the buffer so it tries to enter the buffered state.
|
||||
/// // After the time out, it will clear itself so Update does nothing until something else is buffered.
|
||||
/// _InputBuffer.Update();
|
||||
/// }
|
||||
/// </code></remarks>
|
||||
///
|
||||
/// https://kybernetik.com.au/animancer/api/Animancer.FSM/InputBuffer_1
|
||||
///
|
||||
public class InputBuffer<TStateMachine> where TStateMachine : StateMachine<TState>
|
||||
{
|
||||
/************************************************************************************************************************/
|
||||
|
||||
private TStateMachine _StateMachine;
|
||||
private Action _ForceDefaultState;
|
||||
|
||||
/// <summary>The <see cref="StateMachine{TState}"/> this buffer is feeding input to.</summary>
|
||||
public TStateMachine StateMachine
|
||||
{
|
||||
get => _StateMachine;
|
||||
set
|
||||
{
|
||||
if (_StateMachine is WithDefault withDefault)
|
||||
withDefault.ForceSetDefaultState = _ForceDefaultState;
|
||||
|
||||
_StateMachine = value;
|
||||
|
||||
TryRegisterForceSetDefaultState();
|
||||
Clear();
|
||||
}
|
||||
}
|
||||
|
||||
private void TryRegisterForceSetDefaultState()
|
||||
{
|
||||
if (_StateMachine is WithDefault withDefault)
|
||||
{
|
||||
_ForceDefaultState = withDefault.ForceSetDefaultState;
|
||||
withDefault.ForceSetDefaultState = TryEnterStateOrForceDefault;
|
||||
}
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>The <typeparamref name="TState"/> this buffer is currently attempting to enter.</summary>
|
||||
public TState State { get; set; }
|
||||
|
||||
/// <summary>The amount of time left before the <see cref="State"/> is cleared.</summary>
|
||||
public float TimeOut { get; set; }
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Is this buffer currently trying to enter a <see cref="State"/>?</summary>
|
||||
public bool IsActive => State != null;
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Creates a new <see cref="InputBuffer{TStateMachine}"/>.</summary>
|
||||
public InputBuffer() { }
|
||||
|
||||
/// <summary>Creates a new <see cref="InputBuffer{TStateMachine}"/> for the specified `stateMachine`.</summary>
|
||||
public InputBuffer(TStateMachine stateMachine)
|
||||
{
|
||||
_StateMachine = stateMachine;
|
||||
TryRegisterForceSetDefaultState();
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Sets the <see cref="State"/> and <see cref="TimeOut"/>.</summary>
|
||||
/// <remarks>Doesn't actually attempt to enter the state until <see cref="Update(float)"/> is called.</remarks>
|
||||
public void Buffer(TState state, float timeOut)
|
||||
{
|
||||
State = state;
|
||||
TimeOut = timeOut;
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Attempts to enter the <see cref="State"/> and returns true if successful.</summary>
|
||||
protected virtual bool TryEnterState()
|
||||
=> StateMachine.TryResetState(State);
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>
|
||||
/// Calls <see cref="TryEnterState"/>. If it fails, then <see cref="WithDefault.ForceSetDefaultState"/>.
|
||||
/// </summary>
|
||||
public void TryEnterStateOrForceDefault()
|
||||
{
|
||||
if (IsActive &&
|
||||
TryEnterState())
|
||||
return;
|
||||
|
||||
_ForceDefaultState();
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Calls <see cref="Update(float)"/> using <see cref="Time.deltaTime"/>.</summary>
|
||||
/// <remarks>This method should be called at the end of a frame after any calls to <see cref="Buffer"/>.</remarks>
|
||||
public bool Update()
|
||||
=> Update(Time.deltaTime);
|
||||
|
||||
/// <summary>
|
||||
/// Attempts to enter the <see cref="State"/> if there is one and returns true if successful. Otherwise the
|
||||
/// <see cref="TimeOut"/> is decreased by `deltaTime` and <see cref="Clear"/> is called if it reaches 0.
|
||||
/// </summary>
|
||||
/// <remarks>This method should be called at the end of a frame after any calls to <see cref="Buffer"/>.</remarks>
|
||||
public bool Update(float deltaTime)
|
||||
{
|
||||
if (IsActive)
|
||||
{
|
||||
if (TryEnterState())
|
||||
{
|
||||
Clear();
|
||||
return true;
|
||||
}
|
||||
else
|
||||
{
|
||||
TimeOut -= deltaTime;
|
||||
|
||||
if (TimeOut < 0)
|
||||
Clear();
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Clears this buffer so it stops trying to enter the <see cref="State"/>.</summary>
|
||||
public virtual void Clear()
|
||||
{
|
||||
State = null;
|
||||
TimeOut = default;
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 3f0140c1027c5254882ca6415a514880
|
||||
labels:
|
||||
- FSM
|
||||
- FiniteStateMachine
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,83 @@
|
||||
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2024 Kybernetik //
|
||||
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Animancer.FSM
|
||||
{
|
||||
/// <summary>An object with a <see cref="Priority"/>.</summary>
|
||||
/// <remarks>
|
||||
/// <strong>Documentation:</strong>
|
||||
/// <see href="https://kybernetik.com.au/animancer/docs/manual/fsm/utilities#state-selectors">
|
||||
/// State Selectors</see>
|
||||
/// </remarks>
|
||||
/// https://kybernetik.com.au/animancer/api/Animancer.FSM/IPrioritizable
|
||||
///
|
||||
public interface IPrioritizable : IState
|
||||
{
|
||||
float Priority { get; }
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
public partial class StateMachine<TState>
|
||||
{
|
||||
/// <summary>A prioritised list of potential states for a <see cref="StateMachine{TState}"/> to enter.</summary>
|
||||
///
|
||||
/// <remarks>
|
||||
/// <strong>Documentation:</strong>
|
||||
/// <see href="https://kybernetik.com.au/animancer/docs/manual/fsm#state-selectors">
|
||||
/// State Selectors</see>
|
||||
/// <para></para>
|
||||
/// <strong>Example:</strong><code>
|
||||
/// public StateMachine<CharacterState> stateMachine;
|
||||
/// public CharacterState run;
|
||||
/// public CharacterState idle;
|
||||
///
|
||||
/// private readonly StateMachine<CharacterState>.StateSelector
|
||||
/// Selector = new();
|
||||
///
|
||||
/// private void Awake()
|
||||
/// {
|
||||
/// Selector.Add(1, run);
|
||||
/// Selector.Add(0, idle);
|
||||
/// }
|
||||
///
|
||||
/// public void RunOrIdle()
|
||||
/// {
|
||||
/// stateMachine.TrySetState(Selector.Values);
|
||||
/// // The "run" state has the highest priority so this will enter it if "run.CanEnterState" returns true.
|
||||
/// // Otherwise if "idle.CanEnterState" returns true it will enter that state instead.
|
||||
/// // If neither allows the transition, nothing happens and "stateMachine.TrySetState" returns false.
|
||||
/// }
|
||||
/// </code></remarks>
|
||||
///
|
||||
/// https://kybernetik.com.au/animancer/api/Animancer.FSM/StateSelector
|
||||
///
|
||||
public class StateSelector : SortedList<float, TState>
|
||||
{
|
||||
public StateSelector() : base(ReverseComparer<float>.Instance) { }
|
||||
|
||||
/// <summary>Adds the `state` to this selector with its <see cref="IPrioritizable.Priority"/>.</summary>
|
||||
public void Add<TPrioritizable>(TPrioritizable state)
|
||||
where TPrioritizable : TState, IPrioritizable
|
||||
=> Add(state.Priority, state);
|
||||
}
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>An <see cref="IComparer{T}"/> which reverses the default comparison.</summary>
|
||||
/// https://kybernetik.com.au/animancer/api/Animancer.FSM/ReverseComparer_1
|
||||
public class ReverseComparer<T> : IComparer<T>
|
||||
{
|
||||
/// <summary>The singleton instance.</summary>
|
||||
public static readonly ReverseComparer<T> Instance = new();
|
||||
|
||||
/// <summary>No need to let users create other instances.</summary>
|
||||
private ReverseComparer() { }
|
||||
|
||||
/// <summary>Uses <see cref="Comparer{T}.Default"/> with the parameters swapped.</summary>
|
||||
public int Compare(T x, T y) => Comparer<T>.Default.Compare(y, x);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 0f882cb524ddbc8419c5beba63a1939f
|
||||
labels:
|
||||
- FSM
|
||||
- FiniteStateMachine
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,145 @@
|
||||
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2024 Kybernetik //
|
||||
|
||||
using System;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Animancer.FSM
|
||||
{
|
||||
/// https://kybernetik.com.au/animancer/api/Animancer.FSM/StateMachine_1
|
||||
partial class StateMachine<TState>
|
||||
{
|
||||
/// <summary>A <see cref="StateMachine{TState}"/> with a <see cref="DefaultState"/>.</summary>
|
||||
/// <remarks>
|
||||
/// See <see cref="InitializeAfterDeserialize"/> if using this class in a serialized field.
|
||||
/// <para></para>
|
||||
/// <strong>Documentation:</strong>
|
||||
/// <see href="https://kybernetik.com.au/animancer/docs/manual/fsm/changing-states#default-states">
|
||||
/// Default States</see>
|
||||
/// </remarks>
|
||||
/// https://kybernetik.com.au/animancer/api/Animancer.FSM/WithDefault
|
||||
///
|
||||
[Serializable]
|
||||
public class WithDefault : StateMachine<TState>
|
||||
{
|
||||
/************************************************************************************************************************/
|
||||
|
||||
[SerializeField]
|
||||
private TState _DefaultState;
|
||||
|
||||
/// <summary>The starting state and main state to return to when nothing else is active.</summary>
|
||||
/// <remarks>
|
||||
/// If the <see cref="CurrentState"/> is <c>null</c> when setting this value, it calls
|
||||
/// <see cref="ForceSetState(TState)"/> to enter the specified state immediately.
|
||||
/// <para></para>
|
||||
/// For a character, this would typically be their <em>Idle</em> state.
|
||||
/// </remarks>
|
||||
public TState DefaultState
|
||||
{
|
||||
get => _DefaultState;
|
||||
set
|
||||
{
|
||||
_DefaultState = value;
|
||||
if (_CurrentState == null && value != null)
|
||||
ForceSetState(value);
|
||||
}
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Calls <see cref="ForceSetState(TState)"/> with the <see cref="DefaultState"/>.</summary>
|
||||
/// <remarks>This delegate is cached to avoid allocating garbage when used in Animancer Events.</remarks>
|
||||
public Action ForceSetDefaultState;
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Creates a new <see cref="WithDefault"/>.</summary>
|
||||
public WithDefault()
|
||||
{
|
||||
// Silly C# doesn't allow instance delegates to be assigned using field initializers.
|
||||
ForceSetDefaultState = () => ForceSetState(_DefaultState);
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Creates a new <see cref="WithDefault"/> and sets the <see cref="DefaultState"/>.</summary>
|
||||
public WithDefault(TState defaultState)
|
||||
: this()
|
||||
{
|
||||
_DefaultState = defaultState;
|
||||
ForceSetState(defaultState);
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override void InitializeAfterDeserialize()
|
||||
{
|
||||
if (_CurrentState != null)
|
||||
{
|
||||
using (new StateChange<TState>(this, null, _CurrentState))
|
||||
_CurrentState.OnEnterState();
|
||||
}
|
||||
else if (_DefaultState != null)
|
||||
{
|
||||
using (new StateChange<TState>(this, null, CurrentState))
|
||||
{
|
||||
_CurrentState = _DefaultState;
|
||||
_CurrentState.OnEnterState();
|
||||
}
|
||||
}
|
||||
|
||||
// Don't call the base method.
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Attempts to enter the <see cref="DefaultState"/> and returns true if successful.</summary>
|
||||
/// <remarks>
|
||||
/// This method returns true immediately if the specified <see cref="DefaultState"/> is already the
|
||||
/// <see cref="CurrentState"/>. To allow directly re-entering the same state, use
|
||||
/// <see cref="TryResetDefaultState"/> instead.
|
||||
/// </remarks>
|
||||
public bool TrySetDefaultState() => TrySetState(DefaultState);
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Attempts to enter the <see cref="DefaultState"/> and returns true if successful.</summary>
|
||||
/// <remarks>
|
||||
/// This method does not check if the <see cref="DefaultState"/> is already the <see cref="CurrentState"/>.
|
||||
/// To do so, use <see cref="TrySetDefaultState"/> instead.
|
||||
/// </remarks>
|
||||
public bool TryResetDefaultState() => TryResetState(DefaultState);
|
||||
|
||||
/************************************************************************************************************************/
|
||||
#if UNITY_EDITOR && UNITY_IMGUI
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override int GUILineCount => 2;
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override void DoGUI(ref Rect area)
|
||||
{
|
||||
area.height = UnityEditor.EditorGUIUtility.singleLineHeight;
|
||||
|
||||
UnityEditor.EditorGUI.BeginChangeCheck();
|
||||
|
||||
var state = StateMachineUtilities.DoGenericField(area, "Default State", DefaultState);
|
||||
|
||||
if (UnityEditor.EditorGUI.EndChangeCheck())
|
||||
DefaultState = state;
|
||||
|
||||
StateMachineUtilities.NextVerticalArea(ref area);
|
||||
|
||||
base.DoGUI(ref area);
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
#endif
|
||||
/************************************************************************************************************************/
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
fileFormatVersion: 2
|
||||
guid: eb5d8db5c4119fd47a2f652836d193f1
|
||||
labels:
|
||||
- FSM
|
||||
- FiniteStateMachine
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,471 @@
|
||||
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2024 Kybernetik //
|
||||
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Animancer.FSM
|
||||
{
|
||||
/// <summary>A simple keyless Finite State Machine system.</summary>
|
||||
/// <remarks>
|
||||
/// <strong>Documentation:</strong>
|
||||
/// <see href="https://kybernetik.com.au/animancer/docs/manual/fsm">
|
||||
/// Finite State Machines</see>
|
||||
/// </remarks>
|
||||
/// https://kybernetik.com.au/animancer/api/Animancer.FSM/IStateMachine
|
||||
///
|
||||
public interface IStateMachine
|
||||
{
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>The currently active state.</summary>
|
||||
object CurrentState { get; }
|
||||
|
||||
/// <summary>The <see cref="StateChange{TState}.PreviousState"/>.</summary>
|
||||
object PreviousState { get; }
|
||||
|
||||
/// <summary>The <see cref="StateChange{TState}.NextState"/>.</summary>
|
||||
object NextState { get; }
|
||||
|
||||
/// <summary>Is it currently possible to enter the specified `state`?</summary>
|
||||
/// <remarks>
|
||||
/// This requires <see cref="IState.CanExitState"/> on the <see cref="CurrentState"/> and
|
||||
/// <see cref="IState.CanEnterState"/> on the specified `state` to both return true.
|
||||
/// </remarks>
|
||||
bool CanSetState(object state);
|
||||
|
||||
/// <summary>Returns the first of the `states` which can currently be entered.</summary>
|
||||
object CanSetState(IList states);
|
||||
|
||||
/// <summary>Attempts to enter the specified `state` and returns true if successful.</summary>
|
||||
/// <remarks>
|
||||
/// This method returns true immediately if the specified `state` is already the <see cref="CurrentState"/>.
|
||||
/// To allow directly re-entering the same state, use <see cref="TryResetState(object)"/> instead.
|
||||
/// </remarks>
|
||||
bool TrySetState(object state);
|
||||
|
||||
/// <summary>Attempts to enter any of the specified `states` and returns true if successful.</summary>
|
||||
/// <remarks>
|
||||
/// This method returns true and does nothing else if the <see cref="CurrentState"/> is in the list.
|
||||
/// To allow directly re-entering the same state, use <see cref="TryResetState(IList)"/> instead.
|
||||
/// <para></para>
|
||||
/// States are checked in ascending order (i.e. from <c>[0]</c> to <c>[states.Count - 1]</c>).
|
||||
/// </remarks>
|
||||
bool TrySetState(IList states);
|
||||
|
||||
/// <summary>Attempts to enter the specified `state` and returns true if successful.</summary>
|
||||
/// <remarks>
|
||||
/// This method does not check if the `state` is already the <see cref="CurrentState"/>. To do so, use
|
||||
/// <see cref="TrySetState(object)"/> instead.
|
||||
/// </remarks>
|
||||
bool TryResetState(object state);
|
||||
|
||||
/// <summary>Attempts to enter any of the specified `states` and returns true if successful.</summary>
|
||||
/// <remarks>
|
||||
/// This method does not check if the `state` is already the <see cref="CurrentState"/>. To do so, use
|
||||
/// <see cref="TrySetState(IList)"/> instead.
|
||||
/// <para></para>
|
||||
/// States are checked in ascending order (i.e. from <c>[0]</c> to <c>[states.Count - 1]</c>).
|
||||
/// </remarks>
|
||||
bool TryResetState(IList states);
|
||||
|
||||
/// <summary>
|
||||
/// Calls <see cref="IState.OnExitState"/> on the <see cref="CurrentState"/> then changes it to the
|
||||
/// specified `state` and calls <see cref="IState.OnEnterState"/> on it.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This method does not check <see cref="IState.CanExitState"/> or
|
||||
/// <see cref="IState.CanEnterState"/>. To do that, you should use <see cref="TrySetState"/> instead.
|
||||
/// </remarks>
|
||||
void ForceSetState(object state);
|
||||
|
||||
#if UNITY_ASSERTIONS
|
||||
/// <summary>[Assert-Only] Should the <see cref="CurrentState"/> be allowed to be set to null? Default is false.</summary>
|
||||
/// <remarks>Can be set by <see cref="SetAllowNullStates"/>.</remarks>
|
||||
bool AllowNullStates { get; }
|
||||
#endif
|
||||
|
||||
/// <summary>[Assert-Conditional] Sets <see cref="AllowNullStates"/>.</summary>
|
||||
void SetAllowNullStates(bool allow = true);
|
||||
|
||||
/************************************************************************************************************************/
|
||||
#if UNITY_EDITOR && UNITY_IMGUI
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>[Editor-Only] The number of standard size lines that <see cref="DoGUI"/> will use.</summary>
|
||||
int GUILineCount { get; }
|
||||
|
||||
/// <summary>[Editor-Only] Draws GUI fields to display the status of this state machine.</summary>
|
||||
void DoGUI();
|
||||
|
||||
/// <summary>[Editor-Only] Draws GUI fields to display the status of this state machine in the given `area`.</summary>
|
||||
void DoGUI(ref Rect area);
|
||||
|
||||
/************************************************************************************************************************/
|
||||
#endif
|
||||
/************************************************************************************************************************/
|
||||
}
|
||||
|
||||
/// <summary>A simple keyless Finite State Machine system.</summary>
|
||||
/// <remarks>
|
||||
/// This class doesn't keep track of any states other than the currently active one.
|
||||
/// See <see cref="StateMachine{TKey, TState}"/> for a system that allows
|
||||
/// states to be pre-registered and accessed using a separate key.
|
||||
/// <para></para>
|
||||
/// See <see cref="InitializeAfterDeserialize"/> if using this class in a serialized field.
|
||||
/// <para></para>
|
||||
/// <strong>Documentation:</strong>
|
||||
/// <see href="https://kybernetik.com.au/animancer/docs/manual/fsm">
|
||||
/// Finite State Machines</see>
|
||||
/// </remarks>
|
||||
/// https://kybernetik.com.au/animancer/api/Animancer.FSM/StateMachine_1
|
||||
///
|
||||
[HelpURL(StateExtensions.APIDocumentationURL + nameof(StateMachine<TState>) + "_1")]
|
||||
[Serializable]
|
||||
public partial class StateMachine<TState> : IStateMachine
|
||||
where TState : class, IState
|
||||
{
|
||||
/************************************************************************************************************************/
|
||||
|
||||
[SerializeField]
|
||||
private TState _CurrentState;
|
||||
|
||||
/// <summary>[<see cref="SerializeField"/>] The currently active state.</summary>
|
||||
public TState CurrentState => _CurrentState;
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>The <see cref="StateChange{TState}.PreviousState"/>.</summary>
|
||||
public TState PreviousState => StateChange<TState>.PreviousState;
|
||||
|
||||
/// <summary>The <see cref="StateChange{TState}.NextState"/>.</summary>
|
||||
public TState NextState => StateChange<TState>.NextState;
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Creates a new <see cref="StateMachine{TState}"/>, leaving the <see cref="CurrentState"/> null.</summary>
|
||||
public StateMachine() { }
|
||||
|
||||
/// <summary>Creates a new <see cref="StateMachine{TState}"/> and immediately enters the `state`.</summary>
|
||||
/// <remarks>This calls <see cref="IState.OnEnterState"/> but not <see cref="IState.CanEnterState"/>.</remarks>
|
||||
public StateMachine(TState state)
|
||||
{
|
||||
#if UNITY_ASSERTIONS
|
||||
if (state == null)// AllowNullStates won't be true yet since this is the constructor.
|
||||
throw new ArgumentNullException(nameof(state), NullNotAllowed);
|
||||
#endif
|
||||
|
||||
using (new StateChange<TState>(this, null, state))
|
||||
{
|
||||
_CurrentState = state;
|
||||
state.OnEnterState();
|
||||
}
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Call this after deserializing to properly initialize the <see cref="CurrentState"/>.</summary>
|
||||
/// <remarks>
|
||||
/// Unfortunately, <see cref="ISerializationCallbackReceiver"/> can't be used to automate this
|
||||
/// because many Unity functions aren't available during serialization such as getting or setting a
|
||||
/// <see cref="Behaviour.enabled"/> like <see cref="StateBehaviour.OnEnterState"/> does.
|
||||
/// <para></para>
|
||||
/// <strong>Example:</strong><code>
|
||||
/// public class MyComponent : MonoBehaviour
|
||||
/// {
|
||||
/// [SerializeField]
|
||||
/// private StateMachine<MyState> _StateMachine;
|
||||
///
|
||||
/// protected virtual void Awake()
|
||||
/// {
|
||||
/// _StateMachine.InitializeAfterDeserialize();
|
||||
/// }
|
||||
/// }
|
||||
/// </code></remarks>
|
||||
public virtual void InitializeAfterDeserialize()
|
||||
{
|
||||
if (_CurrentState != null)
|
||||
using (new StateChange<TState>(this, null, _CurrentState))
|
||||
_CurrentState.OnEnterState();
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Is it currently possible to enter the specified `state`?</summary>
|
||||
/// <remarks>
|
||||
/// This requires <see cref="IState.CanExitState"/> on the <see cref="CurrentState"/> and
|
||||
/// <see cref="IState.CanEnterState"/> on the specified `state` to both return true.
|
||||
/// </remarks>
|
||||
public bool CanSetState(TState state)
|
||||
{
|
||||
#if UNITY_ASSERTIONS
|
||||
if (state == null && !AllowNullStates)
|
||||
throw new ArgumentNullException(nameof(state), NullNotAllowed);
|
||||
#endif
|
||||
|
||||
using (new StateChange<TState>(this, _CurrentState, state))
|
||||
{
|
||||
if (_CurrentState != null && !_CurrentState.CanExitState)
|
||||
return false;
|
||||
|
||||
if (state != null && !state.CanEnterState)
|
||||
return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Returns the first of the `states` which can currently be entered.</summary>
|
||||
/// <remarks>
|
||||
/// This requires <see cref="IState.CanExitState"/> on the <see cref="CurrentState"/> and
|
||||
/// <see cref="IState.CanEnterState"/> on one of the `states` to both return true.
|
||||
/// <para></para>
|
||||
/// States are checked in ascending order (i.e. from <c>[0]</c> to <c>[states.Count - 1]</c>).
|
||||
/// </remarks>
|
||||
public TState CanSetState(IList<TState> states)
|
||||
{
|
||||
// We call CanSetState so that it will check CanExitState for each individual pair in case it does
|
||||
// something based on the next state.
|
||||
|
||||
var count = states.Count;
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
var state = states[i];
|
||||
if (CanSetState(state))
|
||||
return state;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Attempts to enter the specified `state` and returns true if successful.</summary>
|
||||
/// <remarks>
|
||||
/// This method returns true immediately if the specified `state` is already the <see cref="CurrentState"/>.
|
||||
/// To allow directly re-entering the same state, use <see cref="TryResetState(TState)"/> instead.
|
||||
/// </remarks>
|
||||
public bool TrySetState(TState state)
|
||||
{
|
||||
if (_CurrentState == state)
|
||||
{
|
||||
#if UNITY_ASSERTIONS
|
||||
if (state == null && !AllowNullStates)
|
||||
throw new ArgumentNullException(nameof(state), NullNotAllowed);
|
||||
#endif
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return TryResetState(state);
|
||||
}
|
||||
|
||||
/// <summary>Attempts to enter any of the specified `states` and returns true if successful.</summary>
|
||||
/// <remarks>
|
||||
/// This method returns true and does nothing else if the <see cref="CurrentState"/> is in the list.
|
||||
/// To allow directly re-entering the same state, use <see cref="TryResetState(IList{TState})"/> instead.
|
||||
/// <para></para>
|
||||
/// States are checked in ascending order (i.e. from <c>[0]</c> to <c>[states.Count - 1]</c>).
|
||||
/// </remarks>
|
||||
public bool TrySetState(IList<TState> states)
|
||||
{
|
||||
var count = states.Count;
|
||||
for (int i = 0; i < count; i++)
|
||||
if (TrySetState(states[i]))
|
||||
return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Attempts to enter the specified `state` and returns true if successful.</summary>
|
||||
/// <remarks>
|
||||
/// This method does not check if the `state` is already the <see cref="CurrentState"/>. To do so, use
|
||||
/// <see cref="TrySetState(TState)"/> instead.
|
||||
/// </remarks>
|
||||
public bool TryResetState(TState state)
|
||||
{
|
||||
if (!CanSetState(state))
|
||||
return false;
|
||||
|
||||
ForceSetState(state);
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>Attempts to enter any of the specified `states` and returns true if successful.</summary>
|
||||
/// <remarks>
|
||||
/// This method does not check if the `state` is already the <see cref="CurrentState"/>. To do so, use
|
||||
/// <see cref="TrySetState(IList{TState})"/> instead.
|
||||
/// <para></para>
|
||||
/// States are checked in ascending order (i.e. from <c>[0]</c> to <c>[states.Count - 1]</c>).
|
||||
/// </remarks>
|
||||
public bool TryResetState(IList<TState> states)
|
||||
{
|
||||
var count = states.Count;
|
||||
for (int i = 0; i < count; i++)
|
||||
if (TryResetState(states[i]))
|
||||
return true;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>
|
||||
/// Calls <see cref="IState.OnExitState"/> on the <see cref="CurrentState"/> then changes it to the
|
||||
/// specified `state` and calls <see cref="IState.OnEnterState"/> on it.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This method does not check <see cref="IState.CanExitState"/> or
|
||||
/// <see cref="IState.CanEnterState"/>. To do that, you should use <see cref="TrySetState"/> instead.
|
||||
/// </remarks>
|
||||
public void ForceSetState(TState state)
|
||||
{
|
||||
#if UNITY_ASSERTIONS
|
||||
if (state == null)
|
||||
{
|
||||
if (!AllowNullStates)
|
||||
throw new ArgumentNullException(nameof(state), NullNotAllowed);
|
||||
}
|
||||
else if (state is IOwnedState<TState> owned && owned.OwnerStateMachine != this)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Attempted to use a state in a machine that is not its owner." +
|
||||
$"\n• State: {state}" +
|
||||
$"\n• Machine: {this}");
|
||||
}
|
||||
#endif
|
||||
|
||||
using (new StateChange<TState>(this, _CurrentState, state))
|
||||
{
|
||||
_CurrentState?.OnExitState();
|
||||
|
||||
_CurrentState = state;
|
||||
|
||||
state?.OnEnterState();
|
||||
}
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Returns a string describing the type of this state machine and its <see cref="CurrentState"/>.</summary>
|
||||
public override string ToString() => $"{GetType().Name} -> {_CurrentState}";
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
#if UNITY_ASSERTIONS
|
||||
/// <summary>[Assert-Only] Should the <see cref="CurrentState"/> be allowed to be set to null? Default is false.</summary>
|
||||
/// <remarks>Can be set by <see cref="SetAllowNullStates"/>.</remarks>
|
||||
public bool AllowNullStates { get; private set; }
|
||||
|
||||
/// <summary>[Assert-Only] The error given when attempting to set the <see cref="CurrentState"/> to null.</summary>
|
||||
private const string NullNotAllowed =
|
||||
"This " + nameof(StateMachine<TState>) + " does not allow its state to be set to null." +
|
||||
" Use " + nameof(SetAllowNullStates) + " to allow it if this is intentional.";
|
||||
#endif
|
||||
|
||||
/// <summary>[Assert-Conditional] Sets <see cref="AllowNullStates"/>.</summary>
|
||||
[System.Diagnostics.Conditional("UNITY_ASSERTIONS")]
|
||||
public void SetAllowNullStates(bool allow = true)
|
||||
{
|
||||
#if UNITY_ASSERTIONS
|
||||
AllowNullStates = allow;
|
||||
#endif
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
#region GUI
|
||||
/************************************************************************************************************************/
|
||||
#if UNITY_EDITOR && UNITY_IMGUI
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>[Editor-Only] The number of standard size lines that <see cref="DoGUI"/> will use.</summary>
|
||||
public virtual int GUILineCount => 1;
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>[Editor-Only] Draws GUI fields to display the status of this state machine.</summary>
|
||||
public void DoGUI()
|
||||
{
|
||||
var spacing = UnityEditor.EditorGUIUtility.standardVerticalSpacing;
|
||||
var lines = GUILineCount;
|
||||
var height =
|
||||
UnityEditor.EditorGUIUtility.singleLineHeight * lines +
|
||||
spacing * (lines - 1);
|
||||
|
||||
var area = GUILayoutUtility.GetRect(0, height);
|
||||
area.height -= spacing;
|
||||
|
||||
DoGUI(ref area);
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>[Editor-Only] Draws GUI fields to display the status of this state machine in the given `area`.</summary>
|
||||
public virtual void DoGUI(ref Rect area)
|
||||
{
|
||||
area.height = UnityEditor.EditorGUIUtility.singleLineHeight;
|
||||
|
||||
UnityEditor.EditorGUI.BeginChangeCheck();
|
||||
|
||||
var state = StateMachineUtilities.DoGenericField(area, "Current State", _CurrentState);
|
||||
|
||||
if (UnityEditor.EditorGUI.EndChangeCheck())
|
||||
{
|
||||
if (Event.current.control)
|
||||
ForceSetState(state);
|
||||
else
|
||||
TrySetState(state);
|
||||
}
|
||||
|
||||
StateMachineUtilities.NextVerticalArea(ref area);
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
#endif
|
||||
#endregion
|
||||
/************************************************************************************************************************/
|
||||
#region IStateMachine
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <inheritdoc/>
|
||||
object IStateMachine.CurrentState => _CurrentState;
|
||||
|
||||
/// <inheritdoc/>
|
||||
object IStateMachine.PreviousState => PreviousState;
|
||||
|
||||
/// <inheritdoc/>
|
||||
object IStateMachine.NextState => NextState;
|
||||
|
||||
/// <inheritdoc/>
|
||||
object IStateMachine.CanSetState(IList states) => CanSetState((List<TState>)states);
|
||||
|
||||
/// <inheritdoc/>
|
||||
bool IStateMachine.CanSetState(object state) => CanSetState((TState)state);
|
||||
|
||||
/// <inheritdoc/>
|
||||
void IStateMachine.ForceSetState(object state) => ForceSetState((TState)state);
|
||||
|
||||
/// <inheritdoc/>
|
||||
bool IStateMachine.TryResetState(IList states) => TryResetState((List<TState>)states);
|
||||
|
||||
/// <inheritdoc/>
|
||||
bool IStateMachine.TryResetState(object state) => TryResetState((TState)state);
|
||||
|
||||
/// <inheritdoc/>
|
||||
bool IStateMachine.TrySetState(IList states) => TrySetState((List<TState>)states);
|
||||
|
||||
/// <inheritdoc/>
|
||||
bool IStateMachine.TrySetState(object state) => TrySetState((TState)state);
|
||||
|
||||
/// <inheritdoc/>
|
||||
void IStateMachine.SetAllowNullStates(bool allow) => SetAllowNullStates(allow);
|
||||
|
||||
/************************************************************************************************************************/
|
||||
#endregion
|
||||
/************************************************************************************************************************/
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 87a6d066d8da93b4bbdc228c47509675
|
||||
labels:
|
||||
- FSM
|
||||
- FiniteStateMachine
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,88 @@
|
||||
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2024 Kybernetik //
|
||||
|
||||
namespace Animancer.FSM
|
||||
{
|
||||
public partial class StateMachine<TKey, TState>
|
||||
{
|
||||
/// <summary>
|
||||
/// A simple system that can <see cref="StateMachine{TState}.InputBuffer{TStateMachine}.State"/> a state then
|
||||
/// try to enter it every time <see cref="StateMachine{TState}.InputBuffer{TStateMachine}.Update(float)"/> is
|
||||
/// called until the <see cref="StateMachine{TState}.InputBuffer{TStateMachine}.TimeOut"/> expires.
|
||||
/// </summary>
|
||||
///
|
||||
/// <remarks>
|
||||
/// <para></para>
|
||||
/// <strong>Documentation:</strong>
|
||||
/// <see href="https://kybernetik.com.au/animancer/docs/manual/fsm/utilities#input-buffers">
|
||||
/// Input Buffers</see>
|
||||
/// <para></para>
|
||||
/// See <see cref="StateMachine{TState}.InputBuffer{TStateMachine}"/> for example usage.
|
||||
/// </remarks>
|
||||
///
|
||||
/// https://kybernetik.com.au/animancer/api/Animancer.FSM/InputBuffer
|
||||
///
|
||||
public new class InputBuffer : InputBuffer<StateMachine<TKey, TState>>
|
||||
{
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>The <typeparamref name="TKey"/> of the state this buffer is currently attempting to enter.</summary>
|
||||
public TKey Key { get; set; }
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Creates a new <see cref="InputBuffer"/>.</summary>
|
||||
public InputBuffer() { }
|
||||
|
||||
/// <summary>Creates a new <see cref="InputBuffer"/> for the specified `stateMachine`.</summary>
|
||||
public InputBuffer(StateMachine<TKey, TState> stateMachine)
|
||||
: base(stateMachine)
|
||||
{ }
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>
|
||||
/// If a state is registered with the `key`, this method calls <see cref="Buffer(TKey, TState, float)"/>
|
||||
/// and returns true. Otherwise it returns false.
|
||||
/// </summary>
|
||||
/// <remarks>Doesn't actually attempt to enter the state until <see cref="Update(float)"/> is called.</remarks>
|
||||
public bool Buffer(TKey key, float timeOut)
|
||||
{
|
||||
if (StateMachine.TryGetValue(key, out var state))
|
||||
{
|
||||
Buffer(key, state, timeOut);
|
||||
return true;
|
||||
}
|
||||
else return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the <see cref="Key"/>, <see cref="StateMachine{TState}.InputBuffer.State"/>, and
|
||||
/// <see cref="TimeOut"/>.
|
||||
/// </summary>
|
||||
/// <remarks>Doesn't actually attempt to enter the state until <see cref="Update(float)"/> is called.</remarks>
|
||||
public void Buffer(TKey key, TState state, float timeOut)
|
||||
{
|
||||
Key = key;
|
||||
Buffer(state, timeOut);
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override bool TryEnterState()
|
||||
=> StateMachine.TryResetState(Key, State);
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override void Clear()
|
||||
{
|
||||
base.Clear();
|
||||
Key = default;
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 2a348c9a4a87c294e960ae27c06c12f1
|
||||
labels:
|
||||
- FSM
|
||||
- FiniteStateMachine
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,142 @@
|
||||
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2024 Kybernetik //
|
||||
|
||||
using System;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Animancer.FSM
|
||||
{
|
||||
/// https://kybernetik.com.au/animancer/api/Animancer.FSM/StateMachine_2
|
||||
partial class StateMachine<TKey, TState>
|
||||
{
|
||||
/// <summary>A <see cref="StateMachine{TKey, TState}"/> with a <see cref="DefaultKey"/>.</summary>
|
||||
/// <remarks>
|
||||
/// See <see cref="InitializeAfterDeserialize"/> if using this class in a serialized field.
|
||||
/// <para></para>
|
||||
/// <strong>Documentation:</strong>
|
||||
/// <see href="https://kybernetik.com.au/animancer/docs/manual/fsm/changing-states#default-states">
|
||||
/// Default States</see>
|
||||
/// </remarks>
|
||||
/// https://kybernetik.com.au/animancer/api/Animancer.FSM/WithDefault
|
||||
///
|
||||
[Serializable]
|
||||
public new class WithDefault : StateMachine<TKey, TState>
|
||||
{
|
||||
/************************************************************************************************************************/
|
||||
|
||||
[SerializeField]
|
||||
private TKey _DefaultKey;
|
||||
|
||||
/// <summary>The starting state and main state to return to when nothing else is active.</summary>
|
||||
/// <remarks>
|
||||
/// If the <see cref="CurrentState"/> is <c>null</c> when setting this value, it calls
|
||||
/// <see cref="ForceSetState(TKey)"/> to enter the specified state immediately.
|
||||
/// <para></para>
|
||||
/// For a character, this would typically be their <em>Idle</em> state.
|
||||
/// </remarks>
|
||||
public TKey DefaultKey
|
||||
{
|
||||
get => _DefaultKey;
|
||||
set
|
||||
{
|
||||
_DefaultKey = value;
|
||||
if (CurrentState == null && value != null)
|
||||
ForceSetState(value);
|
||||
}
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Calls <see cref="ForceSetState(TKey)"/> with the <see cref="DefaultKey"/>.</summary>
|
||||
/// <remarks>This delegate is cached to avoid allocating garbage when used in Animancer Events.</remarks>
|
||||
public readonly Action ForceSetDefaultState;
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Creates a new <see cref="WithDefault"/>.</summary>
|
||||
public WithDefault()
|
||||
{
|
||||
// Silly C# doesn't allow instance delegates to be assigned using field initializers.
|
||||
ForceSetDefaultState = () => ForceSetState(_DefaultKey);
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Creates a new <see cref="WithDefault"/> and sets the <see cref="DefaultKey"/>.</summary>
|
||||
public WithDefault(TKey defaultKey)
|
||||
: this()
|
||||
{
|
||||
_DefaultKey = defaultKey;
|
||||
ForceSetState(defaultKey);
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override void InitializeAfterDeserialize()
|
||||
{
|
||||
if (CurrentState != null)
|
||||
{
|
||||
using (new KeyChange<TKey>(this, default, _DefaultKey))
|
||||
using (new StateChange<TState>(this, null, CurrentState))
|
||||
CurrentState.OnEnterState();
|
||||
}
|
||||
else
|
||||
{
|
||||
ForceSetState(_DefaultKey);
|
||||
}
|
||||
|
||||
// Don't call the base method.
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Attempts to enter the <see cref="DefaultKey"/> and returns true if successful.</summary>
|
||||
/// <remarks>
|
||||
/// This method returns true immediately if the specified <see cref="DefaultKey"/> is already the
|
||||
/// <see cref="CurrentKey"/>. To allow directly re-entering the same state, use
|
||||
/// <see cref="TryResetDefaultState"/> instead.
|
||||
/// </remarks>
|
||||
public TState TrySetDefaultState() => TrySetState(_DefaultKey);
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Attempts to enter the <see cref="DefaultKey"/> and returns true if successful.</summary>
|
||||
/// <remarks>
|
||||
/// This method does not check if the <see cref="DefaultKey"/> is already the <see cref="CurrentKey"/>.
|
||||
/// To do so, use <see cref="TrySetDefaultState"/> instead.
|
||||
/// </remarks>
|
||||
public TState TryResetDefaultState() => TryResetState(_DefaultKey);
|
||||
|
||||
/************************************************************************************************************************/
|
||||
#if UNITY_EDITOR && UNITY_IMGUI
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override int GUILineCount => 2;
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override void DoGUI(ref Rect area)
|
||||
{
|
||||
area.height = UnityEditor.EditorGUIUtility.singleLineHeight;
|
||||
|
||||
UnityEditor.EditorGUI.BeginChangeCheck();
|
||||
|
||||
var state = StateMachineUtilities.DoGenericField(area, "Default Key", DefaultKey);
|
||||
|
||||
if (UnityEditor.EditorGUI.EndChangeCheck())
|
||||
DefaultKey = state;
|
||||
|
||||
StateMachineUtilities.NextVerticalArea(ref area);
|
||||
|
||||
base.DoGUI(ref area);
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
#endif
|
||||
/************************************************************************************************************************/
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
fileFormatVersion: 2
|
||||
guid: a4aaa753173eb1c45bd98cd956086a93
|
||||
labels:
|
||||
- FSM
|
||||
- FiniteStateMachine
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,404 @@
|
||||
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2024 Kybernetik //
|
||||
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Animancer.FSM
|
||||
{
|
||||
/// <summary>Interface for accessing <see cref="StateMachine{TKey, TState}"/> without the <c>TState</c>.</summary>
|
||||
/// <remarks>
|
||||
/// <strong>Documentation:</strong>
|
||||
/// <see href="https://kybernetik.com.au/animancer/docs/manual/fsm/keys">
|
||||
/// Keyed State Machines</see>
|
||||
/// </remarks>
|
||||
/// https://kybernetik.com.au/animancer/api/Animancer.FSM/IKeyedStateMachine_1
|
||||
///
|
||||
public interface IKeyedStateMachine<TKey>
|
||||
{
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>The key which identifies the <see cref="StateMachine{TState}.CurrentState"/>.</summary>
|
||||
TKey CurrentKey { get; }
|
||||
|
||||
/// <summary>The <see cref="KeyChange{TKey}.PreviousKey"/>.</summary>
|
||||
TKey PreviousKey { get; }
|
||||
|
||||
/// <summary>The <see cref="KeyChange{TKey}.NextKey"/>.</summary>
|
||||
TKey NextKey { get; }
|
||||
|
||||
/// <summary>Attempts to enter the state registered with the specified `key` and returns it if successful.</summary>
|
||||
/// <remarks>
|
||||
/// This method returns true immediately if the specified `key` is already the <see cref="CurrentKey"/>. To
|
||||
/// allow directly re-entering the same state, use <see cref="TryResetState(TKey)"/> instead.
|
||||
/// </remarks>
|
||||
object TrySetState(TKey key);
|
||||
|
||||
/// <summary>Attempts to enter the state registered with the specified `key` and returns it if successful.</summary>
|
||||
/// <remarks>
|
||||
/// This method does not check if the `key` is already the <see cref="CurrentKey"/>. To do so, use
|
||||
/// <see cref="TrySetState(TKey)"/> instead.
|
||||
/// </remarks>
|
||||
object TryResetState(TKey key);
|
||||
|
||||
/// <summary>
|
||||
/// Uses <see cref="StateMachine{TKey, TState}.ForceSetState(TKey, TState)"/> to change to the state registered
|
||||
/// with the `key`. If nothing is registered, it changes to <c>default(TState)</c>.
|
||||
/// </summary>
|
||||
object ForceSetState(TKey key);
|
||||
|
||||
/************************************************************************************************************************/
|
||||
}
|
||||
|
||||
/// <summary>A simple Finite State Machine system that registers each state with a particular key.</summary>
|
||||
/// <remarks>
|
||||
/// This class allows states to be registered with a particular key upfront and then accessed later using that key.
|
||||
/// See <see cref="StateMachine{TState}"/> for a system that does not bother keeping track of any states other than
|
||||
/// the active one.
|
||||
/// <para></para>
|
||||
/// See <see cref="InitializeAfterDeserialize"/> if using this class in a serialized field.
|
||||
/// <para></para>
|
||||
/// <strong>Documentation:</strong>
|
||||
/// <see href="https://kybernetik.com.au/animancer/docs/manual/fsm/keys">
|
||||
/// Keyed State Machines</see>
|
||||
/// </remarks>
|
||||
/// https://kybernetik.com.au/animancer/api/Animancer.FSM/StateMachine_2
|
||||
///
|
||||
[HelpURL(StateExtensions.APIDocumentationURL + nameof(StateMachine<TState>) + "_2")]
|
||||
[Serializable]
|
||||
public partial class StateMachine<TKey, TState> : StateMachine<TState>, IKeyedStateMachine<TKey>, IDictionary<TKey, TState>
|
||||
where TState : class, IState
|
||||
{
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>The collection of states mapped to a particular key.</summary>
|
||||
public IDictionary<TKey, TState> Dictionary { get; set; }
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
[SerializeField]
|
||||
private TKey _CurrentKey;
|
||||
|
||||
/// <summary>The key which identifies the <see cref="StateMachine{TState}.CurrentState"/>.</summary>
|
||||
public TKey CurrentKey => _CurrentKey;
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>The <see cref="KeyChange{TKey}.PreviousKey"/>.</summary>
|
||||
public TKey PreviousKey => KeyChange<TKey>.PreviousKey;
|
||||
|
||||
/// <summary>The <see cref="KeyChange{TKey}.NextKey"/>.</summary>
|
||||
public TKey NextKey => KeyChange<TKey>.NextKey;
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new <see cref="StateMachine{TKey, TState}"/> with a new <see cref="Dictionary"/>, leaving the
|
||||
/// <see cref="CurrentState"/> null.
|
||||
/// </summary>
|
||||
public StateMachine()
|
||||
{
|
||||
Dictionary = new Dictionary<TKey, TState>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new <see cref="StateMachine{TKey, TState}"/> which uses the specified `dictionary`, leaving the
|
||||
/// <see cref="CurrentState"/> null.
|
||||
/// </summary>
|
||||
public StateMachine(IDictionary<TKey, TState> dictionary)
|
||||
{
|
||||
Dictionary = dictionary;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Constructs a new <see cref="StateMachine{TKey, TState}"/> with a new <see cref="Dictionary"/> and
|
||||
/// immediately uses the `defaultKey` to enter the `defaultState`.
|
||||
/// </summary>
|
||||
/// <remarks>This calls <see cref="IState.OnEnterState"/> but not <see cref="IState.CanEnterState"/>.</remarks>
|
||||
public StateMachine(TKey defaultKey, TState defaultState)
|
||||
{
|
||||
Dictionary = new Dictionary<TKey, TState>
|
||||
{
|
||||
{ defaultKey, defaultState }
|
||||
};
|
||||
ForceSetState(defaultKey, defaultState);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Constructs a new <see cref="StateMachine{TKey, TState}"/> which uses the specified `dictionary` and
|
||||
/// immediately uses the `defaultKey` to enter the `defaultState`.
|
||||
/// </summary>
|
||||
/// <remarks>This calls <see cref="IState.OnEnterState"/> but not <see cref="IState.CanEnterState"/>.</remarks>
|
||||
public StateMachine(IDictionary<TKey, TState> dictionary, TKey defaultKey, TState defaultState)
|
||||
{
|
||||
Dictionary = dictionary;
|
||||
dictionary.Add(defaultKey, defaultState);
|
||||
ForceSetState(defaultKey, defaultState);
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override void InitializeAfterDeserialize()
|
||||
{
|
||||
if (CurrentState != null)
|
||||
{
|
||||
using (new KeyChange<TKey>(this, default, _CurrentKey))
|
||||
using (new StateChange<TState>(this, null, CurrentState))
|
||||
CurrentState.OnEnterState();
|
||||
}
|
||||
else if (Dictionary.TryGetValue(_CurrentKey, out var state))
|
||||
{
|
||||
ForceSetState(_CurrentKey, state);
|
||||
}
|
||||
|
||||
// Don't call the base method.
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Attempts to enter the specified `state` and returns true if successful.</summary>
|
||||
/// <remarks>
|
||||
/// This method returns true immediately if the specified `state` is already the
|
||||
/// <see cref="StateMachine{TState}.CurrentState"/>. To allow directly re-entering the same state, use
|
||||
/// <see cref="TryResetState(TKey, TState)"/> instead.
|
||||
/// </remarks>
|
||||
public bool TrySetState(TKey key, TState state)
|
||||
{
|
||||
if (CurrentState == state)
|
||||
return true;
|
||||
else
|
||||
return TryResetState(key, state);
|
||||
}
|
||||
|
||||
/// <summary>Attempts to enter the state registered with the specified `key` and returns it if successful.</summary>
|
||||
/// <remarks>
|
||||
/// This method returns true immediately if the specified `key` is already the <see cref="CurrentKey"/>. To
|
||||
/// allow directly re-entering the same state, use <see cref="TryResetState(TKey)"/> instead.
|
||||
/// </remarks>
|
||||
public TState TrySetState(TKey key)
|
||||
{
|
||||
if (EqualityComparer<TKey>.Default.Equals(_CurrentKey, key))
|
||||
return CurrentState;
|
||||
else
|
||||
return TryResetState(key);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
object IKeyedStateMachine<TKey>.TrySetState(TKey key) => TrySetState(key);
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Attempts to enter the specified `state` and returns true if successful.</summary>
|
||||
/// <remarks>
|
||||
/// This method does not check if the `state` is already the <see cref="StateMachine{TState}.CurrentState"/>.
|
||||
/// To do so, use <see cref="TrySetState(TKey, TState)"/> instead.
|
||||
/// </remarks>
|
||||
public bool TryResetState(TKey key, TState state)
|
||||
{
|
||||
using (new KeyChange<TKey>(this, _CurrentKey, key))
|
||||
{
|
||||
if (!CanSetState(state))
|
||||
return false;
|
||||
|
||||
_CurrentKey = key;
|
||||
ForceSetState(state);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Attempts to enter the state registered with the specified `key` and returns it if successful.</summary>
|
||||
/// <remarks>
|
||||
/// This method does not check if the `key` is already the <see cref="CurrentKey"/>. To do so, use
|
||||
/// <see cref="TrySetState(TKey)"/> instead.
|
||||
/// </remarks>
|
||||
public TState TryResetState(TKey key)
|
||||
{
|
||||
if (Dictionary.TryGetValue(key, out var state) &&
|
||||
TryResetState(key, state))
|
||||
return state;
|
||||
else
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
object IKeyedStateMachine<TKey>.TryResetState(TKey key) => TryResetState(key);
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>
|
||||
/// Calls <see cref="IState.OnExitState"/> on the <see cref="StateMachine{TState}.CurrentState"/> then changes
|
||||
/// to the specified `key` and `state` and calls <see cref="IState.OnEnterState"/> on it.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This method does not check <see cref="IState.CanExitState"/> or <see cref="IState.CanEnterState"/>. To do
|
||||
/// that, you should use <see cref="TrySetState(TKey, TState)"/> instead.
|
||||
/// </remarks>
|
||||
public void ForceSetState(TKey key, TState state)
|
||||
{
|
||||
using (new KeyChange<TKey>(this, _CurrentKey, key))
|
||||
{
|
||||
_CurrentKey = key;
|
||||
ForceSetState(state);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Uses <see cref="ForceSetState(TKey, TState)"/> to change to the state registered with the `key`. If nothing
|
||||
/// is registered, it use <c>null</c> and will throw an exception unless
|
||||
/// <see cref="StateMachine{TState}.AllowNullStates"/> is enabled.
|
||||
/// </summary>
|
||||
public TState ForceSetState(TKey key)
|
||||
{
|
||||
Dictionary.TryGetValue(key, out var state);
|
||||
ForceSetState(key, state);
|
||||
return state;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
object IKeyedStateMachine<TKey>.ForceSetState(TKey key) => ForceSetState(key);
|
||||
|
||||
/************************************************************************************************************************/
|
||||
#region Dictionary Wrappers
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>The state registered with the `key` in the <see cref="Dictionary"/>.</summary>
|
||||
public TState this[TKey key] { get => Dictionary[key]; set => Dictionary[key] = value; }
|
||||
|
||||
/// <summary>Gets the state registered with the specified `key` in the <see cref="Dictionary"/>.</summary>
|
||||
public bool TryGetValue(TKey key, out TState state) => Dictionary.TryGetValue(key, out state);
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Gets an <see cref="ICollection{T}"/> containing the keys of the <see cref="Dictionary"/>.</summary>
|
||||
public ICollection<TKey> Keys => Dictionary.Keys;
|
||||
|
||||
/// <summary>Gets an <see cref="ICollection{T}"/> containing the state of the <see cref="Dictionary"/>.</summary>
|
||||
public ICollection<TState> Values => Dictionary.Values;
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Gets the number of states contained in the <see cref="Dictionary"/>.</summary>
|
||||
public int Count => Dictionary.Count;
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Adds a state to the <see cref="Dictionary"/>.</summary>
|
||||
public void Add(TKey key, TState state) => Dictionary.Add(key, state);
|
||||
|
||||
/// <summary>Adds a state to the <see cref="Dictionary"/>.</summary>
|
||||
public void Add(KeyValuePair<TKey, TState> item) => Dictionary.Add(item);
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Removes a state from the <see cref="Dictionary"/>.</summary>
|
||||
public bool Remove(TKey key) => Dictionary.Remove(key);
|
||||
|
||||
/// <summary>Removes a state from the <see cref="Dictionary"/>.</summary>
|
||||
public bool Remove(KeyValuePair<TKey, TState> item) => Dictionary.Remove(item);
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Removes all state from the <see cref="Dictionary"/>.</summary>
|
||||
public void Clear() => Dictionary.Clear();
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Determines whether the <see cref="Dictionary"/> contains a specific value.</summary>
|
||||
public bool Contains(KeyValuePair<TKey, TState> item) => Dictionary.Contains(item);
|
||||
|
||||
/// <summary>Determines whether the <see cref="Dictionary"/> contains a state with the specified `key`.</summary>
|
||||
public bool ContainsKey(TKey key) => Dictionary.ContainsKey(key);
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Returns an enumerator that iterates through the <see cref="Dictionary"/>.</summary>
|
||||
public IEnumerator<KeyValuePair<TKey, TState>> GetEnumerator() => Dictionary.GetEnumerator();
|
||||
|
||||
/// <summary>Returns an enumerator that iterates through the <see cref="Dictionary"/>.</summary>
|
||||
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Copies the contents of the <see cref="Dictionary"/> to the `array` starting at the `arrayIndex`.</summary>
|
||||
public void CopyTo(KeyValuePair<TKey, TState>[] array, int arrayIndex) => Dictionary.CopyTo(array, arrayIndex);
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Indicates whether the <see cref="Dictionary"/> is read-only.</summary>
|
||||
bool ICollection<KeyValuePair<TKey, TState>>.IsReadOnly => Dictionary.IsReadOnly;
|
||||
|
||||
/************************************************************************************************************************/
|
||||
#endregion
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Returns the state registered with the specified `key`, or null if none is present.</summary>
|
||||
public TState GetState(TKey key)
|
||||
{
|
||||
TryGetValue(key, out var state);
|
||||
return state;
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Adds the specified `keys` and `states`. Both arrays must be the same size.</summary>
|
||||
public void AddRange(TKey[] keys, TState[] states)
|
||||
{
|
||||
Debug.Assert(keys.Length == states.Length,
|
||||
$"The '{nameof(keys)}' and '{nameof(states)}' arrays must be the same size.");
|
||||
|
||||
for (int i = 0; i < keys.Length; i++)
|
||||
{
|
||||
Dictionary.Add(keys[i], states[i]);
|
||||
}
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>
|
||||
/// Sets the <see cref="CurrentKey"/> without changing the <see cref="StateMachine{TState}.CurrentState"/>.
|
||||
/// </summary>
|
||||
public void SetFakeKey(TKey key) => _CurrentKey = key;
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>
|
||||
/// Returns a string describing the type of this state machine and its <see cref="CurrentKey"/> and
|
||||
/// <see cref="StateMachine{TState}.CurrentState"/>.
|
||||
/// </summary>
|
||||
public override string ToString()
|
||||
=> $"{GetType().FullName} -> {_CurrentKey} -> {(CurrentState != null ? CurrentState.ToString() : "null")}";
|
||||
|
||||
/************************************************************************************************************************/
|
||||
#if UNITY_EDITOR && UNITY_IMGUI
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override int GUILineCount => 2;
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override void DoGUI(ref Rect area)
|
||||
{
|
||||
area.height = UnityEditor.EditorGUIUtility.singleLineHeight;
|
||||
|
||||
UnityEditor.EditorGUI.BeginChangeCheck();
|
||||
|
||||
var key = StateMachineUtilities.DoGenericField(area, "Current Key", _CurrentKey);
|
||||
|
||||
if (UnityEditor.EditorGUI.EndChangeCheck())
|
||||
SetFakeKey(key);
|
||||
|
||||
StateMachineUtilities.NextVerticalArea(ref area);
|
||||
|
||||
base.DoGUI(ref area);
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
#endif
|
||||
/************************************************************************************************************************/
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 9d7e35072ba28604d95afbcda2209721
|
||||
labels:
|
||||
- FSM
|
||||
- FiniteStateMachine
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,58 @@
|
||||
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2024 Kybernetik //
|
||||
|
||||
#if UNITY_EDITOR && UNITY_IMGUI
|
||||
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
using Object = UnityEngine.Object;
|
||||
|
||||
namespace Animancer.FSM
|
||||
{
|
||||
/// <summary>[Editor-Only] Utilities used by the <see cref="FSM"/> system.</summary>
|
||||
/// <remarks>
|
||||
/// <strong>Documentation:</strong>
|
||||
/// <see href="https://kybernetik.com.au/animancer/docs/manual/fsm">
|
||||
/// Finite State Machines</see>
|
||||
/// </remarks>
|
||||
/// https://kybernetik.com.au/animancer/api/Animancer.FSM/StateMachineUtilities
|
||||
///
|
||||
public static class StateMachineUtilities
|
||||
{
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Draws a GUI field for the `value`.</summary>
|
||||
public static T DoGenericField<T>(Rect area, string label, T value)
|
||||
{
|
||||
if (typeof(Object).IsAssignableFrom(typeof(T)))
|
||||
{
|
||||
return (T)(object)EditorGUI.ObjectField(
|
||||
area,
|
||||
label,
|
||||
value as Object,
|
||||
typeof(T),
|
||||
true);
|
||||
}
|
||||
|
||||
var stateName = value != null ? value.ToString() : "Null";
|
||||
EditorGUI.LabelField(area, label, stateName);
|
||||
return value;
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>
|
||||
/// If the <see cref="Rect.height"/> is positive, this method moves the <see cref="Rect.y"/> by that amount and
|
||||
/// adds the <see cref="EditorGUIUtility.standardVerticalSpacing"/>.
|
||||
/// </summary>
|
||||
public static void NextVerticalArea(ref Rect area)
|
||||
{
|
||||
if (area.height > 0)
|
||||
area.y += area.height + EditorGUIUtility.standardVerticalSpacing;
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 58fae748392df8546bad814552de49af
|
||||
labels:
|
||||
- FSM
|
||||
- FiniteStateMachine
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,87 @@
|
||||
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2024 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();
|
||||
|
||||
/************************************************************************************************************************/
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 22624bd4cfa616c4f96ef8534fdbdd8e
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 2dfb135533444044db8f6fc06502a12e
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
+120
@@ -0,0 +1,120 @@
|
||||
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2024 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);
|
||||
}
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
}
|
||||
}
|
||||
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 5d3350b34d54d9e4b885e1a21932ae98
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
+57
@@ -0,0 +1,57 @@
|
||||
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2024 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
|
||||
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: d90d197b5f47af544aff6260d9dfadf2
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
+47
@@ -0,0 +1,47 @@
|
||||
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2024 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
|
||||
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 8bb9852087f39824f9647d710481937e
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
+43
@@ -0,0 +1,43 @@
|
||||
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2024 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;
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
}
|
||||
}
|
||||
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: f11a5059824879545908e1135dbb0928
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,302 @@
|
||||
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2024 Kybernetik //
|
||||
|
||||
#pragma warning disable CS0649 // Field is never assigned to, and will always have its default value.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Animancer
|
||||
{
|
||||
/// <summary>
|
||||
/// Replaces the <see cref="SpriteRenderer.sprite"/> with a copy of it that uses a different <see cref="Texture"/>
|
||||
/// during every <see cref="LateUpdate"/>.
|
||||
/// </summary>
|
||||
///
|
||||
/// <remarks>This script is not specific to Animancer and will work with any animation system.</remarks>
|
||||
/// https://kybernetik.com.au/animancer/api/Animancer/SpriteRendererTextureSwap
|
||||
///
|
||||
[AddComponentMenu("Animancer/Sprite Renderer Texture Swap")]
|
||||
[HelpURL("https://kybernetik.com.au/animancer/api/Animancer/" + nameof(SpriteRendererTextureSwap))]
|
||||
[DefaultExecutionOrder(DefaultExecutionOrder)]
|
||||
public class SpriteRendererTextureSwap : MonoBehaviour
|
||||
{
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Execute very late (32000 is last).</summary>
|
||||
public const int DefaultExecutionOrder = 30000;
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
[SerializeField]
|
||||
[Tooltip("The SpriteRenderer that will have its Sprite modified")]
|
||||
private SpriteRenderer _Renderer;
|
||||
|
||||
/// <summary>The <see cref="SpriteRenderer"/> that will have its <see cref="Sprite"/> modified.</summary>
|
||||
public ref SpriteRenderer Renderer => ref _Renderer;
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
[SerializeField]
|
||||
[Tooltip("The replacement for the original Sprite texture")]
|
||||
private Texture2D _Texture;
|
||||
|
||||
/// <summary>The replacement for the original <see cref="Sprite.texture"/>.</summary>
|
||||
/// <remarks>
|
||||
/// If this texture has any <see cref="Sprite"/>s set up in its import settings, they will be completely
|
||||
/// ignored because this system creates new <see cref="Sprite"/>s at runtime. The texture doesn't even need to
|
||||
/// be set to <see cref="Sprite"/> mode.
|
||||
/// <para></para>
|
||||
/// Call <see cref="ClearCache"/> before setting this if you want to destroy any sprites created for the
|
||||
/// previous texture.
|
||||
/// </remarks>
|
||||
public Texture2D Texture
|
||||
{
|
||||
get => _Texture;
|
||||
set
|
||||
{
|
||||
_Texture = value;
|
||||
RefreshSpriteMap();
|
||||
}
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
[SerializeField]
|
||||
[Tooltip("Should the secondary textures be swapped as well?")]
|
||||
private bool _SwapSecondaryTextures = true;
|
||||
|
||||
/// <summary>Should the secondary textures be swapped as well?</summary>
|
||||
public bool SwapSecondaryTextures
|
||||
{
|
||||
get => _SwapSecondaryTextures;
|
||||
set
|
||||
{
|
||||
_SwapSecondaryTextures = value;
|
||||
RefreshSpriteMap();
|
||||
}
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
[SerializeField]
|
||||
[Tooltip("The replacement secondary textures for the Sprite to use")]
|
||||
private SecondarySpriteTexture[] _SecondaryTextures;
|
||||
|
||||
/// <summary>The replacement for the original <see cref="Sprite.GetSecondaryTextures"/>.</summary>
|
||||
/// <remarks>
|
||||
/// Swapped sprites are cached statically so they can be shared between instances. Unfortunately, the cache
|
||||
/// isn't tied to secondary textures so if multiple instances replace TextureA with TextureB and have different
|
||||
/// secondary textures then they would interfere with each other. That's unlikely to be a real issue because
|
||||
/// TextureB should always have TextureBNormals as a secondary texture. It would be possible to avoid this
|
||||
/// if necessary, but doing so would cost more performance and increase the complexity of this system.
|
||||
/// </remarks>
|
||||
public SecondarySpriteTexture[] SecondaryTextures
|
||||
{
|
||||
get => _SecondaryTextures;
|
||||
set
|
||||
{
|
||||
_SecondaryTextures = value;
|
||||
RefreshSpriteMap();
|
||||
}
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
private Dictionary<Sprite, Sprite> _SpriteMap;
|
||||
|
||||
private void RefreshSpriteMap() => _SpriteMap = GetSpriteMap(_Texture);
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
protected virtual void OnValidate()
|
||||
{
|
||||
if (_Renderer == null)
|
||||
TryGetComponent(out _Renderer);
|
||||
|
||||
if (_SwapSecondaryTextures &&
|
||||
(_SecondaryTextures == null || _SecondaryTextures.Length == 0) &&
|
||||
_Renderer != null &&
|
||||
_Renderer.sprite != null)
|
||||
{
|
||||
var sprite = _Renderer.sprite;
|
||||
var count = sprite.GetSecondaryTextureCount();
|
||||
if (count > 0)
|
||||
{
|
||||
_SecondaryTextures = new SecondarySpriteTexture[count];
|
||||
sprite.GetSecondaryTextures(_SecondaryTextures);
|
||||
}
|
||||
}
|
||||
|
||||
if (_SpriteMap != null)
|
||||
DestroySprites(_SpriteMap);
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
protected virtual void Awake() => RefreshSpriteMap();
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
protected virtual void LateUpdate()
|
||||
{
|
||||
if (_Renderer == null)
|
||||
return;
|
||||
|
||||
var sprite = _Renderer.sprite;
|
||||
var secondaryTextures = _SwapSecondaryTextures ? _SecondaryTextures : null;
|
||||
if (TrySwapTexture(_SpriteMap, _Texture, secondaryTextures, ref sprite))
|
||||
_Renderer.sprite = sprite;
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Destroys all sprites created for the current <see cref="Texture"/>.</summary>
|
||||
public void ClearCache()
|
||||
{
|
||||
DestroySprites(_SpriteMap);
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
private static readonly Dictionary<Texture2D, Dictionary<Sprite, Sprite>>
|
||||
TextureToSpriteMap = new();
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Returns a cached dictionary mapping original sprites to duplicates using the specified `texture`.</summary>
|
||||
public static Dictionary<Sprite, Sprite> GetSpriteMap(Texture2D texture)
|
||||
{
|
||||
if (texture == null)
|
||||
return null;
|
||||
|
||||
if (!TextureToSpriteMap.TryGetValue(texture, out var map))
|
||||
TextureToSpriteMap.Add(texture, map = new());
|
||||
|
||||
return map;
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>
|
||||
/// If the <see cref="Sprite.texture"/> is not already using the specified `texture`, this method replaces the
|
||||
/// `sprite` with a cached duplicate which uses that `texture` instead.
|
||||
/// </summary>
|
||||
public static bool TrySwapTexture(
|
||||
Dictionary<Sprite, Sprite> spriteMap,
|
||||
Texture2D texture,
|
||||
SecondarySpriteTexture[] secondaryTextures,
|
||||
ref Sprite sprite)
|
||||
{
|
||||
if (spriteMap == null ||
|
||||
sprite == null ||
|
||||
texture == null ||
|
||||
sprite.texture == texture)
|
||||
return false;
|
||||
|
||||
if (!spriteMap.TryGetValue(sprite, out var otherSprite))
|
||||
{
|
||||
var pivot = sprite.pivot;
|
||||
pivot.x /= sprite.rect.width;
|
||||
pivot.y /= sprite.rect.height;
|
||||
|
||||
secondaryTextures ??= GetSecondaryTexturesCached(sprite);
|
||||
|
||||
otherSprite = Sprite.Create(texture,
|
||||
sprite.rect, pivot, sprite.pixelsPerUnit,
|
||||
0, SpriteMeshType.FullRect, sprite.border, false, secondaryTextures);
|
||||
|
||||
#if UNITY_ASSERTIONS
|
||||
var name = sprite.name;
|
||||
var originalTextureName = sprite.texture.name;
|
||||
var index = name.IndexOf(originalTextureName);
|
||||
if (index >= 0)
|
||||
{
|
||||
var newName =
|
||||
texture.name +
|
||||
name[(index + originalTextureName.Length)..];
|
||||
|
||||
if (index > 0)
|
||||
newName = name[..index] + newName;
|
||||
|
||||
name = newName;
|
||||
}
|
||||
|
||||
otherSprite.name = name;
|
||||
#endif
|
||||
|
||||
spriteMap.Add(sprite, otherSprite);
|
||||
}
|
||||
|
||||
sprite = otherSprite;
|
||||
return true;
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
private static List<SecondarySpriteTexture[]> _SecondaryTextureCache;
|
||||
|
||||
/// <summary>A wrapper around <see cref="Sprite.GetSecondaryTextures"/> which reuses arrays of the same size.</summary>
|
||||
public static SecondarySpriteTexture[] GetSecondaryTexturesCached(Sprite sprite)
|
||||
{
|
||||
var count = sprite.GetSecondaryTextureCount();
|
||||
if (count == 0)
|
||||
return System.Array.Empty<SecondarySpriteTexture>();
|
||||
|
||||
_SecondaryTextureCache ??= new();
|
||||
|
||||
while (_SecondaryTextureCache.Count < count)
|
||||
_SecondaryTextureCache.Add(null);
|
||||
|
||||
var textures = _SecondaryTextureCache[count - 1];
|
||||
if (textures == null)
|
||||
{
|
||||
textures = new SecondarySpriteTexture[count];
|
||||
_SecondaryTextureCache[count - 1] = textures;
|
||||
}
|
||||
|
||||
sprite.GetSecondaryTextures(textures);
|
||||
return textures;
|
||||
}
|
||||
|
||||
/// <summary>A wrapper around <see cref="Sprite.GetSecondaryTextures"/>.</summary>
|
||||
public static SecondarySpriteTexture[] GetSecondaryTextures(Sprite sprite)
|
||||
{
|
||||
var count = sprite.GetSecondaryTextureCount();
|
||||
if (count == 0)
|
||||
return System.Array.Empty<SecondarySpriteTexture>();
|
||||
|
||||
var textures = new SecondarySpriteTexture[count];
|
||||
sprite.GetSecondaryTextures(textures);
|
||||
return textures;
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Destroys all the <see cref="Dictionary{TKey, TValue}.Values"/>.</summary>
|
||||
public static void DestroySprites(Dictionary<Sprite, Sprite> spriteMap)
|
||||
{
|
||||
if (spriteMap == null)
|
||||
return;
|
||||
|
||||
foreach (var sprite in spriteMap.Values)
|
||||
Destroy(sprite);
|
||||
|
||||
spriteMap.Clear();
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Destroys all sprites created for the `texture`.</summary>
|
||||
public static void DestroySprites(Texture2D texture)
|
||||
{
|
||||
if (TextureToSpriteMap.TryGetValue(texture, out var spriteMap))
|
||||
{
|
||||
TextureToSpriteMap.Remove(texture);
|
||||
DestroySprites(spriteMap);
|
||||
}
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 2a050df026bbd064c9f71fe41ae48d00
|
||||
labels:
|
||||
- Example
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,159 @@
|
||||
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2024 Kybernetik //
|
||||
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Animancer
|
||||
{
|
||||
/// <summary>
|
||||
/// A system for synchronizing the <see cref="AnimancerState.NormalizedTime"/>
|
||||
/// of animations within the same "group".
|
||||
/// </summary>
|
||||
///
|
||||
/// <remarks>
|
||||
/// <list type="number">
|
||||
/// <item>Store a <see cref="TimeSynchronizer{T}"/> in a field.</item>
|
||||
/// <item>Call any of the <see cref="StoreTime(AnimancerState)"/> methods before playing a new animation.</item>
|
||||
/// <item>Then call any of the <see cref="SyncTime(AnimancerState, T, float)"/> methods after playing the animation.</item>
|
||||
/// </list>
|
||||
/// <strong>Sample:</strong>
|
||||
/// <see href="https://kybernetik.com.au/animancer/docs/samples/sprites/character#synchronization">
|
||||
/// Character Controller -> Synchronization</see>
|
||||
/// <code>
|
||||
/// // 1. Define your group type.
|
||||
/// // You could use strings or ints or whatever you want, but enums are often best.
|
||||
/// public enum AnimationGroup
|
||||
/// {
|
||||
/// None,
|
||||
/// Movement,
|
||||
/// }
|
||||
///
|
||||
/// [SerializeField] private AnimancerComponent _Animancer;
|
||||
///
|
||||
/// // 2. Store a TimeSynchronizer in a field.
|
||||
/// private readonly TimeSynchronizer<AnimationGroup>
|
||||
/// TimeSynchronizer = new();
|
||||
///
|
||||
/// public AnimancerState Play(AnimationClip clip, AnimationGroup group)
|
||||
/// {
|
||||
/// // 3. Call one of the StoreTime methods before playing a new animation.
|
||||
/// TimeSynchronizer.StoreTime(_Animancer);
|
||||
///
|
||||
/// // 4. Play an animation.
|
||||
/// var state = _Animancer.Play(clip);
|
||||
///
|
||||
/// // 5. Call one of the SyncTime methods after playing the animation.
|
||||
/// // If the `group` was the same as the value from last time you called it,
|
||||
/// // then the state's NormalizedTime will be set to the stored value.
|
||||
/// TimeSynchronizer.SyncTime(state, group);
|
||||
///
|
||||
/// return state;
|
||||
/// }
|
||||
/// </code></remarks>
|
||||
///
|
||||
/// https://kybernetik.com.au/animancer/api/Animancer/TimeSynchronizer_1
|
||||
///
|
||||
public class TimeSynchronizer<T>
|
||||
{
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>The group that the current animation is in.</summary>
|
||||
public T CurrentGroup { get; set; }
|
||||
|
||||
/// <summary>Should synchronization be applied when the <see cref="CurrentGroup"/> is at its default value?</summary>
|
||||
/// <remarks>This is false by default so that the <c>default</c> group represents "ungrouped".</remarks>
|
||||
public bool SynchronizeDefaultGroup { get; set; }
|
||||
|
||||
/// <summary>The state which the <see cref="NormalizedTime"/> came from (to avoid syncing with itself).</summary>
|
||||
public AnimancerState State { get; set; }
|
||||
|
||||
/// <summary>The stored <see cref="AnimancerState.NormalizedTimeD"/>.</summary>
|
||||
public double NormalizedTime { get; set; } = double.NaN;
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Creates a new <see cref="TimeSynchronizer{T}"/>.</summary>
|
||||
public TimeSynchronizer()
|
||||
{ }
|
||||
|
||||
/// <summary>Creates a new <see cref="TimeSynchronizer{T}"/>.</summary>
|
||||
public TimeSynchronizer(T group, bool synchronizeDefaultGroup = false)
|
||||
{
|
||||
CurrentGroup = group;
|
||||
SynchronizeDefaultGroup = synchronizeDefaultGroup;
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>
|
||||
/// Stores the <see cref="AnimancerState.NormalizedTimeD"/> of the <see cref="AnimancerLayer.CurrentState"/>.
|
||||
/// </summary>
|
||||
public void StoreTime(AnimancerLayer layer)
|
||||
=> StoreTime(layer.CurrentState);
|
||||
|
||||
/// <summary>Stores the <see cref="AnimancerState.NormalizedTimeD"/> of the `state`.</summary>
|
||||
public void StoreTime(AnimancerState state)
|
||||
=> StoreTime(state, state != null ? state.NormalizedTimeD : double.NaN);
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Sets the <see cref="State"/> and <see cref="NormalizedTime"/>.</summary>
|
||||
public void StoreTime(AnimancerState state, double normalizedTime)
|
||||
{
|
||||
State = state;
|
||||
NormalizedTime = normalizedTime;
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>
|
||||
/// Applies the <see cref="NormalizedTime"/> to the <see cref="AnimancerLayer.CurrentState"/>
|
||||
/// if the `group` matches the <see cref="CurrentGroup"/>.
|
||||
/// </summary>
|
||||
public bool SyncTime(AnimancerLayer layer, T group)
|
||||
=> SyncTime(layer.CurrentState, group, Time.deltaTime);
|
||||
|
||||
/// <summary>
|
||||
/// Applies the <see cref="NormalizedTime"/> to the <see cref="AnimancerLayer.CurrentState"/>
|
||||
/// if the `group` matches the <see cref="CurrentGroup"/>.
|
||||
/// </summary>
|
||||
public bool SyncTime(AnimancerLayer layer, T group, float deltaTime)
|
||||
=> SyncTime(layer.CurrentState, group, deltaTime);
|
||||
|
||||
/// <summary>
|
||||
/// Applies the <see cref="NormalizedTime"/> to the `state`
|
||||
/// if the `group` matches the <see cref="CurrentGroup"/>.
|
||||
/// </summary>
|
||||
public bool SyncTime(AnimancerState state, T group)
|
||||
=> SyncTime(state, group, Time.deltaTime);
|
||||
|
||||
/// <summary>
|
||||
/// Applies the <see cref="NormalizedTime"/> to the `state`
|
||||
/// and returns true if the `group` matches the <see cref="CurrentGroup"/>.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// If the `state` is the same one the time was stored from, this method does nothing and returns false.
|
||||
/// </remarks>
|
||||
public bool SyncTime(AnimancerState state, T group, float deltaTime)
|
||||
{
|
||||
if (state == null ||
|
||||
state == State ||
|
||||
double.IsNaN(NormalizedTime) ||
|
||||
!EqualityComparer<T>.Default.Equals(CurrentGroup, group) ||
|
||||
(!SynchronizeDefaultGroup && EqualityComparer<T>.Default.Equals(default, group)))
|
||||
{
|
||||
CurrentGroup = group;
|
||||
return false;
|
||||
}
|
||||
|
||||
// Setting the Time forces it to stay at that value after the next animation update.
|
||||
// But we actually want it to keep playing, so we need to add deltaTime manually.
|
||||
state.MoveTime(NormalizedTime * state.Length + deltaTime * state.EffectiveSpeed, false);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 1c373f5c844eda041b2c3b5c5519575d
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: cf141fdc4b87a9e42934fb46809fc654
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 79e7dc2b7dc4516429fdb2fa86e1d380
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
+117
@@ -0,0 +1,117 @@
|
||||
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2024 Kybernetik //
|
||||
|
||||
using System;
|
||||
using System.Runtime.CompilerServices;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Animancer.TransitionLibraries
|
||||
{
|
||||
/// <summary>[<see cref="SerializableAttribute"/>] A <see cref="StringAsset"/> and <see cref="int"/> pair.</summary>
|
||||
/// <remarks>
|
||||
/// <strong>Documentation:</strong>
|
||||
/// <see href="https://kybernetik.com.au/animancer/docs/manual/transitions/libraries">
|
||||
/// Transition Libraries</see>
|
||||
/// </remarks>
|
||||
/// https://kybernetik.com.au/animancer/api/Animancer.TransitionLibraries/NamedIndex
|
||||
[Serializable]
|
||||
public struct NamedIndex :
|
||||
IComparable<NamedIndex>,
|
||||
IEquatable<NamedIndex>
|
||||
{
|
||||
/************************************************************************************************************************/
|
||||
|
||||
[SerializeField]
|
||||
private StringAsset _Name;
|
||||
|
||||
/// <summary>The name.</summary>
|
||||
public readonly StringAsset Name
|
||||
=> _Name;
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
[SerializeField]
|
||||
private int _Index;
|
||||
|
||||
/// <summary>The index.</summary>
|
||||
public readonly int Index
|
||||
=> _Index;
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Creates a new <see cref="NamedIndex"/>.</summary>
|
||||
public NamedIndex(StringAsset name, int index)
|
||||
{
|
||||
_Name = name;
|
||||
_Index = index;
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Creates a new <see cref="NamedIndex"/>.</summary>
|
||||
public readonly NamedIndex With(StringAsset name)
|
||||
=> new(name, _Index);
|
||||
|
||||
/// <summary>Creates a new <see cref="NamedIndex"/>.</summary>
|
||||
public readonly NamedIndex With(int index)
|
||||
=> new(_Name, index);
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Describes this value.</summary>
|
||||
public override readonly string ToString()
|
||||
=> $"[{_Index}]{_Name}";
|
||||
|
||||
/************************************************************************************************************************/
|
||||
#region Equality
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Compares the <see cref="Index"/> then <see cref="Name"/>.</summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public readonly int CompareTo(NamedIndex other)
|
||||
{
|
||||
var result = _Index.CompareTo(other._Index);
|
||||
if (result != 0)
|
||||
return result;
|
||||
else
|
||||
return StringAsset.Compare(_Name, other._Name);
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Are all fields in this object equal to the equivalent in `obj`?</summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public override readonly bool Equals(object obj)
|
||||
=> obj is NamedIndex value
|
||||
&& Equals(value);
|
||||
|
||||
/// <summary>Are all fields in this object equal to the equivalent fields in `other`?</summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public readonly bool Equals(NamedIndex other)
|
||||
=> _Index == other._Index
|
||||
&& _Name == other._Name;
|
||||
|
||||
/// <summary>Are all fields in `a` equal to the equivalent fields in `b`?</summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static bool operator ==(NamedIndex a, NamedIndex b)
|
||||
=> a.Equals(b);
|
||||
|
||||
/// <summary>Are any fields in `a` not equal to the equivalent fields in `b`?</summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static bool operator !=(NamedIndex a, NamedIndex b)
|
||||
=> !(a == b);
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Returns a hash code based on the values of this object's fields.</summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public override readonly int GetHashCode()
|
||||
=> AnimancerUtilities.Hash(-871379578,
|
||||
_Index.SafeGetHashCode(),
|
||||
_Name.SafeGetHashCode());
|
||||
|
||||
/************************************************************************************************************************/
|
||||
#endregion
|
||||
/************************************************************************************************************************/
|
||||
}
|
||||
}
|
||||
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 2b95ff193221b8141b0f3c98600a8f55
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
+571
@@ -0,0 +1,571 @@
|
||||
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2024 Kybernetik //
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Animancer.TransitionLibraries
|
||||
{
|
||||
/// <summary>[Pro-Only]
|
||||
/// A library of <see cref="ITransition"/>s which allows specific
|
||||
/// transition combinations to be overridden without needing to be hard coded.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <strong>Documentation:</strong>
|
||||
/// <see href="https://kybernetik.com.au/animancer/docs/manual/transitions/libraries">
|
||||
/// Transition Libraries</see>
|
||||
/// </remarks>
|
||||
/// https://kybernetik.com.au/animancer/api/Animancer.TransitionLibraries/TransitionLibrary
|
||||
public class TransitionLibrary :
|
||||
IAnimationClipSource,
|
||||
ICopyable<TransitionLibrary>
|
||||
{
|
||||
/************************************************************************************************************************/
|
||||
#region Fields and Properties
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>[Pro-Only] Modifiers in the order they are created.</summary>
|
||||
/// <remarks>The <see cref="TransitionModifierGroup.Index"/> of each item corresponds to its position in this list.</remarks>
|
||||
private readonly List<TransitionModifierGroup>
|
||||
TransitionModifiers = new();
|
||||
|
||||
/// <summary>[Pro-Only] Modifiers registered by their <see cref="IHasKey.Key"/> as well as any custom aliases.</summary>
|
||||
private readonly Dictionary<object, TransitionModifierGroup>
|
||||
KeyedTransitionModifiers = new();
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>[Pro-Only] The number of transitions in this library.</summary>
|
||||
public int Count
|
||||
=> TransitionModifiers.Count;
|
||||
|
||||
/// <summary>[Pro-Only] The number of transitions in this library plus any additional aliases.</summary>
|
||||
public int AliasCount
|
||||
=> KeyedTransitionModifiers.Count;
|
||||
|
||||
/************************************************************************************************************************/
|
||||
#endregion
|
||||
/************************************************************************************************************************/
|
||||
#region Queries
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>[Pro-Only]
|
||||
/// Does this library contain a transition registered with the `key`?
|
||||
/// </summary>
|
||||
public bool ContainsKey(object key)
|
||||
=> KeyedTransitionModifiers.ContainsKey(key);
|
||||
|
||||
/// <summary>[Pro-Only]
|
||||
/// Does this library contain a transition registered with the <see cref="IHasKey.Key"/>?
|
||||
/// </summary>
|
||||
public bool ContainsKey(IHasKey hasKey)
|
||||
=> ContainsKey(hasKey.Key);
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>[Pro-Only]
|
||||
/// Tries to find a <see cref="TransitionModifierGroup"/> registered with the `key`.
|
||||
/// </summary>
|
||||
public bool TryGetTransition(object key, out TransitionModifierGroup transition)
|
||||
{
|
||||
#if UNITY_ASSERTIONS
|
||||
if (KeyedTransitionModifiers.TryGetValue(key, out transition))
|
||||
return true;
|
||||
|
||||
AssertStringReference(key);
|
||||
return false;
|
||||
#else
|
||||
return KeyedTransitionModifiers.TryGetValue(key, out transition);
|
||||
#endif
|
||||
}
|
||||
|
||||
/// <summary>[Pro-Only]
|
||||
/// Tries to find a <see cref="TransitionModifierGroup"/> registered with the <see cref="IHasKey.Key"/>.
|
||||
/// </summary>
|
||||
public bool TryGetTransition(IHasKey hasKey, out TransitionModifierGroup transition)
|
||||
=> TryGetTransition(hasKey.Key, out transition);
|
||||
|
||||
/// <summary>[Pro-Only]
|
||||
/// Tries to find a <see cref="TransitionModifierGroup"/>
|
||||
/// via its <see cref="TransitionModifierGroup.Index"/>.
|
||||
/// </summary>
|
||||
public bool TryGetTransition(int index, out TransitionModifierGroup transition)
|
||||
=> TransitionModifiers.TryGet(index, out transition)
|
||||
&& transition != null;
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>[Pro-Only]
|
||||
/// Finds the <see cref="TransitionModifierGroup.Index"/> of the group registered with the `key`
|
||||
/// or returns <c>-1</c>.
|
||||
/// </summary>
|
||||
public int IndexOf(object key)
|
||||
=> TryGetTransition(key, out var group)
|
||||
? group.Index
|
||||
: -1;
|
||||
|
||||
/// <summary>[Pro-Only]
|
||||
/// Finds the <see cref="TransitionModifierGroup.Index"/> of the group registered with the `key`
|
||||
/// or returns <c>-1</c>.
|
||||
/// </summary>
|
||||
public int IndexOf(IHasKey hasKey)
|
||||
=> IndexOf(hasKey.Key);
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>[Pro-Only]
|
||||
/// Returns the fade duration to use when transitioning from `from` to the `transition`.
|
||||
/// </summary>
|
||||
public float GetFadeDuration(
|
||||
object from,
|
||||
ITransition to)
|
||||
{
|
||||
if (from != null &&
|
||||
TryGetTransition(to.Key, out var group))
|
||||
return group.GetFadeDuration(from);
|
||||
|
||||
return to.FadeDuration;
|
||||
}
|
||||
|
||||
/// <summary>[Pro-Only]
|
||||
/// Returns the fade duration to use when transitioning from `from` to the `transition`.
|
||||
/// </summary>
|
||||
public float GetFadeDuration(
|
||||
IHasKey from,
|
||||
ITransition to)
|
||||
=> GetFadeDuration(from?.Key, to);
|
||||
|
||||
/// <summary>[Pro-Only]
|
||||
/// Returns the fade duration to use when transitioning from the
|
||||
/// <see cref="AnimancerLayer.CurrentState"/> to the `transition`.
|
||||
/// </summary>
|
||||
public float GetFadeDuration(
|
||||
AnimancerLayer layer,
|
||||
ITransition transition)
|
||||
=> GetFadeDuration(layer.CurrentState?.Key, transition);
|
||||
|
||||
/// <summary>[Pro-Only]
|
||||
/// Returns the fade duration to use when transitioning from the
|
||||
/// <see cref="AnimancerLayer.CurrentState"/> to the `key`.
|
||||
/// </summary>
|
||||
public float GetFadeDuration(
|
||||
AnimancerLayer layer,
|
||||
object key,
|
||||
float fadeDuration)
|
||||
{
|
||||
AssertStringReference(key);
|
||||
|
||||
var from = layer.CurrentState?.Key;
|
||||
if (from != null &&
|
||||
TryGetTransition(key, out var group))
|
||||
return group.GetFadeDuration(from);
|
||||
|
||||
return fadeDuration;
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>[Pro-Only] Gathers all the animations in this library.</summary>
|
||||
public void GetAnimationClips(List<AnimationClip> results)
|
||||
{
|
||||
for (int i = TransitionModifiers.Count - 1; i >= 0; i--)
|
||||
results.GatherFromSource(TransitionModifiers[i].Transition);
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
#endregion
|
||||
/************************************************************************************************************************/
|
||||
#region Add
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>[Pro-Only] Adds the contents of the `definition` to this library.</summary>
|
||||
/// <remarks>Existing values will be completely replaced.</remarks>
|
||||
public void Initialize(TransitionLibraryDefinition definition)
|
||||
{
|
||||
Clear();
|
||||
|
||||
if (definition == null)
|
||||
return;
|
||||
|
||||
var count = definition.Transitions.Length;
|
||||
|
||||
if (TransitionModifiers.Capacity < count)
|
||||
{
|
||||
var capacity = Math.Max(count, 16);
|
||||
TransitionModifiers.Capacity = capacity;
|
||||
KeyedTransitionModifiers.EnsureCapacity(capacity);
|
||||
}
|
||||
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
var transition = definition.Transitions[i];
|
||||
if (transition != null)
|
||||
SetTransition(transition);
|
||||
}
|
||||
|
||||
for (int i = 0; i < definition.Modifiers.Length; i++)
|
||||
SetFadeDuration(definition.Modifiers[i]);
|
||||
|
||||
if (definition.AliasAllTransitions)
|
||||
{
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
var transition = definition.Transitions[i];
|
||||
var modifier = TransitionModifiers[i];
|
||||
KeyedTransitionModifiers[StringReference.Get(transition.name)] = modifier;
|
||||
}
|
||||
}
|
||||
|
||||
for (int i = 0; i < definition.Aliases.Length; i++)
|
||||
{
|
||||
var alias = definition.Aliases[i];
|
||||
if (alias.Name != null &&
|
||||
TransitionModifiers.TryGet(alias.Index, out var group))
|
||||
KeyedTransitionModifiers[alias.Name.Name] = group;
|
||||
}
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>[Pro-Only] Adds the `transition` to this library.</summary>
|
||||
/// <exception cref="ArgumentException">A transition is already registered with the `key`.</exception>
|
||||
public TransitionModifierGroup AddTransition(
|
||||
object key,
|
||||
ITransition transition)
|
||||
{
|
||||
AssertStringReference(key);
|
||||
|
||||
var modifier = new TransitionModifierGroup(TransitionModifiers.Count, transition);
|
||||
KeyedTransitionModifiers.Add(key, modifier);
|
||||
TransitionModifiers.Add(modifier);
|
||||
return modifier;
|
||||
}
|
||||
|
||||
/// <summary>[Pro-Only] Adds the `transition` to this library.</summary>
|
||||
/// <exception cref="ArgumentException">A transition is already registered with the `key`.</exception>
|
||||
public TransitionModifierGroup AddTransition(
|
||||
IHasKey hasKey,
|
||||
ITransition transition)
|
||||
=> AddTransition(hasKey.Key, transition);
|
||||
|
||||
/// <summary>[Pro-Only] Adds the `transition` to this library.</summary>
|
||||
/// <exception cref="ArgumentException">A transition is already registered with the `key`.</exception>
|
||||
public TransitionModifierGroup AddTransition(
|
||||
ITransition transition)
|
||||
=> AddTransition(transition, transition);
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>[Pro-Only]
|
||||
/// Adds the `transition` to this library or replaces the existing one registered with the `key`.
|
||||
/// </summary>
|
||||
public TransitionModifierGroup SetTransition(
|
||||
object key,
|
||||
ITransition transition)
|
||||
{
|
||||
if (TryGetTransition(key, out var oldModifier))
|
||||
{
|
||||
oldModifier.Transition = transition;
|
||||
return oldModifier;
|
||||
}
|
||||
|
||||
return AddTransition(key, transition);
|
||||
}
|
||||
|
||||
/// <summary>[Pro-Only]
|
||||
/// Adds the `transition` to this library or replaces the existing one registered with the `key`.
|
||||
/// </summary>
|
||||
public TransitionModifierGroup SetTransition(
|
||||
IHasKey hasKey,
|
||||
ITransition transition)
|
||||
=> SetTransition(hasKey.Key, transition);
|
||||
|
||||
/// <summary>[Pro-Only]
|
||||
/// Adds the `transition` to this library or replaces the existing one registered with the `key`.
|
||||
/// </summary>
|
||||
public TransitionModifierGroup SetTransition(
|
||||
ITransition transition)
|
||||
=> SetTransition(transition, transition);
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>[Pro-Only]
|
||||
/// Sets the <see cref="ITransition.FadeDuration"/> to use when transitioning from `from` to `to`.
|
||||
/// </summary>
|
||||
public void SetFadeDuration(
|
||||
object from,
|
||||
ITransition to,
|
||||
float fadeDuration)
|
||||
{
|
||||
var group = SetTransition(to.Key, to);
|
||||
group.SetFadeDuration(
|
||||
from,
|
||||
fadeDuration);
|
||||
}
|
||||
|
||||
/// <summary>[Pro-Only]
|
||||
/// Sets the <see cref="ITransition.FadeDuration"/> to use when transitioning from `from` to `to`.
|
||||
/// </summary>
|
||||
public void SetFadeDuration(
|
||||
IHasKey from,
|
||||
ITransition to,
|
||||
float fadeDuration)
|
||||
=> SetFadeDuration(from.Key, to, fadeDuration);
|
||||
|
||||
/// <summary>[Pro-Only]
|
||||
/// Sets the <see cref="ITransition.FadeDuration"/> to use when transitioning from
|
||||
/// <see cref="TransitionModifierDefinition.FromIndex"/> to <see cref="TransitionModifierDefinition.ToIndex"/>.
|
||||
/// </summary>
|
||||
public bool SetFadeDuration(
|
||||
TransitionModifierDefinition modifier)
|
||||
{
|
||||
if (!TransitionModifiers.TryGet(modifier.FromIndex, out var from) ||
|
||||
!TransitionModifiers.TryGet(modifier.ToIndex, out var to))
|
||||
return false;
|
||||
|
||||
to.SetFadeDuration(
|
||||
from.Transition.Key,
|
||||
modifier.FadeDuration);
|
||||
return true;
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>[Pro-Only] Registers the `group` with another `key`.</summary>
|
||||
public void AddAlias(
|
||||
object key,
|
||||
TransitionModifierGroup group)
|
||||
{
|
||||
AssertStringReference(key);
|
||||
AssertGroup(group);
|
||||
KeyedTransitionModifiers.Add(key, group);
|
||||
}
|
||||
|
||||
/// <summary>[Pro-Only] Registers the `transition` with the `key`.</summary>
|
||||
/// <remarks>Also registers it with its <see cref="IHasKey.Key"/> if it wasn't already.</remarks>
|
||||
public TransitionModifierGroup AddAlias(
|
||||
object key,
|
||||
ITransition transition)
|
||||
{
|
||||
var group = SetTransition(transition);
|
||||
AddAlias(key, group);
|
||||
return group;
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>[Pro-Only] Adds the contents of `copyFrom` into this library.</summary>
|
||||
/// <remarks>
|
||||
/// This method adds and replaces values, but does not remove any
|
||||
/// (unlike <see cref="CopyFrom(TransitionLibrary, CloneContext)"/>.
|
||||
/// </remarks>
|
||||
public void AddLibrary(TransitionLibrary library, CloneContext context)
|
||||
{
|
||||
if (library == null)
|
||||
return;
|
||||
|
||||
for (int i = 0; i < TransitionModifiers.Count; i++)
|
||||
{
|
||||
var group = TransitionModifiers[i];
|
||||
context[group.Transition] = group;
|
||||
}
|
||||
|
||||
foreach (var group in library.KeyedTransitionModifiers)
|
||||
{
|
||||
var transition = group.Value.Transition;
|
||||
|
||||
if (context.TryGetClone(transition, out var clone) &&
|
||||
clone is TransitionModifierGroup cloneGroup)
|
||||
{
|
||||
AssertGroup(cloneGroup);
|
||||
KeyedTransitionModifiers[group.Key] = cloneGroup;
|
||||
}
|
||||
else
|
||||
{
|
||||
cloneGroup = SetTransition(group.Key, group.Value.Transition);
|
||||
cloneGroup.CopyFrom(group.Value);
|
||||
context[transition] = cloneGroup;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>[Pro-Only] Adds the contents of `copyFrom` into this library.</summary>
|
||||
/// <remarks>
|
||||
/// This method adds and replaces values, but does not remove any
|
||||
/// (unlike <see cref="CopyFrom(TransitionLibrary, CloneContext)"/>.
|
||||
/// </remarks>
|
||||
public void AddLibrary(TransitionLibrary library)
|
||||
{
|
||||
var context = CloneContext.Pool.Instance.Acquire();
|
||||
AddLibrary(library, context);
|
||||
CloneContext.Pool.Instance.Release(context);
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <inheritdoc/>
|
||||
/// <remarks>See also <see cref="AddLibrary(TransitionLibrary, CloneContext)"/>.</remarks>
|
||||
public void CopyFrom(TransitionLibrary copyFrom, CloneContext context)
|
||||
{
|
||||
Clear();
|
||||
|
||||
if (copyFrom == null)
|
||||
return;
|
||||
|
||||
var count = copyFrom.TransitionModifiers.Count;
|
||||
for (int i = 0; i < count; i++)
|
||||
TransitionModifiers.Add(copyFrom.TransitionModifiers[i].Clone(context));
|
||||
|
||||
foreach (var group in copyFrom.KeyedTransitionModifiers)
|
||||
{
|
||||
var clone = TransitionModifiers[group.Value.Index];
|
||||
KeyedTransitionModifiers.Add(group.Key, clone);
|
||||
}
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
#endregion
|
||||
/************************************************************************************************************************/
|
||||
#region Remove
|
||||
/************************************************************************************************************************/
|
||||
|
||||
// Remove from the dictionary but not the list because there might be multiple aliases for that index.
|
||||
|
||||
/// <summary>[Pro-Only] Removes the transition registered with the `key`.</summary>
|
||||
public bool RemoveTransition(object key)
|
||||
=> KeyedTransitionModifiers.Remove(key);
|
||||
|
||||
/// <summary>[Pro-Only] Removes the transition registered with the <see cref="IHasKey.Key"/>.</summary>
|
||||
public bool RemoveTransition(IHasKey hasKey)
|
||||
=> RemoveTransition(hasKey.Key);
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>[Pro-Only] Removes a modified fade duration for transitioning from `from` to `to`.</summary>
|
||||
public bool RemoveFadeDuration(object from, object to)
|
||||
=> TryGetTransition(to, out var group)
|
||||
&& group.FromKeyToFadeDuration != null
|
||||
&& group.FromKeyToFadeDuration.Remove(from);
|
||||
|
||||
/// <summary>[Pro-Only] Removes a modified fade duration for transitioning from `from` to `to`.</summary>
|
||||
public bool RemoveFadeDuration(IHasKey from, IHasKey to)
|
||||
=> RemoveFadeDuration(from.Key, to.Key);
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>[Pro-Only] Removes everything from this library, leaving it empty.</summary>
|
||||
public void Clear()
|
||||
{
|
||||
TransitionModifiers.Clear();
|
||||
KeyedTransitionModifiers.Clear();
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
#endregion
|
||||
/************************************************************************************************************************/
|
||||
#region Play
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>
|
||||
/// Calls <see cref="AnimancerLayer.Play(ITransition, float, FadeMode)"/>
|
||||
/// with the fade duration potentially modified by this library.
|
||||
/// </summary>
|
||||
public AnimancerState Play(
|
||||
AnimancerLayer layer,
|
||||
ITransition transition)
|
||||
=> layer.Play(
|
||||
transition,
|
||||
GetFadeDuration(layer, transition),
|
||||
transition.FadeMode);
|
||||
|
||||
/// <summary>
|
||||
/// Calls <see cref="AnimancerLayer.Play(ITransition, float, FadeMode)"/>
|
||||
/// with the fade duration potentially modified by this library.
|
||||
/// </summary>
|
||||
public AnimancerState Play(
|
||||
AnimancerLayer layer,
|
||||
TransitionModifierGroup transition)
|
||||
{
|
||||
var from = layer.CurrentState?.Key;
|
||||
var to = transition.Transition;
|
||||
|
||||
var fadeDuration = from != null
|
||||
? transition.GetFadeDuration(from)
|
||||
: to.FadeDuration;
|
||||
|
||||
return layer.Play(
|
||||
to,
|
||||
fadeDuration,
|
||||
to.FadeMode);
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>
|
||||
/// Plays the transition registered with the specified `key` if there is one.
|
||||
/// Otherwise, returns <c>null</c>.
|
||||
/// </summary>
|
||||
public AnimancerState TryPlay(
|
||||
AnimancerLayer layer,
|
||||
object key)
|
||||
=> TryGetTransition(key, out var transition)
|
||||
? Play(layer, transition)
|
||||
: null;
|
||||
|
||||
/************************************************************************************************************************/
|
||||
#endregion
|
||||
/************************************************************************************************************************/
|
||||
#region Assertions
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>[Assert-Conditional]
|
||||
/// Logs <see cref="OptionalWarning.StringReference"/> if the `key` is a <see cref="string"/>.
|
||||
/// </summary>
|
||||
[System.Diagnostics.Conditional(Strings.Assertions)]
|
||||
private void AssertStringReference(object key)
|
||||
{
|
||||
#if UNITY_ASSERTIONS
|
||||
if (key is string keyString)
|
||||
{
|
||||
if (StringReference.TryGet(keyString, out var keyReference) &&
|
||||
KeyedTransitionModifiers.ContainsKey(keyReference))
|
||||
Debug.LogError(
|
||||
$"{nameof(TransitionLibrary)} key type mismatch:" +
|
||||
$" attempted to use string '{keyString}'," +
|
||||
$" but that value is registered as a {nameof(StringReference)}." +
|
||||
$" Use a {nameof(StringReference)} to ensure the correct lookup.");
|
||||
else
|
||||
OptionalWarning.StringReference.Log(
|
||||
$"A string '{keyString}' is being used as a key in a {nameof(TransitionLibrary)}." +
|
||||
$" {nameof(StringReference)}s should be used instead of strings because they are more efficient" +
|
||||
$" and to avoid mismatches with aliases in a {nameof(TransitionLibraryDefinition)}.");
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>[Assert-Conditional]
|
||||
/// Asserts that the <see cref="TransitionModifierGroup.Index"/>
|
||||
/// corresponds to the <see cref="TransitionModifiers"/>.
|
||||
/// </summary>
|
||||
[System.Diagnostics.Conditional(Strings.Assertions)]
|
||||
[HideInCallstack]
|
||||
internal void AssertGroup(TransitionModifierGroup group)
|
||||
{
|
||||
#if UNITY_ASSERTIONS
|
||||
if (!TransitionModifiers.TryGet(group.Index, out var registered) ||
|
||||
registered != group)
|
||||
Debug.LogError(
|
||||
$"{nameof(CloneContext)} contains an {nameof(TransitionModifierGroup)}" +
|
||||
$" which isn't part of this {nameof(TransitionLibrary)}." +
|
||||
$" It must have been added to the context manually.");
|
||||
#endif
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
#endregion
|
||||
/************************************************************************************************************************/
|
||||
}
|
||||
}
|
||||
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: ab6185b55c780f744ada43f1408d8482
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
+99
@@ -0,0 +1,99 @@
|
||||
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2024 Kybernetik //
|
||||
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Animancer.TransitionLibraries
|
||||
{
|
||||
/// <summary>[Pro-Only]
|
||||
/// A <see cref="ScriptableObject"/> which serializes a <see cref="TransitionLibraryDefinition"/>
|
||||
/// and creates a <see cref="TransitionLibrary"/> from it at runtime.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <strong>Documentation:</strong>
|
||||
/// <see href="https://kybernetik.com.au/animancer/docs/manual/transitions/libraries">
|
||||
/// Transition Libraries</see>
|
||||
/// </remarks>
|
||||
/// https://kybernetik.com.au/animancer/api/Animancer.TransitionLibraries/TransitionLibraryAsset
|
||||
[CreateAssetMenu(
|
||||
menuName = Strings.MenuPrefix + "Transition Library",
|
||||
order = Strings.AssetMenuOrder + 0)]
|
||||
[AnimancerHelpUrl(typeof(TransitionLibraryAsset))]
|
||||
public class TransitionLibraryAsset : ScriptableObject,
|
||||
IAnimationClipSource
|
||||
{
|
||||
/************************************************************************************************************************/
|
||||
|
||||
[SerializeField]
|
||||
private TransitionLibraryDefinition _Definition;
|
||||
|
||||
/// <summary>[<see cref="SerializeField"/>]
|
||||
/// The serialized data which will be used to initialize the <see cref="Library"/> at runtime.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// If you modify the contents of this reference, either re-assign this property
|
||||
/// or call <see cref="OnDefinitionModified"/> to apply any changes to the <see cref="Library"/>.
|
||||
/// </remarks>
|
||||
public TransitionLibraryDefinition Definition
|
||||
{
|
||||
get => _Definition;
|
||||
set
|
||||
{
|
||||
_Definition = value ?? new();
|
||||
OnDefinitionModified();
|
||||
}
|
||||
}
|
||||
|
||||
#if UNITY_EDITOR
|
||||
/// <summary>[Editor-Only] [Internal]
|
||||
/// The name of the field which stores the <see cref="Definition"/>.
|
||||
/// </summary>
|
||||
internal const string DefinitionField = nameof(_Definition);
|
||||
#endif
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>The runtime <see cref="TransitionLibrary"/> created from the <see cref="Definition"/>.</summary>
|
||||
public TransitionLibrary Library { get; private set; }
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Initializes the <see cref="Library"/>.</summary>
|
||||
protected virtual void OnEnable()
|
||||
{
|
||||
_Definition ??= new();
|
||||
|
||||
Library = new();
|
||||
Library.Initialize(_Definition);
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>
|
||||
/// Adds the contents of the <see cref="Definition"/>
|
||||
/// to the <see cref="Library"/> if it was already initialized.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Call this after modifying the contents of the <see cref="Definition"/>
|
||||
/// to ensure that the <see cref="Library"/> reflects any changes.
|
||||
/// <para></para>
|
||||
/// Note that this doesn't remove anything from the <see cref="Library"/>,
|
||||
/// it only adds or replaces values.
|
||||
/// </remarks>
|
||||
public void OnDefinitionModified()
|
||||
{
|
||||
Library.Initialize(_Definition);
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Gathers all the animations in the <see cref="Definition"/> and <see cref="Library"/>.</summary>
|
||||
public void GetAnimationClips(List<AnimationClip> results)
|
||||
{
|
||||
results.GatherFromSource(_Definition);
|
||||
results.GatherFromSource(Library);
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
}
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 6f08fdd50e79d0c4bb235a5c2696516d
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
+401
@@ -0,0 +1,401 @@
|
||||
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2024 Kybernetik //
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Animancer.TransitionLibraries
|
||||
{
|
||||
/// <summary>[<see cref="SerializableAttribute"/>]
|
||||
/// A library of transitions and other details which can create a <see cref="TransitionLibrary"/>.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <strong>Documentation:</strong>
|
||||
/// <see href="https://kybernetik.com.au/animancer/docs/manual/transitions/libraries">
|
||||
/// Transition Libraries</see>
|
||||
/// </remarks>
|
||||
/// https://kybernetik.com.au/animancer/api/Animancer.TransitionLibraries/TransitionLibraryDefinition
|
||||
[Serializable]
|
||||
public class TransitionLibraryDefinition :
|
||||
IAnimationClipSource,
|
||||
ICopyable<TransitionLibraryDefinition>,
|
||||
IEquatable<TransitionLibraryDefinition>,
|
||||
IHasDescription
|
||||
{
|
||||
/************************************************************************************************************************/
|
||||
#region Fields and Properties
|
||||
/************************************************************************************************************************/
|
||||
|
||||
[SerializeField]
|
||||
private TransitionAssetBase[]
|
||||
_Transitions = Array.Empty<TransitionAssetBase>();
|
||||
|
||||
/// <summary>[<see cref="SerializeField"/>] The transitions in this library.</summary>
|
||||
/// <remarks>This property uses an empty array instead of <c>null</c>.</remarks>
|
||||
public TransitionAssetBase[] Transitions
|
||||
{
|
||||
get => _Transitions;
|
||||
set => _Transitions = value.NullIsEmpty();
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
[SerializeField]
|
||||
private TransitionModifierDefinition[]
|
||||
_Modifiers = Array.Empty<TransitionModifierDefinition>();
|
||||
|
||||
/// <summary>[<see cref="SerializeField"/>] Modified fade durations for specific transition combinations.</summary>
|
||||
/// <remarks>This property uses an empty array instead of <c>null</c>.</remarks>
|
||||
public TransitionModifierDefinition[] Modifiers
|
||||
{
|
||||
get => _Modifiers;
|
||||
set => _Modifiers = value.NullIsEmpty();
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
[SerializeField]
|
||||
private NamedIndex[]
|
||||
_Aliases = Array.Empty<NamedIndex>();
|
||||
|
||||
/// <summary>[<see cref="SerializeField"/>] Alternate names that can be used to look up transitions.</summary>
|
||||
/// <remarks>
|
||||
/// This array should always be sorted, use <see cref="SortAliases"/> if necessary.
|
||||
/// <para></para>
|
||||
/// This property uses an empty array instead of <c>null</c>.
|
||||
/// </remarks>
|
||||
public NamedIndex[] Aliases
|
||||
{
|
||||
get => _Aliases;
|
||||
set => _Aliases = value.NullIsEmpty();
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
[SerializeField]
|
||||
[Tooltip(AliasAllTransitionsTooltip)]
|
||||
private bool _AliasAllTransitions;
|
||||
|
||||
/// <summary>[<see cref="SerializeField"/>]
|
||||
/// Should all Transitions automatically be registered using their name as an Alias?
|
||||
/// </summary>
|
||||
public ref bool AliasAllTransitions
|
||||
=> ref _AliasAllTransitions;
|
||||
|
||||
#if UNITY_EDITOR
|
||||
/// <summary>[Editor-Only] [Internal]
|
||||
/// The name of the field which stores the <see cref="AliasAllTransitions"/>.
|
||||
/// </summary>
|
||||
internal const string AliasAllTransitionsField = nameof(_AliasAllTransitions);
|
||||
#endif
|
||||
|
||||
/// <summary>Tooltip for the <see cref="AliasAllTransitions"/> field.</summary>
|
||||
public const string AliasAllTransitionsTooltip =
|
||||
"Should all Transitions automatically be registered using their name as an Alias?";
|
||||
|
||||
/************************************************************************************************************************/
|
||||
#endregion
|
||||
/************************************************************************************************************************/
|
||||
#region Transitions
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>
|
||||
/// <see cref="AnimancerUtilities.TryGet{T}(IList{T}, int, out T)"/> for the <see cref="Transitions"/>.
|
||||
/// </summary>
|
||||
public bool TryGetTransition(
|
||||
int index,
|
||||
out TransitionAssetBase transition)
|
||||
=> _Transitions.TryGet(index, out transition)
|
||||
&& transition != null;
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Adds an item to the end of the <see cref="Transitions"/>.</summary>
|
||||
public void AddTransition(
|
||||
TransitionAssetBase transition)
|
||||
=> AnimancerUtilities.InsertAt(
|
||||
ref _Transitions,
|
||||
_Transitions.Length,
|
||||
transition);
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>
|
||||
/// Removes an item from the <see cref="Transitions"/>
|
||||
/// and adjusts the other fields to account for the moved indices.
|
||||
/// </summary>
|
||||
public void RemoveTransition(int index)
|
||||
{
|
||||
if ((uint)index >= _Transitions.Length)
|
||||
return;
|
||||
|
||||
AnimancerUtilities.RemoveAt(ref _Transitions, index);
|
||||
|
||||
for (int i = _Modifiers.Length - 1; i >= 0; i--)
|
||||
{
|
||||
var modifier = _Modifiers[i];
|
||||
|
||||
// Remove any modifiers targeting that transition.
|
||||
if (modifier.FromIndex == index ||
|
||||
modifier.ToIndex == index)
|
||||
{
|
||||
AnimancerUtilities.RemoveAt(ref _Modifiers, i);
|
||||
}
|
||||
else// Adjust the indices of any modifiers after it.
|
||||
{
|
||||
var fromIndex = modifier.FromIndex;
|
||||
if (fromIndex > index)
|
||||
fromIndex--;
|
||||
|
||||
var toIndex = modifier.ToIndex;
|
||||
if (toIndex > index)
|
||||
toIndex--;
|
||||
|
||||
_Modifiers[i] = modifier.WithIndices(fromIndex, toIndex);
|
||||
}
|
||||
}
|
||||
|
||||
for (int i = _Aliases.Length - 1; i >= 0; i--)
|
||||
{
|
||||
var alias = _Aliases[i];
|
||||
|
||||
// Remove any aliases targeting that transition.
|
||||
if (alias.Index == index)
|
||||
{
|
||||
AnimancerUtilities.RemoveAt(ref _Aliases, i);
|
||||
}
|
||||
else// Adjust the indices of any aliases after it.
|
||||
{
|
||||
if (alias.Index > index)
|
||||
_Aliases[i] = alias.With(alias.Index - 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
#endregion
|
||||
/************************************************************************************************************************/
|
||||
#region Modifiers
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Tries to find an item in the <see cref="Modifiers"/> with the specified indices.</summary>
|
||||
/// <remarks>
|
||||
/// If unsuccessful, the `modifier` is given the <see cref="ITransition.FadeDuration"/>
|
||||
/// from the <see cref="Transitions"/> at the `toIndex`. and this method returns false.
|
||||
/// </remarks>
|
||||
public bool TryGetModifier(
|
||||
int fromIndex,
|
||||
int toIndex,
|
||||
out TransitionModifierDefinition modifier)
|
||||
{
|
||||
var index = IndexOfModifier(fromIndex, toIndex);
|
||||
if (index >= 0)
|
||||
{
|
||||
modifier = _Modifiers[index];
|
||||
return true;
|
||||
}
|
||||
|
||||
var fadeDuration = TryGetTransition(toIndex, out var transition)
|
||||
? transition.TryGetFadeDuration()
|
||||
: float.NaN;
|
||||
modifier = new(fromIndex, toIndex, fadeDuration);
|
||||
return false;
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>
|
||||
/// Returns the index in the <see cref="Modifiers"/> which matches the given
|
||||
/// <see cref="TransitionModifierDefinition.FromIndex"/> and
|
||||
/// <see cref="TransitionModifierDefinition.ToIndex"/> or -1 if no such item exists.
|
||||
/// </summary>
|
||||
public int IndexOfModifier(int fromIndex, int toIndex)
|
||||
{
|
||||
for (int i = _Modifiers.Length - 1; i >= 0; i--)
|
||||
{
|
||||
var modifier = _Modifiers[i];
|
||||
if (modifier.FromIndex == fromIndex &&
|
||||
modifier.ToIndex == toIndex)
|
||||
return i;
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Adds or replaces an item in the <see cref="Modifiers"/>.</summary>
|
||||
public void SetModifier(
|
||||
TransitionModifierDefinition modifier)
|
||||
{
|
||||
if (float.IsNaN(modifier.FadeDuration))
|
||||
{
|
||||
RemoveModifier(modifier);
|
||||
return;
|
||||
}
|
||||
|
||||
if (modifier.FadeDuration < 0)
|
||||
modifier = modifier.WithFadeDuration(0);
|
||||
|
||||
var index = IndexOfModifier(modifier.FromIndex, modifier.ToIndex);
|
||||
if (index >= 0)
|
||||
{
|
||||
_Modifiers[index] = modifier;
|
||||
}
|
||||
else
|
||||
{
|
||||
AnimancerUtilities.InsertAt(ref _Modifiers, _Modifiers.Length, modifier);
|
||||
}
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Removes an item from the <see cref="Modifiers"/>.</summary>
|
||||
public bool RemoveModifier(
|
||||
TransitionModifierDefinition modifier)
|
||||
=> RemoveModifier(modifier.FromIndex, modifier.ToIndex);
|
||||
|
||||
/// <summary>Removes an item from the <see cref="Modifiers"/>.</summary>
|
||||
public bool RemoveModifier(int fromIndex, int toIndex)
|
||||
{
|
||||
var index = IndexOfModifier(fromIndex, toIndex);
|
||||
if (index < 0)
|
||||
return false;
|
||||
|
||||
AnimancerUtilities.RemoveAt(ref _Modifiers, index);
|
||||
return true;
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
#endregion
|
||||
/************************************************************************************************************************/
|
||||
#region Aliases
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Adds an item to the <see cref="Aliases"/>, sorted by its values.</summary>
|
||||
public int AddAlias(NamedIndex alias)
|
||||
{
|
||||
int i = 0;
|
||||
for (; i < _Aliases.Length; i++)
|
||||
if (alias.CompareTo(_Aliases[i]) <= 0)
|
||||
break;
|
||||
|
||||
AnimancerUtilities.InsertAt(ref _Aliases, i, alias);
|
||||
return i;
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Removes an item from the <see cref="Aliases"/>.</summary>
|
||||
public bool RemoveAlias(NamedIndex alias)
|
||||
{
|
||||
var index = Array.IndexOf(_Aliases, alias);
|
||||
if (index < 0)
|
||||
return false;
|
||||
|
||||
RemoveAlias(index);
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>Removes an item from the <see cref="Aliases"/>.</summary>
|
||||
public void RemoveAlias(int index)
|
||||
=> AnimancerUtilities.RemoveAt(ref _Aliases, index);
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Ensures that the <see cref="Aliases"/> are sorted.</summary>
|
||||
/// <remarks>This method shouldn't need to be called manually since aliases are always added in order.</remarks>
|
||||
public void SortAliases()
|
||||
=> Array.Sort(_Aliases, (a, b) => a.CompareTo(b));
|
||||
|
||||
/************************************************************************************************************************/
|
||||
#endregion
|
||||
/************************************************************************************************************************/
|
||||
#region Equality
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Are all fields in this object equal to the equivalent in `obj`?</summary>
|
||||
public override bool Equals(object obj)
|
||||
=> Equals(obj as TransitionLibraryDefinition);
|
||||
|
||||
/// <summary>Are all fields in this object equal to the equivalent fields in `other`?</summary>
|
||||
public bool Equals(TransitionLibraryDefinition other)
|
||||
=> other != null
|
||||
&& AnimancerUtilities.ContentsAreEqual(_Transitions, other._Transitions)
|
||||
&& AnimancerUtilities.ContentsAreEqual(_Modifiers, other._Modifiers)
|
||||
&& AnimancerUtilities.ContentsAreEqual(_Aliases, other._Aliases)
|
||||
&& _AliasAllTransitions == other._AliasAllTransitions;
|
||||
|
||||
/// <summary>Are all fields in `a` equal to the equivalent fields in `b`?</summary>
|
||||
public static bool operator ==(TransitionLibraryDefinition a, TransitionLibraryDefinition b)
|
||||
=> a is null
|
||||
? b is null
|
||||
: a.Equals(b);
|
||||
|
||||
/// <summary>Are any fields in `a` not equal to the equivalent fields in `b`?</summary>
|
||||
public static bool operator !=(TransitionLibraryDefinition a, TransitionLibraryDefinition b)
|
||||
=> !(a == b);
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Returns a hash code based on the values of this object's fields.</summary>
|
||||
public override int GetHashCode()
|
||||
=> AnimancerUtilities.Hash(-871379578,
|
||||
_Transitions.SafeGetHashCode(),
|
||||
_Modifiers.SafeGetHashCode(),
|
||||
_Aliases.SafeGetHashCode(),
|
||||
_AliasAllTransitions.SafeGetHashCode());
|
||||
|
||||
/************************************************************************************************************************/
|
||||
#endregion
|
||||
/************************************************************************************************************************/
|
||||
#region Other
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <summary>Gathers all the animations in this definition.</summary>
|
||||
public void GetAnimationClips(List<AnimationClip> results)
|
||||
=> results.GatherFromSource(_Transitions);
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void CopyFrom(TransitionLibraryDefinition copyFrom, CloneContext context)
|
||||
{
|
||||
AnimancerUtilities.CopyExactArray(copyFrom._Transitions, ref _Transitions);
|
||||
AnimancerUtilities.CopyExactArray(copyFrom._Modifiers, ref _Modifiers);
|
||||
AnimancerUtilities.CopyExactArray(copyFrom._Aliases, ref _Aliases);
|
||||
_AliasAllTransitions = copyFrom._AliasAllTransitions;
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void AppendDescription(StringBuilder text, string separator = "\n")
|
||||
{
|
||||
text.Append(GetType().Name);
|
||||
|
||||
if (!separator.StartsWithNewLine())
|
||||
separator = "\n" + separator;
|
||||
|
||||
var indentedSeparator = separator + Strings.Indent;
|
||||
|
||||
text.AppendField(separator, nameof(Transitions), Transitions.Length);
|
||||
for (int i = 0; i < Transitions.Length; i++)
|
||||
text.AppendField(indentedSeparator, i.ToString(), Transitions[i]);
|
||||
|
||||
text.AppendField(separator, nameof(Modifiers), Modifiers.Length);
|
||||
for (int i = 0; i < Modifiers.Length; i++)
|
||||
text.AppendField(indentedSeparator, i.ToString(), Modifiers[i]);
|
||||
|
||||
text.AppendField(separator, nameof(Aliases), Aliases.Length);
|
||||
for (int i = 0; i < Aliases.Length; i++)
|
||||
text.AppendField(indentedSeparator, i.ToString(), Aliases[i]);
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
#endregion
|
||||
/************************************************************************************************************************/
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user