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,84 @@
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
using UnityEngine;
namespace Animancer
{
/// <summary>Interface for components that manage an <see cref="AnimancerGraph"/>.</summary>
/// <remarks>
/// Despite the name, this interface is not necessarily limited to only <see cref="Component"/>s.
/// <para></para>
/// This interface allows Animancer Lite to reference an <see cref="AnimancerComponent"/> inside the pre-compiled
/// DLL while allowing that component to remain outside as a regular script. Otherwise everything would need to be
/// in the DLL which would cause Unity to lose all the script references when upgrading from Animancer Lite to Pro.
/// <para></para>
/// <strong>Documentation:</strong>
/// <see href="https://kybernetik.com.au/animancer/docs/manual/playing/component-types">
/// Component Types</see>
/// </remarks>
/// https://kybernetik.com.au/animancer/api/Animancer/IAnimancerComponent
///
public interface IAnimancerComponent
{
/************************************************************************************************************************/
#pragma warning disable IDE1006 // Naming Styles.
/************************************************************************************************************************/
/// <summary>Will this component be updated?</summary>
bool enabled { get; }
/// <summary>The <see cref="GameObject"/> this component is attached to.</summary>
GameObject gameObject { get; }
/************************************************************************************************************************/
#pragma warning restore IDE1006 // Naming Styles.
/************************************************************************************************************************/
/// <summary>The <see cref="UnityEngine.Animator"/> component which this script controls.</summary>
Animator Animator { get; set; }
/// <summary>The internal system which manages the playing animations.</summary>
AnimancerGraph Graph { get; }
/// <summary>Has the <see cref="Graph"/> been initialized?</summary>
bool IsGraphInitialized { get; }
/// <summary>Will the object be reset to its original values when disabled?</summary>
bool ResetOnDisable { get; }
/// <summary>
/// Determines when animations are updated and which time source is used. This property is mainly a wrapper
/// around the <see cref="Animator.updateMode"/>.
/// </summary>
AnimatorUpdateMode UpdateMode { get; set; }
/************************************************************************************************************************/
/// <summary>Returns the dictionary key to use for the `clip`.</summary>
object GetKey(AnimationClip clip);
/************************************************************************************************************************/
#if UNITY_EDITOR
/************************************************************************************************************************/
/// <summary>[Editor-Only] The name of the serialized backing field for the <see cref="Animator"/> property.</summary>
string AnimatorFieldName { get; }
/// <summary>[Editor-Only]
/// The name of the serialized backing field for the <see cref="AnimancerComponent.ActionOnDisable"/> property.
/// </summary>
string ActionOnDisableFieldName { get; }
/// <summary>[Editor-Only] The <see cref="UpdateMode"/> that was first used when this script initialized.</summary>
/// <remarks>
/// This is used to give a warning when changing to or from <see cref="AnimatorUpdateMode.AnimatePhysics"/> at
/// runtime since it won't work correctly.
/// </remarks>
AnimatorUpdateMode? InitialUpdateMode { get; }
/************************************************************************************************************************/
#endif
/************************************************************************************************************************/
}
}

View File

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

View File

@@ -0,0 +1,265 @@
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Playables;
namespace Animancer
{
/// <summary>
/// A variant of <see cref="IAnimationClipSource"/> which uses a <see cref="ICollection{T}"/>
/// instead of a <see cref="List{T}"/> so that it can take a <see cref="HashSet{T}"/>
/// to efficiently avoid adding duplicates. <see cref="AnimancerUtilities"/> contains
/// various extension methods for this purpose.
/// </summary>
/// <remarks>
/// <see cref="IAnimationClipSource"/> still needs to be the main point of entry
/// for the Animation Window, so this interface is only used internally.
/// </remarks>
/// https://kybernetik.com.au/animancer/api/Animancer/IAnimationClipCollection
///
public interface IAnimationClipCollection
{
/************************************************************************************************************************/
/// <summary>Adds all the animations associated with this object to the `clips`.</summary>
void GatherAnimationClips(ICollection<AnimationClip> clips);
/************************************************************************************************************************/
}
/************************************************************************************************************************/
/// https://kybernetik.com.au/animancer/api/Animancer/AnimancerUtilities
public static partial class AnimancerUtilities
{
/************************************************************************************************************************/
/// <summary>[Animancer Extension]
/// Adds the `clip` to the `clips` if it wasn't there already.
/// </summary>
public static void Gather(this ICollection<AnimationClip> clips, AnimationClip clip)
{
if (clip != null && !clips.Contains(clip))
clips.Add(clip);
}
/************************************************************************************************************************/
/// <summary>[Animancer Extension]
/// Calls <see cref="Gather(ICollection{AnimationClip}, AnimationClip)"/> for each of the `newClips`.
/// </summary>
public static void Gather(this ICollection<AnimationClip> clips, IList<AnimationClip> gatherFrom)
{
if (gatherFrom == null)
return;
for (int i = gatherFrom.Count - 1; i >= 0; i--)
clips.Gather(gatherFrom[i]);
}
/************************************************************************************************************************/
/// <summary>[Animancer Extension]
/// Calls <see cref="Gather(ICollection{AnimationClip}, AnimationClip)"/> for each of the `newClips`.
/// </summary>
public static void Gather(this ICollection<AnimationClip> clips, IEnumerable<AnimationClip> gatherFrom)
{
if (gatherFrom == null)
return;
foreach (var clip in gatherFrom)
clips.Gather(clip);
}
/************************************************************************************************************************/
/// <summary>[Animancer Extension]
/// Calls <see cref="Gather(ICollection{AnimationClip}, AnimationClip)"/> for each clip in the `asset`.
/// </summary>
public static void GatherFromAsset(this ICollection<AnimationClip> clips, PlayableAsset asset)
{
if (asset == null)
return;
// We want to get the tracks out of a TimelineAsset without actually referencing that class directly
// because it comes from an optional package and Animancer does not need to depend on that package.
var method = asset.GetType().GetMethod("GetRootTracks");
if (method != null &&
typeof(IEnumerable).IsAssignableFrom(method.ReturnType) &&
method.GetParameters().Length == 0)
{
var rootTracks = method.Invoke(asset, null);
GatherFromTracks(clips, rootTracks as IEnumerable);
}
}
/************************************************************************************************************************/
/// <summary>Gathers all the animations in the `tracks`.</summary>
private static void GatherFromTracks(ICollection<AnimationClip> clips, IEnumerable tracks)
{
if (tracks.IsNullOrDestroyed())
return;
foreach (var track in tracks)
{
if (track.IsNullOrDestroyed())
continue;
var trackType = track.GetType();
var getClips = trackType.GetMethod("GetClips");
if (getClips != null &&
typeof(IEnumerable).IsAssignableFrom(getClips.ReturnType) &&
getClips.GetParameters().Length == 0)
{
if (getClips.Invoke(track, null) is IEnumerable trackClips)
{
foreach (var clip in trackClips)
{
var animationClip = clip.GetType().GetProperty("animationClip");
if (animationClip != null &&
animationClip.PropertyType == typeof(AnimationClip))
{
var getClip = animationClip.GetGetMethod();
clips.Gather(getClip.Invoke(clip, null) as AnimationClip);
}
}
}
}
var getChildTracks = trackType.GetMethod("GetChildTracks");
if (getChildTracks != null &&
typeof(IEnumerable).IsAssignableFrom(getChildTracks.ReturnType) &&
getChildTracks.GetParameters().Length == 0)
{
var childTracks = getChildTracks.Invoke(track, null);
GatherFromTracks(clips, childTracks as IEnumerable);
}
}
}
/************************************************************************************************************************/
/// <summary>[Animancer Extension]
/// Calls <see cref="Gather(ICollection{AnimationClip}, AnimationClip)"/>
/// for each clip gathered by <see cref="IAnimationClipSource.GetAnimationClips"/>.
/// </summary>
public static void GatherFromSource(this ICollection<AnimationClip> clips, IAnimationClipSource source)
{
if (source.IsNullOrDestroyed())
return;
var list = ListPool.Acquire<AnimationClip>();
source.GetAnimationClips(list);
clips.Gather(list);
ListPool.Release(list);
}
/************************************************************************************************************************/
/// <summary>[Animancer Extension]
/// Calls <see cref="GatherFromSource(ICollection{AnimationClip}, object)"/> for each item in the `source`.
/// </summary>
public static void GatherFromSource(this ICollection<AnimationClip> clips, IEnumerable source)
{
if (source.IsNullOrDestroyed() ||
AnimationGathererRecursionGuard.DontGatherFrom.Contains(source.GetType()))
return;
using var _ = AnimationGathererRecursionGuard.Begin();
if (AnimationGathererRecursionGuard.HasCheckedObject(source))
return;
foreach (var item in source)
clips.GatherFromSource(item);
}
/************************************************************************************************************************/
/// <summary>[Animancer Extension]
/// Calls <see cref="Gather(ICollection{AnimationClip}, AnimationClip)"/>
/// for each clip in the `source`, supporting both
/// <see cref="IAnimationClipSource"/> and <see cref="IAnimationClipCollection"/>.
/// </summary>
public static bool GatherFromSource(this ICollection<AnimationClip> clips, object source)
{
if (source.IsNullOrDestroyed() ||
AnimationGathererRecursionGuard.DontGatherFrom.Contains(source.GetType()))
return false;
using var _ = AnimationGathererRecursionGuard.Begin();
if (AnimationGathererRecursionGuard.HasCheckedObject(source))
return false;
if (TryGetWrappedObject(source, out AnimationClip clip))
{
clips.Gather(clip);
return true;
}
if (TryGetWrappedObject(source, out IAnimationClipCollection collectionSource))
{
collectionSource.GatherAnimationClips(clips);
return true;
}
if (TryGetWrappedObject(source, out IAnimationClipSource listSource))
{
clips.GatherFromSource(listSource);
return true;
}
if (TryGetWrappedObject(source, out IEnumerable enumerable))
{
clips.GatherFromSource(enumerable);
return true;
}
return false;
}
/************************************************************************************************************************/
/// <summary>
/// Attempts to get the <see cref="AnimationClip.frameRate"/> from the `clipSource`
/// and returns true if successful. If it has multiple animations with different rates,
/// this method returns false.
/// </summary>
public static bool TryGetFrameRate(object clipSource, out float frameRate)
{
using (SetPool<AnimationClip>.Instance.Acquire(out var clips))
{
clips.GatherFromSource(clipSource);
if (clips.Count == 0)
{
frameRate = float.NaN;
return false;
}
frameRate = float.NaN;
foreach (var clip in clips)
{
if (float.IsNaN(frameRate))
{
frameRate = clip.frameRate;
}
else if (frameRate != clip.frameRate)
{
frameRate = float.NaN;
return false;
}
}
return frameRate > 0;
}
}
/************************************************************************************************************************/
}
}

