chore: initial commit

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

View File

@@ -0,0 +1,199 @@
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
#if UNITY_EDITOR
using System.Collections.Generic;
using UnityEditor;
using UnityEngine;
using Object = UnityEngine.Object;
namespace Animancer.Editor
{
/// <summary>[Editor-Only]
/// Keeps track of <see cref="AnimancerGraph"/> instances
/// to ensure that they're properly cleaned up.
/// </summary>
/// https://kybernetik.com.au/animancer/api/Animancer.Editor/AnimancerGraphCleanup
public static class AnimancerGraphCleanup
{
/************************************************************************************************************************/
private static List<AnimancerGraph> _AllGraphs;
/// <summary>[Editor-Only] Registers a `graph` to make sure it gets cleaned up properly.</summary>
public static void AddGraph(AnimancerGraph graph)
{
if (_AllGraphs == null)
{
_AllGraphs = new();
AssemblyReloadEvents.beforeAssemblyReload +=
() => DestroyAll(EditorApplication.isPlaying);
EditorApplication.playModeStateChanged += change =>
{
switch (change)
{
case PlayModeStateChange.EnteredEditMode:
DestroyAll(true);
break;
case PlayModeStateChange.ExitingEditMode:
DestroyAll(false);
break;
}
};
}
else
{
EditModeDestroyOldInstances();
}
_AllGraphs.Add(graph);
}
/// <summary>[Editor-Only] Removes the `graph` from the list of instances.</summary>
public static void RemoveGraph(AnimancerGraph graph)
{
_AllGraphs?.Remove(graph);
AnimancerGraph.ClearInactiveInitializationStackTrace(graph);
}
/************************************************************************************************************************/
private static void DestroyAll(bool isPlaying)
{
for (int i = _AllGraphs.Count - 1; i >= 0; i--)
{
var graph = _AllGraphs[i];
if (graph.IsValidOrDispose())
{
if (isPlaying && graph.InactiveInitializationStackTrace != null)
{
Debug.LogWarning(
$"{graph} was not properly destroyed." +
$" Its {nameof(GameObject)} was inactive and never activated," +
$" meaning that Unity didn't call its AnimancerComponent.OnDestroy." +
$"\n\nIf you need to use Animancer on an object that never gets activated," +
$" you must call animancerComponent.Graph.Destroy() on it manually." +
$"\n\nThis graph was created:\n{graph.InactiveInitializationStackTrace}\n",
graph.Component as Object);
AnimancerGraph.ClearInactiveInitializationStackTrace(graph);
}
graph.Destroy();
}
}
_AllGraphs.Clear();
}
/************************************************************************************************************************/
private static void EditModeDestroyOldInstances()
{
if (EditorApplication.isPlayingOrWillChangePlaymode)
return;
for (int i = _AllGraphs.Count - 1; i >= 0; i--)
{
var graph = _AllGraphs[i];
if (!ShouldStayAlive(graph))
{
if (graph.IsValidOrDispose())
graph.Destroy();// This will remove it.
else
_AllGraphs.RemoveAt(i);
}
}
}
/************************************************************************************************************************/
/// <summary>Should this graph should stay alive instead of being destroyed?</summary>
private static bool ShouldStayAlive(AnimancerGraph graph)
{
if (!graph.IsValidOrDispose())
return false;
if (graph.Component == null)
return true;
if (graph.Component is Object obj && obj == null)
return false;
if (graph.Component.Animator == null)
return false;
return true;
}
/************************************************************************************************************************/
/// <summary>[Editor-Only]
/// Returns true if the `initial` mode was <see cref="AnimatorUpdateMode.AnimatePhysics"/>
/// and the `current` has changed to another mode or if the `initial` mode was something else
/// and the `current` has changed to <see cref="AnimatorUpdateMode.AnimatePhysics"/>.
/// </summary>
public static bool HasChangedToOrFromAnimatePhysics(AnimatorUpdateMode? initial, AnimatorUpdateMode current)
{
if (initial == null)
return false;
#if UNITY_2023_1_OR_NEWER
var wasAnimatePhysics = initial.Value == AnimatorUpdateMode.Fixed;
var isAnimatePhysics = current == AnimatorUpdateMode.Fixed;
#else
var wasAnimatePhysics = initial.Value == AnimatorUpdateMode.AnimatePhysics;
var isAnimatePhysics = current == AnimatorUpdateMode.AnimatePhysics;
#endif
return wasAnimatePhysics != isAnimatePhysics;
}
/************************************************************************************************************************/
}
}
namespace Animancer
{
/// https://kybernetik.com.au/animancer/api/Animancer/AnimancerGraph
public partial class AnimancerGraph
{
/************************************************************************************************************************/
/// <summary>[Editor-Only] [Internal]
/// A stack trace captured in <see cref="Initialize(IAnimancerComponent, bool)"/>
/// if the <see cref="GameObject.activeInHierarchy"/> is false.
/// </summary>
/// <remarks>
/// This is used to warn if the graph isn't destroyed
/// because Unity won't call <c>OnDestroy</c> if the object is never activated.
/// </remarks>
internal System.Diagnostics.StackTrace InactiveInitializationStackTrace { get; private set; }
/************************************************************************************************************************/
/// <summary>[Editor-Only] Captures the <see cref="InactiveInitializationStackTrace"/>.</summary>
private void CaptureInactiveInitializationStackTrace(IAnimancerComponent animancer)
{
if (!animancer.gameObject.activeInHierarchy &&
EditorApplication.isPlayingOrWillChangePlaymode)
InactiveInitializationStackTrace = new(1, true);
}
/************************************************************************************************************************/
/// <summary>[Editor-Only] [Internal] Discards the <see cref="InactiveInitializationStackTrace"/>.</summary>
public static void ClearInactiveInitializationStackTrace(AnimancerGraph graph)
{
graph.InactiveInitializationStackTrace = null;
}
/************************************************************************************************************************/
}
}
#endif

View File

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

View File

@@ -0,0 +1,410 @@
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
using System;
using System.Collections.Generic;
using System.Reflection;
using System.Runtime.Serialization;
using System.Text;
using UnityEngine;
namespace Animancer
{
/// <summary>Reflection utilities used throughout Animancer.</summary>
/// https://kybernetik.com.au/animancer/api/Animancer/AnimancerReflection
public static class AnimancerReflection
{
/************************************************************************************************************************/
/// <summary>Commonly used <see cref="BindingFlags"/> combinations.</summary>
public const BindingFlags
AnyAccessBindings = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static,
InstanceBindings = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance,
StaticBindings = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static;
/************************************************************************************************************************/
/// <summary>
/// Creates a new instance of the `type` using its parameterless constructor if it has one or a fully
/// uninitialized object if it doesn't. Or returns <c>null</c> if the <see cref="Type.IsAbstract"/>.
/// </summary>
public static object CreateDefaultInstance(Type type)
{
if (type == null ||
type.IsAbstract)
return default;
var constructor = type.GetConstructor(InstanceBindings, null, Type.EmptyTypes, null);
if (constructor != null)
return constructor.Invoke(null);
return FormatterServices.GetUninitializedObject(type);
}
/// <summary>
/// Creates a <typeparamref name="T"/> using its parameterless constructor if it has one or a fully
/// uninitialized object if it doesn't. Or returns <c>null</c> if the <see cref="Type.IsAbstract"/>.
/// </summary>
public static T CreateDefaultInstance<T>()
=> (T)CreateDefaultInstance(typeof(T));
/************************************************************************************************************************/
/// <summary>[Animancer Extension]
/// Returns the first <typeparamref name="TAttribute"/> attribute on the `member`
/// or <c>null</c> if there is none.
/// </summary>
public static TAttribute GetAttribute<TAttribute>(
this ICustomAttributeProvider member,
bool inherit = false)
where TAttribute : class
{
var type = typeof(TAttribute);
return member.IsDefined(type, inherit)
? (TAttribute)member.GetCustomAttributes(type, inherit)[0]
: null;
}
/************************************************************************************************************************/
/// <summary>Invokes a method with the specified `methodName` if it exists on the `obj`.</summary>
[Obfuscation(Exclude = true)]// Obfuscation seems to break IL2CPP Android builds here.
public static object TryInvoke(
object obj,
string methodName,
BindingFlags bindings = InstanceBindings | BindingFlags.FlattenHierarchy,
Type[] parameterTypes = null,
object[] parameters = null)
{
if (obj == null)
return null;
parameterTypes ??= Type.EmptyTypes;
var method = obj.GetType().GetMethod(methodName, bindings, null, parameterTypes, null);
return method?.Invoke(obj, parameters);
}
/************************************************************************************************************************/
#region Delegates
/************************************************************************************************************************/
/// <summary>Returns a string describing the details of the `method`.</summary>
public static string ToStringDetailed<T>(
this T method,
bool includeType = false)
where T : Delegate
{
var text = StringBuilderPool.Instance.Acquire();
text.AppendDelegate(method, includeType);
return text.ReleaseToString();
}
/// <summary>Appends the details of the `method` to the `text`.</summary>
public static StringBuilder AppendDelegate<T>(
this StringBuilder text,
T method,
bool includeType = false)
where T : Delegate
{
var type = method != null
? method.GetType()
: typeof(T);
if (method == null)
{
return includeType
? text.Append("Null(")
.Append(type.GetNameCS())
.Append(')')
: text.Append("Null");
}
if (includeType)
text.Append(type.GetNameCS())
.Append('(');
if (method.Target != null)
text.Append("Method: ");
text.Append(method.Method.DeclaringType.GetNameCS())
.Append('.')
.Append(method.Method.Name);
if (method.Target != null)
text.Append(", Target: '")
.Append(method.Target)
.Append("'");
if (includeType)
text.Append(')');
return text;
}
/************************************************************************************************************************/
/// <summary>Returns the `method`'s <c>DeclaringType.Name</c>.</summary>
public static string GetFullName(MethodInfo method)
=> $"{method.DeclaringType.Name}.{method.Name}";
/************************************************************************************************************************/
private static FieldInfo _DelegatesField;
private static bool _GotDelegatesField;
/// <summary>
/// Uses reflection to achieve the same as <see cref="Delegate.GetInvocationList"/> without allocating
/// garbage every time.
/// <list type="bullet">
/// <item>If the delegate is <c>null</c> or , this method returns <c>false</c> and outputs <c>null</c>.</item>
/// <item>If the underlying <c>delegate</c> field was not found, this method returns <c>false</c> and outputs <c>null</c>.</item>
/// <item>If the delegate is not multicast, this method this method returns <c>true</c> and outputs <c>null</c>.</item>
/// <item>If the delegate is multicast, this method this method returns <c>true</c> and outputs its invocation list.</item>
/// </list>
/// </summary>
public static bool TryGetInvocationListNonAlloc(MulticastDelegate multicast, out Delegate[] delegates)
{
if (multicast == null)
{
delegates = null;
return false;
}
if (!_GotDelegatesField)
{
const string FieldName = "delegates";
_GotDelegatesField = true;
_DelegatesField = typeof(MulticastDelegate).GetField("delegates",
BindingFlags.Public |
BindingFlags.NonPublic |
BindingFlags.Instance);
if (_DelegatesField != null && _DelegatesField.FieldType != typeof(Delegate[]))
_DelegatesField = null;
if (_DelegatesField == null)
Debug.LogError($"Unable to find {nameof(MulticastDelegate)}.{FieldName} field.");
}
if (_DelegatesField == null)
{
delegates = null;
return false;
}
else
{
delegates = (Delegate[])_DelegatesField.GetValue(multicast);
return true;
}
}
/************************************************************************************************************************/
/// <summary>
/// Tries to use <see cref="TryGetInvocationListNonAlloc"/>.
/// Otherwise uses the regular <see cref="MulticastDelegate.GetInvocationList"/>.
/// </summary>
public static Delegate[] GetInvocationList(MulticastDelegate multicast)
=> TryGetInvocationListNonAlloc(multicast, out var delegates) && delegates != null
? delegates
: multicast?.GetInvocationList();
/************************************************************************************************************************/
#endregion
/************************************************************************************************************************/
#region Type Names
/************************************************************************************************************************/
private static readonly Dictionary<Type, string>
TypeNames = new()
{
{ typeof(object), "object" },
{ typeof(void), "void" },
{ typeof(bool), "bool" },
{ typeof(byte), "byte" },
{ typeof(sbyte), "sbyte" },
{ typeof(char), "char" },
{ typeof(string), "string" },
{ typeof(short), "short" },
{ typeof(int), "int" },
{ typeof(long), "long" },
{ typeof(ushort), "ushort" },
{ typeof(uint), "uint" },
{ typeof(ulong), "ulong" },
{ typeof(float), "float" },
{ typeof(double), "double" },
{ typeof(decimal), "decimal" },
};
private static readonly Dictionary<Type, string>
FullTypeNames = new(TypeNames);
/************************************************************************************************************************/
/// <summary>Returns the name of the `type` as it would appear in C# code.</summary>
/// <remarks>
/// Returned values are stored in a dictionary to speed up repeated use.
/// <para></para>
/// <strong>Example:</strong>
/// <para></para>
/// <c>typeof(List&lt;float&gt;).FullName</c> would give you:
/// <para></para>
/// <c>System.Collections.Generic.List`1[[System.Single, mscorlib, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089]]</c>
/// <para></para>
/// This method would instead return <c>System.Collections.Generic.List&lt;float&gt;</c> if `fullName` is <c>true</c>, or
/// just <c>List&lt;float&gt;</c> if it is <c>false</c>.
/// </remarks>
public static string GetNameCS(this Type type, bool fullName = true)
{
if (type == null)
return "null";
// Check if we have already got the name for that type.
var names = fullName
? FullTypeNames
: TypeNames;
if (names.TryGetValue(type, out var name))
return name;
var text = StringBuilderPool.Instance.Acquire();
if (type.IsArray)// Array = TypeName[].
{
text.Append(type.GetElementType().GetNameCS(fullName));
text.Append('[');
var dimensions = type.GetArrayRank();
while (dimensions-- > 1)
text.Append(',');
text.Append(']');
goto Return;
}
if (type.IsPointer)// Pointer = TypeName*.
{
text.Append(type.GetElementType().GetNameCS(fullName));
text.Append('*');
goto Return;
}
if (type.IsGenericParameter)// Generic Parameter = TypeName (for unspecified generic parameters).
{
text.Append(type.Name);
goto Return;
}
var underlyingType = Nullable.GetUnderlyingType(type);
if (underlyingType != null)// Nullable = TypeName != null ?
{
text.Append(underlyingType.GetNameCS(fullName));
text.Append('?');
goto Return;
}
// Other Type = Namespace.NestedTypes.TypeName<GenericArguments>.
if (fullName && type.Namespace != null)// Namespace.
{
text.Append(type.Namespace);
text.Append('.');
}
var genericArguments = 0;
if (type.DeclaringType != null)// Account for Nested Types.
{
// Count the nesting level.
var nesting = 1;
var declaringType = type.DeclaringType;
while (declaringType.DeclaringType != null)
{
declaringType = declaringType.DeclaringType;
nesting++;
}
// Append the name of each outer type, starting from the outside.
while (nesting-- > 0)
{
// Walk out to the current nesting level.
// This avoids the need to make a list of types in the nest or to insert type names instead of appending them.
declaringType = type;
for (int i = nesting; i >= 0; i--)
declaringType = declaringType.DeclaringType;
// Nested Type Name.
genericArguments = AppendNameAndGenericArguments(text, declaringType, fullName, genericArguments);
text.Append('.');
}
}
// Type Name.
AppendNameAndGenericArguments(text, type, fullName, genericArguments);
Return:// Remember and return the name.
name = text.ReleaseToString();
names.Add(type, name);
return name;
}
/************************************************************************************************************************/
/// <summary>Appends the generic arguments of `type` (after skipping the specified number).</summary>
public static int AppendNameAndGenericArguments(StringBuilder text, Type type, bool fullName = true, int skipGenericArguments = 0)
{
var name = type.Name;
text.Append(name);
if (type.IsGenericType)
{
var backQuote = name.IndexOf('`');
if (backQuote >= 0)
{
text.Length -= name.Length - backQuote;
var genericArguments = type.GetGenericArguments();
if (skipGenericArguments < genericArguments.Length)
{
text.Append('<');
var firstArgument = genericArguments[skipGenericArguments];
skipGenericArguments++;
if (firstArgument.IsGenericParameter)
{
while (skipGenericArguments < genericArguments.Length)
{
text.Append(',');
skipGenericArguments++;
}
}
else
{
text.Append(firstArgument.GetNameCS(fullName));
while (skipGenericArguments < genericArguments.Length)
{
text.Append(", ");
text.Append(genericArguments[skipGenericArguments].GetNameCS(fullName));
skipGenericArguments++;
}
}
text.Append('>');
}
}
}
return skipGenericArguments;
}
/************************************************************************************************************************/
#endregion
/************************************************************************************************************************/
}
}

View File

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

View File

@@ -0,0 +1,336 @@
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
#if UNITY_EDITOR
using System;
using System.Collections;
using System.Collections.Generic;
using System.Reflection;
using UnityEngine;
namespace Animancer.Editor
{
/// <summary>[Editor-Only]
/// A system that procedurally gathers animations throughout the hierarchy without needing explicit references.
/// </summary>
/// https://kybernetik.com.au/animancer/api/Animancer.Editor/AnimationGatherer
///
public class AnimationGatherer : IAnimationClipCollection
{
/************************************************************************************************************************/
#region Fields and Accessors
/************************************************************************************************************************/
/// <summary>All the <see cref="AnimationClip"/>s that have been gathered.</summary>
public readonly HashSet<AnimationClip> Clips = new();
/// <summary>All the <see cref="ITransition"/>s that have been gathered.</summary>
public readonly HashSet<ITransition> Transitions = new();
/************************************************************************************************************************/
/// <inheritdoc/>
public void GatherAnimationClips(ICollection<AnimationClip> clips)
{
try
{
foreach (var clip in Clips)
clips.Add(clip);
foreach (var transition in Transitions)
clips.GatherFromSource(transition);
}
catch (Exception exception)
{
HandleException(exception);
}
}
/************************************************************************************************************************/
#endregion
/************************************************************************************************************************/
#region Cache
/************************************************************************************************************************/
private static readonly Dictionary<GameObject, AnimationGatherer>
ObjectToGatherer = new();
/************************************************************************************************************************/
static AnimationGatherer()
{
UnityEditor.Selection.selectionChanged += ClearCache;
}
/************************************************************************************************************************/
/// <summary>Clears all cached gatherers.</summary>
public static void ClearCache()
=> ObjectToGatherer.Clear();
/************************************************************************************************************************/
#endregion
/************************************************************************************************************************/
/// <summary>
/// Should exceptions thrown while gathering animations be logged?
/// Default is false to ignore them.
/// </summary>
public static bool LogExceptions { get; set; }
/// <summary>
/// Logs the `exception` if <see cref="LogExceptions"/> is true.
/// Otherwise does nothing.
/// </summary>
private static void HandleException(Exception exception)
{
if (LogExceptions)
Debug.LogException(exception);
}
/************************************************************************************************************************/
/// <summary>
/// Returns a cached <see cref="AnimationGatherer"/> containing any <see cref="AnimationClip"/>s
/// referenced by components in the same hierarchy as the `gameObject`.
/// See <see cref="ICharacterRoot"/> for details.
/// </summary>
public static AnimationGatherer GatherFromGameObject(GameObject gameObject)
{
using var _ = AnimationGathererRecursionGuard.Begin();
if (AnimationGathererRecursionGuard.HasCheckedObject(gameObject))
return null;
try
{
if (!ObjectToGatherer.TryGetValue(gameObject, out var gatherer))
{
gatherer = new();
ObjectToGatherer.Add(gameObject, gatherer);
gatherer.GatherFromComponents(gameObject);
}
return gatherer;
}
catch (Exception exception)
{
HandleException(exception);
return null;
}
}
/// <summary>
/// Fills the `clips` with any <see cref="AnimationClip"/>s
/// referenced by components in the same hierarchy as the `gameObject`.
/// See <see cref="ICharacterRoot"/> for details.
/// </summary>
public static void GatherFromGameObject(GameObject gameObject, ICollection<AnimationClip> clips)
{
var gatherer = GatherFromGameObject(gameObject);
gatherer?.GatherAnimationClips(clips);
}
/// <summary>
/// Fills the `clips` with any <see cref="AnimationClip"/>s
/// referenced by components in the same hierarchy as the `gameObject`.
/// See <see cref="ICharacterRoot"/> for details.
/// </summary>
public static void GatherFromGameObject(GameObject gameObject, ref AnimationClip[] clips, bool sort)
{
var gatherer = GatherFromGameObject(gameObject);
if (gatherer == null)
return;
using (SetPool<AnimationClip>.Instance.Acquire(out var clipSet))
{
gatherer.GatherAnimationClips(clipSet);
AnimancerUtilities.SetLength(ref clips, clipSet.Count);
clipSet.CopyTo(clips);
}
if (sort)
Array.Sort(clips, (a, b) => a.GetCachedName().CompareTo(b.GetCachedName()));
}
/************************************************************************************************************************/
private void GatherFromComponents(GameObject gameObject)
{
var root = AnimancerUtilities.FindRoot(gameObject);
using (ListPool<MonoBehaviour>.Instance.Acquire(out var components))
{
root.GetComponentsInChildren(true, components);
GatherFromComponents(components);
}
}
/************************************************************************************************************************/
private void GatherFromComponents(List<MonoBehaviour> components)
{
var i = components.Count;
GatherClips:
try
{
while (--i >= 0)
{
GatherFromObject(components[i], 0);
}
}
catch (Exception exception)
{
HandleException(exception);
goto GatherClips;
}
}
/************************************************************************************************************************/
/// <summary>Gathers all animations from the `source`s fields.</summary>
private void GatherFromObject(object source, int depth)
{
if (source.IsNullOrDestroyed())
return;
if (AnimationGathererRecursionGuard.HasCheckedObject(source))
return;
if (source is AnimationClip clip)
{
Clips.Add(clip);
return;
}
if (!MightContainAnimations(source.GetType()))
return;
try
{
if (Clips.GatherFromSource(source))
return;
}
catch (Exception exception)
{
HandleException(exception);
}
GatherFromFields(source, depth);
}
/************************************************************************************************************************/
/// <summary>Types mapped to a delegate that can quickly gather their clips.</summary>
private static readonly Dictionary<Type, Action<object, AnimationGatherer>>
TypeToGathererDelegate = new();
/// <summary>
/// Uses reflection to gather <see cref="AnimationClip"/>s from fields on the `source` object.
/// </summary>
private void GatherFromFields(object source, int depth)
{
if (depth >= AnimationGathererRecursionGuard.MaxFieldDepth ||
source.IsNullOrDestroyed())
return;
var type = source.GetType();
if (!TypeToGathererDelegate.TryGetValue(type, out var gatherClips))
{
gatherClips = BuildClipGathererDelegate(type, depth);
TypeToGathererDelegate.Add(type, gatherClips);
if (gatherClips == null)
AnimationGathererRecursionGuard.DontGatherFrom.Add(type);
}
gatherClips?.Invoke(source, this);
}
/************************************************************************************************************************/
/// <summary>
/// Creates a delegate to gather <see cref="AnimationClip"/>s
/// from all relevant fields in a given `type`.
/// </summary>
private static Action<object, AnimationGatherer> BuildClipGathererDelegate(Type type, int depth)
{
if (!MightContainAnimations(type))
return null;
Action<object, AnimationGatherer> gathererDelegate = null;
while (type != null)
{
var fields = type.GetFields(AnimancerReflection.InstanceBindings | BindingFlags.DeclaredOnly);
for (int i = 0; i < fields.Length; i++)
{
var field = fields[i];
var fieldType = field.FieldType;
if (!MightContainAnimations(fieldType))
continue;
if (fieldType == typeof(AnimationClip))
{
gathererDelegate += (obj, gatherer) =>
{
var clip = (AnimationClip)field.GetValue(obj);
if (clip != null)
gatherer.Clips.Add(clip);
};
}
else if (typeof(IAnimationClipSource).IsAssignableFrom(fieldType) ||
typeof(IAnimationClipCollection).IsAssignableFrom(fieldType))
{
gathererDelegate += (obj, gatherer) =>
{
var source = field.GetValue(obj);
gatherer.Clips.GatherFromSource(source);
};
}
else if (typeof(ICollection).IsAssignableFrom(fieldType))
{
gathererDelegate += (obj, gatherer) =>
{
var collection = (ICollection)field.GetValue(obj);
if (collection != null)
{
foreach (var item in collection)
{
gatherer.GatherFromObject(item, depth + 1);
}
}
};
}
else
{
gathererDelegate += (obj, gatherer) =>
{
var source = field.GetValue(obj);
if (source.IsNullOrDestroyed())
return;
gatherer.GatherFromObject(source, depth + 1);
};
}
}
type = type.BaseType;
}
return gathererDelegate;
}
/************************************************************************************************************************/
private static bool MightContainAnimations(Type type)
=> !type.IsPrimitive
&& !type.IsEnum
&& !type.IsAutoClass
&& !type.IsPointer
&& !AnimationGathererRecursionGuard.DontGatherFrom.Contains(type);
/************************************************************************************************************************/
}
}
#endif