View File

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

View File

@@ -0,0 +1,203 @@
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
using UnityEngine;
namespace Animancer
{
/// <summary>
/// Interface for components to indicate which <see cref="GameObject"/> is the root of a character when
/// <see cref="AnimancerUtilities.FindRoot(GameObject)"/> is called.
/// </summary>
/// https://kybernetik.com.au/animancer/api/Animancer/ICharacterRoot
///
public interface ICharacterRoot
{
/************************************************************************************************************************/
#pragma warning disable IDE1006 // Naming Styles.
/************************************************************************************************************************/
/// <summary>
/// The <see cref="Transform"/> to search for <see cref="AnimationClip"/>s beneath.
/// </summary>
///
/// <remarks>
/// <strong>Example:</strong>
/// Implementing this interface in a <see cref="MonoBehaviour"/>
/// will automatically inherit this property so you don't need to do anything else:
/// <para></para><code>
/// public class MyComponent : MonoBehaviour, ICharacterRoot
/// {
/// }
/// </code>
/// But if you want to have your script point to a different object as the root,
/// you can explicitly implement this property:
/// <para></para><code>
/// public class MyComponent : MonoBehaviour, ICharacterRoot
/// {
/// Transform ICharacterRoot.transform => ???;
/// }
/// </code></remarks>
Transform transform { get; }
/************************************************************************************************************************/
#pragma warning restore IDE1006 // Naming Styles.
/************************************************************************************************************************/
}
/************************************************************************************************************************/
#if UNITY_EDITOR
/************************************************************************************************************************/
/// https://kybernetik.com.au/animancer/api/Animancer/AnimancerUtilities
public static partial class AnimancerUtilities
{
/************************************************************************************************************************/
/// <summary>[Editor-Only]
/// Takes a `gameObject` and returns the root <see cref="Transform"/> of the character it is part of.
/// </summary>
///
/// <remarks>
/// This method first searches all parents for a component implementing <see cref="ICharacterRoot"/>.
/// If it finds one, it returns the <see cref="ICharacterRoot.transform"/>.
/// <para></para>
/// Otherwise, if the object is part of a prefab then it returns the root of that prefab instance.
/// <para></para>
/// Otherwise, it counts the number of Animators in the children of the `gameObject` then does
/// the same for each parent. If it finds a parent with a different number of child Animators, it
/// assumes that object is the parent of multiple characters and returns the previous parent as the root.
/// </remarks>
///
/// <example>
/// <h2>Simple Hierarchy</h2>
/// <code>
/// - Character - Rigidbody, etc.
/// - Model - Animator, AnimancerComponent
/// - States - Various components which reference the AnimationClips they will play
/// </code>
/// Passing the <c>Model</c> into this method will return the <c>Character</c> because it has the same
/// number of Animator components in its children.
///
/// <h2>Shared Hierarchy</h2>
/// <code>
/// - Characters - Empty object used to group all characters
/// - Character - Rigidbody, etc.
/// - Model - Animator, AnimancerComponent
/// - States - Various components which reference the AnimationClips they will play
/// - Another Character
/// - Model
/// - States
/// </code>
/// <list type="bullet">
/// <item><c>Model</c> has one Animator and no more in its children.</item>
/// <item>And <c>Character</c> has one Animator in its children (the same one).</item>
/// <item>But <c>Characters</c> has two Animators in its children (one on each character).</item>
/// </list>
/// So it picks the <c>Character</c> as the root.
///
/// <h2>Complex Hierarchy</h2>
/// <code>
/// - Character - Rigidbody, etc.
/// - Model - Animator, AnimancerComponent
/// - States - Various components which reference the AnimationClips they will play
/// - Another Model - Animator (maybe the character is holding a gun which has a reload animation)
/// </code>
/// In this case, the automatic system would see that the <c>Character</c> already has more child
/// <see cref="Animator"/>s than the selected <c>Model</c> so it would only return the <c>Model</c> itself.
/// This can be fixed by making any of the scripts on the <c>Character</c> implement <see cref="ICharacterRoot"/>
/// to tell the system which object you want it to use as the root.
/// </example>
public static Transform FindRoot(GameObject gameObject)
{
var root = gameObject.GetComponentInParent<ICharacterRoot>();
if (root != null)
return root.transform;
#if UNITY_EDITOR
var path = UnityEditor.AssetDatabase.GetAssetPath(gameObject);
if (!string.IsNullOrEmpty(path))
return gameObject.transform.root;
var status = UnityEditor.PrefabUtility.GetPrefabInstanceStatus(gameObject);
if (status != UnityEditor.PrefabInstanceStatus.NotAPrefab)
{
gameObject = UnityEditor.PrefabUtility.GetOutermostPrefabInstanceRoot(gameObject);
return gameObject.transform;
}
#endif
var animators = ListPool.Acquire<Animator>();
gameObject.GetComponentsInChildren(true, animators);
var animatorCount = animators.Count;
var parent = gameObject.transform;
while (parent.parent != null)
{
animators.Clear();
parent.parent.GetComponentsInChildren(true, animators);
if (animatorCount == 0)
animatorCount = animators.Count;
else if (animatorCount != animators.Count)
break;
parent = parent.parent;
}
ListPool.Release(animators);
return parent;
}
/************************************************************************************************************************/
/// <summary>[Editor-Only]
/// Calls <see cref="FindRoot(GameObject)"/> if the specified `obj` is a
/// <see cref="GameObject"/> or <see cref="Component"/>.
/// </summary>
public static Transform FindRoot(Object obj)
{
if (obj is ICharacterRoot iRoot)
return iRoot.transform;
return TryGetGameObject(obj, out var gameObject)
? FindRoot(gameObject)
: null;
}
/************************************************************************************************************************/
/// <summary>[Editor-Only]
/// Outputs the <see cref="GameObject"/> assignated with the `obj` and returns true if it exists.
/// </summary>
/// <remarks>
/// If the `obj` is a <see cref="GameObject"/> it is used as the result.
/// <para></para>
/// Or if the `obj` is a <see cref="Component"/> then its <see cref="Component.gameObject"/>
/// is used as the result.
/// </remarks>
public static bool TryGetGameObject(Object obj, out GameObject gameObject)
{
if (obj is GameObject go)
{
gameObject = go;
return true;
}
if (obj is Component component)
{
gameObject = component.gameObject;
return true;
}
gameObject = null;
return false;
}
/************************************************************************************************************************/
}
/************************************************************************************************************************/
#endif
/************************************************************************************************************************/
}