View File

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

View File

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

View File

@@ -0,0 +1,53 @@
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
using System;
using UnityEngine;
namespace Animancer
{
/// <summary>[Assert-Conditional]
/// A <see cref="HelpURLAttribute"/> which points to Animancer's documentation.
/// </summary>
[AttributeUsage(AttributeTargets.All, AllowMultiple = false)]
[System.Diagnostics.Conditional(Strings.Assertions)]
public class AnimancerHelpUrlAttribute : HelpURLAttribute
{
/************************************************************************************************************************/
/// <summary>Creates a new <see cref="AnimancerHelpUrlAttribute"/>.</summary>
public AnimancerHelpUrlAttribute(string url)
: base(url)
{ }
/************************************************************************************************************************/
/// <summary>Creates a new <see cref="AnimancerHelpUrlAttribute"/>.</summary>
public AnimancerHelpUrlAttribute(Type type)
: base(GetApiDocumentationUrl(type))
{ }
/************************************************************************************************************************/
/// <summary>Returns a URL for the given `type`'s API Documentation page.</summary>
public static string GetApiDocumentationUrl(Type type)
=> GetApiDocumentationUrl(Strings.DocsURLs.Documentation + "/api/", type);
/// <summary>Returns a URL for the given `type`'s API Documentation page.</summary>
public static string GetApiDocumentationUrl(string prefix, Type type)
{
var url = StringBuilderPool.Instance.Acquire();
url.Append(prefix);
if (!string.IsNullOrEmpty(type.Namespace))
url.Append(type.Namespace).Append('/');
url.Append(type.Name.Replace('`', '_'));
return url.ReleaseToString();
}
/************************************************************************************************************************/
}
}

View File

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

View File

@@ -0,0 +1,29 @@
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
namespace Animancer
{
/// <summary>[Editor-Conditional]
/// A <see cref="DefaultValueAttribute"/> which uses the <see cref="AnimancerGraph.DefaultFadeDuration"/> and 0.
/// </summary>
/// https://kybernetik.com.au/animancer/api/Animancer/DefaultFadeValueAttribute
///
public class DefaultFadeValueAttribute : DefaultValueAttribute
{
/************************************************************************************************************************/
/// <inheritdoc/>
public override object Primary => AnimancerGraph.DefaultFadeDuration;
/************************************************************************************************************************/
/// <summary>Creates a new <see cref="DefaultValueAttribute"/>.</summary>
public DefaultFadeValueAttribute()
{
// This won't change so there's no need to box the value every time by overriding the property.
Secondary = 0f;
}
/************************************************************************************************************************/
}
}

View File

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

View File

@@ -0,0 +1,40 @@
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
using System;
namespace Animancer
{
/// <summary>[Editor-Conditional] Specifies the default value of a field and a secondary fallback.</summary>
/// https://kybernetik.com.au/animancer/api/Animancer/DefaultValueAttribute
[AttributeUsage(AttributeTargets.Field)]
[System.Diagnostics.Conditional(Strings.UnityEditor)]
public class DefaultValueAttribute : Attribute
{
/************************************************************************************************************************/
/// <summary>The main default value.</summary>
public virtual object Primary { get; protected set; }
/************************************************************************************************************************/
/// <summary>The fallback value to use if the target value was already equal to the <see cref="Primary"/>.</summary>
public virtual object Secondary { get; protected set; }
/************************************************************************************************************************/
/// <summary>Creates a new <see cref="DefaultValueAttribute"/>.</summary>
public DefaultValueAttribute(object primary, object secondary = null)
{
Primary = primary;
Secondary = secondary;
}
/************************************************************************************************************************/
/// <summary>Creates a new <see cref="DefaultValueAttribute"/>.</summary>
protected DefaultValueAttribute() { }
/************************************************************************************************************************/
}
}

View File

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

View File

@@ -0,0 +1,17 @@
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
using System;
namespace Animancer
{
/// <summary>[Editor-Conditional]
/// Causes an Inspector field in an <see cref="ITransition"/>
/// to be drawn after its events where the events would normally be drawn last.
/// </summary>
/// https://kybernetik.com.au/animancer/api/Animancer/DrawAfterEventsAttribute
///
[AttributeUsage(AttributeTargets.Field)]
[System.Diagnostics.Conditional(Strings.UnityEditor)]
public sealed class DrawAfterEventsAttribute : Attribute { }
}

View File

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

View File

@@ -0,0 +1,366 @@
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
using System;
using System.Reflection;
#if UNITY_EDITOR
using System.Collections;
#endif
namespace Animancer
{
/// <summary>[Editor-Conditional]
/// Specifies a set of acceptable names for <see cref="AnimancerEvent"/>s
/// so they can display a warning in the Inspector if an unexpected name is used.
/// </summary>
///
/// <remarks>
/// Placing this attribute on a type applies it to all fields in that type.
/// <para></para>
/// <strong>Documentation:</strong>
/// <see href="https://kybernetik.com.au/animancer/docs/manual/events/animancer/usage#event-names">
/// Event Names</see>
/// <para></para>
/// <strong>Example:</strong><code>
/// [EventNames(...)]// Apply to all fields in this class.
/// public class AttackState
/// {
/// [SerializeField]
/// [EventNames(...)]// Apply to only this field.
/// private ClipTransition _Action;
/// }
/// </code>
/// See the constructors for examples of their usage.
/// </remarks>
///
/// https://kybernetik.com.au/animancer/api/Animancer/EventNamesAttribute
///
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Class | AttributeTargets.Struct, Inherited = true)]
[System.Diagnostics.Conditional(Strings.UnityEditor)]
public sealed class EventNamesAttribute : Attribute
#if UNITY_EDITOR
, IInitializable<MemberInfo>
#endif
{
/************************************************************************************************************************/
#if UNITY_EDITOR
/// <summary>[Editor-Only] The names that can be used for events in the attributed field.</summary>
public StringReference[] Names { get; private set; }
/// <summary>[Editor-Only] Has the <see cref="Names"/> array been initialized?</summary>
public bool HasNames
=> !Names.IsNullOrEmpty();
#endif
/************************************************************************************************************************/
/// <summary>
/// Creates a new <see cref="EventNamesAttribute"/>
/// with <see cref="Names"/> from the attributed type or declaring type of the attributed member.</summary>
///
/// <remarks>
/// <strong>Example:</strong><code>
/// [EventNames]// Use all StringReference fields in this class for any transitions in this class.
/// public class AttackState
/// {
/// public static readonly StringReference HitStart = "Hit Start";
/// public static readonly StringReference HitEnd = "Hit End";
///
/// [SerializeField]
/// [EventNames]// Use all StringReference fields in this class.
/// private ClipTransition _Animation;
///
/// protected virtual void Awake()
/// {
/// _Animation.Events.SetCallback(HitStart, OnHitStart);
/// _Animation.Events.SetCallback(HitEnd, OnHitEnd);
/// }
///
/// private void OnHitStart() { }
/// private void OnHitEnd() { }
/// }
/// </code></remarks>
public EventNamesAttribute()
{
}
/************************************************************************************************************************/
/// <summary>Creates a new <see cref="EventNamesAttribute"/> containing the specified `names`.</summary>
/// <remarks>
/// <strong>Example:</strong><code>
/// public class AttackState
/// {
/// [SerializeField]
/// [EventNames("Hit Start", "Hit End")]
/// private ClipTransition _Animation;
///
/// protected virtual void Awake()
/// {
/// _Animation.Events.SetCallback("Hit Start", OnHitStart);
/// _Animation.Events.SetCallback("Hit End", OnHitEnd);
/// }
///
/// private void OnHitStart() { }
/// private void OnHitEnd() { }
/// }
/// </code></remarks>
public EventNamesAttribute(params string[] names)
{
#if UNITY_EDITOR
Names = StringReference.Get(names);
#endif
}
/************************************************************************************************************************/
/// <summary>Creates a new <see cref="EventNamesAttribute"/> with <see cref="Names"/> from the `type`.</summary>
///
/// <remarks>
/// If the `type` is an enum, all of its values will be used.
/// <para></para>
/// Otherwise the values of all static <see cref="string"/> and
/// <see cref="StringReference"/> fields will be used.
/// <para></para>
/// <strong>Example:</strong><code>
/// public class AttackState
/// {
/// public static readonly StringReference HitStart = "Hit Start";
/// public static readonly StringReference HitEnd = "Hit End";
///
/// [SerializeField]
/// [EventNames(typeof(AttackState))]// Use all StringReference fields in this class.
/// private ClipTransition _Animation;
///
/// protected virtual void Awake()
/// {
/// _Animation.Events.SetCallback(HitStart, OnHitStart);
/// _Animation.Events.SetCallback(HitEnd, OnHitEnd);
/// }
///
/// private void OnHitStart() { }
/// private void OnHitEnd() { }
/// }
/// </code></remarks>
///
/// <exception cref="ArgumentNullException"/>
public EventNamesAttribute(Type type)
{
#if UNITY_EDITOR
Initialize(type);
#endif
}
/************************************************************************************************************************/
/// <summary>
/// Creates a new <see cref="EventNamesAttribute"/> with <see cref="Names"/> from a member in the `type`
/// with the specified `name`.
/// </summary>
///
/// <remarks>
/// The specified member must be static and can be a Field, Property, or Method.
/// <para></para>
/// The member type can be anything implementing <see cref="IEnumerable"/> (including arrays, lists, and
/// coroutines).
/// <para></para>
/// <strong>Example:</strong><code>
/// public class AttackState
/// {
/// public static readonly StringReference[] Events = { "Hit Start", "Hit End" };
///
/// [SerializeField]
/// [EventNames(typeof(AttackState), nameof(Events))]// Get the names from AttackState.Events.
/// private ClipTransition _Animation;
///
/// protected virtual void Awake()
/// {
/// _Animation.Events.SetCallback(Events[0], OnHitStart);
/// _Animation.Events.SetCallback(Events[1], OnHitEnd);
/// }
///
/// private void OnHitStart() { }
/// private void OnHitEnd() { }
/// }
/// </code></remarks>
///
/// <exception cref="ArgumentNullException"/>
/// <exception cref="ArgumentException">No member with the specified `name` exists in the `type`.</exception>
///
public EventNamesAttribute(Type type, string name)
{
#if UNITY_EDITOR
var obj = GetValue(type, name)
?? throw new ArgumentException(
$"The collection retrieved from {type.GetNameCS()}.{name} is null");
if (obj is not IEnumerable collection)
throw new ArgumentException(
$"The object retrieved from {type.GetNameCS()}.{name} is not an {nameof(IEnumerable)}");
using (ListPool<StringReference>.Instance.Acquire(out var names))
{
foreach (var item in collection)
{
if (item == null)
continue;
var itemName = item.ToString();
if (string.IsNullOrEmpty(itemName))
continue;
names.Add(itemName);
}
if (names.Count == 0)
throw new ArgumentException($"The collection retrieved from {type.GetNameCS()}.{name} is empty");
Names = names.ToArray();
}
#endif
}
/************************************************************************************************************************/
#if UNITY_EDITOR
/************************************************************************************************************************/
/// <summary>Initializes the <see cref="Names"/> if they weren't already set in the constructor.</summary>
public void Initialize(MemberInfo member)
{
if (HasNames)
return;
if (member == null)
throw new ArgumentNullException(nameof(member));
if (member is Type type)
{
Initialize(type);
}
else
{
Names = GatherNames(member.DeclaringType);
}
}
/************************************************************************************************************************/
/// <summary>Initializes the <see cref="Names"/> if they weren't already set in the constructor.</summary>
public void Initialize(Type type)
{
if (HasNames)
return;
if (type == null)
throw new ArgumentNullException(nameof(type));
if (type.IsEnum)
{
Names = StringReference.Get(Enum.GetNames(type));
}
else
{
Names = GatherNames(type);
}
}
/************************************************************************************************************************/
private static object GetValue(Type type, string name)
{
if (type == null)
throw new ArgumentNullException(nameof(type));
if (name == null)
throw new ArgumentNullException(nameof(name));
var field = type.GetField(name, AnimancerReflection.StaticBindings);
if (field != null)
return field.GetValue(null);
var property = type.GetProperty(name, AnimancerReflection.StaticBindings);
if (property != null)
return property.GetValue(null, null);
var method = type.GetMethod(name, AnimancerReflection.StaticBindings, null, Type.EmptyTypes, null);
if (method != null)
return method.Invoke(null, null);
throw new ArgumentException($"{type.GetNameCS()} does not contain a member named '{name}'");
}
/************************************************************************************************************************/
private static StringReference[] GatherNames(Type type)
{
using (ListPool<StringReference>.Instance.Acquire(out var names))
{
while (type != null)
{
var fields = type.GetFields(AnimancerReflection.StaticBindings | BindingFlags.DeclaredOnly);
for (int i = 0; i < fields.Length; i++)
{
var field = fields[i];
if (field.DeclaringType.Assembly.FullName.StartsWith("Unity"))
continue;
StringReference name;
if (field.FieldType == typeof(string))
{
name = (string)field.GetValue(null);
}
else if (field.FieldType == typeof(StringReference))
{
name = (StringReference)field.GetValue(null);
}
else continue;
if (!name.IsNullOrEmpty() && !names.Contains(name))
names.Add(name);
}
type = type.BaseType;
}
if (names.Count == 0)
return null;
names.Sort();
return names.ToArray();
}
}
/************************************************************************************************************************/
private string _Prefix;
private string _NamesToString;
/// <summary>Returns a string containing all the <see cref="Names"/>.</summary>
public string NamesToString(string prefix, string delimiter = "\n• ")
{
if (!HasNames)
return prefix;
if (_NamesToString != null && _Prefix == prefix)
return _NamesToString;
var text = StringBuilderPool.Instance.Acquire();
_Prefix = prefix;
text.Append(prefix);
for (int i = 0; i < Names.Length; i++)
text.Append(delimiter).Append(Names[i]);
return _NamesToString = text.ReleaseToString();
}
/************************************************************************************************************************/
#endif
/************************************************************************************************************************/
}
}