View File

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

View File

@@ -0,0 +1,315 @@
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
//#define ASSERT_CLONE_TYPES
using System.Collections.Generic;
using UnityEngine;
namespace Animancer
{
/// <summary>An object that can be cloned.</summary>
/// <remarks>See <see cref="Clone(CloneContext)"/> for example usage.</remarks>
/// https://kybernetik.com.au/animancer/api/Animancer/ICloneable_1
public interface ICloneable<out T>
{
/************************************************************************************************************************/
/// <summary>Creates a new object with the same type and values this.</summary>
///
/// <remarks>
/// Calling this method directly is not generally recommended.
/// Use <see cref="CloneableExtensions.Clone{T}(ICloneable{T})"/> if you don't have a `context`
/// or <see cref="CloneContext.GetOrCreateClone{T}(ICloneable{T})"/> if you do have one.
/// <para></para>
/// <strong>Example:</strong><code>
/// class BaseClass : ICloneable
/// {
/// // Explicit implementation so that the recommended methods will be used instead.
/// object ICloneable.Clone(CloneContext context)
/// {
/// var clone = (BaseClass)MemberwiseClone();
/// clone.CopyFrom(this, context);
/// return clone;
/// }
///
/// // Protected method which should only be called by Clone.
/// protected virtual void InitializeClone(CloneContext context)
/// {
/// // Alter any necessary BaseClass fields according to the context.
/// }
/// }
///
/// class DerivedClass : BaseClass
/// {
/// protected override void InitializeClone(CloneContext context)
/// {
/// base.CopyFrom(copyFrom, context);
///
/// var derived = (DerivedClass)copyFrom;
/// // Alter any necessary DerivedClass fields according to the context.
/// }
/// }
/// </code></remarks>
T Clone(CloneContext context);
/************************************************************************************************************************/
}
/// <summary>Extension methods for <see cref="ICloneable{T}"/>.</summary>
/// https://kybernetik.com.au/animancer/api/Animancer/CloneableExtensions
public static partial class CloneableExtensions
{
/************************************************************************************************************************/
/// <summary>
/// Calls <see cref="ICloneable{T}.Clone"/> using a <see cref="CloneContext"/> from the
/// <see cref="CloneContext.Pool"/> and casts the result.
/// </summary>
/// <remarks>
/// Returns <c>null</c> if the `original` is <c>null</c>.
/// <para></para>
/// Use <see cref="CloneContext.GetOrCreateClone{T}(ICloneable{T})"/>
/// if you already have a <see cref="CloneContext"/>.
/// </remarks>
public static T Clone<T>(this ICloneable<T> original)
{
if (original == null)
return default;
var context = CloneContext.Pool.Instance.Acquire();
var clone = original.Clone(context);
CloneContext.Pool.Instance.Release(context);
return clone;
}
/************************************************************************************************************************/
/// <summary>[Assert-Conditional] Asserts that the `clone` has the same type as the `original`.</summary>
[System.Diagnostics.Conditional(Strings.Assertions)]
internal static void AssertCloneType<T>(this ICloneable<T> original, object clone)
{
#if UNITY_ASSERTIONS
var cloneType = clone.GetType();
var originalType = original.GetType();
if (cloneType != originalType)
Debug.LogError($"Cloned object type ({cloneType.FullName}" +
$" doesn't match original {originalType.FullName}." +
$"\n• Original: {original}" +
$"\n• Clone: {clone}");
#endif
}
/************************************************************************************************************************/
}
/// <summary>A dictionary which maps objects to their copies.</summary>
/// <remarks>
/// This class is used to clone complex object trees with potentially interconnected references so that
/// references to original objects can be replaced with references to their equivalent clones.
/// </remarks>
public class CloneContext : Dictionary<object, object>
{
/************************************************************************************************************************/
/// <summary>Will the <see cref="IUpdatable"/>s be cloned as part of the current operation?</summary>
/// <remarks>
/// This is used to prevent <see cref="AnimancerNode"/>s from cloning their <see cref="FadeGroup"/>s
/// individually while cloning a whole <see cref="AnimancerGraph"/> because it will clone the whole groups
/// after cloning all the nodes.
/// </remarks>
public bool WillCloneUpdatables { get; set; }
/************************************************************************************************************************/
#region Pooling
/************************************************************************************************************************/
/// <summary>An <see cref="ObjectPool{T}"/> for <see cref="CloneContext"/>.</summary>
/// https://kybernetik.com.au/animancer/api/Animancer/Pool
public class Pool : ObjectPool<CloneContext>
{
/************************************************************************************************************************/
/// <summary>Singleton.</summary>
public static Pool Instance = new();
/************************************************************************************************************************/
/// <inheritdoc/>
protected override CloneContext New()
=> new();
/************************************************************************************************************************/
/// <inheritdoc/>
public override CloneContext Acquire()
{
var context = base.Acquire();
CollectionPool<KeyValuePair<object, object>, CloneContext>.AssertEmpty(context);
return context;
}
/************************************************************************************************************************/
/// <inheritdoc/>
public override void Release(CloneContext context)
{
context.Clear();
base.Release(context);
}
/************************************************************************************************************************/
}
/************************************************************************************************************************/
#endregion
/************************************************************************************************************************/
/// <summary>
/// Returns the value registered using `original` as its key if there is one.
/// Otherwise, calls <see cref="CloneableExtensions.Clone"/>, adds the clone to this dictionary, and returns it.
/// </summary>
public T GetOrCreateClone<T>(ICloneable<T> original)
{
if (original == null)
return default;
if (TryGetValue(original, out var value))
return (T)value;
return Clone(original);
}
/************************************************************************************************************************/
/// <summary>
/// Returns the value registered using `original` as its key if there is one.
/// Otherwise, if the `original` is <see cref="ICloneable{T}"/> it calls <see cref="CloneableExtensions.Clone"/>,
/// adds the clone to this dictionary, and returns it.
/// Otherwise, just returns the `original`.
/// </summary>
/// <remarks>
/// If <typeparamref name="T"/> is <see cref="ICloneable{T}"/>,
/// use <see cref="GetOrCreateClone{T}(ICloneable{T})"/> instead.
/// </remarks>
public T GetOrCreateCloneOrOriginal<T>(T original)
{
TryGetOrCreateCloneOrOriginal(original, out original);
return original;
}
/// <summary>
/// Returns <c>true</c> if there is a `clone` registered for the `original`.
/// Otherwise, if the `original` is <see cref="ICloneable{T}"/> it calls <see cref="CloneableExtensions.Clone"/>,
/// adds the `clone` to this dictionary, and returns <c>true</c>.
/// Otherwise, outputs the `original` as the `clone` and returns <c>false</c>.
/// </summary>
/// <remarks>Outputs <c>null</c> and returns <c>true</c> if the `original` is <c>null</c>.</remarks>
public bool TryGetOrCreateCloneOrOriginal<T>(T original, out T clone)
{
if (original == null)
{
clone = default;
return true;
}
if (TryGetValue(original, out var value))
{
clone = (T)value;
return true;
}
if (original is ICloneable<T> cloneable)
{
clone = Clone(cloneable);
return true;
}
clone = original;
return false;
}
/************************************************************************************************************************/
/// <summary>Calls <see cref="ICloneable{T}.Clone"/> and registers the clone.</summary>
/// <exception cref="System.ArgumentException">A clone is already registered for the `original`.</exception>
public T Clone<T>(ICloneable<T> original)
{
var clone = original.Clone(this);
if (clone != null)
{
original.AssertCloneType(clone);
Add(original, clone);
}
return clone;
}
/************************************************************************************************************************/
/// <summary>Returns the clone of the `original` if one was registered. Otherwise, throws.</summary>
/// <exception cref="KeyNotFoundException">No clone of the `original` is registered.</exception>
public T GetClone<T>(T original)
=> (T)this[original];
/************************************************************************************************************************/
/// <summary>Returns the clone of the `original` if one is registered. Otherwise, returns the `original`.</summary>
public T GetCloneOrOriginal<T>(T original)
=> original != null && TryGetValue(original, out var value)
? (T)value
: original;
/************************************************************************************************************************/
/// <summary>Replaces the `item` with its clone and returns true if one is registered.</summary>
public bool TryGetClone<T>(ref T item)
{
if (item == null)
return false;
if (!TryGetValue(item, out var value) ||
value is not T valueT)
{
item = default;
return false;
}
item = valueT;
return true;
}
/// <summary>Calls <see cref="Dictionary{TKey, TValue}.TryGetValue(TKey, out TValue)"/> and casts the result.</summary>
public bool TryGetClone<T>(T original, out T clone)
{
clone = original;
return TryGetClone(ref clone);
}
/************************************************************************************************************************/
/// <summary>
/// Ensures the `destination` has the same size as `source`
/// then clones all its items to the `destination`.
/// </summary>
public void CloneArray<T>(T[] source, ref T[] destination)
where T : class
{
if (source == null)
{
destination = null;
return;
}
if (destination == null || destination.Length != source.Length)
destination = new T[source.Length];
for (int i = 0; i < source.Length; i++)
{
var item = source[i];
destination[i] = item != null ? GetCloneOrOriginal(item) : null;
}
}
/************************************************************************************************************************/
}
}

View File

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

View File

@@ -0,0 +1,133 @@
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
using System;
using System.Collections.Generic;
using UnityEngine;
namespace Animancer
{
/// <summary>An object which can be converted to another type.</summary>
/// https://kybernetik.com.au/animancer/api/Animancer/IConvertable_1
public interface IConvertable<out T>
{
/************************************************************************************************************************/
/// <summary>Returns the equivalent of this object as <typeparamref name="T"/>.</summary>
T Convert();
/************************************************************************************************************************/
}
/// <summary>Utility methods for <see cref="IConvertable{T}"/>.</summary>
/// https://kybernetik.com.au/animancer/api/Animancer/ConvertableUtilities
public static partial class ConvertableUtilities
{
/************************************************************************************************************************/
/// <summary>
/// Custom conversion functions used as a fallback
/// for types that can't implement <see cref="IConvertable{T}"/>.
/// </summary>
public static readonly Dictionary<Type, Func<object, Type, object>>
CustomConverters = new();
/************************************************************************************************************************/
/// <summary>Tries to convert the `original` to <typeparamref name="T"/>.</summary>
/// <remarks>
/// <list type="bullet">
/// <item>If the `original` is already a <typeparamref name="T"/> then it's returned directly.</item>
/// <item>Or if it's an <see cref="IConvertable{T}"/> then <see cref="IConvertable{T}.Convert"/> is used.</item>
/// <item>Otherwise, this method throws an <see cref="ArgumentException"/>.</item>
/// </list>
/// </remarks>
public static T ConvertOrThrow<T>(object original)
{
if (TryConvert<T>(original, out var converted))
return converted;
throw new ArgumentException(
$"Unable to convert '{AnimancerUtilities.ToStringOrNull(original)}'" +
$" to '{typeof(T).GetNameCS()}'.");
}
/************************************************************************************************************************/
/// <summary>Tries to convert the `original` to <typeparamref name="T"/>.</summary>
/// <remarks>
/// <list type="bullet">
/// <item>If the `original` is already a <typeparamref name="T"/> then it's returned directly.</item>
/// <item>Or if it's an <see cref="IConvertable{T}"/> then <see cref="IConvertable{T}.Convert"/> is used.</item>
/// <item>Otherwise, this method returns the <c>default(T)</c>.</item>
/// </list>
/// </remarks>
public static T ConvertOrDefault<T>(object original)
{
TryConvert<T>(original, out var converted);
return converted;
}
/************************************************************************************************************************/
/// <summary>Tries to convert the `original` to <typeparamref name="T"/>.</summary>
/// <remarks>
/// <list type="bullet">
/// <item>If the `original` is already a <typeparamref name="T"/> then it's returned directly.</item>
/// <item>Or if it's an <see cref="IConvertable{T}"/> then <see cref="IConvertable{T}.Convert"/> is used.</item>
/// <item>Otherwise, this method returns <c>false</c>.</item>
/// </list>
/// </remarks>
public static bool TryConvert<T>(object original, out T converted)
{
if (original is null)
{
converted = default;
return converted == null;// True for value type, false for reference type.
}
if (original is T t)
{
converted = t;
return true;
}
if (original is IConvertable<T> convertable)
{
converted = convertable.Convert();
return true;
}
if (CustomConverters.TryGetValue(original.GetType(), out var converter))
{
converted = (T)converter(original, typeof(T));
return converted != null;
}
converted = default;
return false;
}
/************************************************************************************************************************/
/// <summary>Initializes the inbuilt custom converters.</summary>
static ConvertableUtilities()
{
CustomConverters.Add(typeof(GameObject), TryGetComponent);
}
/************************************************************************************************************************/
/// <summary>Tries to get a component if the `original` is a <see cref="GameObject"/>.</summary>
private static object TryGetComponent(object original, Type type)
{
if (original is GameObject gameObject &&
gameObject.TryGetComponent(type, out var component))
return component;
return null;
}
/************************************************************************************************************************/
}
}