View File

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

View File

@@ -0,0 +1,32 @@
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
using System;
using UnityEngine.Scripting.APIUpdating;
namespace Animancer.Editor
{
/// <summary>[Editor-Conditional]
/// A <see cref="MovedFromAttribute"/> which indicates that a type may have been previously
/// defined in the pre-compiled Animancer Lite DLL in an earlier version of Animancer.
/// </summary>
/// <remarks>
/// This allows <see cref="UnityEngine.SerializeReference"/> fields of the attributed type
/// to retain their values when upgrading from Animancer Lite to Animancer Pro.
/// </remarks>
/// https://kybernetik.com.au/animancer/api/Animancer.Editor/InternalSerializableTypeAttribute
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct, Inherited = true)]
[System.Diagnostics.Conditional(Strings.UnityEditor)]
public sealed class InternalSerializableTypeAttribute : MovedFromAttribute
{
/************************************************************************************************************************/
/// <summary>Creates a new <see cref="InternalSerializableTypeAttribute"/>.</summary>
public InternalSerializableTypeAttribute()
: base(true, sourceAssembly: Strings.LiteAssemblyName)
{
}
/************************************************************************************************************************/
}
}

View File

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

View File

@@ -0,0 +1,36 @@
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
using System;
namespace Animancer
{
/// <summary>[Editor-Conditional]
/// Specifies a custom display label for the <c>Thresholds</c> column of a mixer transition.
/// </summary>
/// https://kybernetik.com.au/animancer/api/Animancer/ThresholdLabelAttribute
///
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Class | AttributeTargets.Struct, Inherited = true)]
[System.Diagnostics.Conditional(Strings.UnityEditor)]
public sealed class ThresholdLabelAttribute : Attribute
{
/************************************************************************************************************************/
#if UNITY_EDITOR
/// <summary>[Editor-Only] The label.</summary>
public readonly string Label;
#endif
/************************************************************************************************************************/
/// <summary>Creates a new <see cref="ThresholdLabelAttribute"/>.</summary>
public ThresholdLabelAttribute(string label)
{
#if UNITY_EDITOR
Label = label;
#endif
}
/************************************************************************************************************************/
}
}

View File

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

View File

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

View File

@@ -0,0 +1,24 @@
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
namespace Animancer.Units
{
/// <summary>[Editor-Conditional] Applies a different GUI for an animation speed field.</summary>
/// https://kybernetik.com.au/animancer/api/Animancer.Units/AnimationSpeedAttribute
[System.Diagnostics.Conditional(Strings.UnityEditor)]
public sealed class AnimationSpeedAttribute : UnitsAttribute
{
/************************************************************************************************************************/
/// <summary>Creates a new <see cref="AnimationTimeAttribute"/>.</summary>
public AnimationSpeedAttribute()
: base("x")
{
Rule = Validate.Value.IsFiniteOrNaN;
IsOptional = true;
DefaultValue = 1;
}
/************************************************************************************************************************/
}
}

View File

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

View File

@@ -0,0 +1,52 @@
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
namespace Animancer.Units
{
/// <summary>[Editor-Conditional]
/// Causes a float field to display using 3 fields: Normalized, Seconds, and Frames.
/// </summary>
/// <remarks>
/// <strong>Documentation:</strong>
/// <see href="https://kybernetik.com.au/animancer/docs/manual/transitions#time-fields">
/// Time Fields</see>
/// </remarks>
/// https://kybernetik.com.au/animancer/api/Animancer.Units/AnimationTimeAttribute
///
[System.Diagnostics.Conditional(Strings.UnityEditor)]
public sealed class AnimationTimeAttribute : UnitsAttribute
{
/************************************************************************************************************************/
/// <summary>A unit of measurement used by the <see cref="AnimationTimeAttribute"/>.</summary>
public enum Units
{
/// <summary>A value of 1 represents the end of the animation.</summary>
Normalized = 0,
/// <summary>A value of 1 represents 1 second.</summary>
Seconds = 1,
/// <summary>A value of 1 represents 1 frame.</summary>
Frames = 2,
}
/// <summary>An explanation of the suffixes used in fields drawn by this attribute.</summary>
public const string Tooltip = "x = Normalized, s = Seconds, f = Frames";
/// <summary>The <see cref="UnitsAttribute.Multipliers"/> used by instances of this attribute.</summary>
private static new readonly float[] Multipliers = new float[3];// Calculated immediately before each use.
/// <summary>The <see cref="UnitsAttribute.Suffixes"/> used by instances of this attribute.</summary>
private static new readonly string[] Suffixes = new string[3] { "x", "s", "f" };
/************************************************************************************************************************/
/// <summary>Cretes a new <see cref="AnimationTimeAttribute"/>.</summary>
public AnimationTimeAttribute(Units units)
: base(Multipliers, Suffixes, (int)units)
{ }
/************************************************************************************************************************/
}
}

View File

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

View File

@@ -0,0 +1,143 @@
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
namespace Animancer.Units
{
/************************************************************************************************************************/
/// <summary>[Editor-Conditional] Angle measured in <c>degrees</c> (<c>º</c>).</summary>
/// <remarks>
/// <strong>Documentation:</strong>
/// <see href="https://kybernetik.com.au/animancer/docs/manual/other/units">
/// Units Attribute</see>
/// </remarks>
/// https://kybernetik.com.au/animancer/api/Animancer.Units/DegreesAttribute
///
[System.Diagnostics.Conditional(Strings.UnityEditor)]
public sealed class DegreesAttribute : UnitsAttribute
{
/// <summary>Creates a new <see cref="DegreesAttribute"/>.</summary>
public DegreesAttribute() : base(" º") { }
}
/************************************************************************************************************************/
/// <summary>[Editor-Conditional] Rotational speed measured in <c>degrees per second</c> (<c>º/s</c>).</summary>
/// <remarks>
/// <strong>Documentation:</strong>
/// <see href="https://kybernetik.com.au/animancer/docs/manual/other/units">
/// Units Attribute</see>
/// </remarks>
/// https://kybernetik.com.au/animancer/api/Animancer.Units/DegreesPerSecondAttribute
///
[System.Diagnostics.Conditional(Strings.UnityEditor)]
public sealed class DegreesPerSecondAttribute : UnitsAttribute
{
/// <summary>Creates a new <see cref="DegreesPerSecondAttribute"/>.</summary>
public DegreesPerSecondAttribute() : base(" º/s") { }
}
/************************************************************************************************************************/
/// <summary>[Editor-Conditional] Distance measured in <c>meters</c> (<c>m</c>).</summary>
/// <remarks>
/// <strong>Documentation:</strong>
/// <see href="https://kybernetik.com.au/animancer/docs/manual/other/units">
/// Units Attribute</see>
/// </remarks>
/// https://kybernetik.com.au/animancer/api/Animancer.Units/MetersAttribute
///
[System.Diagnostics.Conditional(Strings.UnityEditor)]
public sealed class MetersAttribute : UnitsAttribute
{
/// <summary>Creates a new <see cref="MetersAttribute"/>.</summary>
public MetersAttribute() : base(" m") { }
}
/************************************************************************************************************************/
/// <summary>[Editor-Conditional] Speed measured in <c>meters per second</c> (<c>m/s</c>).</summary>
/// <remarks>
/// <strong>Documentation:</strong>
/// <see href="https://kybernetik.com.au/animancer/docs/manual/other/units">
/// Units Attribute</see>
/// </remarks>
/// https://kybernetik.com.au/animancer/api/Animancer.Units/MetersPerSecondAttribute
///
[System.Diagnostics.Conditional(Strings.UnityEditor)]
public sealed class MetersPerSecondAttribute : UnitsAttribute
{
/// <summary>Creates a new <see cref="MetersPerSecondAttribute"/>.</summary>
public MetersPerSecondAttribute() : base(" m/s") { }
}
/************************************************************************************************************************/
/// <summary>[Editor-Conditional] Acceleration measured in <c>meters per second per second</c> (<c>m/s²</c>).</summary>
/// <remarks>
/// <strong>Documentation:</strong>
/// <see href="https://kybernetik.com.au/animancer/docs/manual/other/units">
/// Units Attribute</see>
/// </remarks>
/// https://kybernetik.com.au/animancer/api/Animancer.Units/MetersPerSecondPerSecondAttribute
///
[System.Diagnostics.Conditional(Strings.UnityEditor)]
public sealed class MetersPerSecondPerSecondAttribute : UnitsAttribute
{
/// <summary>Creates a new <see cref="MetersPerSecondPerSecondAttribute"/>.</summary>
public MetersPerSecondPerSecondAttribute() : base(" m/s\xB2") { }
}
/************************************************************************************************************************/
/// <summary>[Editor-Conditional] A multiplier displayed with an <c>x</c> suffix.</summary>
/// <remarks>
/// <strong>Documentation:</strong>
/// <see href="https://kybernetik.com.au/animancer/docs/manual/other/units">
/// Units Attribute</see>
/// </remarks>
/// https://kybernetik.com.au/animancer/api/Animancer.Units/MultiplierAttribute
///
[System.Diagnostics.Conditional(Strings.UnityEditor)]
public sealed class MultiplierAttribute : UnitsAttribute
{
/// <summary>Creates a new <see cref="MultiplierAttribute"/>.</summary>
public MultiplierAttribute() : base(" x") { }
}
/************************************************************************************************************************/
/// <summary>[Editor-Conditional] Time measured in <c>seconds</c> (<c>s</c>).</summary>
/// <remarks>
/// <strong>Documentation:</strong>
/// <see href="https://kybernetik.com.au/animancer/docs/manual/other/units">
/// Units Attribute</see>
/// </remarks>
/// https://kybernetik.com.au/animancer/api/Animancer.Units/SecondsAttribute
///
[System.Diagnostics.Conditional(Strings.UnityEditor)]
public sealed class SecondsAttribute : UnitsAttribute
{
/// <summary>Creates a new <see cref="SecondsAttribute"/>.</summary>
public SecondsAttribute() : base(" s") { }
}
/************************************************************************************************************************/
/// <summary>[Editor-Conditional] A value measured <c>per second</c> (<c>/s</c>).</summary>
/// <remarks>
/// <strong>Documentation:</strong>
/// <see href="https://kybernetik.com.au/animancer/docs/manual/other/units">
/// Units Attribute</see>
/// </remarks>
/// https://kybernetik.com.au/animancer/api/Animancer.Units/PerSecondAttribute
///
[System.Diagnostics.Conditional(Strings.UnityEditor)]
public sealed class PerSecondAttribute : UnitsAttribute
{
/// <summary>Creates a new <see cref="PerSecondAttribute"/>.</summary>
public PerSecondAttribute() : base(" /s") { }
}
/************************************************************************************************************************/
}