View File

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

View File

@@ -0,0 +1,82 @@
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
using System;
namespace Animancer
{
/// <summary>An object that can be copied.</summary>
/// https://kybernetik.com.au/animancer/api/Animancer/ICopyable_1
public interface ICopyable<in T>
{
/************************************************************************************************************************/
/// <summary>Copies the contents of `copyFrom` into this object, replacing its previous contents.</summary>
void CopyFrom(T copyFrom, CloneContext context);
/************************************************************************************************************************/
}
/// <summary>Extension methods for <see cref="ICopyable{T}"/>.</summary>
/// https://kybernetik.com.au/animancer/api/Animancer/CopyableExtensions
public static partial class CopyableExtensions
{
/************************************************************************************************************************/
/// <summary>
/// Calls <see cref="ICopyable{T}.CopyFrom"/>
/// using a <see cref="CloneContext"/> from the <see cref="CloneContext.Pool"/>.
/// </summary>
public static void CopyFrom<T>(this T copyTo, T copyFrom)
where T : ICopyable<T>
{
var context = CloneContext.Pool.Instance.Acquire();
copyTo.CopyFrom(copyFrom, context);
CloneContext.Pool.Instance.Release(context);
}
/************************************************************************************************************************/
/// <summary>
/// Creates a new <typeparamref name="T"/> and calls <see cref="ICopyable{T}.CopyFrom"/>.
/// </summary>
public static T CopyableClone<T>(this T original, CloneContext context)
where T : ICopyable<T>
{
if (original == null)
return default;
var clone = (T)Activator.CreateInstance(original.GetType());
clone.CopyFrom(original, context);
return clone;
}
/// <summary>
/// Creates a new <typeparamref name="T"/> and calls <see cref="ICopyable{T}.CopyFrom"/>
/// using a <see cref="CloneContext"/> from the <see cref="CloneContext.Pool"/>.
/// </summary>
public static T CopyableClone<T>(this T original)
where T : ICopyable<T>
{
var context = CloneContext.Pool.Instance.Acquire();
var clone = original.CopyableClone(context);
CloneContext.Pool.Instance.Release(context);
return clone;
}
/************************************************************************************************************************/
/// <summary>Calls <see cref="ICopyable{T}.CopyFrom"/> using the appropriate type.</summary>
public static void CopyFromBase<TChild, TBase>(this TChild copyTo, TBase copyFrom, CloneContext context)
where TChild : ICopyable<TChild>, ICopyable<TBase>
where TBase : ICopyable<TBase>
{
if (copyFrom is TChild copyFromChild)
copyTo.CopyFrom(copyFromChild, context);
else
copyTo.CopyFrom(copyFrom, context);
}
/************************************************************************************************************************/
}
}

View File

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

View File

@@ -0,0 +1,111 @@
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
using System.Text;
namespace Animancer
{
/// <summary>An object which can give a detailed description of itself.</summary>
/// https://kybernetik.com.au/animancer/api/Animancer/IHasDescription
///
public interface IHasDescription
{
/************************************************************************************************************************/
/// <summary>Appends a detailed descrption of the current details of this object.</summary>
/// <remarks>
/// <see cref="AnimancerUtilities.GetDescription"/> calls this method with a pooled
/// <see cref="StringBuilder"/>.
/// </remarks>
void AppendDescription(StringBuilder text, string separator = "\n");
/************************************************************************************************************************/
}
/// https://kybernetik.com.au/animancer/api/Animancer/AnimancerUtilities
///
public static partial class AnimancerUtilities
{
/************************************************************************************************************************/
/// <summary>
/// Calls <see cref="IHasDescription.AppendDescription"/> with a pooled <see cref="StringBuilder"/>.
/// </summary>
public static string GetDescription(
this IHasDescription hasDescription,
string separator = "\n")
{
if (hasDescription == null)
return "Null";
var text = StringBuilderPool.Instance.Acquire();
hasDescription.AppendDescription(text, separator);
return text.ReleaseToString();
}
/************************************************************************************************************************/
/// <summary>
/// Appends "Null" if `maybeHasDescription` is null. Otherwise calls
/// <see cref="IHasDescription.AppendDescription"/>.
/// </summary>
public static StringBuilder AppendDescription<T>(
this StringBuilder text,
T maybeHasDescription,
string separator = "\n",
bool fullNodeDescription = false)
=> maybeHasDescription is IHasDescription hasDescription
? text.AppendDescription(hasDescription, separator, fullNodeDescription)
: text.Append(ToStringOrNull(maybeHasDescription));
/// <summary>
/// Appends "Null" if `hasDescription` is null. Otherwise calls
/// <see cref="IHasDescription.AppendDescription"/>.
/// </summary>
public static StringBuilder AppendDescription(
this StringBuilder text,
IHasDescription hasDescription,
string separator = "\n",
bool fullNodeDescription = false)
{
if (hasDescription == null)
return text.Append("Null");
if (!fullNodeDescription && hasDescription is AnimancerNode node)
return text.Append(node.GetPath());
hasDescription.AppendDescription(text, separator);
return text;
}
/************************************************************************************************************************/
/// <summary>Appends <c>{prefix}{name}: {value}</c>.</summary>
public static StringBuilder AppendField<T>(
this StringBuilder text,
string prefix,
string name,
T value,
string separator = "\n",
bool fullNodeDescription = false)
=> text
.Append(prefix)
.Append(name)
.Append(": ")
.AppendDescription(value, separator, fullNodeDescription);
/************************************************************************************************************************/
/// <summary>Does the `text` start with a new line character?</summary>
public static bool StartsWithNewLine(this string text)
{
if (text == null || text.Length == 0)
return false;
var start = text[0];
return start == '\n' || start == '\r';
}
/************************************************************************************************************************/
}
}

View File

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

View File

@@ -0,0 +1,25 @@
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
namespace Animancer
{
/// <summary>An object which has an <see cref="AnimancerEvent.Sequence.Serializable"/>.</summary>
/// <remarks>
/// <strong>Documentation:</strong>
/// <see href="https://kybernetik.com.au/animancer/docs/manual/events/animancer">
/// Animancer Events</see>
/// </remarks>
/// https://kybernetik.com.au/animancer/api/Animancer/IHasEvents
public interface IHasEvents
{
/************************************************************************************************************************/
/// <summary>Events which will be triggered as the animation plays.</summary>
AnimancerEvent.Sequence Events { get; }
/// <summary>Events which will be triggered as the animation plays.</summary>
AnimancerEvent.Sequence.Serializable SerializedEvents { get; set; }
/************************************************************************************************************************/
}
}

View File

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

View File

@@ -0,0 +1,24 @@
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
namespace Animancer
{
/// <summary>
/// An object with a <see cref="Key"/> which can be used in dictionaries and hash sets.
/// </summary>
/// <remarks>
/// <strong>Documentation:</strong>
/// <see href="https://kybernetik.com.au/animancer/docs/manual/playing/keys">
/// Keys</see>
/// </remarks>
/// https://kybernetik.com.au/animancer/api/Animancer/IHasKey
public interface IHasKey
{
/************************************************************************************************************************/
/// <summary>A key which can be used in dictionaries and hash sets.</summary>
object Key { get; }
/************************************************************************************************************************/
}
}

View File

@@ -0,0 +1,13 @@
fileFormatVersion: 2
guid: 1b8aa66f0b587ce42bfad1625ac75b74
labels:
- Interface
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 //
namespace Animancer
{
/// <summary>An object with an <see cref="Initialize(T)"/> method.</summary>
/// https://kybernetik.com.au/animancer/api/Animancer/IInitializable_1
public interface IInitializable<T>
{
/************************************************************************************************************************/
/// <summary>Initializes this object.</summary>
void Initialize(T details);
/************************************************************************************************************************/
}
}

View File

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

View File

@@ -0,0 +1,20 @@
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
namespace Animancer
{
/// <summary>An object with an <see cref="Invoke"/> method.</summary>
/// https://kybernetik.com.au/animancer/api/Animancer/IInvokable
#if UNITY_ASSERTIONS
[AnimancerHelpUrl(Strings.DocsURLs.AnimancerEventParameters)]
#endif
public interface IInvokable : IPolymorphic
{
/************************************************************************************************************************/
/// <summary>Executes the main function of this object.</summary>
void Invoke();
/************************************************************************************************************************/
}
}

View File

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

View File

@@ -0,0 +1,77 @@
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
using UnityEngine;
namespace Animancer
{
/// <summary>An object with an <see cref="AverageAngularSpeed"/> and <see cref="AverageVelocity"/>.</summary>
/// https://kybernetik.com.au/animancer/api/Animancer/IMotion
///
public interface IMotion
{
/************************************************************************************************************************/
/// <summary>The initial <see cref="Motion.averageAngularSpeed"/> that the created state will have.</summary>
/// <remarks>The actual average can vary in states like <see cref="ManualMixerState"/>.</remarks>
float AverageAngularSpeed { get; }
/// <summary>The initial <see cref="Motion.averageSpeed"/> that the created state will have.</summary>
/// <remarks>The actual average can vary in states like <see cref="ManualMixerState"/>.</remarks>
Vector3 AverageVelocity { get; }
/************************************************************************************************************************/
}
/// https://kybernetik.com.au/animancer/api/Animancer/AnimancerUtilities
public static partial class AnimancerUtilities
{
/************************************************************************************************************************/
/// <summary>Outputs the <see cref="Motion.averageAngularSpeed"/> or <see cref="IMotion.AverageAngularSpeed"/>.</summary>
/// <remarks>Returns false if the `motion` is null or an unsupported type.</remarks>
public static bool TryGetAverageAngularSpeed(object motion, out float averageAngularSpeed)
{
if (motion is Motion unityMotion)
{
averageAngularSpeed = unityMotion.averageAngularSpeed;
return true;
}
else if (AnimancerUtilities.TryGetWrappedObject(motion, out IMotion iMotion))
{
averageAngularSpeed = iMotion.AverageAngularSpeed;
return true;
}
else
{
averageAngularSpeed = default;
return false;
}
}
/************************************************************************************************************************/
/// <summary>Outputs the <see cref="Motion.averageSpeed"/> or <see cref="IMotion.AverageVelocity"/>.</summary>
/// <remarks>Returns false if the `motion` is null or an unsupported type.</remarks>
public static bool TryGetAverageVelocity(object motion, out Vector3 averageVelocity)
{
if (motion is Motion unityMotion)
{
averageVelocity = unityMotion.averageSpeed;
return true;
}
else if (AnimancerUtilities.TryGetWrappedObject(motion, out IMotion iMotion))
{
averageVelocity = iMotion.AverageVelocity;
return true;
}
else
{
averageVelocity = default;
return false;
}
}
/************************************************************************************************************************/
}
}

View File

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

View File

@@ -0,0 +1,41 @@
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
using UnityEngine;
namespace Animancer
{
/************************************************************************************************************************/
/// <summary>
/// An object that will be drawn by a <see cref="Editor.PolymorphicDrawer"/>
/// which allows the user to select its type in the Inspector.
/// </summary>
/// <remarks>
/// Implement this interface in a <see cref="UnityEditor.PropertyDrawer"/> to indicate that it
/// should entirely replace the <see cref="Editor.PolymorphicDrawer"/>.
/// </remarks>
/// https://kybernetik.com.au/animancer/api/Animancer/IPolymorphic
public interface IPolymorphic { }
/************************************************************************************************************************/
/// <summary>An <see cref="IPolymorphic"/> with a <see cref="Reset"/> method.</summary>
/// https://kybernetik.com.au/animancer/api/Animancer/IPolymorphicReset
public interface IPolymorphicReset : IPolymorphic
{
/// <summary>Called when an instance of this type is created in a [<see cref="SerializeReference"/>] field.</summary>
void Reset(object oldValue = null);
}
/************************************************************************************************************************/
/// <summary>
/// The attributed field will be drawn by a <see cref="Editor.PolymorphicDrawer"/>
/// which allows the user to select its type in the Inspector.
/// </summary>
/// https://kybernetik.com.au/animancer/api/Animancer/PolymorphicAttribute
public sealed class PolymorphicAttribute : PropertyAttribute { }
/************************************************************************************************************************/
}

View File

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

View File