View File

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

View File

@@ -0,0 +1,76 @@
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
using UnityEngine;
namespace Animancer.Units
{
/// <summary>[Editor-Conditional]
/// Causes a float field to display a suffix to indicate what kind of units the value represents as well as
/// displaying it as several different fields which convert the value between different units.
/// </summary>
/// <remarks>
/// <strong>Documentation:</strong>
/// <see href="https://kybernetik.com.au/animancer/docs/manual/other/units">
/// Units Attribute</see>
/// </remarks>
/// https://kybernetik.com.au/animancer/api/Animancer.Units/UnitsAttribute
///
[System.Diagnostics.Conditional(Strings.UnityEditor)]
public class UnitsAttribute : PropertyAttribute
{
/************************************************************************************************************************/
/// <summary>The multipliers to convert from the field's actual value to each unit type.</summary>
/// <remarks><c>valueInUnitX = valueInBaseUnits * Multipliers[x];</c></remarks>
public readonly float[] Multipliers;
/// <summary>The unit suffix to display at the end of the value in each field.</summary>
public readonly string[] Suffixes;
/// <summary>The index of the multiplier where the field stores its actual value.</summary>
/// <remarks>The multiplier at this index must always be 1.</remarks>
public readonly int UnitIndex;
/************************************************************************************************************************/
/// <summary>The validation rule applied to the value.</summary>
public Validate.Value Rule { get; set; }
/// <summary>Should the field have a toggle to set its value to <see cref="float.NaN"/>?</summary>
public bool IsOptional { get; set; }
/// <summary>The value to display if the actual value is <see cref="float.NaN"/>.</summary>
public float DefaultValue { get; set; }
/// <summary>Optional text to display instead of the regular fields when the value is <see cref="float.NaN"/>.</summary>
public string DisabledText { get; set; }
/************************************************************************************************************************/
/// <summary>Creates a new <see cref="UnitsAttribute"/>.</summary>
protected UnitsAttribute() { }
/// <summary>Creates a new <see cref="UnitsAttribute"/>.</summary>
public UnitsAttribute(string suffix)
{
Multipliers = new float[] { 1 };
Suffixes = new string[] { suffix };
}
/// <summary>Creates a new <see cref="UnitsAttribute"/>.</summary>
public UnitsAttribute(float[] multipliers, string[] suffixes, int unitIndex = 0)
{
Multipliers = multipliers;
Suffixes = suffixes;
UnitIndex = unitIndex;
Debug.Assert(multipliers.Length == suffixes.Length,
$"[Units] The {nameof(multipliers)} and {nameof(suffixes)} arrays have different lengths.");
Debug.Assert((uint)UnitIndex < (uint)multipliers.Length,
$"[Units] The {nameof(unitIndex)} is outside the {nameof(multipliers)} array.");
}
/************************************************************************************************************************/
}
}

View File

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

View File

@@ -0,0 +1,122 @@
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
#if UNITY_ASSERTIONS
//#define ANIMANCER_DISABLE_NAME_CACHE
using System.Runtime.CompilerServices;
using UnityEngine;
namespace Animancer
{
/// <summary>[Assert-Only]
/// A simple system for caching <see cref="Object.name"/> since it allocates garbage every time it's accessed.
/// </summary>
public static class NameCache
{
/************************************************************************************************************************/
private static readonly ConditionalWeakTable<Object, string>
ObjectToName = new();
/************************************************************************************************************************/
/// <summary>Caches and returns the <see cref="Object.name"/>.</summary>
public static string GetCachedName(this Object obj)
{
#if ANIMANCER_DISABLE_NAME_CACHE
return obj.name;
#else
if (obj == null)
{
if (obj is not null)
ObjectToName.Remove(obj);
return null;
}
if (!ObjectToName.TryGetValue(obj, out var name))
{
name = obj.name;
ObjectToName.Add(obj, name);
}
return name;
#endif
}
/************************************************************************************************************************/
/// <summary>Tries to get the <see cref="Object.name"/> or <see cref="object.ToString"/>.</summary>
public static bool TryToString(object obj, out string name)
{
if (obj == null)
{
name = null;
return false;
}
if (obj is Object unityObject)
{
if (unityObject != null)
{
name = unityObject.GetCachedName();
}
else
{
name = null;
return false;
}
}
else
{
name = obj.ToString();
}
return !string.IsNullOrEmpty(name);
}
/************************************************************************************************************************/
/// <summary>Clears all cached names so they will be re-gathered when next accessed.</summary>
public static void Clear()
=> ObjectToName.Clear();
/************************************************************************************************************************/
/// <summary>Sets the <see cref="Object.name"/> and caches it.</summary>
public static void SetName(this Object obj, string name)
{
obj.name = name;
ObjectToName.AddOrUpdate(obj, name);
}
/************************************************************************************************************************/
#if UNITY_EDITOR
/************************************************************************************************************************/
private class Cleaner : UnityEditor.AssetPostprocessor
{
/************************************************************************************************************************/
private static void OnPostprocessAllAssets(
string[] importedAssets,
string[] deletedAssets,
string[] movedAssets,
string[] movedFromAssetPaths,
bool didDomainReload)
{
Clear();
}
/************************************************************************************************************************/
}
/************************************************************************************************************************/
#endif
/************************************************************************************************************************/
}
}
#endif

View File

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

View File

@@ -0,0 +1,487 @@
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
using System;
using UnityEngine;
using Object = UnityEngine.Object;
namespace Animancer
{
/// <summary>
/// Bitwise flags used to determine which warnings Animancer should give.
/// <para></para>
/// <strong>These warnings are all optional</strong>.
/// Feel free to disable any of them if you understand the <em>potential</em> issues they're referring to.
/// </summary>
///
/// <remarks>
/// All warnings are enabled by default, but are compiled out of runtime builds (except development builds).
/// <para></para>
/// You can manually disable warnings using the <c>AnimancerSettings</c> asset
/// or the Animancer Settings panel in the Animancer Tools Window (<c>Window/Animation/Animancer Tools</c>).
/// <para></para>
/// <strong>Example:</strong>
/// You can put a method like this in any class to disable whatever warnings you don't want on startup:
/// <para></para><code>
/// #if UNITY_ASSERTIONS
/// [UnityEngine.RuntimeInitializeOnLoadMethod(UnityEngine.RuntimeInitializeLoadType.BeforeSceneLoad)]
/// private static void DisableAnimancerWarnings()
/// {
/// Animancer.OptionalWarning.ProOnly.Disable();
///
/// // You could disable OptionalWarning.All, but that's not recommended for obvious reasons.
/// }
/// #endif
/// </code></remarks>
///
/// https://kybernetik.com.au/animancer/api/Animancer/OptionalWarning
///
[Flags]
public enum OptionalWarning
{
/// <summary><c>default</c></summary>
None = 0,
/// <summary>
/// A <see href="https://kybernetik.com.au/animancer/docs/introduction/features">Pro-Only Feature</see>
/// has been used in <see href="https://kybernetik.com.au/animancer/redirect/lite">Animancer Lite</see>.
/// </summary>
///
/// <remarks>
/// Some <see href="https://kybernetik.com.au/animancer/docs/introduction/features">Features</see>
/// are only available in <see href="https://kybernetik.com.au/animancer/redirect/pro">Animancer Pro</see>.
/// <para></para>
/// <see href="https://kybernetik.com.au/animancer/redirect/lite">Animancer Lite</see>
/// allows you to try out those features in the Unity Editor and gives this warning the
/// first time each one is used to inform you that they will not work in runtime builds.
/// </remarks>
ProOnly = 1 << 0,
/// <summary>
/// An <see cref="AnimancerComponent.Graph"/> is being initialized
/// during a type of GUI event that isn't supposed to cause side effects.
/// </summary>
///
/// <remarks>
/// <see cref="EventType.Layout"/> and <see cref="EventType.Repaint"/>
/// should display the current details of things, but they should not modify things.
/// </remarks>
CreateGraphDuringGuiEvent = 1 << 1,
/// <summary>
/// The <see cref="AnimancerComponent.Animator"/> is disabled so Animancer won't be able to play animations.
/// </summary>
///
/// <remarks>
/// The <see cref="Animator"/> doesn't need an Animator Controller,
/// it just needs to be enabled via the checkbox in the Inspector
/// or by setting <c>animancerComponent.Animator.enabled = true;</c> in code.
/// </remarks>
AnimatorDisabled = 1 << 2,
/// <summary>
/// An <see cref="Animator.runtimeAnimatorController"/> is assigned
/// but the Rig is Humanoid so it can't be blended with Animancer.
/// </summary>
///
/// <remarks>
/// <see href="https://kybernetik.com.au/animancer/docs/manual/animator-controllers#native">Native</see>
/// Animator Controllers can blend with Animancer on Generic Rigs, but not on Humanoid Rigs
/// (you can swap back and forth between the Animator Controller and Animancer,
/// but it won't smoothly blend between them).
/// <para></para>
/// If you don't intend to blend between them, you can just disable this warning.
/// </remarks>
NativeControllerHumanoid = 1 << 3,
/// <summary>
/// An <see cref="Animator.runtimeAnimatorController"/> is being used at the same time as a
/// <see cref="ControllerState"/> or <see cref="HybridAnimancerComponent"/>.
/// </summary>
///
/// <remarks>
/// It is possible to use both, but it usually only happens when misunderstanding how the system works.
/// The differences are explained on the
/// <see href="https://kybernetik.com.au/animancer/docs/manual/animator-controllers">
/// Animator Controllers</see> page.
/// If you do want both systems, just disable this warning.
/// </remarks>
NativeControllerState = 1 << 4,
/// <summary>
/// An <see href="https://kybernetik.com.au/animancer/docs/manual/events/end">End Event</see>
/// didn't actually end the animation.
/// </summary>
///
/// <remarks>
/// Animancer doesn't automatically do anything during an End Event
/// so it's up to you to end the animation (usually by playing something else).
/// <para></para>
/// This warning is given when the event isn't used to stop the animation that caused it
/// (usually by playing something else). This often indicates that the event hasn't been
/// configured correctly, however it is sometimes intentional such as if the event doesn't
/// immediately stop the animation but sets a flag to indicate the animation has ended for
/// another system to act on at a later time. In that case this warning should be disabled.
/// <para></para>
/// <see href="https://kybernetik.com.au/animancer/docs/manual/events/end">End Events</see>
/// are triggered every frame after their time has passed, so in this case it might be
/// necessary to clear the event or simply use a regular
/// <see href="https://kybernetik.com.au/animancer/docs/manual/events/animancer">Animancer Event</see>.
/// </remarks>
EndEventInterrupt = 1 << 5,
/// <summary>
/// An <see href="https://kybernetik.com.au/animancer/docs/manual/events/animancer">Animancer Event</see>
/// triggered by one character was used to play an animation on a different character.
/// </summary>
///
/// <remarks>
/// This most commonly happens when a Transition is shared by multiple characters and they
/// all register their own callbacks to its events which leads to those events controlling
/// the wrong character.
/// </remarks>
EventPlayMismatch = 1 << 6,
/// <summary>
/// An <see cref="AnimancerEvent"/> called <see cref="AnimancerEvent.InvokeBoundCallback"/>
/// but there was no callback bound to its name. This may mean the event is useless and therefore
/// should be removed to avoid wasting performance checking and invoking it.
/// </summary>
///
/// <remarks>
/// Events in Transitions automatically call <see cref="AnimancerEvent.InvokeBoundCallback"/>
/// by default so this warning is triggered if you don't bind a callback to the event's name
/// in the <see cref="AnimancerComponent.Events"/>
/// or use <see cref="AnimancerEvent.Sequence.SetCallback(int, Action)"/> on the Transition's events.
/// </remarks>
UselessEvent = 1 << 7,
/// <summary>
/// <see href="https://kybernetik.com.au/animancer/docs/manual/events/animancer">Animancer Events</see>
/// are being used on a state which does not properly support them so they might not work as intended.
/// </summary>
///
/// <remarks>
/// <see href="https://kybernetik.com.au/animancer/docs/manual/events/animancer">Animancer Events</see> on a
/// <see cref="ControllerState"/> will be triggered based on its <see cref="AnimancerState.NormalizedTime"/>,
/// which comes from the current state of its Animator Controller regardless of which state that may be.
/// <para></para>
/// If you intend for the event to be associated with a specific state inside the Animator Controller,
/// you need to use Unity's regular
/// <see href="https://kybernetik.com.au/animancer/docs/manual/events/animation">Animation Events</see>
/// instead.
/// <para></para>
/// But if you intend the event to be triggered by any state inside the Animator Controller,
/// then you can simply disable this warning.
/// </remarks>
UnsupportedEvents = 1 << 8,
/// <summary>
/// <see href="https://kybernetik.com.au/animancer/docs/manual/ik">Inverse Kinematics</see>
/// cannot be dynamically enabled on some
/// <see href="https://kybernetik.com.au/animancer/docs/manual/playing/states">State</see> types.
/// </summary>
///
/// <remarks>
/// To use IK on a <see cref="ControllerState"/>
/// you must instead enable it on the desired layer inside the Animator Controller.
/// <para></para>
/// IK is not supported by <see cref="PlayableAssetState"/>.
/// <para></para>
/// Setting <see cref="AnimancerNode.ApplyAnimatorIK"/> on such a state will simply do nothing,
/// so feel free to disable this warning if you are enabling IK on states without checking their type.
/// </remarks>
UnsupportedIK = 1 << 9,
/// <summary>
/// A Mixer State is being initialized with its <see cref="AnimancerNode.ChildCount"/> &lt;= 1.
/// </summary>
///
/// <remarks>
/// The purpose of a mixer is to mix multiple child states
/// so you are probably initializing it with incorrect parameters.
/// <para></para>
/// A mixer with only one child will simply play that child,
/// so feel free to disable this warning if that's what you intend to do.
/// </remarks>
MixerMinChildren = 1 << 10,
/// <summary>
/// A Mixer State is synchronizing a child with <see cref="AnimancerState.Length"/> = 0.
/// </summary>
///
/// <remarks>
/// Synchronization is based on the <see cref="AnimancerState.NormalizedTime"/>
/// which can't be calculated if the <see cref="AnimancerState.Length"/> is 0.
/// <para></para>
/// Some state types can change their <see cref="AnimancerState.Length"/>,
/// in which case you can just disable this warning.
/// But otherwise, the indicated state should not be added to the synchronization list.
/// </remarks>
MixerSynchronizeZeroLength = 1 << 11,
/// <summary>
/// When a transition with a non-zero <see cref="ITransition.FadeDuration"/>
/// creates a state, that state will log this warning if it's ever played
/// without a fade duration.
/// </summary>
/// <remarks>
/// This helps identify situations where a state is accidentally played directly
/// where the transition should be played instead to allow it to apply its fade
/// and any other details.
/// </remarks>
ExpectFade = 1 << 12,
/// <summary>
/// A <see href="https://kybernetik.com.au/animancer/docs/manual/blending/fading/custom">Custom Easing</see>
/// is being started but its weight calculation does not go from 0 to 1.
/// </summary>
///
/// <remarks>
/// The <see cref="FadeGroup.Easing"/> method is expected to return 0 when the parameter is 0 and
/// 1 when the parameter is 1. It can do anything you want with other values,
/// but starting or ending at different values will likely lead to undesirable results.
/// <para></para>
/// If your <see cref="FadeGroup.Easing"/> method is expensive you could disable this warning to save
/// some performance, but violating the above guidelines is not recommended.
/// </remarks>
FadeEasingBounds = 1 << 13,
/// <summary>
/// The <see cref="Animator.speed"/> property does not affect Animancer.
/// Use <see cref="AnimancerGraph.Speed"/> instead.
/// </summary>
///
/// <remarks>
/// The <see cref="Animator.speed"/> property only works with Animator Controllers but does not affect the
/// Playables API so Animancer has its own <see cref="AnimancerGraph.Speed"/> property.
/// </remarks>
AnimatorSpeed = 1 << 14,
/// <summary>
/// An <see cref="AnimancerNodeBase.Graph"/> is null during finalization (garbage collection).
/// </summary>
///
/// <remarks>
/// This probably means that node was never used for anything and should not have been created.
/// <para></para>
/// This warning can be prevented for a specific node by calling <see cref="AnimancerNodeBase.MarkAsUsed"/>.
/// <para></para>
/// To minimise the performance cost of checking this warning, it does not capture the stack trace of the
/// node's creation by default. However, you can enable <see cref="AnimancerNode.TraceConstructor"/> on startup
/// so that it can include the stack trace in the warning message for any nodes that end up being unused.
/// </remarks>
UnusedNode = 1 << 15,
/// <summary>
/// An <see cref="AnimancerState.MainObject"/> doesn't match the <see cref="Transition{TState}.MainObject"/>
/// of the transition being applied to it.
/// </summary>
/// <remarks>
/// If you set the <see cref="Transition{TState}.MainObject"/> of a transition, you should also call
/// <see cref="Transition{TState}.ReconcileMainObject"/> to create a new state for the new object.
/// </remarks>
MainObjectMismatch = 1 << 16,
/// <summary>
/// <see cref="PlayableAssetState.InitializeBindings"/> is trying to bind to the same <see cref="Animator"/>
/// that is being used by Animancer.
/// </summary>
/// <remarks>
/// Doing this will replace Animancer's output so its animations would not work anymore.
/// </remarks>
PlayableAssetAnimatorBinding = 1 << 17,
/// <summary>
/// <see cref="AnimancerLayer.GetOrCreateWeightlessState"/> is cloning a complex state such as a
/// <see cref="ManualMixerState"/> or <see cref="ControllerState"/>.
/// This has a larger performance cost than cloning a <see cref="ClipState"/> and these states
/// generally have parameters that need to be controlled which may result in undesired behaviour
/// if your scripts are only expecting to have one state to control.
/// </summary>
/// <remarks>
/// The <see href="https://kybernetik.com.au/animancer/docs/manual/blending/fading/modes">Fade Modes</see>
/// page explains why clones are created.
/// </remarks>
CloneComplexState = 1 << 18,
/// <summary>
/// Unity doesn't suppport dynamically creating animations for Animancer in runtime builds
/// so this warning is given when attempting to use an animation which isn't saved as an
/// asset to explain this limitation as early as possible.
/// </summary>
///
/// <remarks>
/// This warning should be disabled if you're loading animations from Asset Bundles or Addressables
/// or if you only intend to use the animation in the Unity Editor.
/// </remarks>
DynamicAnimation = 1 << 19,
/// <summary>
/// <see cref="Animancer.StringReference"/>s are generally more efficient for comparisons
/// than raw <see cref="string"/>s and are not interchangeable so references should be preferred.
/// </summary>
StringReference = 1 << 20,
/// <summary>All warning types.</summary>
All = ~0,
}
/// https://kybernetik.com.au/animancer/api/Animancer/Validate
public static partial class Validate
{
/************************************************************************************************************************/
#if UNITY_ASSERTIONS
/// <summary>[Assert-Only]
/// The <see cref="OptionalWarning"/> flags that are currently disabled (default none).
/// </summary>
private static OptionalWarning _DisabledWarnings;
#endif
/************************************************************************************************************************/
/// <summary>[Animancer Extension] [Assert-Conditional]
/// Disables the specified warning type. Supports bitwise combinations.
/// </summary>
/// <remarks>
/// <strong>Example:</strong>
/// You can put a method like this in any class to disable whatever warnings you don't want on startup:
/// <para></para><code>
/// #if UNITY_ASSERTIONS
/// [UnityEngine.RuntimeInitializeOnLoadMethod(UnityEngine.RuntimeInitializeLoadType.BeforeSceneLoad)]
/// private static void DisableAnimancerWarnings()
/// {
/// Animancer.OptionalWarning.ProOnly.Disable();
///
/// // You could disable OptionalWarning.All, but that's not recommended for obvious reasons.
/// }
/// #endif
/// </code></remarks>
[System.Diagnostics.Conditional(Strings.Assertions)]
public static void Disable(this OptionalWarning type)
{
#if UNITY_ASSERTIONS
_DisabledWarnings |= type;
#endif
}
/************************************************************************************************************************/
/// <summary>[Animancer Extension] [Assert-Conditional]
/// Enables the specified warning type. Supports bitwise combinations.
/// </summary>
[System.Diagnostics.Conditional(Strings.Assertions)]
public static void Enable(this OptionalWarning type)
{
#if UNITY_ASSERTIONS
_DisabledWarnings &= ~type;
#endif
}
/************************************************************************************************************************/
/// <summary>[Animancer Extension] [Assert-Conditional]
/// Enables or disables the specified warning type. Supports bitwise combinations.
/// </summary>
[System.Diagnostics.Conditional(Strings.Assertions)]
public static void SetEnabled(this OptionalWarning type, bool enable)
{
#if UNITY_ASSERTIONS
if (enable)
type.Enable();
else
type.Disable();
#endif
}
/************************************************************************************************************************/
/// <summary>[Animancer Extension] [Assert-Conditional]
/// Logs the `message` as a warning if the `type` is enabled.
/// </summary>
/// <remarks>Does nothing if the `message` is <c>null</c>.</remarks>
[System.Diagnostics.Conditional(Strings.Assertions)]
[HideInCallstack]
public static void Log(this OptionalWarning type, string message, object context = null)
{
#if UNITY_ASSERTIONS
if (message == null || type.IsDisabled())
return;
Debug.LogWarning(
$"{nameof(OptionalWarning)}.{type} - Possible Issue Detected: {message}" +
$"\n\nThis warning can be disabled via '{Strings.AnimancerSettingsPath}'" +
$" or by calling {nameof(Animancer)}.{nameof(OptionalWarning)}.{type}.{nameof(Disable)}()" +
" and it will automatically be compiled out of Runtime Builds (except for Development Builds)." +
$" More information can be found at {Strings.DocsURLs.OptionalWarning.AsHtmlLink()}\n",
context as Object);
#endif
}
/************************************************************************************************************************/
#if UNITY_ASSERTIONS
/************************************************************************************************************************/
/// <summary>[Animancer Extension] [Assert-Only] Are none of the specified warning types disabled?</summary>
public static bool IsEnabled(this OptionalWarning type) => (_DisabledWarnings & type) == 0;
/************************************************************************************************************************/
/// <summary>[Animancer Extension] [Assert-Only] Are all of the specified warning types disabled?</summary>
public static bool IsDisabled(this OptionalWarning type) => (_DisabledWarnings & type) == type;
/************************************************************************************************************************/
/// <summary>[Animancer Extension] [Assert-Only]
/// Disables the specified warnings and returns those that were previously enabled.
/// </summary>
/// <remarks>Call <see cref="Enable"/> on the returned value to re-enable it.</remarks>
public static OptionalWarning DisableTemporarily(this OptionalWarning type)
{
var previous = _DisabledWarnings;
type.Disable();
return ~previous & type;
}
/************************************************************************************************************************/
private const string PermanentlyDisabledWarningsKey = nameof(Animancer) + "." + nameof(PermanentlyDisabledWarnings);
/// <summary>[Assert-Only] Warnings that are automatically disabled</summary>
/// <remarks>
/// This value is stored in <see cref="PlayerPrefs"/>
/// and can be manually edited via <see cref="Strings.AnimancerSettingsPath"/>.
/// </remarks>
public static OptionalWarning PermanentlyDisabledWarnings
{
#if NO_RUNTIME_PLAYER_PREFS && ! UNITY_EDITOR
get => default;
set
{
_DisabledWarnings = value;
}
#else
get => (OptionalWarning)PlayerPrefs.GetInt(PermanentlyDisabledWarningsKey);
set
{
_DisabledWarnings = value;
PlayerPrefs.SetInt(PermanentlyDisabledWarningsKey, (int)value);
}
#endif
}
#if UNITY_EDITOR
[UnityEditor.InitializeOnLoadMethod]
#endif
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.SubsystemRegistration)]
private static void InitializePermanentlyDisabledWarnings()
{
_DisabledWarnings = PermanentlyDisabledWarnings;
}
/************************************************************************************************************************/
#endif
/************************************************************************************************************************/
}
}