@@ -0,0 +1,110 @@
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
using UnityEngine;
namespace Animancer
{
/// <summary>An object which can create an <see cref="AnimancerState"/> and set its details.</summary>
/// <remarks>
/// Transitions are generally used as arguments for <see cref="AnimancerLayer.Play(ITransition)"/>.
/// <para></para>
/// <strong>Documentation:</strong>
/// <see href="https://kybernetik.com.au/animancer/docs/manual/transitions">
/// Transitions</see>
/// </remarks>
/// https://kybernetik.com.au/animancer/api/Animancer/ITransition
public interface ITransition :
IHasEvents,
IHasKey,
IPolymorphic
// Most transition types return themselves as the IHasKey.Key, but some wrapper types like
// TransitionAsset and TransitionAssetReference contain another transition which they want to
// use as the key instead so all transitions must implement it so systems can support any type.
{
/************************************************************************************************************************/
// Core Members - The main features required for a transition to be usable.
/************************************************************************************************************************/
/// <summary>The amount of time this transition should take (in seconds).</summary>
float FadeDuration { get; }
/// <summary>
/// The <see cref="Animancer.FadeMode"/> which should be used when this transition is passed into
/// <see cref="AnimancerLayer.Play(ITransition)"/>.
/// </summary>
FadeMode FadeMode { get; }
/// <summary>Creates and returns a new <see cref="AnimancerState"/> defuned by this transition.</summary>
/// <remarks>
/// The first time a transition is used on an object, this method creates a state
/// which is registered in the internal dictionary using the <see cref="IHasKey.Key"/>
/// so that it can be reused later on.
/// <para></para>
/// Methods like <see cref="AnimancerLayer.Play(ITransition)"/> will also call <see cref="Apply"/>,
/// so if you call this method manually you may want to call that method as well.
/// Or you can just use <see cref="AnimancerUtilities.CreateStateAndApply"/>.
/// </remarks>
AnimancerState CreateState();
/// <summary>Applies the details of this transition to the `state`.</summary>
/// <remarks>This method is called by every <see cref="AnimancerLayer.Play(ITransition)"/>.</remarks>
void Apply(AnimancerState state);
/************************************************************************************************************************/
// Extra Members - Additional features for convenience.
/************************************************************************************************************************/
/// <summary>Can this transition create a valid <see cref="AnimancerState"/>?</summary>
/// <remarks>
/// Use <see cref="AnimancerUtilities.IsValid(ITransition)"/> to check for <c>null</c> as well.
/// </remarks>
bool IsValid { get; }
/// <summary>What will the value of <see cref="AnimancerState.IsLooping"/> be for the created state?</summary>
bool IsLooping { get; }
/// <summary>The <see cref="AnimancerState.NormalizedTime"/> to start the animation at.</summary>
/// <remarks><see cref="float.NaN"/> allows the animation to continue from its current time.</remarks>
float NormalizedStartTime { get; set; }
/// <summary>The maximum expected value of the <see cref="AnimancerState.Length"/>.</summary>
/// <remarks>
/// In a <see cref="ClipState"/> this is equal to the <see cref="AnimationClip.length"/>
/// but the actual length can vary depending on the current parameters in states like
/// <see cref="LinearMixerState"/> and <see cref="ControllerState"/>.
/// </remarks>
float MaximumLength { get; }
/// <summary>The <see cref="AnimancerNodeBase.Speed"/> to play the animation at.</summary>
float Speed { get; set; }
/************************************************************************************************************************/
}
/// <summary>An <see cref="ITransition"/> which creates a specific type of <see cref="AnimancerState"/>.</summary>
/// <remarks>
/// <strong>Documentation:</strong>
/// <see href="https://kybernetik.com.au/animancer/docs/manual/transitions">
/// Transitions</see>
/// </remarks>
/// https://kybernetik.com.au/animancer/api/Animancer/ITransition_1
public interface ITransition<out TState> : ITransition
where TState : AnimancerState
{
/************************************************************************************************************************/
/// <summary>
/// The state that was created by this object. Specifically, this is the state that was most recently
/// passed into <see cref="ITransition.Apply"/> (usually by <see cref="AnimancerLayer.Play(ITransition)"/>).
/// </summary>
TState State { get; }
/************************************************************************************************************************/
/// <summary>Creates and returns a new <typeparamref name="TState"/>.</summary>
new TState CreateState();
/************************************************************************************************************************/
}
}

View File

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

View File

@@ -0,0 +1,141 @@
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
#if UNITY_EDITOR
using UnityEngine;
namespace Animancer.Editor
{
/// <summary>[Editor-Only] An object that can draw custom GUI elements relating to transitions.</summary>
/// <remarks>
/// Implement this in a custom transition type to give it custom GUI elements.
/// <para></para>
/// <strong>Example:</strong><code>
/// using Animancer;
/// using UnityEngine;
///
/// // AttackTransition.cs contains your custom transition type.
/// public partial class AttackTransition : ClipTransition
/// {
/// [SerializeField] private Bounds _HitBox;
/// [SerializeField] private float _HitStartTime;
/// [SerializeField] private float _HitEndTime;
///
/// // Damage, Knockback, etc.
/// }
///
/// // AttackTransition.Drawer.cs contains the custom GUI for it.
/// #if UNITY_EDITOR
///
/// using Animancer.Editor;
/// using UnityEditor;
/// using UnityEngine;
///
/// public partial class AttackTransition : ITransitionGUI
/// {
/// // See each method for an example.
/// public void OnPreviewSceneGUI(TransitionPreviewDetails details) { }
/// public void OnTimelineBackgroundGUI() { }
/// public void OnTimelineForegroundGUI() { }
/// }
///
/// #endif
/// </code></remarks>
/// https://kybernetik.com.au/animancer/api/Animancer.Editor/ITransitionGUI
public interface ITransitionGUI
{
/************************************************************************************************************************/
/// <summary>Called while drawing the GUI for the <see cref="Previews.TransitionPreviewWindow"/> scene.</summary>
/// <remarks>
/// <strong>Example:</strong><code>
/// // For the AttackTransition example from ITransitionGUI.
/// // Draw the hit box as a wireframe cube.
/// public void OnPreviewSceneGUI(TransitionPreviewDetails details)
/// {
/// Color color = Handles.color;
/// Handles.color = new(0.5f, 1, 0.5f);
///
/// Transform transform = details.Transform;
///
/// Handles.DrawWireCube(
/// transform.TransformPoint(_HitBox.center),
/// _HitBox.size);
///
/// Handles.color = color;
/// }
/// </code></remarks>
void OnPreviewSceneGUI(TransitionPreviewDetails details);
/// <summary>
/// Called while drawing the background GUI for the <see cref="TimelineGUI"/> for the
/// <see cref="IHasEvents.Events"/>.
/// </summary>
/// <remarks>
/// <strong>Example:</strong><code>
/// // For the AttackTransition example from ITransitionGUI.
/// // Draw the hit time as a highlighted area.
/// public void OnTimelineBackgroundGUI()
/// {
/// if (Event.current.type != EventType.Repaint)
/// return;
///
/// Color previousColor = GUI.color;
/// TimelineGUI timelineGUI = TimelineGUI.Current;
///
/// float start = timelineGUI.SecondsToPixels(_HitStartTime);
/// float end = timelineGUI.SecondsToPixels(_HitEndTime);
/// Rect area = new Rect(
/// start,
/// 0,
/// end - start,
/// timelineGUI.Area.height - timelineGUI.TickHeight);
///
/// Color color = new Color(0.9f, 0.4f, 0.25f, 0.5f);
///
/// EditorGUI.DrawRect(area, color);
///
/// GUI.color = previousColor;
/// }
/// </code></remarks>
void OnTimelineBackgroundGUI();
/// <summary>
/// Called while drawing the foreground GUI for the <see cref="TimelineGUI"/> for the
/// <see cref="IHasEvents.Events"/>.
/// </summary>
/// <remarks>
/// This method can be used similarly to the <see cref="OnTimelineBackgroundGUI"/> example,
/// except that it draws in front of everything else.
/// </remarks>
void OnTimelineForegroundGUI();
/************************************************************************************************************************/
}
/// <summary>[Editor-Only] Details about the current preview used by <see cref="ITransitionGUI.OnPreviewSceneGUI"/>.</summary>
/// https://kybernetik.com.au/animancer/api/Animancer.Editor/TransitionPreviewDetails
public readonly struct TransitionPreviewDetails
{
/************************************************************************************************************************/
/// <summary>The <see cref="AnimancerGraph"/> used to play the preview.</summary>
public readonly AnimancerGraph Animancer;
/// <summary>The <see cref="UnityEngine.Transform"/> of the <see cref="Animator"/> used to play the preview.</summary>
public Transform Transform => Animancer.Component.Animator.transform;
/************************************************************************************************************************/
/// <summary>Creates a new <see cref="TransitionPreviewDetails"/>.</summary>
public TransitionPreviewDetails(AnimancerGraph animancer)
{
Animancer = animancer;
}
/************************************************************************************************************************/
}
}
#endif