View File

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

View File

@@ -0,0 +1,190 @@
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
using System;
using System.Diagnostics;
using System.Runtime.CompilerServices;
using System.Text;
namespace Animancer
{
/// <summary>A very simple timer system based on a <see cref="System.Diagnostics.Stopwatch"/>.</summary>
public struct SimpleTimer : IDisposable
{
/************************************************************************************************************************/
/// <summary>The default <see cref="format"/> contains 3 decimal places.</summary>
const string Format3DP = "0.000";
/// <summary>A default timer that hasn't been started.</summary>
public static SimpleTimer Default = new(null);
/************************************************************************************************************************/
/// <summary>The system used to track time.</summary>
public static readonly Stopwatch
Stopwatch = Stopwatch.StartNew();
/************************************************************************************************************************/
/// <summary>An optional prefix for <see cref="ToString"/>.</summary>
public string name;
/// <summary>The string format to use for <see cref="ToString"/>.</summary>
/// <remarks>
/// If <c>null</c>, ticks will be used directly.
/// Otherwise, the ticks will be converted to seconds and this format will be used.
/// </remarks>
public string format;
/// <summary>The <see cref="Stopwatch.ElapsedTicks"/> from when this timer was started.</summary>
/// <remarks>If not started, this value will be <c>-1</c>.</remarks>
public long startTicks;
/// <summary>The total number of ticks that have elapsed since the <see cref="startTicks"/>.</summary>
/// <remarks>This value is updated by <see cref="Count"/>.</remarks>
public long totalTicks;
/************************************************************************************************************************/
/// <summary>Converts the <see cref="startTicks"/> to seconds.</summary>
public readonly double StartTimeSeconds
=> startTicks / (double)Stopwatch.Frequency;
/// <summary>Converts the <see cref="totalTicks"/> to seconds.</summary>
public readonly double TotalTimeSeconds
=> totalTicks / (double)Stopwatch.Frequency;
/************************************************************************************************************************/
/// <summary>Has <see cref="Start()"/> been called and <see cref="Count"/> not?</summary>
public readonly bool IsStarted
=> startTicks != -1;
/************************************************************************************************************************/
/// <summary>Creates a new <see cref="SimpleTimer"/> with the specified `name`.</summary>
/// <remarks>
/// You will need to call <see cref="Start()"/> to start the timer.
/// Or use the static <see cref="Start(string, string)"/>.
/// <para></para>
/// Use <c>null</c> as the `format` to have <see cref="Format"/> return the ticks instead of seconds.
/// </remarks>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public SimpleTimer(string name, string format = Format3DP)
{
this.name = name;
this.format = format;
startTicks = -1;
totalTicks = 0;
}
/************************************************************************************************************************/
/// <summary>Creates a new <see cref="SimpleTimer"/> with the specified `name` and starts it.</summary>
/// <remarks>Use <c>null</c> as the `format` to have <see cref="Format"/> return the ticks instead of seconds.</remarks>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static SimpleTimer Start(string name = null, string format = Format3DP)
=> new()
{
name = name,
format = format,
startTicks = Stopwatch.ElapsedTicks,
};
/************************************************************************************************************************/
/// <summary>
/// Stores the <see cref="Stopwatch.ElapsedTicks"/> in <see cref="startTicks"/>
/// so that <see cref="Count"/> will be able to calculate how much time has passed.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void Start()
=> startTicks = Stopwatch.ElapsedTicks;
/// <summary>Clears the <see cref="startTicks"/>.</summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void Cancel()
=> startTicks = -1;
/************************************************************************************************************************/
/// <summary>
/// Calculates the amount of time that has passed since the <see cref="startTicks"/>
/// and returns it after adding it to the <see cref="totalTicks"/>.
/// Also resumes this timer.
/// </summary>
/// <remarks>Returns -1 if this timer wasn't started.</remarks>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public long Count()
{
var endTicks = Stopwatch.ElapsedTicks;
long count;
if (startTicks >= 0)
{
count = endTicks - startTicks;
totalTicks += count;
}
else
{
count = -1;
}
startTicks = endTicks;
return count;
}
/************************************************************************************************************************/
private static StringBuilder _StringBuilder;
/// <summary>Calls <see cref="Count"/> and returns a string describing the current values of this timer.</summary>
public override string ToString()
{
var count = Count();
if (_StringBuilder == null)
_StringBuilder = new();
else
_StringBuilder.Length = 0;
if (!string.IsNullOrEmpty(name))
_StringBuilder.Append(name)
.Append(": ");
if (count != totalTicks && count >= 0)
{
_StringBuilder
.Append("Count ")
.Append(Format(count))
.Append(", Total ");
}
_StringBuilder.Append(Format(totalTicks));
return _StringBuilder.ToString();
}
/************************************************************************************************************************/
/// <summary>Converts the given `ticks` to a string using the <see cref="format"/>.</summary>
public readonly string Format(long ticks)
=> format is null
? $"{ticks} Ticks"
: $"{(ticks / (double)Stopwatch.Frequency).ToString(format)}s";
/************************************************************************************************************************/
/// <summary>Logs <see cref="ToString"/> and calls <see cref="Cancel"/>.</summary>
public void Dispose()
{
UnityEngine.Debug.Log(ToString());
Cancel();
totalTicks = 0;
}
/************************************************************************************************************************/
}
}

View File

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

View File

@@ -0,0 +1,219 @@
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
using Animancer.Units;
using UnityEngine;
namespace Animancer
{
/// <summary>Various string constants used throughout <see cref="Animancer"/>.</summary>
/// https://kybernetik.com.au/animancer/api/Animancer/Strings
///
public static class Strings
{
/************************************************************************************************************************/
/// <summary>The name of this product.</summary>
public const string ProductName = nameof(Animancer);
/// <summary>The standard prefix for <see cref="AddComponentMenu"/>.</summary>
public const string MenuPrefix = ProductName + "/";
/// <summary>The standard prefix for the asset creation menu (for <see cref="UnityEditor.MenuItem"/>).</summary>
public const string CreateMenuPrefix = "Assets/Create/" + MenuPrefix;
/// <summary>The standard prefix for <see cref="AddComponentMenu"/> for the samples.</summary>
public const string SamplesMenuPrefix = MenuPrefix + "Samples/";
/// <summary>The menu path of the <see cref="Editor.Tools.AnimancerToolsWindow"/>.</summary>
public const string AnimancerToolsMenuPath = "Window/Animation/Animancer Tools";
/// <summary>The menu path of the Animancer settings.</summary>
public const string AnimancerSettingsPath = "Edit/Project Settings/Animancer";
/// <summary>
/// The base value for <see cref="CreateAssetMenuAttribute.order"/> to group
/// "Assets/Create/Animancer/..." menu items just under "Avatar Mask".
/// </summary>
public const int AssetMenuOrder = 410;
/************************************************************************************************************************/
/// <summary>The conditional compilation symbol for Editor-Only code.</summary>
public const string UnityEditor = "UNITY_EDITOR";
/// <summary>The conditional compilation symbol for assertions exists in the Unity Editor and Development Builds.</summary>
public const string Assertions = "UNITY_ASSERTIONS";
/************************************************************************************************************************/
/// <summary>4 spaces for indentation.</summary>
public const string Indent = " ";
/// <summary>A prefix for tooltips on Pro-Only features.</summary>
/// <remarks><c>"[Pro-Only] "</c> in Animancer Lite or <c>""</c> in Animancer Pro.</remarks>
public const string ProOnlyTag = "";
/// <summary>The Assembly name of the pre-compiled Animancer Lite DLL.</summary>
public const string LiteAssemblyName = "Kybernetik.Animancer.Lite";
/************************************************************************************************************************/
/// <summary>An error message for when <see cref="AnimancerUtilities.IsFinite(float)"/> fails.</summary>
public const string MustBeFinite = "must not be NaN or Infinity";
/************************************************************************************************************************/
#if UNITY_ASSERTIONS
/// <summary>[Assert-Only] A message for <see cref="OptionalWarning.AnimatorDisabled"/>.</summary>
public const string AnimatorDisabledMessage
= "The " + nameof(Animator) + " is disabled so Animancer won't be able to play animations.";
#endif
/************************************************************************************************************************/
/// <summary>URLs of various documentation pages.</summary>
/// https://kybernetik.com.au/animancer/api/Animancer/DocsURLs
///
public static class DocsURLs
{
/************************************************************************************************************************/
/// <summary>The URL of the website where the Animancer documentation is hosted.</summary>
public const string Documentation = "https://kybernetik.com.au/animancer";
/// <summary>The URL of the website where the Animancer API documentation is hosted.</summary>
public const string APIDocumentation = Documentation + "/api/" + nameof(Animancer);
/// <summary>The email address which handles support for Animancer.</summary>
public const string DeveloperEmail = "animancer@kybernetik.com.au";
/// <summary>The URL of the file which lists Animancer's latest version.</summary>
public const string LatestVersion = Documentation + "/latest-version.txt";
/************************************************************************************************************************/
public const string OptionalWarning = APIDocumentation + "/" + nameof(Animancer.OptionalWarning);
/************************************************************************************************************************/
#if UNITY_ASSERTIONS
/************************************************************************************************************************/
public const string Docs = Documentation + "/docs/";
public const string AnimancerEvents = Docs + "manual/events/animancer";
public const string EndEvents = Docs + "manual/events/end";
public const string AnimancerEventParameters = AnimancerEvents + "/parameters";
public const string AnimatorControllers = Docs + "manual/animator-controllers";
public const string AnimatorControllersNative = AnimatorControllers + "#native";
public const string Fading = Docs + "manual/blending/fading";
public const string FadeModes = Fading + "/modes";
/************************************************************************************************************************/
#endif
/************************************************************************************************************************/
#if UNITY_EDITOR
/************************************************************************************************************************/
public const string Samples = Docs + "samples";
public const string UnevenGround = Docs + "samples/ik/uneven-ground";
public const string OdinInspector = Docs + "samples/integration/odin-inspector";
public const string AnimancerTools = Docs + "manual/tools";
public const string PackTextures = AnimancerTools + "/pack-textures";
public const string ModifySprites = AnimancerTools + "/modify-sprites";
public const string RenameSprites = AnimancerTools + "/rename-sprites";
public const string GenerateSpriteAnimations = AnimancerTools + "/generate-sprite-animations";
public const string RemapSpriteAnimation = AnimancerTools + "/remap-sprite-animation";
public const string RemapAnimationBindings = AnimancerTools + "/remap-animation-bindings";
public const string Inspector = Docs + "manual/playing/inspector";
public const string States = Docs + "manual/playing/states";
public const string Layers = Docs + "manual/blending/layers";
public const string Parameters = Docs + "manual/parameters";
public const string TransitionPreviews = Docs + "manual/transitions/previews";
public const string TransitionLibraries = Docs + "manual/transitions/libraries";
public const string UpdateModes = Docs + "bugs/update-modes";
public const string VersionName = "v8.3.0";
public const string ChangeLogURL = Docs + "changes/animancer-v8-3";
public const string UpgradeGuideURL = ChangeLogURL + "/upgrade-guide";
public const string Discussions = "https://discussions.unity.com/t/animancer-less-animator-controller-more-animator-control/717489/99999";
public const string Issues = "https://github.com/KybernetikGames/animancer/issues";
/************************************************************************************************************************/
#endif
/************************************************************************************************************************/
}
/************************************************************************************************************************/
/// <summary>Tooltips for various fields.</summary>
/// https://kybernetik.com.au/animancer/api/Animancer/Tooltips
///
public static class Tooltips
{
/************************************************************************************************************************/
public const string MiddleClickReset =
"\n• Middle Click = reset to default value";
public const string FadeDuration = ProOnlyTag +
"The amount of time the transition will take, e.g:" +
"\n• 0s = instant" +
"\n• 0.25s = quarter of a second (Default)" +
"\n• 0.25x = quarter of the animation length" +
"\n• " + AnimationTimeAttribute.Tooltip +
MiddleClickReset;
public const string Speed = ProOnlyTag +
"How fast the animation will play, e.g:" +
"\n• 0x = paused" +
"\n• 1x = normal speed" +
"\n• -2x = double speed backwards";
public const string OptionalSpeed = Speed +
"\n• Disabled = continue at current speed" +
MiddleClickReset;
public const string SpeedDisabled =
"Continue at current speed";
public const string NormalizedStartTime = ProOnlyTag +
"• Enabled = use " + nameof(FadeMode) + "." + nameof(FadeMode.FromStart) +
" and restart at this time." +
"\n• Disabled = use " + nameof(FadeMode) + "." + nameof(FadeMode.FixedSpeed) +
" and continue from the current time if already playing." +
"\n• " + AnimationTimeAttribute.Tooltip;
public const string StartTimeDisabled =
"Continue from current time";
public const string EndTime = ProOnlyTag +
"The time when the End Callback will be triggered." +
"\n• " + AnimationTimeAttribute.Tooltip +
"\n\nDisabling the toggle automates the value:" +
"\n• Speed >= 0 ends at 1x" +
"\n• Speed < 0 ends at 0x";
public const string CallbackTime = ProOnlyTag +
"The time when the Event Callback will be triggered." +
"\n• " + AnimationTimeAttribute.Tooltip;
public const string MixerParameterBinding =
"[Optional] The mixer's parameter will be controlled by the Animancer parameter with this name";
/************************************************************************************************************************/
}
/************************************************************************************************************************/
}
}

View File

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

View File

@@ -0,0 +1,74 @@
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
namespace Animancer
{
/// https://kybernetik.com.au/animancer/api/Animancer/Validate
public static partial class Validate
{
/************************************************************************************************************************/
/// <summary>A rule that defines which values are valid.</summary>
/// https://kybernetik.com.au/animancer/api/Animancer/Value
public enum Value
{
/// <summary>Any value is allowed.</summary>
Any,
/// <summary>Only values between 0 (inclusive) and 1 (inclusive) are allowed.</summary>
ZeroToOne,
/// <summary>Only 0 or positive values are allowed.</summary>
IsNotNegative,
/// <summary>Infinity and NaN are not allowed.</summary>
IsFinite,
/// <summary>Infinity is not allowed.</summary>
IsFiniteOrNaN,
}
/************************************************************************************************************************/
/// <summary>Enforces the `rule` on the `value`.</summary>
public static void ValueRule(ref float value, Value rule)
{
switch (rule)
{
case Value.Any:
default:
return;
case Value.ZeroToOne:
if (!(value >= 0))// Reversed comparison to include NaN.
value = 0;
else if (value > 1)
value = 1;
break;
case Value.IsNotNegative:
if (!(value >= 0))// Reversed comparison to include NaN.
value = 0;
break;
case Value.IsFinite:
if (float.IsNaN(value))
value = 0;
else if (float.IsPositiveInfinity(value))
value = float.MaxValue;
else if (float.IsNegativeInfinity(value))
value = float.MinValue;
break;
case Value.IsFiniteOrNaN:
if (float.IsPositiveInfinity(value))
value = float.MaxValue;
else if (float.IsNegativeInfinity(value))
value = float.MinValue;
break;
}
}
/************************************************************************************************************************/
}
}

View File

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

View File

@@ -0,0 +1,188 @@
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
using System;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Playables;
namespace Animancer
{
/// <summary>
/// Enforces various rules throughout the system, most of which are compiled out if UNITY_ASSERTIONS is not defined
/// (by default, it is only defined in the Unity Editor and in Development Builds).
/// </summary>
/// https://kybernetik.com.au/animancer/api/Animancer/Validate
///
public static partial class Validate
{
/************************************************************************************************************************/
/// <summary>[Assert-Conditional]
/// Throws if the `clip` is <c>null</c>, not an asset, or marked as <see cref="AnimationClip.legacy"/>.
/// </summary>
/// <exception cref="NullReferenceException"/>
/// <exception cref="ArgumentException"/>
[System.Diagnostics.Conditional(Strings.Assertions)]
public static void AssertAnimationClip(AnimationClip clip, bool throwIfNull, string operation)
{
#if UNITY_ASSERTIONS
if (clip == null)
{
if (!throwIfNull)
return;
#pragma warning disable IDE0041 // Use 'is null' check (that would suggest changing to == which is wrong).
var error = ReferenceEquals(clip, null)
? $"Unable to {operation} because the {nameof(AnimationClip)} is null."
: $"Unable to {operation} because the {nameof(AnimationClip)} has been destroyed.";
throw new NullReferenceException(error);
#pragma warning restore IDE0041 // Use 'is null' check.
}
#if UNITY_EDITOR
if (OptionalWarning.DynamicAnimation.IsEnabled() &&
!UnityEditor.EditorUtility.IsPersistent(clip))
OptionalWarning.DynamicAnimation.Log(
$"Attempted to {operation} using an {nameof(AnimationClip)} '{clip.name}' which is not an asset." +
" Unity doesn't support dynamically creating animations for Animancer in runtime builds." +
" This warning should be disabled if you're loading animations from Asset Bundles or Addressables" +
" or if you only intend to use the animation in the Unity Editor.",
clip);
#endif
if (clip.legacy)
throw new ArgumentException(
$"Unable to {operation} because the {nameof(AnimationClip)} '{clip.name}' is a lagacy animation" +
" and therefore cannot be used by Animancer" +
" If it was imported as part of a model then the model's Rig type must be Humanoid or Generic." +
" Otherwise you can use the 'Toggle Legacy' function in the clip's context menu" +
" (via the cog icon in the top right of its Inspector).");
#endif
}
/************************************************************************************************************************/
/// <summary>[Assert-Conditional] Throws if the <see cref="AnimancerNodeBase.Graph"/> is not the `graph`.</summary>
/// <exception cref="ArgumentException"/>
[System.Diagnostics.Conditional(Strings.Assertions)]
public static void AssertGraph(AnimancerNode node, AnimancerGraph graph)
{
#if UNITY_ASSERTIONS
if (node.Graph != graph)
{
AnimancerNodeBase.MarkAsUsed(node);
throw new ArgumentException(
$"{nameof(AnimancerNode)}.{nameof(AnimancerNode.Graph)} mismatch:" +
$" cannot use a node in an {nameof(AnimancerGraph)} that is not its {nameof(AnimancerNode.Graph)}: " +
node.GetDescription());
}
#endif
}
/************************************************************************************************************************/
/// <summary>[Assert-Conditional] Throws if the `node`'s <see cref="Playable"/> is invalid.</summary>
/// <exception cref="InvalidOperationException"/>
[System.Diagnostics.Conditional(Strings.Assertions)]
public static void AssertPlayable(AnimancerNode node)
{
#if UNITY_ASSERTIONS
if (node._Playable.IsValid() &&
node.Graph._PlayableGraph.IsValid())
return;
var description = node.ToString();
var stackTrace = AnimancerNode.GetConstructorStackTrace(node);
if (stackTrace != null)
description += "\n\n" + stackTrace;
AnimancerNodeBase.MarkAsUsed(node);
if (node is AnimancerState state)
state.Destroy();
if (node.Graph == null)
throw new InvalidOperationException(
$"{nameof(AnimancerNode)}.{nameof(AnimancerNode.Graph)} hasn't been set so its" +
$" {nameof(Playable)} hasn't been created. It can be set by playing the state" +
$" or calling {nameof(AnimancerState.SetGraph)} on it directly." +
$" {nameof(AnimancerState.SetParent)} would also work if the parent has a" +
$" {nameof(AnimancerNode.Graph)}." +
$"\n• Node: {description}");
else if (!node.Graph._PlayableGraph.IsValid())
throw new InvalidOperationException(
$"{nameof(AnimancerGraph)}.{nameof(AnimancerGraph.PlayableGraph)} has already been destroyed." +
$" This is often caused by a character attempting to access a state on a different character," +
$" such as if they share a Transition and are both accessing its State without realising it" +
$" only holds the most recently played state." +
$"\n• Graph: {node.Graph}" +
$"\n• Node: {description}");
else
throw new InvalidOperationException(
$"{nameof(AnimancerNode)}.{nameof(AnimancerNodeBase.Playable)}" +
$" has either been destroyed or was never created." +
$"\n• Graph: {node.Graph}" +
$"\n• Node: {description}");
#endif
}
/************************************************************************************************************************/
/// <summary>[Assert-Conditional]
/// Throws if the `state` was not actually assigned to its specified <see cref="AnimancerNode.Index"/> in
/// the `states`.
/// </summary>
/// <exception cref="InvalidOperationException"/>
/// <exception cref="IndexOutOfRangeException">
/// The <see cref="AnimancerNode.Index"/> is larger than the number of `states`.
/// </exception>
[System.Diagnostics.Conditional(Strings.Assertions)]
public static void AssertCanRemoveChild(AnimancerState state, IList<AnimancerState> childStates, int childCount)
{
#if UNITY_ASSERTIONS
var index = state.Index;
if (index < 0)
throw new InvalidOperationException(
$"Cannot remove a child state that did not have an {nameof(state.Index)} assigned");
if ((uint)index >= (uint)childCount)
throw new IndexOutOfRangeException(
$"{nameof(AnimancerState)}.{nameof(state.Index)} ({index})" +
$" is outside the collection of states (Count {childCount})");
if (childStates[index] != state)
throw new InvalidOperationException(
$"Cannot remove a child state that was not actually connected to its port on {state.Parent}:" +
$"\n• Port: {index}" +
$"\n• Connected Child: {AnimancerUtilities.ToStringOrNull(childStates[index])}" +
$"\n• Disconnecting Child: {AnimancerUtilities.ToStringOrNull(state)}");
#endif
}
/************************************************************************************************************************/
/// <summary>[Assert-Conditional] Throws if the `weight` is negative, infinity, or NaN.</summary>
/// <exception cref="ArgumentOutOfRangeException"/>
[System.Diagnostics.Conditional(Strings.Assertions)]
public static void AssertSetWeight(AnimancerNode node, float weight)
{
#if UNITY_ASSERTIONS
if (!(weight >= 0) || weight == float.PositiveInfinity)// Reversed comparison includes NaN.
{
AnimancerNodeBase.MarkAsUsed(node);
throw new ArgumentOutOfRangeException(
nameof(weight),
weight,
$"{nameof(AnimancerNode.Weight)} must be a finite positive value");
}
#endif
}
/************************************************************************************************************************/
}
}

View File

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