View File

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

View File

@@ -0,0 +1,169 @@
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
using System;
using System.Runtime.CompilerServices;
using UnityEngine;
using Object = UnityEngine.Object;
namespace Animancer
{
/// <summary>[Pro-Only] An object that can be updated during Animancer's animation updates.</summary>
///
/// <remarks>
/// <strong>Example:</strong>
/// Register to receive updates using <see cref="AnimancerGraph.RequirePreUpdate"/> or
/// <see cref="AnimancerGraph.RequirePostUpdate"/> and stop
/// receiving updates using <see cref="AnimancerGraph.CancelPreUpdate"/> or
/// <see cref="AnimancerGraph.CancelPostUpdate"/>.
/// <para></para><code>
/// public sealed class MyUpdatable : IUpdatable
/// {
/// // Implement IUpdatable.
/// // You can avoid this by inheriting from Updatable instead.
/// int IUpdatable.UpdatableIndex { get; set; } = IUpdatable.List.NotInList;
///
/// private AnimancerComponent _Animancer;
///
/// public void StartUpdating(AnimancerComponent animancer)
/// {
/// _Animancer = animancer;
///
/// // If you want Update to be called before the playables get updated.
/// _Animancer.Graph.RequirePreUpdate(this);
///
/// // If you want Update to be called after the playables get updated.
/// _Animancer.Graph.RequirePostUpdate(this);
/// }
///
/// public void StopUpdating()
/// {
/// // If you used RequirePreUpdate.
/// _Animancer.Graph.CancelPreUpdate(this);
///
/// // If you used RequirePostUpdate.
/// _Animancer.Graph.CancelPostUpdate(this);
/// }
///
/// void IUpdatable.Update()
/// {
/// // Called during every animation update.
///
/// // AnimancerGraph.Current can be used to access the system it is being updated by.
/// }
/// }
/// </code></remarks>
///
/// https://kybernetik.com.au/animancer/api/Animancer/IUpdatable
///
public interface IUpdatable
{
/************************************************************************************************************************/
/// <summary>The index of this object in its <see cref="IndexedList{TItem, TIndexer}"/>.</summary>
/// <remarks>Must be initialized to -1 to indicate that this object is not yet in a list.</remarks>
int UpdatableIndex { get; set; }
/// <summary>Updates this object.</summary>
void Update();
/************************************************************************************************************************/
/// <summary>An <see cref="IIndexer{T}"/> for <see cref="IUpdatable"/>.</summary>
public readonly struct Indexer : IIndexer<IUpdatable>
{
/************************************************************************************************************************/
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public readonly int GetIndex(IUpdatable item)
=> item.UpdatableIndex;
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public readonly void SetIndex(IUpdatable item, int index)
=> item.UpdatableIndex = index;
/// <inheritdoc/>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public readonly void ClearIndex(IUpdatable item)
=> item.UpdatableIndex = -1;
/************************************************************************************************************************/
}
/************************************************************************************************************************/
/// <summary>An <see cref="IndexedList{TItem, TAccessor}"/> of <see cref="IUpdatable"/>.</summary>
public class List : IndexedList<IUpdatable, Indexer>
{
/************************************************************************************************************************/
/// <summary>The default <see cref="IndexedList{TItem, TIndexer}.Capacity"/> for newly created lists.</summary>
/// <remarks>Default value is 4.</remarks>
public static new int DefaultCapacity { get; set; } = 4;
/************************************************************************************************************************/
/// <summary>Creates a new <see cref="List"/> with the <see cref="DefaultCapacity"/>.</summary>
public List()
: base(DefaultCapacity, new())
{ }
/************************************************************************************************************************/
/// <summary>Calls <see cref="Update"/> on all items in this list.</summary>
/// <remarks>
/// Uses <see cref="Debug.LogException(Exception, Object)"/> to handle exceptions and continues executing
/// the remaining items if any occur.
/// </remarks>
public void UpdateAll()
{
BeginEnumeraton();
ContinueEnumeration:
try
{
while (TryEnumerateNext())
{
Current.Update();
}
}
catch (Exception exception)
{
Debug.LogException(exception, AnimancerGraph.Current?.Component as Object);
goto ContinueEnumeration;
}
}
/************************************************************************************************************************/
/// <summary>Clones any <see cref="ICloneable{T}"/> items.</summary>
public void CloneFrom(
List copyFrom,
CloneContext context)
{
var count = copyFrom.Count;
for (int i = 0; i < count; i++)
if (copyFrom[i] is ICloneable<object> cloneable &&
context.GetOrCreateClone(cloneable) is IUpdatable clone)
Add(clone);
}
/************************************************************************************************************************/
}
/************************************************************************************************************************/
}
/// https://kybernetik.com.au/animancer/api/Animancer/AnimancerUtilities
public static partial class AnimancerUtilities
{
/************************************************************************************************************************/
/// <summary>Is the `updatable` currently in a list?</summary>
public static bool IsInList(this IUpdatable updatable)
=> updatable.UpdatableIndex >= 0;
/************************************************************************************************************************/
}
}

View File

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

View File

@@ -0,0 +1,72 @@
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
using System;
using UnityEngine;
namespace Animancer
{
/// <summary>An object which wraps a <see cref="WrappedObject"/> object.</summary>
/// https://kybernetik.com.au/animancer/api/Animancer/IWrapper
public interface IWrapper
{
/************************************************************************************************************************/
/// <summary>The wrapped object.</summary>
/// <remarks>
/// Use <see cref="AnimancerUtilities.TryGetWrappedObject"/>
/// in case the <see cref="WrappedObject"/> is also an <see cref="IWrapper"/>.
/// </remarks>
object WrappedObject { get; }
/************************************************************************************************************************/
}
/// https://kybernetik.com.au/animancer/api/Animancer/AnimancerUtilities
public partial class AnimancerUtilities
{
/************************************************************************************************************************/
/// <summary>
/// Returns the last <see cref="IWrapper.WrappedObject"/>
/// which is a <typeparamref name="T"/>, including the `wrapper` itself.
/// </summary>
public static bool TryGetWrappedObject<T>(
object wrapper,
out T wrapped,
bool logException = false)
where T : class
{
wrapped = default;
while (true)
{
if (wrapper is T t)
wrapped = t;
if (wrapper is IWrapper targetWrapper)
{
try
{
wrapper = targetWrapper.WrappedObject;
}
catch (Exception exception)
{
if (logException)
Debug.LogException(exception);
break;
}
}
else
{
break;
}
}
return wrapped != null;
}
/************************************************************************************************************************/
}
}

View File

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