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

File diff suppressed because it is too large Load Diff

View File

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

View File

@@ -0,0 +1,305 @@
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Playables;
namespace Animancer
{
/// <summary>A list of <see cref="AnimancerLayer"/>s with methods to control their mixing and masking.</summary>
/// <remarks>
/// The default implementation of this class is <see cref="AnimancerLayerMixerList"/>.
/// <para></para>
/// <strong>Documentation:</strong>
/// <see href="https://kybernetik.com.au/animancer/docs/manual/blending/layers">
/// Layers</see>
/// </remarks>
/// https://kybernetik.com.au/animancer/api/Animancer/AnimancerLayerList
public abstract class AnimancerLayerList :
IEnumerable<AnimancerLayer>,
IAnimationClipCollection
{
/************************************************************************************************************************/
#region Fields
/************************************************************************************************************************/
/// <summary>The <see cref="AnimancerGraph"/> containing this list.</summary>
public readonly AnimancerGraph Graph;
/// <summary>The layers which each manage their own set of animations.</summary>
/// <remarks>This field should never be null so it shouldn't need null-checking.</remarks>
private AnimancerLayer[] _Layers;
/// <summary>The number of layers that have actually been created.</summary>
private int _Count;
/// <summary>The <see cref="UnityEngine.Playables.Playable"/> which blends the layers.</summary>
public Playable Playable { get; protected set; }
/************************************************************************************************************************/
/// <summary>Creates a new <see cref="AnimancerLayerList"/>.</summary>
/// <remarks>The <see cref="Playable"/> must be assigned by the end of the derived constructor.</remarks>
protected AnimancerLayerList(AnimancerGraph graph, int capacity)
{
Graph = graph;
_Layers = new AnimancerLayer[capacity];
}
/************************************************************************************************************************/
#endregion
/************************************************************************************************************************/
#region List Operations
/************************************************************************************************************************/
/// <summary>[Pro-Only] The number of layers in this list.</summary>
/// <exception cref="ArgumentOutOfRangeException">
/// The value is set higher than the <see cref="DefaultCapacity"/>. This is simply a safety measure,
/// so if you do actually need more layers you can just increase the limit.
/// </exception>
/// <exception cref="IndexOutOfRangeException">The value is set to a negative number.</exception>
public int Count
{
get => _Count;
set
{
var count = _Count;
if (value == count)
return;
CheckAgain:
if (value > count)// Increasing.
{
Add();
count++;
goto CheckAgain;
}
else// Decreasing.
{
while (value < count--)
{
var layer = _Layers[count];
if (layer._Playable.IsValid())
Graph._PlayableGraph.DestroySubgraph(layer._Playable);
layer.DestroyStates();
}
Array.Clear(_Layers, value, _Count - value);
_Count = value;
Playable.SetInputCount(value);
}
}
}
/************************************************************************************************************************/
/// <summary>[Pro-Only]
/// If the <see cref="Count"/> is below the specified `min`, this method increases it to that value.
/// </summary>
public void SetMinCount(int min)
{
if (Count < min)
Count = min;
}
/************************************************************************************************************************/
/// <summary>[Pro-Only]
/// The maximum number of layers that can be created before an <see cref="ArgumentOutOfRangeException"/> will
/// be thrown (default 4).
/// <para></para>
/// Lowering this value will not affect layers that have already been created.
/// </summary>
/// <remarks>
/// <strong>Example:</strong>
/// To set this value automatically when the application starts, place a method like this in any class:
/// <para></para><code>
/// [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)]
/// private static void SetMaxLayerCount()
/// {
/// Animancer.AnimancerLayerList.DefaultCapacity = 8;
/// }
/// </code>
/// Otherwise you can set the <see cref="Capacity"/> of each individual list:
/// <para></para><code>
/// AnimancerComponent animancer;
/// animancer.Layers.Capacity = 8;
/// </code></remarks>
public static int DefaultCapacity { get; set; } = 4;
/// <summary>[Pro-Only]
/// If the <see cref="DefaultCapacity"/> is below the specified `min`, this method increases it to that value.
/// </summary>
public static void SetMinDefaultCapacity(int min)
{
if (DefaultCapacity < min)
DefaultCapacity = min;
}
/************************************************************************************************************************/
/// <summary>[Pro-Only]
/// The maximum number of layers that can be created before an <see cref="ArgumentOutOfRangeException"/> will
/// be thrown. The initial capacity is determined by <see cref="DefaultCapacity"/>.
/// </summary>
///
/// <remarks>
/// Lowering this value will destroy any layers beyond the specified value.
/// <para></para>
/// Changing this value will cause the allocation of a new array and garbage collection of the old one,
/// so you should generally set the <see cref="DefaultCapacity"/> before initializing this list.
/// </remarks>
///
/// <exception cref="ArgumentOutOfRangeException">The value is not greater than 0.</exception>
public int Capacity
{
get => _Layers.Length;
set
{
if (value <= 0)
throw new ArgumentOutOfRangeException(nameof(Capacity), $"must be greater than 0 ({value} <= 0)");
if (_Count > value)
Count = value;
Array.Resize(ref _Layers, value);
}
}
/************************************************************************************************************************/
/// <summary>[Pro-Only] Creates and returns a new <see cref="AnimancerLayer"/> at the end of this list.</summary>
/// <remarks>If the <see cref="Capacity"/> would be exceeded, it will be doubled.</remarks>
public virtual AnimancerLayer Add()
{
var index = _Count;
if (index >= _Layers.Length)
Capacity *= 2;
var layer = new AnimancerLayer(Graph, index);
_Count = index + 1;
Playable.SetInputCount(_Count);
Graph._PlayableGraph.Connect(Playable, layer._Playable, index, 0);
_Layers[index] = layer;
return layer;
}
/************************************************************************************************************************/
/// <summary>Returns the layer at the specified index. If it didn't already exist, this method creates it.</summary>
/// <remarks>To only get an existing layer without creating new ones, use <see cref="GetLayer"/> instead.</remarks>
public AnimancerLayer this[int index]
{
get
{
SetMinCount(index + 1);
return _Layers[index];
}
}
/************************************************************************************************************************/
/// <summary>Returns the layer at the specified index.</summary>
/// <remarks>To create a new layer if the target doesn't exist, use <see cref="this[int]"/> instead.</remarks>
public AnimancerLayer GetLayer(int index)
=> _Layers[index];
/************************************************************************************************************************/
#endregion
/************************************************************************************************************************/
#region Enumeration
/************************************************************************************************************************/
/// <summary>Returns an enumerator that will iterate through all layers.</summary>
public FastEnumerator<AnimancerLayer> GetEnumerator()
=> new(_Layers, _Count);
/// <inheritdoc/>
IEnumerator<AnimancerLayer> IEnumerable<AnimancerLayer>.GetEnumerator()
=> GetEnumerator();
/// <inheritdoc/>
IEnumerator IEnumerable.GetEnumerator()
=> GetEnumerator();
/************************************************************************************************************************/
/// <summary>[<see cref="IAnimationClipCollection"/>] Gathers all the animations in all layers.</summary>
public void GatherAnimationClips(ICollection<AnimationClip> clips)
=> clips.GatherFromSource(_Layers);
/************************************************************************************************************************/
#endregion
/************************************************************************************************************************/
#region Layer Details
/************************************************************************************************************************/
/// <summary>[Pro-Only]
/// Is the layer at the specified index is set to additive blending?
/// Otherwise it will override lower layers.
/// </summary>
public virtual bool IsAdditive(int index)
=> false;
/// <summary>[Pro-Only]
/// Sets the layer at the specified index to blend additively with earlier layers (if true)
/// or to override them (if false). Newly created layers will override by default.
/// </summary>
public virtual void SetAdditive(int index, bool value) { }
/************************************************************************************************************************/
/// <summary>[Pro-Only]
/// Sets an <see cref="AvatarMask"/> to determine which bones the layer at the specified index will affect.
/// </summary>
/// <remarks>
/// Don't assign the same mask repeatedly unless you have modified it.
/// This property doesn't check if the mask is the same
/// so repeatedly assigning the same thing will simply waste performance.
/// </remarks>
public virtual void SetMask(int index, AvatarMask mask) { }
/************************************************************************************************************************/
/// <summary>[Editor-Conditional] Sets the Inspector display name of the layer at the specified index.</summary>
[System.Diagnostics.Conditional(Strings.UnityEditor)]
public void SetDebugName(int index, string name)
=> this[index].SetDebugName(name);
/************************************************************************************************************************/
/// <summary>
/// The average velocity of the root motion of all currently playing animations,
/// taking their current <see cref="AnimancerNode.Weight"/> into account.
/// </summary>
public Vector3 AverageVelocity
{
get
{
var velocity = default(Vector3);
for (int i = 0; i < _Count; i++)
{
var layer = _Layers[i];
velocity += layer.AverageVelocity * layer.Weight;
}
return velocity;
}
}
/************************************************************************************************************************/
#endregion
/************************************************************************************************************************/
}
}

View File

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

View File

@@ -0,0 +1,75 @@
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
using UnityEngine;
using UnityEngine.Animations;
namespace Animancer
{
/// <summary>An <see cref="AnimancerLayerList"/> which uses an <see cref="AnimationLayerMixerPlayable"/>.</summary>
/// <remarks>
/// <strong>Documentation:</strong>
/// <see href="https://kybernetik.com.au/animancer/docs/manual/blending/layers">
/// Layers</see>
/// </remarks>
/// https://kybernetik.com.au/animancer/api/Animancer/AnimancerLayerMixerList
public class AnimancerLayerMixerList : AnimancerLayerList
{
/************************************************************************************************************************/
/// <summary>Creates a new <see cref="AnimancerLayerMixerList"/>.</summary>
public AnimancerLayerMixerList(AnimancerGraph graph)
: base(graph, DefaultCapacity)
{
LayerMixer = AnimationLayerMixerPlayable.Create(graph._PlayableGraph, 1);
Playable = LayerMixer;
}
/************************************************************************************************************************/
/// <summary>The <see cref="AnimationLayerMixerPlayable"/> which blends the layers.</summary>
public AnimationLayerMixerPlayable LayerMixer { get; protected set; }
/************************************************************************************************************************/
/// <inheritdoc/>
public override bool IsAdditive(int index)
=> LayerMixer.IsLayerAdditive((uint)index);
/// <inheritdoc/>
public override void SetAdditive(int index, bool value)
{
SetMinCount(index + 1);
LayerMixer.SetLayerAdditive((uint)index, value);
}
/************************************************************************************************************************/
private static AvatarMask _DefaultMask;
/// <inheritdoc/>
public override void SetMask(int index, AvatarMask mask)
{
var layer = this[index];
if (mask == null)
{
// If the existing mask was already null, do nothing.
// If it was destroyed, we still need to continue and set it to the default.
if (layer._Mask is null)
return;
_DefaultMask ??= new();
mask = _DefaultMask;
}
// Don't check if the same mask was already assigned because it might have been modified.
layer._Mask = mask;
LayerMixer.SetLayerMaskFromAvatarMask((uint)index, mask);
}
/************************************************************************************************************************/
}
}

View File

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

View File

@@ -0,0 +1,537 @@
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
using System;
using System.Collections;
using System.Collections.Generic;
using System.Text;
using UnityEngine;
namespace Animancer
{
/// <summary>A dictionary of <see cref="AnimancerState"/>s mapped to their <see cref="AnimancerState.Key"/>.</summary>
/// <remarks>
/// <strong>Documentation:</strong>
/// <see href="https://kybernetik.com.au/animancer/docs/manual/playing/states">
/// States</see>
/// </remarks>
/// https://kybernetik.com.au/animancer/api/Animancer/AnimancerStateDictionary
public class AnimancerStateDictionary :
IAnimationClipCollection,
IEnumerable<AnimancerState>
{
/************************************************************************************************************************/
/// <summary>The <see cref="AnimancerGraph"/> at the root of the graph.</summary>
private readonly AnimancerGraph Graph;
/************************************************************************************************************************/
/// <summary><see cref="AnimancerState.Key"/> mapped to <see cref="AnimancerState"/>.</summary>
private readonly Dictionary<object, AnimancerState>
States = new();
/************************************************************************************************************************/
/// <summary>[Internal] Creates a new <see cref="AnimancerStateDictionary"/>.</summary>
internal AnimancerStateDictionary(AnimancerGraph graph)
=> Graph = graph;
/************************************************************************************************************************/
/// <summary>The number of states that have been registered with a <see cref="AnimancerState.Key"/>.</summary>
public int Count
=> States.Count;
/************************************************************************************************************************/
/// <inheritdoc/>
public void AppendDescriptionOrOrphans(
StringBuilder text,
string separator = "\n")
{
string stateSeparator = null;
foreach (var state in States.Values)
{
if (state.Parent != null)
continue;
if (stateSeparator is null)
{
text.Append(separator)
.Append("Orphan States:");
separator += Strings.Indent;
stateSeparator = separator + Strings.Indent;
}
text.Append(separator);
state.AppendDescription(text, stateSeparator);
}
}
/************************************************************************************************************************/
#region Create
/************************************************************************************************************************/
/// <summary>Creates and returns a new <see cref="ClipState"/> to play the `clip`.</summary>
/// <remarks>
/// To create a state on a specific layer, use <c>animancer.Layers[x].CreateState(clip)</c> instead.
/// <para></para>
/// <see cref="AnimancerGraph.GetKey"/> is used to determine the <see cref="AnimancerState.Key"/>.
/// </remarks>
public ClipState Create(AnimationClip clip)
=> Create(Graph.GetKey(clip), clip);
/// <summary>
/// Creates and returns a new <see cref="ClipState"/> to play the `clip` and registers it with the `key`.
/// </summary>
/// <remarks>
/// To create a state on a specific layer, use <c>animancer.Layers[x].CreateState(key, clip)</c> instead.
/// </remarks>
public ClipState Create(object key, AnimationClip clip)
{
var state = new ClipState(clip);
state.SetGraph(Graph);
state._Key = key;
Register(state);
return state;
}
/************************************************************************************************************************/
/// <summary>Calls <see cref="GetOrCreate(AnimationClip, bool)"/> for each of the specified clips.</summary>
public void CreateIfNew(AnimationClip clip0, AnimationClip clip1)
{
GetOrCreate(clip0);
GetOrCreate(clip1);
}
/// <summary>Calls <see cref="GetOrCreate(AnimationClip, bool)"/> for each of the specified clips.</summary>
public void CreateIfNew(AnimationClip clip0, AnimationClip clip1, AnimationClip clip2)
{
GetOrCreate(clip0);
GetOrCreate(clip1);
GetOrCreate(clip2);
}
/// <summary>Calls <see cref="GetOrCreate(AnimationClip, bool)"/> for each of the specified clips.</summary>
public void CreateIfNew(AnimationClip clip0, AnimationClip clip1, AnimationClip clip2, AnimationClip clip3)
{
GetOrCreate(clip0);
GetOrCreate(clip1);
GetOrCreate(clip2);
GetOrCreate(clip3);
}
/// <summary>Calls <see cref="GetOrCreate(AnimationClip, bool)"/> for each of the specified `clips`.</summary>
public void CreateIfNew(params AnimationClip[] clips)
{
if (clips == null)
return;
var count = clips.Length;
for (int i = 0; i < count; i++)
{
var clip = clips[i];
if (clip != null)
GetOrCreate(clip);
}
}
/************************************************************************************************************************/
#endregion
/************************************************************************************************************************/
#region Access
/************************************************************************************************************************/
/// <summary>
/// The <see cref="AnimancerLayer.CurrentState"/> on layer 0.
/// <para></para>
/// Specifically, this is the state that was most recently started using any of the Play methods on that layer.
/// States controlled individually via methods in the <see cref="AnimancerState"/> itself will not register in
/// this property.
/// </summary>
public AnimancerState Current
=> Graph.Layers[0].CurrentState;
/************************************************************************************************************************/
/// <summary>Calls <see cref="AnimancerGraph.GetKey"/> then returns the state registered with that key.</summary>
/// <exception cref="ArgumentNullException">The key is null.</exception>
/// <exception cref="KeyNotFoundException">No state is registered with the key.</exception>
public AnimancerState this[AnimationClip clip]
=> States[Graph.GetKey(clip)];
/// <summary>Returns the state registered with the <see cref="IHasKey.Key"/>.</summary>
/// <exception cref="ArgumentNullException">The `key` is null.</exception>
/// <exception cref="KeyNotFoundException">No state is registered with the `key`.</exception>
public AnimancerState this[IHasKey hasKey]
=> States[hasKey.Key];
/// <summary>Returns the state registered with the `key`.</summary>
/// <exception cref="ArgumentNullException">The `key` is null.</exception>
/// <exception cref="KeyNotFoundException">No state is registered with the `key`.</exception>
public AnimancerState this[object key]
=> States[key];
/************************************************************************************************************************/
/// <summary>
/// Calls <see cref="AnimancerGraph.GetKey"/> then passes the key to
/// <see cref="TryGet(object, out AnimancerState)"/> and returns the result.
/// </summary>
public bool TryGet(AnimationClip clip, out AnimancerState state)
{
if (clip == null)
{
state = null;
return false;
}
return TryGet(Graph.GetKey(clip), out state);
}
/// <summary>
/// Passes the <see cref="IHasKey.Key"/> into <see cref="TryGet(object, out AnimancerState)"/>
/// and returns the result.
/// </summary>
public bool TryGet(IHasKey hasKey, out AnimancerState state)
=> TryGet(hasKey?.Key, out state);
/// <summary>
/// If a `state` is registered with the `key`, this method outputs it and returns true.
/// Otherwise the `state` is set to null and this method returns false.
/// </summary>
public bool TryGet(object key, out AnimancerState state)
{
if (key == null)
{
state = null;
return false;
}
return States.TryGetValue(key, out state);
}
/************************************************************************************************************************/
/// <summary>
/// Passes the <see cref="IHasKey.Key"/> into <see cref="TryGetAlias(object, out AnimancerState)"/>
/// and returns the result.
/// </summary>
public bool TryGetAlias(IHasKey hasKey, out AnimancerState state)
=> TryGetAlias(hasKey?.Key, out state);
/// <summary>
/// If a `state` is registered with the `key` or the <see cref="TransitionLibraries.TransitionLibrary"/>,
/// is using it as an alias, this method outputs it and returns true.
/// Otherwise the `state` is set to null and this method returns false.
/// </summary>
public bool TryGetAlias(object key, out AnimancerState state)
{
if (key == null)
{
state = null;
return false;
}
if (Graph.Transitions != null &&
Graph.Transitions.TryGetTransition(key, out var group))
key = group.Transition.Key;
return States.TryGetValue(key, out state);
}
/************************************************************************************************************************/
/// <summary>
/// Calls <see cref="AnimancerGraph.GetKey"/> and returns the state registered with that key
/// or creates one if it doesn't exist.
/// </summary>
/// <remarks>
/// If the state already exists but has the wrong <see cref="AnimancerState.Clip"/>, the `allowSetClip`
/// parameter determines what will happen. False causes it to throw an <see cref="ArgumentException"/> while
/// true allows it to change the <see cref="AnimancerState.Clip"/>. Note that the change is somewhat costly to
/// performance so use with caution.
/// </remarks>
/// <exception cref="ArgumentException"/>
public AnimancerState GetOrCreate(AnimationClip clip, bool allowSetClip = false)
=> GetOrCreate(Graph.GetKey(clip), clip, allowSetClip);
/// <summary>
/// Returns the state registered with the `transition`s <see cref="IHasKey.Key"/> if there is one.
/// Otherwise this method uses <see cref="ITransition.CreateState"/> to create a new one
/// and registers it with that key before returning it.
/// </summary>
public AnimancerState GetOrCreate(ITransition transition)
{
var key = transition.Key;
if (!TryGet(key, out var state))
{
state = transition.CreateState();
state._Key = key;
state.SetGraph(Graph);
}
return state;
}
/// <summary>
/// Returns the state which registered with the `key` or creates one if it doesn't exist.
/// <para></para>
/// If the state already exists but has the wrong <see cref="AnimancerState.Clip"/>, the `allowSetClip`
/// parameter determines what will happen. False causes it to throw an <see cref="ArgumentException"/>
/// while true allows it to change the <see cref="AnimancerState.Clip"/>.
/// Note that the change is somewhat costly to performance so use with caution.
/// </summary>
/// <exception cref="ArgumentException"/>
/// <remarks>See also: <see cref="AnimancerLayer.GetOrCreateState(object, AnimationClip, bool)"/></remarks>
public AnimancerState GetOrCreate(object key, AnimationClip clip, bool allowSetClip = false)
{
if (TryGet(key, out var state))
{
// If a state exists with the 'key' but has the wrong clip, either change it or complain.
if (!ReferenceEquals(state.Clip, clip))
{
if (allowSetClip)
{
state.Clip = clip;
}
else
{
throw new ArgumentException(GetClipMismatchError(key, state.Clip, clip));
}
}
}
else
{
state = Create(key, clip);
}
return state;
}
/************************************************************************************************************************/
/// <summary>Returns an error message explaining that a state already exists with the specified `key`.</summary>
public static string GetClipMismatchError(object key, AnimationClip oldClip, AnimationClip newClip)
=> $"A state already exists using the specified '{nameof(key)}', but has a different {nameof(AnimationClip)}:" +
$"\n• Key: {key}" +
$"\n• Old Clip: {oldClip}" +
$"\n• New Clip: {newClip}";
/************************************************************************************************************************/
/// <summary>[Internal]
/// Registers the `state` in this dictionary so the <see cref="AnimancerState.Key"/> can be used to get it
/// later on using any of the lookup methods such as <see cref="this[object]"/> or
/// <see cref="TryGet(object, out AnimancerState)"/>.
/// </summary>
/// <remarks>Does nothing if the <see cref="AnimancerState.Key"/> is <c>null</c>.</remarks>
internal void Register(AnimancerState state)
{
var key = state._Key;
if (key != null)
{
#if UNITY_ASSERTIONS
if (state.Graph != Graph)
throw new ArgumentException(
$"{nameof(AnimancerStateDictionary)} cannot register a state with a different {nameof(Graph)}: " + state);
#endif
States.Add(key, state);
}
}
/// <summary>[Internal] Removes the `state` from this dictionary (the opposite of <see cref="Register"/>).</summary>
internal void Unregister(AnimancerState state)
{
var key = state._Key;
if (key != null)
States.Remove(key);
}
/************************************************************************************************************************/
#region Enumeration
/************************************************************************************************************************/
// IEnumerable for 'foreach' statements.
/************************************************************************************************************************/
/// <summary>Returns an enumerator that will iterate through all registered states.</summary>
public Dictionary<object, AnimancerState>.ValueCollection.Enumerator GetEnumerator()
=> States.Values.GetEnumerator();
/// <inheritdoc/>
IEnumerator<AnimancerState> IEnumerable<AnimancerState>.GetEnumerator()
=> GetEnumerator();
/// <inheritdoc/>
IEnumerator IEnumerable.GetEnumerator()
=> GetEnumerator();
/************************************************************************************************************************/
/// <summary>[<see cref="IAnimationClipCollection"/>]
/// Adds all the animations of states with a <see cref="AnimancerState.Key"/> to the `clips`.
/// </summary>
public void GatherAnimationClips(ICollection<AnimationClip> clips)
{
foreach (var state in States.Values)
clips.GatherFromSource(state);
}
/************************************************************************************************************************/
#endregion
/************************************************************************************************************************/
#endregion
/************************************************************************************************************************/
#region Destroy
/************************************************************************************************************************/
/// <summary>
/// Calls <see cref="AnimancerState.Destroy"/> on the state associated with the `clip` (if any).
/// Returns true if the state existed.
/// </summary>
public bool Destroy(AnimationClip clip)
{
if (clip == null)
return false;
return Destroy(Graph.GetKey(clip));
}
/// <summary>
/// Calls <see cref="AnimancerState.Destroy"/> on the state associated with the <see cref="IHasKey.Key"/>
/// (if any). Returns true if the state existed.
/// </summary>
public bool Destroy(IHasKey hasKey)
{
if (hasKey == null)
return false;
return Destroy(hasKey.Key);
}
/// <summary>
/// Calls <see cref="AnimancerState.Destroy"/> on the state associated with the `key` (if any).
/// Returns true if the state existed.
/// </summary>
public bool Destroy(object key)
{
if (!TryGet(key, out var state))
return false;
state.Destroy();
return true;
}
/************************************************************************************************************************/
/// <summary>Calls <see cref="Destroy(AnimationClip)"/> on each of the `clips`.</summary>
public void DestroyAll(IList<AnimationClip> clips)
{
if (clips == null)
return;
for (int i = clips.Count - 1; i >= 0; i--)
Destroy(clips[i]);
}
/// <summary>Calls <see cref="Destroy(AnimationClip)"/> on each of the `clips`.</summary>
public void DestroyAll(IEnumerable<AnimationClip> clips)
{
if (clips == null)
return;
foreach (var clip in clips)
Destroy(clip);
}
/************************************************************************************************************************/
/// <summary>
/// Calls <see cref="Destroy(AnimationClip)"/> on all states gathered by
/// <see cref="IAnimationClipSource.GetAnimationClips"/>.
/// </summary>
public void DestroyAll(IAnimationClipSource source)
{
if (source == null)
return;
var clips = ListPool.Acquire<AnimationClip>();
source.GetAnimationClips(clips);
DestroyAll(clips);
ListPool.Release(clips);
}
/// <summary>
/// Calls <see cref="Destroy(AnimationClip)"/> on all states gathered by
/// <see cref="IAnimationClipCollection.GatherAnimationClips"/>.
/// </summary>
public void DestroyAll(IAnimationClipCollection source)
{
if (source == null)
return;
var clips = SetPool.Acquire<AnimationClip>();
source.GatherAnimationClips(clips);
DestroyAll(clips);
SetPool.Release(clips);
}
/************************************************************************************************************************/
#endregion
/************************************************************************************************************************/
#region Key Error Methods
#if UNITY_EDITOR
/************************************************************************************************************************/
// These are overloads of other methods that take a System.Object key to ensure the user doesn't try to use an
// AnimancerState as a key, since the whole point of a key is to identify a state in the first place.
/************************************************************************************************************************/
/// <summary>[Warning]
/// You should not use an <see cref="AnimancerState"/> as a key.
/// The whole point of a key is to identify a state in the first place.
/// </summary>
[Obsolete("You should not use an AnimancerState as a key. The whole point of a key is to identify a state in the first place.", true)]
public AnimancerState this[AnimancerState key]
=> key;
/// <summary>[Warning]
/// You should not use an <see cref="AnimancerState"/> as a key.
/// The whole point of a key is to identify a state in the first place.
/// </summary>
[Obsolete("You should not use an AnimancerState as a key. The whole point of a key is to identify a state in the first place.", true)]
public bool TryGet(AnimancerState key, out AnimancerState state)
{
state = key;
return true;
}
/// <summary>[Warning]
/// You should not use an <see cref="AnimancerState"/> as a key.
/// The whole point of a key is to identify a state in the first place.
/// </summary>
[Obsolete("You should not use an AnimancerState as a key. The whole point of a key is to identify a state in the first place.", true)]
public AnimancerState GetOrCreate(AnimancerState key, AnimationClip clip)
=> key;
/// <summary>[Warning]
/// You should not use an <see cref="AnimancerState"/> as a key.
/// Just call <see cref="AnimancerState.Destroy"/>.
/// </summary>
[Obsolete("You should not use an AnimancerState as a key. Just call AnimancerState.Destroy.", true)]
public bool Destroy(AnimancerState key)
{
key.Destroy();
return true;
}
/************************************************************************************************************************/
#endif
#endregion
/************************************************************************************************************************/
}
}

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

@@ -0,0 +1,551 @@
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
using System;
using System.Runtime.CompilerServices;
using System.Text;
using UnityEngine;
namespace Animancer
{
/// https://kybernetik.com.au/animancer/api/Animancer/AnimancerEvent
partial struct AnimancerEvent
{
/// <summary>Details about a state which determine how it triggers events.</summary>
/// https://kybernetik.com.au/animancer/api/Animancer/DispatchInfo
public readonly struct DispatchInfo
{
/************************************************************************************************************************/
/// <summary><see cref="AnimancerState.Length"/></summary>
public readonly float Length;
/// <summary><see cref="AnimancerState.NormalizedTime"/></summary>
public readonly float NormalizedTime;
/// <summary><see cref="AnimancerState.IsLooping"/></summary>
public readonly bool IsLooping;
/************************************************************************************************************************/
/// <summary>Creates a new <see cref="DispatchInfo"/>.</summary>
public DispatchInfo(float length, float normalizedTime, bool isLooping)
{
Length = length;
NormalizedTime = normalizedTime;
IsLooping = isLooping;
}
/************************************************************************************************************************/
}
/// <summary>
/// A system which triggers events in an <see cref="Sequence"/>
/// based on a target <see cref="State"/>.
/// </summary>
/// https://kybernetik.com.au/animancer/api/Animancer/Dispatcher
public class Dispatcher : IHasDescription
{
/************************************************************************************************************************/
/// <summary>The target state.</summary>
public readonly AnimancerState State;
/// <summary>
/// <see cref="AnimancerState.OwnedEvents"/> and
/// <see cref="AnimancerState.SharedEvents"/>.
/// </summary>
/// <remarks>Should never be null.</remarks>
public Sequence Events { get; private set; }
/// <summary><see cref="AnimancerState.HasOwnedEvents"/></summary>
public bool HasOwnEvents { get; private set; }
private float _PreviousNormalizedTime;
private int _NextEventIndex = RecalculateEventIndex;
private int _SequenceVersion = -1;// When version changes, next event index is invalid.
private bool _WasPlayingForwards;// When direction changes, next event index is invalid.
/// <summary>
/// A special value for the <see cref="_NextEventIndex"/>
/// which indicates that it needs to be recalculated.
/// </summary>
private const int RecalculateEventIndex = int.MinValue;
/************************************************************************************************************************/
/// <summary>Creates a new <see cref="Dispatcher"/>.</summary>
public Dispatcher(AnimancerState state)
{
State = state;
_PreviousNormalizedTime = state.NormalizedTime;
#if UNITY_ASSERTIONS
OptionalWarning.UnsupportedEvents.Log(state.UnsupportedEventsMessage, state.Graph?.Component);
#endif
}
/************************************************************************************************************************/
/// <summary>
/// Setters for <see cref="AnimancerState.OwnedEvents"/>
/// and <see cref="AnimancerState.SharedEvents"/>.
/// </summary>
public void SetEvents(Sequence events, bool isOwned)
{
Events = events;
_NextEventIndex = RecalculateEventIndex;
_SequenceVersion = Events.Version;
HasOwnEvents = isOwned;
}
/************************************************************************************************************************/
/// <summary>Sets <see cref="HasOwnEvents"/> to <c>false</c>.</summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void DismissEventOwnership()
=> HasOwnEvents = false;
/************************************************************************************************************************/
/// <summary><see cref="AnimancerState.Events(object, out Sequence)"/>.</summary>
public bool InitializeEvents(out Sequence events)
{
if (HasOwnEvents)
{
events = Events;
return false;
}
Events = events = new(Events);
_NextEventIndex = RecalculateEventIndex;
_SequenceVersion = Events.Version;
HasOwnEvents = true;
return true;
}
/************************************************************************************************************************/
/// <summary>[Internal]
/// Notifies this dispatcher that the target's <see cref="Time"/> has changed.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal void OnSetTime()
{
// The Playable's time won't move in the same frame it was set,
// so we'll just let the next frame grab its time.
_PreviousNormalizedTime = float.NaN;
}
/************************************************************************************************************************/
/// <inheritdoc/>
public void UpdateEvents(bool raiseEvents)
{
var info = State.GetEventDispatchInfo();
// If we aren't raising events or don't have a previous time, just keep track of the time.
if (!raiseEvents || float.IsNaN(_PreviousNormalizedTime))
{
_PreviousNormalizedTime = info.NormalizedTime;
// Since we aren't paying attention to the events,
// we also aren't paying attention to which index the time corresponds to.
_NextEventIndex = RecalculateEventIndex;
return;
}
// If the sequence is modified, we need to recalculate the next event index.
var sequenceVersion = Events.Version;
if (_SequenceVersion != sequenceVersion)
{
_SequenceVersion = sequenceVersion;
_NextEventIndex = RecalculateEventIndex;
}
if (info.Length > 0)
{
if (_PreviousNormalizedTime == info.NormalizedTime)
return;
CheckGeneralEvents(info.NormalizedTime, info.IsLooping);
CheckEndEvent(info.NormalizedTime);
_PreviousNormalizedTime = info.NormalizedTime;
}
else// Length zero, negative, or NaN.
{
UpdateZeroLength();
}
}
/************************************************************************************************************************/
/// <summary>If the state has zero length, trigger its events every frame.</summary>
private void UpdateZeroLength()
{
var speed = State.EffectiveSpeed;
if (speed == 0)
return;
if (Events.Count > 0)
{
int playDirectionInt;
if (speed < 0)
{
playDirectionInt = -1;
if (_NextEventIndex == RecalculateEventIndex ||
_WasPlayingForwards)
{
_NextEventIndex = Events.Count - 1;
_WasPlayingForwards = false;
}
}
else
{
playDirectionInt = 1;
if (_NextEventIndex == RecalculateEventIndex ||
!_WasPlayingForwards)
{
_NextEventIndex = 0;
_WasPlayingForwards = true;
}
}
if (!InvokeAllEvents(Events, 1, playDirectionInt))
return;
}
var endEvent = Events.EndEvent;
if (endEvent.callback != null)
endEvent.DelayInvoke(EndEventName, State);
}
/************************************************************************************************************************/
/// <summary>General events are triggered on the frame when their time passes.</summary>
/// <remarks>Looping animations trigger their events every loop.</remarks>
private void CheckGeneralEvents(float normalizedTime, bool isLooping)
{
var count = Events.Count;
if (count == 0)
{
_NextEventIndex = 0;
return;
}
ValidateNextEventIndex(
isLooping,
ref normalizedTime,
out var playDirectionFloat,
out var playDirectionInt);
if (isLooping)// Looping.
{
var animancerEvent = Events[_NextEventIndex];
var eventTime = animancerEvent.normalizedTime * playDirectionFloat;
var loopDelta = GetLoopDelta(_PreviousNormalizedTime, normalizedTime, eventTime);
if (loopDelta == 0)
return;
// For each additional loop, invoke all events without needing to check their times.
if (!InvokeAllEvents(Events, loopDelta - 1, playDirectionInt))
return;
var loopStartIndex = _NextEventIndex;
Invoke:
animancerEvent.DelayInvoke(Events.GetName(_NextEventIndex), State);
if (!NextEventLooped(Events, playDirectionInt) ||
_NextEventIndex == loopStartIndex)
return;
animancerEvent = Events[_NextEventIndex];
eventTime = animancerEvent.normalizedTime * playDirectionFloat;
if (loopDelta == GetLoopDelta(_PreviousNormalizedTime, normalizedTime, eventTime))
goto Invoke;
}
else// Non-Looping.
{
while ((uint)_NextEventIndex < (uint)count)
{
var animancerEvent = Events[_NextEventIndex];
var eventTime = animancerEvent.normalizedTime * playDirectionFloat;
if (normalizedTime <= eventTime)
return;
animancerEvent.DelayInvoke(Events.GetName(_NextEventIndex), State);
_NextEventIndex += playDirectionInt;
}
}
}
/************************************************************************************************************************/
private void ValidateNextEventIndex(
bool isLooping,
ref float normalizedTime,
out float playDirectionFloat,
out int playDirectionInt)
{
if (normalizedTime < _PreviousNormalizedTime)// Playing Backwards.
{
var previousTime = _PreviousNormalizedTime;
_PreviousNormalizedTime = -previousTime;
normalizedTime = -normalizedTime;
playDirectionFloat = -1;
playDirectionInt = -1;
if (_NextEventIndex == RecalculateEventIndex ||
_WasPlayingForwards)
{
_NextEventIndex = Events.Count - 1;
_WasPlayingForwards = false;
if (isLooping)
previousTime = AnimancerUtilities.Wrap01(previousTime);
while (Events[_NextEventIndex].normalizedTime > previousTime)
{
_NextEventIndex--;
if (_NextEventIndex < 0)
{
if (isLooping)
_NextEventIndex = Events.Count - 1;
break;
}
}
Events.AssertNormalizedTimes(State, isLooping);
}
}
else// Playing Forwards.
{
playDirectionFloat = 1;
playDirectionInt = 1;
if (_NextEventIndex == RecalculateEventIndex ||
!_WasPlayingForwards)
{
_NextEventIndex = 0;
_WasPlayingForwards = true;
var previousTime = _PreviousNormalizedTime;
if (isLooping)
previousTime = AnimancerUtilities.Wrap01(previousTime);
var max = Events.Count - 1;
while (Events[_NextEventIndex].normalizedTime < previousTime)
{
_NextEventIndex++;
if (_NextEventIndex > max)
{
if (isLooping)
_NextEventIndex = 0;
break;
}
}
Events.AssertNormalizedTimes(State, isLooping);
}
}
// This method could be slightly optimised for playback direction changes by using the current index
// as the starting point instead of iterating from the edge of the sequence, but that would make it
// significantly more complex for something that shouldn't happen very often and would only matter if
// there are lots of events (in which case the optimisation would be tiny compared to the cost of
// actually invoking all those events and running the rest of the application).
}
/************************************************************************************************************************/
/// <summary>
/// Calculates the number of times an event at `eventTime` should be invoked when the
/// <see cref="AnimancerState.NormalizedTime"/> goes from `previousTime` to `nextTime` on a looping animation.
/// </summary>
private static int GetLoopDelta(float previousTime, float nextTime, float eventTime)
{
previousTime -= eventTime;
nextTime -= eventTime;
var previousLoopCount = Mathf.FloorToInt(previousTime);
var nextLoopCount = Mathf.FloorToInt(nextTime);
var loopCount = nextLoopCount - previousLoopCount;
// Previous time must be inclusive.
// And next time must be exclusive.
// So if the previous time is exactly on a looped increment of the event time, count one more.
// And if the next time is exactly on a looped increment of the event time, count one less.
if (previousTime == previousLoopCount)
loopCount++;
if (nextTime == nextLoopCount)
loopCount--;
return loopCount;
}
/************************************************************************************************************************/
private static int _MaximumFullLoopCount = 3;
/// <summary>
/// The maximum number of times a looping animation can trigger all of its events in a single frame.
/// Default 3, Minimum 1.
/// </summary>
/// <remarks>
/// This limit should only ever be reached when a state has a very short length and high speed.
/// </remarks>
public static int MaximumFullLoopCount
{
get => _MaximumFullLoopCount;
set => _MaximumFullLoopCount = Math.Max(value, 1);
}
private bool InvokeAllEvents(Sequence events, int count, int playDirectionInt)
{
if (count > _MaximumFullLoopCount)
count = _MaximumFullLoopCount;
var loopStartIndex = _NextEventIndex;
while (count-- > 0)
{
do
{
events[_NextEventIndex].DelayInvoke(events.GetName(_NextEventIndex), State);
if (!NextEventLooped(events, playDirectionInt))
return false;
}
while (_NextEventIndex != loopStartIndex);
}
return true;
}
/************************************************************************************************************************/
private bool NextEventLooped(Sequence events, int playDirectionInt)
{
_NextEventIndex += playDirectionInt;
var count = events.Count;
if (_NextEventIndex >= count)
_NextEventIndex = 0;
else if (_NextEventIndex < 0)
_NextEventIndex = count - 1;
return true;
}
/************************************************************************************************************************/
/// <summary>End events are triggered every frame after their time passes.</summary>
/// <remarks>
/// This ensures that assigning the event after the time has passed
/// will still trigger it rather than leaving it playing indefinitely.
/// </remarks>
private void CheckEndEvent(float normalizedTime)
{
var endEvent = Events.EndEvent;
if (endEvent.callback == null)
return;
if (normalizedTime > _PreviousNormalizedTime)// Playing Forwards.
{
var eventTime = float.IsNaN(endEvent.normalizedTime)
? 1
: endEvent.normalizedTime;
if (normalizedTime > eventTime)
endEvent.DelayInvoke(EndEventName, State);
}
else// Playing Backwards.
{
var eventTime = float.IsNaN(endEvent.normalizedTime)
? 0
: endEvent.normalizedTime;
if (normalizedTime < eventTime)
endEvent.DelayInvoke(EndEventName, State);
}
}
/************************************************************************************************************************/
/// <summary>
/// Sets the <see cref="AnimancerState.NormalizedTime"/>
/// to the <see cref="AnimancerState.NormalizedEndTime"/>
/// and invokes any remaining <see cref="AnimancerEvent"/>s.
/// </summary>
public void FinishImmediately()
{
var index = _NextEventIndex;
if (index == RecalculateEventIndex && Events.Count > 0)
{
var normalizedTime = State.NormalizedTime;
ValidateNextEventIndex(
State.IsLooping,
ref normalizedTime,
out _,
out _);
index = _NextEventIndex;
}
State.NormalizedTime = State.NormalizedEndTime;
if (Events.Count > 0)
{
if (State.EffectiveSpeed < 0)// Backwards.
{
for (int i = index; i >= 0; i--)
Events[i].DelayInvoke(Events.GetName(i), State);
}
else// Forwards, 0, or NaN.
{
for (int i = index; i < Events.Count; i++)
Events[i].DelayInvoke(Events.GetName(i), State);
}
}
var endEvent = Events.EndEvent;
if (endEvent.callback != null)
endEvent.DelayInvoke(EndEventName, State);
Invoker.InvokeAllAndClear();
}
/************************************************************************************************************************/
/// <summary>Returns "<see cref="Dispatcher"/> (Target State)".</summary>
public override string ToString()
=> State != null
? $"{nameof(Dispatcher)} ({State})"
: $"{nameof(Dispatcher)} (No Target State)";
/************************************************************************************************************************/
/// <inheritdoc/>
public void AppendDescription(StringBuilder text, string separator = "\n")
{
text.AppendField(separator, "State", State.GetPath());
text.AppendField(separator, "IsLooping", State.IsLooping);
text.AppendField(separator, "PreviousNormalizedTime", _PreviousNormalizedTime);
text.AppendField(separator, "NextEventIndex", _NextEventIndex);
text.AppendField(separator, "SequenceVersion", _SequenceVersion);
text.AppendField(separator, "WasPlayingForwards", _WasPlayingForwards);
}
/************************************************************************************************************************/
}
}
}

View File

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

View File

@@ -0,0 +1,206 @@
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
using System;
using System.Runtime.CompilerServices;
using UnityEngine;
using Object = UnityEngine.Object;
namespace Animancer
{
/// https://kybernetik.com.au/animancer/api/Animancer/AnimancerEvent
partial struct AnimancerEvent
{
/// <summary>An <see cref="AnimancerEvent"/> and other associated details used to invoke it.</summary>
/// https://kybernetik.com.au/animancer/api/Animancer/Invocation
public readonly struct Invocation
{
/************************************************************************************************************************/
/// <summary>The details of the event currently being triggered.</summary>
/// <remarks>Cleared after the event is invoked.</remarks>
public static Invocation Current { get; private set; }
/************************************************************************************************************************/
/// <summary>The <see cref="AnimancerEvent"/>.</summary>
public readonly AnimancerEvent Event;
/// <summary>The name of the <see cref="Event"/>.</summary>
public readonly StringReference Name;
/// <summary>The <see cref="AnimancerState"/> triggering the <see cref="Event"/>.</summary>
public readonly AnimancerState State;
/************************************************************************************************************************/
/// <summary>Creates a new <see cref="Invocation"/>.</summary>
public Invocation(
AnimancerEvent animancerEvent,
StringReference eventName,
AnimancerState state)
{
Event = animancerEvent;
State = state;
Name = eventName;
}
/************************************************************************************************************************/
/// <summary>
/// Sets the <see cref="Current"/>, invokes the <see cref="callback"/>,
/// then reverts the <see cref="Current"/>.
/// </summary>
/// <remarks>This method catches and logs any exception thrown by the <see cref="callback"/>.</remarks>
/// <exception cref="NullReferenceException">The <see cref="callback"/> is null.</exception>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public readonly void Invoke()
{
#if UNITY_ASSERTIONS
var oldLayer = State.Layer;
var oldCommandCount = oldLayer.CommandCount;
#endif
var previous = Current;
var parameter = CurrentParameter;
Current = this;
CurrentParameter = null;
try
{
Event.callback();
}
catch (Exception exception)
{
Debug.LogException(exception, State?.Graph?.Component as Object);
}
Current = previous;
CurrentParameter = parameter;
#if UNITY_ASSERTIONS
if (Name == EndEventName)
AssertEndEventInvoked(oldLayer, oldCommandCount);
#endif
}
/************************************************************************************************************************/
/// <summary>
/// Returns the callback registered in the <see cref="AnimancerGraph.Events"/>
/// with the <see cref="Name"/> (or null if there isn't one).
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public readonly Action GetBoundCallback()
=> Name.IsNullOrEmpty()
? null
: State.Graph._Events?.Get(Name);
/************************************************************************************************************************/
/// <summary>Returns a string describing the contents of this invocation.</summary>
public override string ToString()
=> $"{nameof(AnimancerEvent)}.{nameof(Invocation)}(" +
$"{nameof(Name)}={AnimancerUtilities.ToStringOrNull(Name)}, " +
$"NormalizedTime={Event.normalizedTime:0.##}, " +
$"Callback=({AnimancerReflection.ToStringDetailed(Event.callback)}), " +
$"{nameof(State)}={AnimancerUtilities.ToStringOrNull(State)})";
/************************************************************************************************************************/
/// <summary>
/// Invokes the callback bound to the <see cref="Name"/>
/// in the <see cref="AnimancerGraph.Events"/>.
/// </summary>
/// <remarks>
/// Logs <see cref="OptionalWarning.UselessEvent"/> if no callback is bound.
/// </remarks>
public void InvokeBoundCallback()
{
if (Name != null &&
State.Graph._Events != null &&
State.Graph._Events.TryGetValue(Name, out var callback))
{
callback();
}
#if UNITY_ASSERTIONS
// If the callback doesn't do anything else, then this is a useless event.
else if (Event.callback == AnimancerEvent.InvokeBoundCallback &&
OptionalWarning.UselessEvent.IsEnabled())
{
OptionalWarning.UselessEvent.Log(
$"An {nameof(AnimancerEvent)} attempted to invoke the callback bound to the name" +
$" '{AnimancerUtilities.ToStringOrNull(Name)}' but there is no callback" +
$" bound to that name so the event may not be configured correctly." +
$"\n• Normalized Time: {Event.normalizedTime}" +
$"\n• State: {State}" +
$"\n• Object: {AnimancerUtilities.ToStringOrNull(State.Graph?.Component)}",
State.Graph?.Component);
}
#endif
}
/************************************************************************************************************************/
#if UNITY_ASSERTIONS
/************************************************************************************************************************/
/// <summary>[Assert-Only]
/// Call after invoking an end event to assert <see cref="OptionalWarning.EndEventInterrupt"/>.
/// </summary>
private readonly void AssertEndEventInvoked(AnimancerLayer oldLayer, int oldCommandCount)
{
if (ShouldLogEndEventInterrupt(oldLayer, oldCommandCount))
{
OptionalWarning.EndEventInterrupt.Log(
$"An End Event callback didn't stop the animation." +
$" Animancer doesn't handle End Events automatically," +
$" so the controlling script is responsible for stopping the animation," +
$" often by playing a different one." +
$"\n• State: {State}" +
$"\n• Callback: {Event.callback.ToStringDetailed()}" +
$"\n• End Events are triggered every frame after their time has passed:" +
$" {Strings.DocsURLs.EndEvents.AsHtmlLink()}" +
$"\n• To avoid this behaviour, use a regular Animancer Event instead:" +
$" {Strings.DocsURLs.AnimancerEvents.AsHtmlLink()}",
State.Graph?.Component);
OptionalWarning.EndEventInterrupt.Disable();
}
}
/************************************************************************************************************************/
/// <summary>[Assert-Only] Should <see cref="OptionalWarning.EndEventInterrupt"/> be logged?</summary>
private readonly bool ShouldLogEndEventInterrupt(AnimancerLayer oldLayer, int oldCommandCount)
{
if (!OptionalWarning.EndEventInterrupt.IsEnabled())
return false;
var events = State.SharedEvents;
if (events == null ||
events.OnEnd != Event.callback)
return false;
var newLayer = State.Layer;
if (oldLayer != newLayer ||
oldCommandCount != newLayer.CommandCount ||
!State.Graph.IsGraphPlaying ||
!State.IsPlaying)
return false;
var speed = State.EffectiveSpeed;
if (speed > 0)
return State.NormalizedTime > State.NormalizedEndTime;
else if (speed < 0)
return State.NormalizedTime < State.NormalizedEndTime;
else// Speed 0.
return false;
}
/************************************************************************************************************************/
#endif
/************************************************************************************************************************/
}
}
}

View File

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

View File

@@ -0,0 +1,136 @@
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using UnityEngine;
namespace Animancer
{
/// https://kybernetik.com.au/animancer/api/Animancer/AnimancerEvent
partial struct AnimancerEvent
{
/************************************************************************************************************************/
/// <summary>Events ready to be invoked by the next <see cref="Invoker.InvokeAllAndClear"/>.</summary>
/// <remarks>
/// This field should be inside the Invoker class.
/// But that can potentially cause a TypeLoadException if Invoker initializes before AnimancerEvent.
/// Having it out in AnimancerEvent avoids that possibility.
/// </remarks>
private static readonly List<Invocation>
InvocationQueue = new();
/************************************************************************************************************************/
/// <summary>Gathers delegates in a static list to be invoked at a later time by any child class.</summary>
/// https://kybernetik.com.au/animancer/api/Animancer/Invoker
[DefaultExecutionOrder(-30000)]// Run as soon as possible in whatever update cycle is being executed.
[ExecuteAlways]
public abstract class Invoker : MonoBehaviour
{
/************************************************************************************************************************/
/// <summary>Ensures that an appropriate <see cref="Invoker"/> has been created.</summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Invoker Initialize(bool fixedUpdate)
=> fixedUpdate
? InvokerFixed.Initialize()
: InvokerDynamic.Initialize();
/// <summary>Ensures that an appropriate <see cref="Invoker"/> has been created.</summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Invoker Initialize(AnimatorUpdateMode updateMode)
{
const AnimatorUpdateMode FixedUpdateMode =
#if UNITY_2023_1_OR_NEWER
AnimatorUpdateMode.Fixed;
#else
AnimatorUpdateMode.AnimatePhysics;
#endif
return Initialize(updateMode == FixedUpdateMode);
}
/************************************************************************************************************************/
/// <summary>[Internal] Adds an event to the queue to be invoked by the next update.</summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
internal static void Add(Invocation invocation)
{
#if UNITY_ASSERTIONS
if (!HasEnabledInstance)
Debug.LogWarning(
$"There is no currently enabled {nameof(AnimancerEvent)}.{nameof(Invoker)}" +
$" so events will not be invoked.");
#endif
InvocationQueue.Add(invocation);
}
/************************************************************************************************************************/
/// <summary>
/// In case <see cref="InvokeAllAndClear"/> gets called recursively,
/// we need to avoid invoking the same event multiple times
/// without the performance cost of immediately removing them each from the queue.
/// </summary>
private static int _CurrentInvocation;
/// <summary>Invokes all queued events and clears the queue.</summary>
public static void InvokeAllAndClear()
{
while (_CurrentInvocation < InvocationQueue.Count)
InvocationQueue[_CurrentInvocation++].Invoke();
InvocationQueue.Clear();
_CurrentInvocation = 0;
}
/************************************************************************************************************************/
/// <summary>Returns an enumerator for all invocations currently in the queue.</summary>
public static List<Invocation>.Enumerator EnumerateInvocationQueue()
=> InvocationQueue.GetEnumerator();
/************************************************************************************************************************/
#if UNITY_ASSERTIONS
/************************************************************************************************************************/
private static readonly List<Invoker>
Instances = new();
/************************************************************************************************************************/
/// <summary>[Assert-Only] Registers this instance.</summary>
protected virtual void Awake()
=> Instances.Add(this);
/************************************************************************************************************************/
/// <summary>[Assert-Only] Un-registers this instance.</summary>
protected virtual void OnDestroy()
=> Instances.Remove(this);
/************************************************************************************************************************/
/// <summary>[Assert-Only] Is there any <see cref="Behaviour.enabled"/> instance?</summary>
private static bool HasEnabledInstance
{
get
{
for (int i = 0; i < Instances.Count; i++)
if (Instances[i].enabled)
return true;
return false;
}
}
/************************************************************************************************************************/
#endif
/************************************************************************************************************************/
}
}
}

View File

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

View File

@@ -0,0 +1,59 @@
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
using System.Runtime.CompilerServices;
using UnityEngine;
namespace Animancer
{
/// https://kybernetik.com.au/animancer/api/Animancer/AnimancerEvent
partial struct AnimancerEvent
{
/// <summary>Executes <see cref="Invoker.InvokeAllAndClear"/> after animations in the Dynamic Update cycle.</summary>
/// https://kybernetik.com.au/animancer/api/Animancer/InvokerDynamic
[AnimancerHelpUrl(typeof(InvokerDynamic))]
[AddComponentMenu("")]// Singleton creates itself.
public class InvokerDynamic : Invoker
{
/************************************************************************************************************************/
private static InvokerDynamic _Instance;
/// <summary>Creates the singleton instance.</summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static InvokerDynamic Initialize()
=> AnimancerUtilities.InitializeSingleton(ref _Instance);
/************************************************************************************************************************/
/// <summary>Should this system execute events?</summary>
/// <remarks>If disabled, this system will not be re-enabled automatically.</remarks>
public static bool Enabled
{
get => _Instance != null && _Instance.enabled;
set
{
if (value)
{
Initialize();
_Instance.enabled = true;
}
else if (_Instance != null)
{
_Instance.enabled = false;
}
}
}
/************************************************************************************************************************/
/// <summary>After animation update with dynamic timestep.</summary>
protected virtual void LateUpdate()
{
InvokeAllAndClear();
}
/************************************************************************************************************************/
}
}
}

View File

@@ -0,0 +1,13 @@
fileFormatVersion: 2
guid: 108ccdfde47cfab418af3bd716413114
timeCreated: 1515060256
licenseType: Store
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 System.Collections;
using System.Runtime.CompilerServices;
using UnityEngine;
namespace Animancer
{
/// https://kybernetik.com.au/animancer/api/Animancer/AnimancerEvent
partial struct AnimancerEvent
{
/// <summary>Executes <see cref="Invoker.InvokeAllAndClear"/> after animations in the Fixed Update cycle.</summary>
/// https://kybernetik.com.au/animancer/api/Animancer/InvokerFixed
[AnimancerHelpUrl(typeof(InvokerFixed))]
[AddComponentMenu("")]// Singleton creates itself.
public class InvokerFixed : Invoker
{
/************************************************************************************************************************/
private static InvokerFixed _Instance;
/// <summary>Creates the singleton instance.</summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static InvokerFixed Initialize()
=> AnimancerUtilities.InitializeSingleton(ref _Instance);
/************************************************************************************************************************/
/// <summary>Should this system execute events?</summary>
/// <remarks>If disabled, this system will not be re-enabled automatically.</remarks>
public static bool Enabled
{
get => _Instance != null && _Instance.enabled;
set
{
if (value)
{
Initialize();
_Instance.enabled = true;
}
else if (_Instance != null)
{
_Instance.enabled = false;
}
}
}
/************************************************************************************************************************/
/// <summary>A cached instance of <see cref="UnityEngine.WaitForFixedUpdate"/>.</summary>
public static readonly WaitForFixedUpdate
WaitForFixedUpdate = new();
/************************************************************************************************************************/
/// <summary>Starts the <see cref="LateFixedUpdate"/> coroutine.</summary>
protected virtual void OnEnable()
=> StartCoroutine(LateFixedUpdate());
/************************************************************************************************************************/
/// <summary>After animation update with fixed timestep.</summary>
private IEnumerator LateFixedUpdate()
{
while (true)
{
yield return WaitForFixedUpdate;
InvokeAllAndClear();
}
}
/************************************************************************************************************************/
}
}
}

View File

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

View File

@@ -0,0 +1,446 @@
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
#pragma warning disable CS0649 // Field is never assigned to, and will always have its default value.
using System;
using UnityEngine;
namespace Animancer
{
/// https://kybernetik.com.au/animancer/api/Animancer/AnimancerEvent
partial struct AnimancerEvent
{
/// https://kybernetik.com.au/animancer/api/Animancer/Sequence
partial class Sequence
{
/// <summary>
/// Serializable data which can be used to construct an <see cref="Sequence"/> using
/// <see cref="StringAsset"/>s and <see cref="IInvokable"/>s.
/// </summary>
/// https://kybernetik.com.au/animancer/api/Animancer/Serializable
[Serializable]
public class Serializable : ICloneable<Serializable>
#if UNITY_EDITOR
, ISerializationCallbackReceiver
#endif
{
/************************************************************************************************************************/
#region Fields and Properties
/************************************************************************************************************************/
[SerializeField]
private float[] _NormalizedTimes;
/// <summary>[<see cref="SerializeField"/>] The serialized <see cref="normalizedTime"/>s.</summary>
/// <remarks>The last item is used for the <see cref="EndEvent"/>.</remarks>
public ref float[] NormalizedTimes => ref _NormalizedTimes;
/************************************************************************************************************************/
[SerializeReference, Polymorphic]
private IInvokable[] _Callbacks;
/// <summary>[<see cref="SerializeField"/>] The serialized <see cref="callback"/>s.</summary>
/// <remarks>
/// This array only needs to be large enough to hold the last item that isn't null.
/// <para></para>
/// If this array is larger than the <see cref="NormalizedTimes"/>, the first item
/// with no corresponding time will be used as the <see cref="OnEnd"/> callback
/// and any others after that will be ignored.
/// </remarks>
public ref IInvokable[] Callbacks => ref _Callbacks;
/************************************************************************************************************************/
[SerializeField]
private StringAsset[] _Names;
/// <summary>[<see cref="SerializeField"/>] The serialized <see cref="Sequence.Names"/>.</summary>
public ref StringAsset[] Names => ref _Names;
/************************************************************************************************************************/
#if UNITY_EDITOR
/************************************************************************************************************************/
/// <summary>[Editor-Only] [Internal]
/// The name of the array field which stores the <see cref="normalizedTime"/>s.
/// </summary>
internal const string NormalizedTimesField = nameof(_NormalizedTimes);
/// <summary>[Editor-Only] [Internal]
/// The name of the array field which stores the serialized <see cref="Callbacks"/>.
/// </summary>
internal const string CallbacksField = nameof(_Callbacks);
/// <summary>[Editor-Only] [Internal]
/// The name of the array field which stores the serialized <see cref="Names"/>.
/// </summary>
internal const string NamesField = nameof(_Names);
/// <summary>[Editor-Only] Disable Inspector Gadgets Nested Object Drawers.</summary>
private const bool NestedObjectDrawers = false;
/************************************************************************************************************************/
#endif
/************************************************************************************************************************/
private Sequence _Events;
/// <summary>Returns the <see cref="Events"/> or <c>null</c> if it wasn't yet initialized.</summary>
public Sequence InitializedEvents
=> _Events;
/// <summary>
/// The runtime <see cref="Sequence"/> compiled from this <see cref="Serializable"/>.
/// Each call after the first will return the same reference.
/// </summary>
/// <remarks>
/// Unlike <see cref="GetEventsOptional"/>, this property will create an empty
/// <see cref="Sequence"/> instead of returning null if there are no events.
/// </remarks>
public Sequence Events
{
get
{
if (_Events == null)
{
GetEventsOptional();
_Events ??= new();
}
return _Events;
}
set => _Events = value;
}
/************************************************************************************************************************/
#endregion
/************************************************************************************************************************/
#region Initialization
/************************************************************************************************************************/
/// <summary>
/// Returns the runtime <see cref="Sequence"/> compiled from this <see cref="Serializable"/>.
/// Each call after the first will return the same reference.
/// </summary>
/// <remarks>
/// This method returns null if the sequence would be empty anyway and is used by the implicit
/// conversion from <see cref="Serializable"/> to <see cref="Sequence"/>.
/// </remarks>
public Sequence GetEventsOptional()
{
if (_Events != null ||
_NormalizedTimes == null)
return _Events;
var timeCount = _NormalizedTimes.Length;
if (timeCount == 0)
return null;
var callbackCount = _Callbacks != null
? _Callbacks.Length
: 0;
var callback = callbackCount >= timeCount--
? GetInvoke(_Callbacks[timeCount])
: null;
var endEvent = new AnimancerEvent(_NormalizedTimes[timeCount], callback);
_Events = new(timeCount)
{
EndEvent = endEvent,
Count = timeCount,
Names = StringAsset.ToStringReferences(_Names),
};
var events = _Events._Events;
for (int i = 0; i < timeCount; i++)
{
callback = i < callbackCount
? GetInvoke(_Callbacks[i])
: InvokeBoundCallback;
events[i] = new(_NormalizedTimes[i], callback);
}
return _Events;
}
/// <summary>Calls <see cref="GetEventsOptional"/>.</summary>
public static implicit operator Sequence(Serializable serializable)
=> serializable?.GetEventsOptional();
/************************************************************************************************************************/
/// <summary>
/// Returns the <see cref="IInvokable.Invoke"/> if the `invokable` isn't <c>null</c>.
/// Otherwise, returns <c>null</c>.
/// </summary>
public static Action GetInvoke(IInvokable invokable)
=> invokable != null
? invokable.Invoke
: InvokeBoundCallback;
/************************************************************************************************************************/
#endregion
/************************************************************************************************************************/
#region End Event
/************************************************************************************************************************/
/// <summary>Returns the <see cref="normalizedTime"/> of the <see cref="EndEvent"/>.</summary>
/// <remarks>If the value is not set, the value is determined by <see cref="GetDefaultNormalizedEndTime"/>.</remarks>
public float GetNormalizedEndTime(float speed = 1)
{
return _NormalizedTimes.IsNullOrEmpty()
? GetDefaultNormalizedEndTime(speed)
: _NormalizedTimes[^1];
}
/************************************************************************************************************************/
/// <summary>Sets the <see cref="normalizedTime"/> of the <see cref="EndEvent"/>.</summary>
public void SetNormalizedEndTime(float normalizedTime)
{
if (_NormalizedTimes.IsNullOrEmpty())
_NormalizedTimes = new float[] { normalizedTime };
else
_NormalizedTimes[^1] = normalizedTime;
}
/************************************************************************************************************************/
/// <summary>Sets the <see cref="callback"/> of the <see cref="EndEvent"/>.</summary>
public void SetEndCallback(IInvokable callback = null)
{
if (_NormalizedTimes.IsNullOrEmpty())
_NormalizedTimes = new float[] { float.NaN };
InsertOptionalItem(ref _Callbacks, _NormalizedTimes.Length - 1, callback);
}
/************************************************************************************************************************/
/// <summary>Sets the data of the <see cref="EndEvent"/>.</summary>
public void SetEndEvent(float normalizedTime = float.NaN, IInvokable callback = null)
{
if (_NormalizedTimes.IsNullOrEmpty())
_NormalizedTimes = new float[] { normalizedTime };
else
_NormalizedTimes[^1] = normalizedTime;
InsertOptionalItem(ref _Callbacks, _NormalizedTimes.Length - 1, callback);
}
/************************************************************************************************************************/
#endregion
/************************************************************************************************************************/
#region Other Events
/************************************************************************************************************************/
/// <summary>Adds an event to the serialized fields.</summary>
public int AddEvent(float normalizedTime, IInvokable callback = null, StringAsset name = null)
{
int index;
if (_NormalizedTimes.IsNullOrEmpty())
{
_NormalizedTimes = new float[] { normalizedTime, float.NaN };
index = 0;
}
else
{
index = _NormalizedTimes.Length - 1;
for (int i = 0; i < _NormalizedTimes.Length - 1; i++)
{
if (_NormalizedTimes[i] > normalizedTime)
{
index = i;
break;
}
}
AnimancerUtilities.InsertAt(ref _NormalizedTimes, index, normalizedTime);
}
InsertOptionalItem(ref _Callbacks, index, callback);
InsertOptionalItem(ref _Names, index, name);
return index;
}
/************************************************************************************************************************/
/// <summary>Inserts an `item` at the specified `index` in an optional `array`.</summary>
/// <remarks>
/// If the `item` is <c>null</c> then the array only needs
/// to be expanded if it was already larger than the `index`.
/// </remarks>
private static void InsertOptionalItem<T>(ref T[] array, int index, T item)
where T : class
{
if (item == null &&
(array == null || array.Length < index))
return;
AnimancerUtilities.InsertAt(ref array, index, item);
}
/************************************************************************************************************************/
/// <summary>Removes an event from the serialized fields.</summary>
public void RemoveEvent(int index)
{
if (_NormalizedTimes.IsNullOrEmpty())
return;
AnimancerUtilities.RemoveAt(ref _NormalizedTimes, index);
if (_Callbacks != null && _Callbacks.Length > index)
AnimancerUtilities.RemoveAt(ref _Callbacks, index);
if (_Names != null && _Names.Length > index)
AnimancerUtilities.RemoveAt(ref _Names, index);
}
/************************************************************************************************************************/
/// <summary>Removes all events.</summary>
public void Clear(bool keepEndEvent = false)
{
if (keepEndEvent)
{
if (_NormalizedTimes != null && _NormalizedTimes.Length > 0)
_NormalizedTimes = new float[] { _NormalizedTimes[^1] };
else
_NormalizedTimes = null;
if (_Callbacks != null && _Callbacks.Length > 0)
_Callbacks = new IInvokable[] { _Callbacks[^1] };
else
_Callbacks = null;
}
else
{
_NormalizedTimes = null;
_Callbacks = null;
}
_Names = null;
}
/************************************************************************************************************************/
#endregion
/************************************************************************************************************************/
#region Copying
/************************************************************************************************************************/
/// <summary>Creates a new <see cref="Serializable"/> and copies the contents of <c>this</c> into it.</summary>
/// <remarks>To copy into an existing sequence, use <see cref="CopyFrom"/> instead.</remarks>
public Serializable Clone()
{
var clone = new Serializable();
clone.CopyFrom(this);
return clone;
}
/// <inheritdoc/>
public Serializable Clone(CloneContext context)
=> Clone();
/************************************************************************************************************************/
/// <inheritdoc/>
public void CopyFrom(Serializable copyFrom)
{
AnimancerUtilities.CopyExactArray(copyFrom._NormalizedTimes, ref _NormalizedTimes);
AnimancerUtilities.CopyExactArray(copyFrom._Callbacks, ref _Callbacks);
AnimancerUtilities.CopyExactArray(copyFrom._Names, ref _Names);
}
/************************************************************************************************************************/
#endregion
/************************************************************************************************************************/
#region Serialization
/************************************************************************************************************************/
#if UNITY_EDITOR
/************************************************************************************************************************/
/// <summary>[Editor-Only] Does nothing.</summary>
void ISerializationCallbackReceiver.OnAfterDeserialize() { }
/************************************************************************************************************************/
/// <summary>[Editor-Only] [Internal]
/// Called by <see cref="ISerializationCallbackReceiver.OnBeforeSerialize"/>.
/// </summary>
internal static event Action<Serializable> OnBeforeSerialize;
/// <summary>[Editor-Only] Ensures that the events are sorted by time (excluding the end event).</summary>
void ISerializationCallbackReceiver.OnBeforeSerialize()
=> OnBeforeSerialize?.Invoke(this);
/************************************************************************************************************************/
/// <summary>[Editor-Only] [Internal]
/// Should the arrays be prevented from reducing their size when their last elements are unused?
/// </summary>
internal static bool DisableCompactArrays { get; set; }
/// <summary>[Editor-Only] [Internal]
/// Removes empty data from the ends of the arrays to reduce the serialized data size.
/// </summary>
internal void CompactArrays()
{
if (DisableCompactArrays)
return;
// If there is only one time and it is NaN, we don't need to store anything.
if (_NormalizedTimes == null ||
(_NormalizedTimes.Length == 1 &&
(_Callbacks == null || _Callbacks.Length == 0) &&
(_Names == null || _Names.Length == 0) &&
float.IsNaN(_NormalizedTimes[0])))
{
_NormalizedTimes = Array.Empty<float>();
_Callbacks = Array.Empty<IInvokable>();
_Names = Array.Empty<StringAsset>();
return;
}
Trim(ref _Callbacks, _NormalizedTimes.Length, callback => callback != null);
Trim(ref _Names, _NormalizedTimes.Length, name => name != null);
}
/************************************************************************************************************************/
/// <summary>[Editor-Only] Removes unimportant values from the end of the `array`.</summary>
private static void Trim<T>(ref T[] array, int maxLength, Func<T, bool> isImportant)
{
if (array == null)
return;
var count = Math.Min(array.Length, maxLength);
while (count >= 1)
{
var item = array[count - 1];
if (isImportant(item))
break;
else
count--;
}
Array.Resize(ref array, count);
}
/************************************************************************************************************************/
#endif
/************************************************************************************************************************/
#endregion
/************************************************************************************************************************/
}
}
}
}

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

@@ -0,0 +1,375 @@
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
using System;
using System.Runtime.CompilerServices;
using System.Text;
using UnityEngine;
namespace Animancer
{
/// <summary>
/// A <see cref="callback"/> delegate
/// paired with a <see cref="normalizedTime"/> which determines when to invoke it.
/// </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/AnimancerEvent
///
public partial struct AnimancerEvent : IEquatable<AnimancerEvent>
{
/************************************************************************************************************************/
#region Event
/************************************************************************************************************************/
/// <summary>The <see cref="AnimancerState.NormalizedTime"/> at which to invoke the <see cref="callback"/>.</summary>
public float normalizedTime;
/// <summary>The delegate to invoke when the <see cref="normalizedTime"/> passes.</summary>
public Action callback;
/************************************************************************************************************************/
/// <summary>The largest possible float value less than 1.</summary>
/// <remarks>
/// This value is useful for placing events at the end of a looping animation since they do not allow the
/// <see cref="normalizedTime"/> to be greater than or equal to 1.
/// </remarks>
public const float
AlmostOne = 0.99999994f;
/************************************************************************************************************************/
/// <summary>The event name used for <see cref="Sequence.EndEvent"/>s.</summary>
/// <remarks>
/// This is a <see cref="StringReference.Unique"/> so that even if the same name happens
/// to be used elsewhere, it would be treated as a different name.
/// The reason for this is explained in <see cref="NamedEventDictionary.AssertNotEndEvent"/>.
/// </remarks>
public static readonly StringReference
EndEventName = StringReference.Unique("EndEvent");
/************************************************************************************************************************/
/// <summary>Does nothing.</summary>
/// <remarks>This delegate can be used for events which would otherwise have a <c>null</c> <see cref="callback"/>.</remarks>
public static readonly Action
DummyCallback = Dummy;
/// <summary>Does nothing.</summary>
/// <remarks>Used by <see cref="DummyCallback"/>.</remarks>
private static void Dummy() { }
/// <summary>Is the `callback` <c>null</c> or the <see cref="DummyCallback"/>?</summary>
public static bool IsNullOrDummy(Action callback)
=> callback == null
|| callback == DummyCallback;
/************************************************************************************************************************/
/// <summary>Creates a new <see cref="AnimancerEvent"/>.</summary>
public AnimancerEvent(float normalizedTime, Action callback)
{
this.normalizedTime = normalizedTime;
this.callback = callback;
}
/************************************************************************************************************************/
/// <summary>Returns a string describing the details of this event.</summary>
public readonly override string ToString()
{
var text = StringBuilderPool.Instance.Acquire();
text.Append($"{nameof(AnimancerEvent)}(");
AppendDetails(text);
text.Append(')');
return text.ReleaseToString();
}
/************************************************************************************************************************/
/// <summary>Appends the details of this event to the `text`.</summary>
public readonly void AppendDetails(StringBuilder text)
{
text.Append("NormalizedTime: ")
.Append(normalizedTime);
var callbacks = AnimancerReflection.GetInvocationList(callback);
if (callbacks != null)
{
text.Append(", Callbacks: [")
.Append(callbacks.Length)
.Append("] { ");
for (int i = 0; i < callbacks.Length; i++)
{
if (i > 0)
text.Append(", ");
text.AppendDelegate(callbacks[i]);
}
text.Append(" }");
}
else
{
text.Append(", Callback: ")
.AppendDelegate(callback);
}
}
/************************************************************************************************************************/
#endregion
/************************************************************************************************************************/
#region Invocation
/************************************************************************************************************************/
/// <summary>The details of the event currently being triggered.</summary>
/// <remarks>Cleared after the event is invoked.</remarks>
// Having the underlying field here can cause type initialization errors due to circular dependencies.
public static Invocation Current
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get => Invocation.Current;
}
/************************************************************************************************************************/
/// <summary>
/// A cached delegate which calls <see cref="Invocation.InvokeBoundCallback"/>
/// on the <see cref="Current"/>.
/// </summary>
public static readonly Action
InvokeBoundCallback = InvokeCurrentBoundCallback;
/// <summary>
/// Calls <see cref="Invocation.InvokeBoundCallback"/> on the <see cref="Current"/>.
/// </summary>
private static void InvokeCurrentBoundCallback()
=> Current.InvokeBoundCallback();
/************************************************************************************************************************/
/// <summary>The custom parameter of the event currently being triggered.</summary>
/// <remarks>Cleared after the event is finished.</remarks>
public static object CurrentParameter { get; private set; }
/// <summary>Calls <see cref="ConvertableUtilities.ConvertOrThrow"/> on the <see cref="CurrentParameter"/>.</summary>
public static T GetCurrentParameter<T>()
=> ConvertableUtilities.ConvertOrThrow<T>(CurrentParameter);
/// <summary>Returns a new delegate which invokes the `callback` using <see cref="GetCurrentParameter{T}"/>.</summary>
/// <remarks>
/// If <typeparamref name="T"/> is <see cref="string"/>,
/// consider using <see cref="Parametize(Action{string})"/> instead of this.
/// </remarks>
/// <exception cref="ArgumentNullException">The `callback` is <c>null</c>.</exception>
public static Action Parametize<T>(Action<T> callback)
{
#if UNITY_ASSERTIONS
if (callback == null)
throw new ArgumentNullException(
nameof(callback),
$"Can't {nameof(Parametize)} a null callback.");
#endif
return () => callback(GetCurrentParameter<T>());
}
/// <summary>Returns a new delegate which invokes the `callback` using the <see cref="CurrentParameter"/>.</summary>
/// <exception cref="ArgumentNullException">The `callback` is <c>null</c>.</exception>
public static Action Parametize(Action<string> callback)
{
#if UNITY_ASSERTIONS
if (callback == null)
throw new ArgumentNullException(
nameof(callback),
$"Can't {nameof(Parametize)} a null callback.");
#endif
return () => callback(CurrentParameter?.ToString());
}
/************************************************************************************************************************/
/// <summary>[Assert-Only]
/// Logs an error if the `callback` doesn't contain a <see cref="Parameter{T}.Invoke"/>
/// so that adding to it with <see cref="Parametize{T}(Action{T})"/> can use that parameter.
/// </summary>
[System.Diagnostics.Conditional(Strings.Assertions)]
public static void AssertContainsParameter<T>(Action callback)
{
if (!ContainsParameterInvoke<T>(callback))
Debug.LogWarning(
$"Adding parametized callback will do nothing because the existing callback" +
$" doesn't contain a {typeof(T).GetNameCS()} parameter." +
$"\n• Existing Callback: {callback.ToStringDetailed()}");
}
/// <summary>Does the `callback` contain a <see cref="Parameter{T}.Invoke"/>?</summary>
private static bool ContainsParameterInvoke<T>(Action callback)
{
if (callback == null)
return false;
if (IsParameterInvoke<T>(callback))
return true;
var invocations = AnimancerReflection.GetInvocationList(callback);
if (invocations.Length == 1 && ReferenceEquals(invocations[0], callback))
return false;
for (int i = 0; i < invocations.Length; i++)
{
var invocation = invocations[i];
if (IsParameterInvoke<T>(invocation))
return true;
}
return false;
}
/// <summary>Is the `callback` a call to <see cref="Parameter{T}.Invoke"/>?</summary>
private static bool IsParameterInvoke<T>(Delegate callback)
=> callback.Target is IParameter parameter
&& callback.Method.Name == nameof(IInvokable.Invoke)
&& typeof(T).IsAssignableFrom(parameter.Value.GetType());
/************************************************************************************************************************/
/// <summary>
/// Adds this event to the <see cref="Invoker"/>
/// which will call <see cref="Invocation.Invoke"/> later in the current frame.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public readonly void DelayInvoke(
StringReference eventName,
AnimancerState state)
=> Invoker.Add(new(this, eventName, state));
/************************************************************************************************************************/
/// <summary>[Assert-Conditional]
/// This method should be called when an animation is played.
/// It asserts that either no event is currently being triggered
/// or that the event is being triggered inside `playing`.
/// Otherwise, it logs <see cref="OptionalWarning.EventPlayMismatch"/>.
/// </summary>
[System.Diagnostics.Conditional(Strings.Assertions)]
public static void AssertEventPlayMismatch(AnimancerGraph playing)
{
#if UNITY_ASSERTIONS
if (Current.State == null ||
Current.State.Graph == playing ||
OptionalWarning.EventPlayMismatch.IsDisabled())
return;
OptionalWarning.EventPlayMismatch.Log(
$"An Animancer Event triggered by '{Current.State}' on '{Current.State.Graph}'" +
$" was used to play an animation on a different character ('{playing}')." +
$"\n\nThis 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 being triggered by the wrong character." +
$"\n\n{Current}",
playing.Component);
#endif
}
/************************************************************************************************************************/
/// <summary>
/// Returns either the <see cref="AnimancerGraph.DefaultFadeDuration"/>
/// or the <see cref="AnimancerState.RemainingDuration"/>
/// of the <see cref="Current"/> state (whichever is higher).
/// </summary>
public static float GetFadeOutDuration()
=> GetFadeOutDuration(Current.State, AnimancerGraph.DefaultFadeDuration);
/// <summary>
/// Returns either the `minDuration` or the <see cref="AnimancerState.RemainingDuration"/>
/// of the <see cref="Current"/> state (whichever is higher).
/// </summary>
public static float GetFadeOutDuration(float minDuration)
=> GetFadeOutDuration(Current.State, minDuration);
/// <summary>
/// Returns either the `minDuration` or the <see cref="AnimancerState.RemainingDuration"/>
/// of the `state` (whichever is higher).
/// </summary>
public static float GetFadeOutDuration(AnimancerState state, float minDuration)
{
if (state == null)
return minDuration;
var time = state.Time;
var speed = state.EffectiveSpeed;
if (speed == 0)
return minDuration;
float remainingDuration;
if (state.IsLooping)
{
var previousTime = time - speed * Time.deltaTime;
var inverseLength = 1f / state.Length;
// If we just passed the end of the animation, the remaining duration would technically be the full
// duration of the animation, so we most likely want to use the minimum duration instead.
if (Math.Floor(time * inverseLength) != Math.Floor(previousTime * inverseLength))
return minDuration;
}
if (speed > 0)
{
remainingDuration = (state.Length - time) / speed;
}
else
{
remainingDuration = time / -speed;
}
return Math.Max(minDuration, remainingDuration);
}
/************************************************************************************************************************/
#endregion
/************************************************************************************************************************/
#region Operators
/************************************************************************************************************************/
/// <summary>Are the <see cref="normalizedTime"/> and <see cref="callback"/> equal?</summary>
public static bool operator ==(AnimancerEvent a, AnimancerEvent b)
=> a.Equals(b);
/// <summary>Are the <see cref="normalizedTime"/> and <see cref="callback"/> not equal?</summary>
public static bool operator !=(AnimancerEvent a, AnimancerEvent b)
=> !a.Equals(b);
/************************************************************************************************************************/
/// <summary>[<see cref="IEquatable{AnimancerEvent}"/>]
/// Are the <see cref="normalizedTime"/> and <see cref="callback"/> of this event equal to `other`?
/// </summary>
public readonly bool Equals(AnimancerEvent other)
=> callback == other.callback
&& normalizedTime.IsEqualOrBothNaN(other.normalizedTime);
/// <inheritdoc/>
public readonly override bool Equals(object obj)
=> obj is AnimancerEvent animancerEvent
&& Equals(animancerEvent);
/// <inheritdoc/>
public readonly override int GetHashCode()
=> AnimancerUtilities.Hash(-78069441,
normalizedTime.GetHashCode(),
callback.SafeGetHashCode());
/************************************************************************************************************************/
#endregion
/************************************************************************************************************************/
}
}

View File

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

View File

@@ -0,0 +1,406 @@
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
using System.Runtime.CompilerServices;
using UnityEngine;
namespace Animancer
{
/// https://kybernetik.com.au/animancer/api/Animancer/AnimancerState
partial class AnimancerState
{
/************************************************************************************************************************/
/// <summary>The system which manages the <see cref="SharedEvents"/>.</summary>
private AnimancerEvent.Dispatcher _EventDispatcher;
/************************************************************************************************************************/
/// <summary>
/// Events which will be triggered while this state plays
/// based on its <see cref="NormalizedTime"/>.
/// </summary>
///
/// <remarks>
/// This property tries to ensure that the event sequence is only referenced by this state.
/// <list type="bullet">
/// <item>
/// If the reference was <c>null</c>,
/// a new sequence will be created.
/// </item>
/// <item>
/// If a reference was assigned to <see cref="SharedEvents"/>,
/// it will be cloned so this state owns the clone.
/// </item>
/// </list>
/// <para></para>
/// Using <see cref="Events(object)"/> or <see cref="Events(object, out AnimancerEvent.Sequence)"/>
/// is often safer than this property since they help detect if multiple scripts are using the same
/// state which could lead to unexpected bugs if they each assign conflicting callbacks.
/// <para></para>
/// <strong>Documentation:</strong>
/// <see href="https://kybernetik.com.au/animancer/docs/manual/events/animancer">
/// Animancer Events</see>
/// </remarks>
public AnimancerEvent.Sequence OwnedEvents
{
get
{
_EventDispatcher ??= new(this);
_EventDispatcher.InitializeEvents(out var events);
return events;
}
set
{
if (value != null)
(_EventDispatcher ??= new(this)).SetEvents(value, true);
else
_EventDispatcher = null;
}
}
/************************************************************************************************************************/
/// <summary>
/// Events which will be triggered while this state plays
/// based on its <see cref="NormalizedTime"/>.
/// </summary>
///
/// <remarks>
/// This reference is <c>null</c> by default and once assigned it may be shared by multiple states.
/// <para></para>
/// <strong>Documentation:</strong>
/// <see href="https://kybernetik.com.au/animancer/docs/manual/events/animancer">
/// Animancer Events</see>
/// </remarks>
public AnimancerEvent.Sequence SharedEvents
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get => _EventDispatcher?.Events;
set
{
if (value != null)
(_EventDispatcher ??= new(this)).SetEvents(value, false);
else
_EventDispatcher = null;
}
}
/************************************************************************************************************************/
/// <summary>Have the <see cref="SharedEvents"/> or <see cref="OwnedEvents"/> been initialized?</summary>
/// <remarks>
/// <strong>Documentation:</strong>
/// <see href="https://kybernetik.com.au/animancer/docs/manual/events/animancer">
/// Animancer Events</see>
/// </remarks>
public bool HasEvents
=> _EventDispatcher != null;
/************************************************************************************************************************/
/// <summary>Have the <see cref="OwnedEvents"/> been initialized?</summary>
/// <remarks>
/// <strong>Documentation:</strong>
/// <see href="https://kybernetik.com.au/animancer/docs/manual/events/animancer">
/// Animancer Events</see>
/// </remarks>
public bool HasOwnedEvents
=> _EventDispatcher != null
&& _EventDispatcher.HasOwnEvents;
/************************************************************************************************************************/
/// <summary>
/// If the <see cref="OwnedEvents"/> haven't been initialized yet,
/// this method gets them and returns <c>true</c>.
/// </summary>
///
/// <remarks>
/// This method tries to ensure that the event sequence is only referenced by this state.
/// <list type="bullet">
/// <item>
/// If the reference was <c>null</c>,
/// a new sequence will be created.
/// </item>
/// <item>
/// If a reference was assigned to <see cref="SharedEvents"/>,
/// it will be cloned so this state owns the clone.
/// </item>
/// </list>
/// In both of those cases, this method returns <c>true</c>
/// to indicate that the caller should initialize their event callbacks.
/// <para></para>
/// Also calls <see cref="AssertOwnership"/>.
/// <para></para>
/// <strong>Documentation:</strong>
/// <see href="https://kybernetik.com.au/animancer/docs/manual/events/animancer">
/// Animancer Events</see>
/// <para></para>
/// <strong>Example:</strong>
/// <code>
/// public static readonly StringReference EventName = "Event Name";
///
/// ...
///
/// AnimancerState state = animancerComponent.Play(animation);
/// if (state.Events(this, out AnimancerEvent.Sequence events))
/// {
/// events.SetCallback(EventName, OnAnimationEvent);
/// events.OnEnd = OnAnimationEnded;
/// }
/// </code>
/// If multiple different owners need to take turns reusing the same state,
/// use <see cref="Events(ref AnimancerEvent.Sequence)"/> instead.
/// <para></para>
/// If you only need to initialize the End Event,
/// consider using <see cref="Events(object)"/> instead.
/// </remarks>
public bool Events(object owner, out AnimancerEvent.Sequence events)
{
AssertOwnership(owner);
_EventDispatcher ??= new(this);
return _EventDispatcher.InitializeEvents(out events);
}
/************************************************************************************************************************/
/// <summary>
/// If the <see cref="OwnedEvents"/> haven't been initialized yet,
/// this method gets them and returns <c>true</c>.
/// </summary>
///
/// <remarks>
/// This method tries to ensure that the event sequence is only referenced by this state.
/// <list type="bullet">
/// <item>
/// If the reference was <c>null</c>,
/// a new sequence will be created.
/// </item>
/// <item>
/// If a reference was assigned to <see cref="SharedEvents"/>,
/// it will be cloned so this state owns the clone.
/// </item>
/// </list>
/// <para></para>
/// Also calls <see cref="AssertOwnership"/>.
/// <para></para>
/// <strong>Documentation:</strong>
/// <see href="https://kybernetik.com.au/animancer/docs/manual/events/animancer">
/// Animancer Events</see>
/// <para></para>
/// <strong>Example:</strong>
/// <code>
/// AnimancerState state = animancerComponent.Play(animation);
/// state.Events(this).OnEnd ??= OnAnimationEnded;
/// </code>
/// If multiple different owners need to take turns reusing the same state,
/// use <see cref="Events(ref AnimancerEvent.Sequence)"/> instead.
/// <para></para>
/// If you need to initialize more than just the End Event,
/// use <see cref="Events(object, out AnimancerEvent.Sequence)"/> instead.
/// </remarks>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public AnimancerEvent.Sequence Events(object owner)
{
Events(owner, out var events);
return events;
}
/************************************************************************************************************************/
/// <summary>
/// If the `events` are <c>null</c>, this method assigns a <c>new</c> <see cref="AnimancerEvent.Sequence"/>
/// and returns <c>true</c> to indicate that the caller should now initialize their event callbacks.
/// Otherwise, this method simply assigns the provided `events` to this state and returns <c>false</c>.
/// </summary>
///
/// <remarks>
/// If this state already had events, the <c>new</c> <see cref="AnimancerEvent.Sequence"/>
/// will be a copy of those events for the caller to own.
/// <para></para>
/// This method allows multiple callers to safely take turns using the same state
/// as long as they each call this method to assign their own events.
/// <para></para>
/// Also calls <see cref="AssertOwnership"/>.
/// <para></para>
/// <strong>Documentation:</strong>
/// <see href="https://kybernetik.com.au/animancer/docs/manual/events/animancer">
/// Animancer Events</see>
/// <para></para>
/// <strong>Example:</strong>
/// <code>
/// public static readonly StringReference EventName = "Event Name";
///
/// private AnimancerEvent.Sequence _Events;// Don't new() this.
///
/// ...
///
/// AnimancerState state = animancerComponent.Play(animation);
///
/// // The first time this is called it will assign a new event sequence
/// // to the _Events and return true so you can initialize it.
///
/// // After that, it will just re-assign the _Events to the state.
/// // and return false so you don't need to re-initialize the events.
///
/// if (state.Events(ref _Events))
/// {
/// _Events.SetCallback(EventName, OnAnimationEvent);
/// _Events.OnEnd = OnAnimationEnded;
/// }
/// </code>
/// </remarks>
public bool Events(ref AnimancerEvent.Sequence events)
{
_EventDispatcher ??= new(this);
var justInitialized = events == null;
if (justInitialized)
events = new(_EventDispatcher.Events);
#if UNITY_ASSERTIONS
// Normally swapping owners is an error,
// but with this method it's fine to swap between event sequences since each caller is responsible for its own.
if (Owner != null &&
Owner != events &&
Owner is not AnimancerEvent.Sequence)
AssertOwnership(events);
else
Owner = events;
#endif
_EventDispatcher.SetEvents(events, false);
return justInitialized;
}
/************************************************************************************************************************/
/// <summary>Copies the contents of the <see cref="_EventDispatcher"/>.</summary>
private void CopyEvents(AnimancerState copyFrom, CloneContext context)
{
if (copyFrom._EventDispatcher != null)
{
var original = copyFrom._EventDispatcher.Events;
var events = context.GetOrCreateCloneOrOriginal(original);
if (events != null)
{
_EventDispatcher ??= new(this);
_EventDispatcher.SetEvents(events, true);
return;
}
}
_EventDispatcher = null;
}
/************************************************************************************************************************/
/// <summary>Should events be raised on a state which is currently fading out?</summary>
/// <remarks>
/// Default <c>false</c>.
/// <para></para>
/// <strong>Documentation:</strong>
/// <see href="https://kybernetik.com.au/animancer/docs/manual/events/animancer">
/// Animancer Events</see>
/// </remarks>
public static bool RaiseEventsDuringFadeOut { get; set; }
/// <summary>Should this state check for events to invoke?</summary>
private bool ShouldRaiseEvents
=> TargetWeight > 0
|| RaiseEventsDuringFadeOut;
/************************************************************************************************************************/
/// <summary>
/// Checks if any events should be invoked based on the current time of this state.
/// </summary>
protected internal virtual void UpdateEvents()
=> _EventDispatcher?.UpdateEvents(ShouldRaiseEvents);
/// <summary>
/// Checks if any events should be invoked on the `parent` and its children recursively.
/// </summary>
public static void UpdateEventsRecursive(AnimancerState parent)
=> UpdateEventsRecursive(
parent,
parent.ShouldRaiseEvents);
/// <summary>
/// Checks if any events should be invoked on the `parent` and its children recursively.
/// </summary>
public static void UpdateEventsRecursive(AnimancerState parent, bool raiseEvents)
{
parent._EventDispatcher?.UpdateEvents(raiseEvents);
for (int i = parent.ChildCount - 1; i >= 0; i--)
{
var child = parent.GetChild(i);
UpdateEventsRecursive(child, raiseEvents && child.Weight > 0);
}
}
/************************************************************************************************************************/
/// <summary>
/// Sets the <see cref="NormalizedTime"/> to the <see cref="NormalizedEndTime"/>
/// and invokes any remaining <see cref="AnimancerEvent"/>s.
/// </summary>
public void FinishImmediately()
{
if (_EventDispatcher != null)
_EventDispatcher.FinishImmediately();
else
NormalizedTime = AnimancerEvent.Sequence.GetDefaultNormalizedEndTime(EffectiveSpeed);
}
/************************************************************************************************************************/
#if UNITY_ASSERTIONS
/************************************************************************************************************************/
/// <summary>[Assert-Only]
/// Returns <c>null</c> if Animancer Events will work properly on this type of state,
/// or a message explaining why they might not work.
/// </summary>
protected internal virtual string UnsupportedEventsMessage
=> null;
/************************************************************************************************************************/
/// <summary>[Assert-Only] An optional reference to the object that owns this state.</summary>
public object Owner { get; private set; }
/************************************************************************************************************************/
#endif
/************************************************************************************************************************/
/// <summary>[Assert-Conditional]
/// Sets the <see cref="Owner"/> and asserts that it wasn't already set to a different object.
/// </summary>
/// <remarks>This helps detect if multiple scripts attempt to manage the same state.</remarks>
[System.Diagnostics.Conditional(Strings.Assertions)]
public void AssertOwnership(object owner)
{
#if UNITY_ASSERTIONS
if (Owner == owner)
return;
if (Owner != null)
{
Debug.LogError(
$"Multiple objects have asserted ownership over the state '{ToString()}':" +
$"\n• Old Owner: {AnimancerUtilities.ToStringOrNull(Owner)}" +
$"\n• New Owner: {AnimancerUtilities.ToStringOrNull(owner)}" +
$"\n• State: {GetPath()}" +
$"\n• Graph: {Graph?.GetDescription("\n ")}",
Graph?.Component as Object);
}
Owner = owner;
#endif
}
/************************************************************************************************************************/
}
}

View File

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

View File

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

View File

@@ -0,0 +1,64 @@
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
using UnityEngine;
namespace Animancer
{
/// https://kybernetik.com.au/animancer/api/Animancer/AnimancerEvent
partial struct AnimancerEvent
{
/// <summary>A non-generic interface for <see cref="Parameter{T}"/>.</summary>
/// https://kybernetik.com.au/animancer/api/Animancer/IParameter
public interface IParameter
{
/************************************************************************************************************************/
/// <summary>The parameter value.</summary>
object Value { get; set; }
/************************************************************************************************************************/
}
/// <summary>
/// Base class for <see cref="IInvokable"/>s which assign the <see cref="CurrentParameter"/>.
/// </summary>
/// <remarks>
/// Inherit from <see cref="ParameterBoxed{T}"/>
/// instead of this if <typeparamref name="T"/> is a value type to avoid repeated boxing costs.
/// </remarks>
/// https://kybernetik.com.au/animancer/api/Animancer/Parameter_1
public abstract class Parameter<T> :
IParameter,
IInvokable
{
/************************************************************************************************************************/
[SerializeField]
private T _Value;
/// <summary>[<see cref="SerializeField"/>] The serialized <typeparamref name="T"/>.</summary>
public virtual T Value
{
get => _Value;
set => _Value = value;
}
/// <inheritdoc/>
object IParameter.Value
{
get => _Value;
set => _Value = (T)value;
}
/// <inheritdoc/>
public virtual void Invoke()
{
CurrentParameter = _Value;
Current.InvokeBoundCallback();
}
/************************************************************************************************************************/
}
}
}

View File

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

View File

@@ -0,0 +1,68 @@
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
using UnityEngine;
namespace Animancer
{
/// https://kybernetik.com.au/animancer/api/Animancer/AnimancerEvent
partial struct AnimancerEvent
{
/// <summary>
/// An <see cref="Parameter{T}"/>s which internally boxes value types
/// to avoid re-boxing them every <see cref="Invoke"/>.
/// </summary>
/// https://kybernetik.com.au/animancer/api/Animancer/ParameterBoxed_1
public abstract class ParameterBoxed<T> : Parameter<T>,
IParameter
#if UNITY_EDITOR
, ISerializationCallbackReceiver
#endif
where T : struct
{
/************************************************************************************************************************/
private object _Boxed;
/// <inheritdoc/>
public override T Value
{
get => base.Value;
set
{
base.Value = value;
_Boxed = null;
}
}
/// <inheritdoc/>
object IParameter.Value
{
get => Value;
set => Value = (T)value;
}
/// <inheritdoc/>
public override void Invoke()
{
CurrentParameter = _Boxed ??= base.Value;
Current.InvokeBoundCallback();
}
/************************************************************************************************************************/
#if UNITY_EDITOR
/************************************************************************************************************************/
/// <inheritdoc/>
void ISerializationCallbackReceiver.OnBeforeSerialize() { }
/// <inheritdoc/>
void ISerializationCallbackReceiver.OnAfterDeserialize()
=> _Boxed = null;
/************************************************************************************************************************/
#endif
/************************************************************************************************************************/
}
}
}

View File

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

View File

@@ -0,0 +1,56 @@
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
using System;
namespace Animancer
{
/// https://kybernetik.com.au/animancer/api/Animancer/AnimancerEvent
partial struct AnimancerEvent
{
/************************************************************************************************************************/
// Reference Types.
/************************************************************************************************************************/
/// <summary>An <see cref="Parameter{T}"/> for <see cref="UnityEngine.Object"/>.</summary>
/// https://kybernetik.com.au/animancer/api/Animancer/ParameterObject
[Serializable]
public class ParameterObject : Parameter<UnityEngine.Object> { }
/// <summary>An <see cref="Parameter{T}"/> for <see cref="string"/>.</summary>
/// https://kybernetik.com.au/animancer/api/Animancer/ParameterString
[Serializable]
public class ParameterString : Parameter<string> { }
/************************************************************************************************************************/
// Value Types.
/************************************************************************************************************************/
/// <summary>An <see cref="Parameter{T}"/> for <see cref="bool"/>.</summary>
/// https://kybernetik.com.au/animancer/api/Animancer/ParameterBool
[Serializable]
public class ParameterBool : ParameterBoxed<bool> { }
/// <summary>An <see cref="Parameter{T}"/> for <see cref="double"/>.</summary>
/// https://kybernetik.com.au/animancer/api/Animancer/ParameterDouble
[Serializable]
public class ParameterDouble : ParameterBoxed<double> { }
/// <summary>An <see cref="Parameter{T}"/> for <see cref="float"/>.</summary>
/// https://kybernetik.com.au/animancer/api/Animancer/ParameterFloat
[Serializable]
public class ParameterFloat : ParameterBoxed<float> { }
/// <summary>An <see cref="Parameter{T}"/> for <see cref="int"/>.</summary>
/// https://kybernetik.com.au/animancer/api/Animancer/ParameterInt
[Serializable]
public class ParameterInt : ParameterBoxed<int> { }
/// <summary>An <see cref="Parameter{T}"/> for <see cref="long"/>.</summary>
/// https://kybernetik.com.au/animancer/api/Animancer/ParameterLong
[Serializable]
public class ParameterLong : ParameterBoxed<long> { }
/************************************************************************************************************************/
}
}

View File

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

View File

@@ -0,0 +1,16 @@
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
#if ULT_EVENTS
using System;
namespace Animancer
{
/// <summary>An <see cref="UltEvents.UltEvent"/> which implements <see cref="IInvokable"/>.</summary>
/// https://kybernetik.com.au/animancer/api/Animancer/UltEvent
[Serializable]
public class UltEvent : UltEvents.UltEvent, IInvokable { }
}
#endif

View File

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

View File

@@ -0,0 +1,12 @@
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
using System;
namespace Animancer
{
/// <summary>A <see cref="UnityEngine.Events.UnityEvent"/> which implements <see cref="IInvokable"/>.</summary>
/// https://kybernetik.com.au/animancer/api/Animancer/UnityEvent
[Serializable]
public class UnityEvent : UnityEngine.Events.UnityEvent, IInvokable { }
}

View File

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

View File

@@ -0,0 +1,334 @@
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
using System;
using System.Collections;
using System.Collections.Generic;
namespace Animancer
{
/// <summary>A dictionary which maps event names to callbacks.</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/NamedEventDictionary
public class NamedEventDictionary : IDictionary<StringReference, Action>
{
/************************************************************************************************************************/
private readonly Dictionary<StringReference, Action>
Dictionary = new();
/************************************************************************************************************************/
/// <summary>The number of items in this dictionary.</summary>
public int Count
=> Dictionary.Count;
/************************************************************************************************************************/
#region Access
/************************************************************************************************************************/
/// <summary>Accesses a callback in this dictionary.</summary>
public Action this[StringReference name]
{
get => Dictionary[name];
set
{
AssertNotEndEvent(name);
Dictionary[name] = value;
}
}
/************************************************************************************************************************/
/// <summary>Returns the callback registered using the `name`.</summary>
/// <remarks>Returns <c>null</c> if nothing was registered.</remarks>
public Action Get(StringReference name)
=> Dictionary.Get(name);
/// <summary>Registers the callback using the `name`, replacing anything previously registered.</summary>
public void Set(StringReference name, Action callback)
{
AssertNotEndEvent(name);
Dictionary[name] = callback;
}
/************************************************************************************************************************/
/// <summary>Are any callbacks registered for the `name`?</summary>
/// <remarks>To get the registered callbacks at the same time, use <see cref="TryGetValue"/> instead.</remarks>
public bool ContainsKey(StringReference name)
=> Dictionary.ContainsKey(name);
/************************************************************************************************************************/
/// <summary>Tries to get the `callback` registered with the `name` and returns <c>true</c> if successful.</summary>
public bool TryGetValue(StringReference name, out Action callback)
=> Dictionary.TryGetValue(name, out callback);
/************************************************************************************************************************/
#endregion
/************************************************************************************************************************/
#region Add
/************************************************************************************************************************/
/// <summary>Adds the `callback` to any existing ones registered with the `name`.</summary>
/// <remarks>
/// If you want an exception to be thrown if something is already registered with the `name`,
/// use <see cref="AddNew(StringReference, Action)"/> instead.
/// </remarks>
public void AddTo(StringReference name, Action callback)
{
AssertNotEndEvent(name);
if (Dictionary.TryGetValue(name, out var existing))
callback = existing + callback;
Dictionary[name] = callback;
}
/************************************************************************************************************************/
/// <summary>
/// Registers the `callback` with the `name` but throws an <see cref="ArgumentException"/>
/// if something was already registered with the same `name`.
/// </summary>
/// <remarks>
/// This matches the standard <see cref="Dictionary{TKey, TValue}.Add(TKey, TValue)"/> behaviour,
/// unlike <see cref="AddTo(StringReference, Action)"/>.
/// </remarks>
public void AddNew(StringReference name, Action callback)
{
AssertNotEndEvent(name);
Dictionary.Add(name, callback);
}
void IDictionary<StringReference, Action>.Add(StringReference name, Action callback)
=> AddNew(name, callback);
/************************************************************************************************************************/
/// <summary>
/// Adds the `callback` to any existing ones registered with the `name`.
/// <para></para>
/// It will be invoked using <see cref="AnimancerEvent.GetCurrentParameter{T}"/> to get its parameter.
/// </summary>
/// <remarks>
/// If you want an exception to be thrown if something is already registered with the `name`,
/// use <see cref="AddNew{T}(StringReference, Action{T})"/> instead.
/// <para></para>
/// If <typeparamref name="T"/> is <see cref="string"/>,
/// consider using <see cref="AddTo(StringReference, Action{string})"/> instead of this overload.
/// <para></para>
/// If you want to later remove the `callback`,
/// you need to store and remove the returned <see cref="Action"/>.
/// </remarks>
public Action AddTo<T>(StringReference name, Action<T> callback)
{
AssertNotEndEvent(name);
var parametized = AnimancerEvent.Parametize(callback);
if (Dictionary.TryGetValue(name, out var existing))
parametized = existing + parametized;
Dictionary[name] = parametized;
return parametized;
}
/// <summary>
/// Registers the `callback` with the `name` but throws an <see cref="ArgumentException"/>
/// if something was already registered with the same `name`.
/// <para></para>
/// It will be invoked using <see cref="AnimancerEvent.GetCurrentParameter{T}"/> to get its parameter.
/// </summary>
/// <remarks>
/// This matches the standard <see cref="Dictionary{TKey, TValue}.Add(TKey, TValue)"/> behaviour,
/// unlike <see cref="AddTo{T}(StringReference, Action{T})"/>.
/// If <typeparamref name="T"/> is <see cref="string"/>,
/// consider using <see cref="AddTo(StringReference, Action{string})"/> instead of this overload.
/// <para></para>
/// If you want to later remove the `callback`,
/// you need to store and remove the returned <see cref="Action"/>.
/// </remarks>
public Action AddNew<T>(StringReference name, Action<T> callback)
{
AssertNotEndEvent(name);
var parametized = AnimancerEvent.Parametize(callback);
Dictionary.Add(name, parametized);
return parametized;
}
/************************************************************************************************************************/
/// <summary>
/// Adds the `callback` to any existing ones registered with the `name`.
/// <para></para>
/// It will be invoked using <see cref="object.ToString"/> on the
/// <see cref="AnimancerEvent.CurrentParameter"/>.
/// </summary>
/// <remarks>
/// If you want an exception to be thrown if something is already registered with the `name`,
/// use <see cref="AddNew{T}(StringReference, Action{T})"/> instead.
/// <para></para>
/// If you want to later remove the `callback`,
/// you need to store and remove the returned <see cref="Action"/>.
/// </remarks>
public Action AddTo(StringReference name, Action<string> callback)
{
AssertNotEndEvent(name);
var parametized = AnimancerEvent.Parametize(callback);
if (Dictionary.TryGetValue(name, out var existing))
parametized = existing + parametized;
Dictionary[name] = parametized;
return parametized;
}
/// <summary>
/// Registers the `callback` with the `name` but throws an <see cref="ArgumentException"/>
/// if something was already registered with the same `name`.
/// <para></para>
/// It will be invoked using <see cref="object.ToString"/> on the
/// <see cref="AnimancerEvent.CurrentParameter"/>.
/// </summary>
/// <remarks>
/// This matches the standard <see cref="Dictionary{TKey, TValue}.Add(TKey, TValue)"/>
/// behaviour, unlike <see cref="AddTo(StringReference, Action{string})"/>.
/// <para></para>
/// If you want to later remove the `callback`,
/// you need to store and remove the returned <see cref="Action"/>.
/// </remarks>
public Action AddNew(StringReference name, Action<string> callback)
{
AssertNotEndEvent(name);
var parametized = AnimancerEvent.Parametize(callback);
Dictionary.Add(name, parametized);
return parametized;
}
/************************************************************************************************************************/
/// <summary>[Assert-Conditional]
/// Throws an <see cref="ArgumentException"/> if the `name` is the <see cref="AnimancerEvent.EndEventName"/>.
/// </summary>
/// <remarks>
/// In order to minimise the performance cost of End Events when there isn't one,
/// the <see cref="AnimancerEvent.Dispatcher"/> won't even check the end time
/// when there is no <see cref="AnimancerEvent.Sequence.OnEnd"/> callback.
/// <para></para>
/// That means if a callback was bound to the <see cref="AnimancerEvent.EndEventName"/>
/// it would be triggered by any state with an <see cref="AnimancerEvent.Sequence.OnEnd"/>
/// callback, but not by states without one. That would be very counterintuitive so it isn't allowed.
/// </remarks>
/// <exception cref="ArgumentException"/>
[System.Diagnostics.Conditional(Strings.Assertions)]
public static void AssertNotEndEvent(StringReference name)
{
if (name == AnimancerEvent.EndEventName)
throw new ArgumentException(
$"Binding event callbacks to the " +
$"{nameof(AnimancerEvent)}.{nameof(AnimancerEvent.EndEventName)}" +
$" is not supported for performance optimization reasons. See the documentation of" +
$" {nameof(NamedEventDictionary)}.{nameof(AssertNotEndEvent)} for more details.",
nameof(name));
}
/************************************************************************************************************************/
#endregion
/************************************************************************************************************************/
#region Remove
/************************************************************************************************************************/
/// <summary>Removes all callbacks registered with the `name`.</summary>
public bool Remove(StringReference name)
=> Dictionary.Remove(name);
/// <summary>Removes a specific `callback` registered with the `name`.</summary>
public bool Remove(StringReference name, Action callback)
{
if (!Dictionary.TryGetValue(name, out var callbacks))
return false;
if (callbacks == callback)
Dictionary.Remove(name);
else
Dictionary[name] = callbacks - callback;
return true;
}
/************************************************************************************************************************/
/// <summary>Removes everything from this dictionary.</summary>
public void Clear()
=> Dictionary.Clear();
/************************************************************************************************************************/
#endregion
/************************************************************************************************************************/
#region Enumeration
/************************************************************************************************************************/
/// <summary>Returns an enumerator to go through every item in this dictionary.</summary>
public Dictionary<StringReference, Action>.Enumerator GetEnumerator()
=> Dictionary.GetEnumerator();
IEnumerator<KeyValuePair<StringReference, Action>>
IEnumerable<KeyValuePair<StringReference, Action>>.GetEnumerator()
=> Dictionary.GetEnumerator();
IEnumerator IEnumerable.GetEnumerator()
=> Dictionary.GetEnumerator();
/************************************************************************************************************************/
/// <summary>The names in this dictionary.</summary>
public Dictionary<StringReference, Action>.KeyCollection Keys
=> Dictionary.Keys;
/// <summary>The values in this dictionary.</summary>
public Dictionary<StringReference, Action>.ValueCollection Values
=> Dictionary.Values;
ICollection<StringReference> IDictionary<StringReference, Action>.Keys
=> Dictionary.Keys;
ICollection<Action> IDictionary<StringReference, Action>.Values
=> Dictionary.Values;
/************************************************************************************************************************/
#endregion
/************************************************************************************************************************/
#region Explicit Dictionary Wrappers
/************************************************************************************************************************/
void ICollection<KeyValuePair<StringReference, Action>>
.Add(KeyValuePair<StringReference, Action> item)
=> AddTo(item.Key, item.Value);
bool ICollection<KeyValuePair<StringReference, Action>>
.Contains(KeyValuePair<StringReference, Action> item)
=> ((ICollection<KeyValuePair<StringReference, Action>>)Dictionary).Contains(item);
void ICollection<KeyValuePair<StringReference, Action>>
.CopyTo(KeyValuePair<StringReference, Action>[] array,
int arrayIndex)
=> ((ICollection<KeyValuePair<StringReference, Action>>)Dictionary).CopyTo(array, arrayIndex);
bool ICollection<KeyValuePair<StringReference, Action>>
.Remove(KeyValuePair<StringReference, Action> item)
=> Dictionary.Remove(item.Key);
bool ICollection<KeyValuePair<StringReference, Action>>.IsReadOnly
=> false;
/************************************************************************************************************************/
#endregion
/************************************************************************************************************************/
}
}

View File

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

View File

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

View File

@@ -0,0 +1,733 @@
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
#if UNITY_ASSERTIONS
//#define ANIMANCER_ASSERT_FADE_GRAPH
#endif
using System;
using System.Collections.Generic;
using System.Text;
using UnityEngine;
using UnityEngine.Playables;
using Object = UnityEngine.Object;
namespace Animancer
{
/// <summary>A group of <see cref="AnimancerNode"/>s which are cross-fading.</summary>
///
/// <remarks>
/// <strong>Documentation:</strong>
/// <see href="https://kybernetik.com.au/animancer/docs/manual/blending/fading/custom">
/// Custom Easing</see>
/// </remarks>
///
/// https://kybernetik.com.au/animancer/api/Animancer/FadeGroup
///
public partial class FadeGroup : Updatable,
ICloneable<FadeGroup>,
ICopyable<FadeGroup>,
IHasDescription
{
/************************************************************************************************************************/
#region Fields and Properties
/************************************************************************************************************************/
// Parameters.
/************************************************************************************************************************/
/// <summary>The 0-1 progress of this fade.</summary>
public float NormalizedTime { get; set; }
/// <summary>The <see cref="AnimancerNode.Weight"/> which the <see cref="FadeIn"/> is moving towards.</summary>
public float TargetWeight { get; set; }
/// <summary>The speed at which the <see cref="NormalizedTime"/> increases.</summary>
public float NormalizedFadeSpeed { get; set; }
/// <summary>The speed at which the <see cref="AnimancerNode.Weight"/>s change.</summary>
public float FadeSpeed
{
get => FadeDistance * NormalizedFadeSpeed;
set => NormalizedFadeSpeed = value / FadeDistance;
}
/// <summary>The distance from the starting weight to the <see cref="TargetWeight"/>.</summary>
public float FadeDistance
=> Math.Abs(TargetWeight - FadeIn.StartingWeight);
/// <summary>The total amount of time this fade will take to complete (in seconds).</summary>
public float FadeDuration
{
get => NormalizedFadeSpeed != 0
? 1 / NormalizedFadeSpeed
: float.PositiveInfinity;
set => NormalizedFadeSpeed = value != 0
? 1 / value
: float.PositiveInfinity;
}
/// <summary>The remaining amount of time this fade will take to complete (in seconds).</summary>
public float RemainingFadeDuration
{
get => NormalizedFadeSpeed != 0
? (1 - NormalizedTime) / NormalizedFadeSpeed
: float.PositiveInfinity;
set => NormalizedFadeSpeed = value != 0
? (1 - NormalizedTime) / value
: float.PositiveInfinity;
}
/************************************************************************************************************************/
// Parent.
/************************************************************************************************************************/
/// <summary>The <see cref="AnimancerNodeBase.Graph"/>.</summary>
public AnimancerGraph Graph { get; private set; }
/// <summary>The <see cref="AnimancerNodeBase.Graph"/>.</summary>
public AnimancerNodeBase Parent { get; private set; }
/// <summary>The <see cref="AnimancerNodeBase.Playable"/> of the <see cref="Parent"/>.</summary>
public Playable ParentPlayable { get; private set; }
/// <summary>Should the fading nodes always be connected to the <see cref="ParentPlayable"/>?</summary>
public bool KeepChildrenConnected { get; private set; }
/************************************************************************************************************************/
// Nodes.
/************************************************************************************************************************/
/// <summary>The node which is fading towards the <see cref="TargetWeight"/>.</summary>
public NodeWeight FadeIn { get; private set; }
internal readonly List<NodeWeight> FadeOutInternal = new();
/// <summary>The nodes which are fading out.</summary>
public IReadOnlyList<NodeWeight> FadeOut => FadeOutInternal;
/************************************************************************************************************************/
// Custom Fade.
/************************************************************************************************************************/
private Func<float, float> _Easing;
/// <summary>[Pro-Only] An optional function for modifying the fade curve.</summary>
/// <remarks>
/// The <see cref="NormalizedTime"/> is passed in and the return value is multiplied by the
/// <see cref="TargetWeight"/> to set the <see cref="AnimancerNode.Weight"/> of the <see cref="FadeIn"/>.
/// <para></para>
/// <see cref="Animancer.Easing"/> has various common functions that could be used here.
/// <para></para>
/// Note that the <see cref="AnimancerNode.FadeGroup"/> may be <c>null</c>
/// right after playing something if it was already playing, so
/// <see cref="FadeGroupExtensions.SetEasing(FadeGroup, Easing.Function)"/>
/// <see cref="FadeGroupExtensions.SetEasing(FadeGroup, Func{float, float})"/>
/// can be used to avoid needing to null-check it.
/// <para></para>
/// <em>Animancer Lite ignores this property in runtime builds.</em>
/// </remarks>
public Func<float, float> Easing
{
get => _Easing;
set
{
_Easing = value;
AssertNormalizedBounds(value, nameof(Easing));
}
}
/************************************************************************************************************************/
#endregion
/************************************************************************************************************************/
#region Initialization
/************************************************************************************************************************/
/// <summary>Assigns the target nodes that will be faded.</summary>
public void SetNodes(
AnimancerNode parent,
AnimancerNode fadeIn,
IReadOnlyList<AnimancerNode> fadeOut,
bool keepChildrenConnected)
{
Parent = parent;
Graph = parent.Graph;
ParentPlayable = parent.Playable;
KeepChildrenConnected = keepChildrenConnected;
FadeIn = new(fadeIn);
if (fadeIn.FadeGroup != this)
fadeIn.FadeGroup = this;
var count = fadeOut.Count;
for (int i = 0; i < count; i++)
{
var node = fadeOut[i];
if (node != fadeIn)
{
FadeOutInternal.Add(new(node));
if (node.FadeGroup != this)
node.FadeGroup = this;
}
}
}
/************************************************************************************************************************/
/// <summary>Assigns the <see cref="FadeIn"/> with no <see cref="FadeOut"/>.</summary>
public void SetFadeIn(AnimancerNode fadeIn)
{
Parent = fadeIn.Parent;
if (Parent != null)
{
Graph = fadeIn.Graph;
ParentPlayable = Parent.Playable;
KeepChildrenConnected = Parent.KeepChildrenConnected;
}
FadeIn = new(fadeIn);
fadeIn.FadeGroup = this;
}
/************************************************************************************************************************/
/// <summary>Adds a node to the <see cref="FadeOut"/> list.</summary>
public void AddFadeOut(AnimancerNode fadeOut)
{
FadeOutInternal.Add(new(fadeOut));
fadeOut.FadeGroup = this;
}
/************************************************************************************************************************/
/// <summary>Sets the starting values and registers this fade to be updated.</summary>
public void StartFade(
float targetWeight,
float normalizedFadeSpeed)
{
NormalizedTime = 0;
TargetWeight = targetWeight;
NormalizedFadeSpeed = normalizedFadeSpeed;
StartFade();
}
/// <summary>Registers this fade to be updated.</summary>
public void StartFade()
{
Graph?.RequirePreUpdate(this);
FadeIn.Node?.OnStartFade();
for (int i = FadeOutInternal.Count - 1; i >= 0; i--)
FadeOutInternal[i].Node.OnStartFade();
}
/************************************************************************************************************************/
#endregion
/************************************************************************************************************************/
#region Queries
/************************************************************************************************************************/
/// <summary>Should this fade continue?</summary>
public bool IsValid
=> NormalizedFadeSpeed > 0;
/************************************************************************************************************************/
/// <summary>Does this fade affect the `node`?</summary>
public bool Contains(AnimancerNode node)
{
if (FadeIn.Node == node)
return true;
for (int i = 0; i < FadeOutInternal.Count; i++)
if (FadeOutInternal[i].Node == node)
return true;
return false;
}
/************************************************************************************************************************/
/// <summary>
/// Returns the <see cref="TargetWeight"/> if the `node` is the <see cref="FadeIn"/>.
/// Otherwise, returns 0.
/// </summary>
public float GetTargetWeight(AnimancerNode node)
{
return FadeIn.Node == node
? TargetWeight
: 0;
}
/************************************************************************************************************************/
#endregion
/************************************************************************************************************************/
#region Methods
/************************************************************************************************************************/
/// <inheritdoc/>
public override void Update()
{
if (!IsValid)
{
Cancel();
return;
}
AssertGraph();
NormalizedTime += Math.Abs(AnimancerGraph.DeltaTime * Parent.EffectiveSpeed * NormalizedFadeSpeed);
if (NormalizedTime < 1)// Fade.
{
ApplyWeights();
}
else// End.
{
Finish();
}
}
/************************************************************************************************************************/
/// <summary>Immediately finishes this fade.</summary>
public void Finish()
{
NormalizedTime = 1;
if (KeepChildrenConnected)
{
ApplyWeights(1);
for (int i = FadeOutInternal.Count - 1; i >= 0; i--)
FadeOutInternal[i].Node.StopWithoutWeight();
if (TargetWeight == 0)
FadeIn.Node?.StopWithoutWeight();
}
else// Disconnect all faded out nodes and only apply the faded in weight.
{
for (int i = FadeOutInternal.Count - 1; i >= 0; i--)
StopAndDisconnect(FadeOutInternal[i].Node);
FadeOutInternal.Clear();
if (FadeIn.Node != null)
{
if (TargetWeight > 0)
FadeIn.Node.SetWeight(TargetWeight);
else
StopAndDisconnect(FadeIn.Node);
}
}
Cancel();
}
/************************************************************************************************************************/
/// <summary>
/// Recalculates the node weights based on the <see cref="NormalizedTime"/>.
/// </summary>
public void ApplyWeights()
{
if (NormalizedTime < 1)// Fade.
{
var progress = NormalizedTime;
if (_Easing != null)
progress = _Easing(progress);
ApplyWeights(progress);
}
else// End.
{
Finish();
}
}
private void ApplyWeights(float progress)
{
// Move FadeIn towards target (usually 1 or 0).
FadeIn.Node?.SetWeight(Mathf.LerpUnclamped(FadeIn.StartingWeight, TargetWeight, progress));
// Move FadeOut towards 0.
progress = 1 - progress;
for (int i = FadeOutInternal.Count - 1; i >= 0; i--)
{
var node = FadeOutInternal[i];
node.Node.SetWeight(node.StartingWeight * progress);
}
}
private void StopAndDisconnect(AnimancerNode node)
{
// Don't InternalClearFade because it's virtual.
node._FadeGroup = null;
node.Stop();
}
/************************************************************************************************************************/
private void Release()
{
NormalizedFadeSpeed = 0;
_Easing = null;
Graph = null;
Parent = null;
if (FadeIn.Node != null)
{
FadeIn.Node.InternalClearFade();
FadeIn = default;
}
for (int i = FadeOutInternal.Count - 1; i >= 0; i--)
FadeOutInternal[i].Node.InternalClearFade();
FadeOutInternal.Clear();
Pool.Instance.Release(this);
}
/************************************************************************************************************************/
/// <summary>Interrupts this fade and releases it to the <see cref="ObjectPool{T}"/>.</summary>
public void Cancel()
{
Graph?.CancelPreUpdate(this);
Release();
}
/************************************************************************************************************************/
/// <summary>Removes the `node` from this <see cref="FadeGroup"/> and returns true if successful.</summary>
public bool Remove(AnimancerNode node)
{
if (FadeIn.Node == node)
{
FadeIn = new(null, FadeIn.StartingWeight);
if (FadeOutInternal.Count == 0)
Cancel();
node.InternalClearFade();
return true;
}
for (int i = FadeOutInternal.Count - 1; i >= 0; i--)
{
if (FadeOutInternal[i].Node == node)
{
FadeOutInternal.RemoveAt(i);
if (FadeIn.Node == null && FadeOutInternal.Count == 0)
Cancel();
node.InternalClearFade();
return true;
}
}
return false;
}
/************************************************************************************************************************/
/// <inheritdoc/>
public virtual void AppendDescription(StringBuilder text, string separator = "\n")
{
text.Append(GetType().FullName);
if (!IsValid)
{
text.Append("(Cancelled)");
return;
}
if (!separator.StartsWithNewLine())
separator = "\n" + separator;
text.AppendField(separator, nameof(NormalizedTime), NormalizedTime);
text.AppendField(separator, nameof(NormalizedFadeSpeed), NormalizedFadeSpeed);
text.AppendField(separator, nameof(Easing), _Easing?.ToStringDetailed());
text.Append(separator).Append($"{nameof(FadeIn)}: ");
FadeIn.AppendDescription(text, TargetWeight);
text.AppendField(separator, nameof(FadeOut), FadeOutInternal.Count);
for (int i = 0; i < FadeOutInternal.Count; i++)
{
text.Append(separator)
.Append(Strings.Indent);
FadeOutInternal[i].AppendDescription(text, 0);
}
}
/************************************************************************************************************************/
/// <summary>[Assert-Conditional] Checks <see cref="OptionalWarning.FadeEasingBounds"/>.</summary>
[System.Diagnostics.Conditional(Strings.Assertions)]
public static void AssertNormalizedBounds(Func<float, float> easing, string name = "function")
{
#if UNITY_ASSERTIONS
if (easing != null && OptionalWarning.FadeEasingBounds.IsEnabled())
{
if (easing(0) != 0)
OptionalWarning.FadeEasingBounds.Log(name + "(0) != 0.");
if (easing(1) != 1)
OptionalWarning.FadeEasingBounds.Log(name + "(1) != 1.");
}
#endif
}
/************************************************************************************************************************/
#endregion
/************************************************************************************************************************/
#region Cloning
/************************************************************************************************************************/
/// <inheritdoc/>
public virtual FadeGroup Clone(CloneContext context)
{
if (!IsValid)
return null;
var clone = new FadeGroup();
clone.CopyFrom(this, context);
return clone;
}
/************************************************************************************************************************/
/// <inheritdoc/>
public virtual void CopyFrom(FadeGroup copyFrom, CloneContext context)
{
CopyNodesFrom(copyFrom, context);
var node = FadeIn.Node;
if (node == null)
{
if (FadeOut.Count == 0)
return;
node = FadeOut[0].Node;
}
ChangeParent(node);
CopyDetailsFrom(copyFrom);
}
/************************************************************************************************************************/
private void CopyNodesFrom(FadeGroup copyFrom, CloneContext context)
{
FadeIn = new(copyFrom.FadeIn, context);
FadeIn.Node.FadeGroup = this;
FadeOutInternal.Clear();
var count = copyFrom.FadeOutInternal.Count;
for (int i = 0; i < count; i++)
{
var nodeWeight = new NodeWeight(copyFrom.FadeOutInternal[i], context);
if (nodeWeight.Node != null)
{
FadeOutInternal.Add(nodeWeight);
nodeWeight.Node.FadeGroup = this;
}
}
}
/************************************************************************************************************************/
internal void ChangeParent(AnimancerNode child)
{
var parent = child.Parent;
if (Parent == parent)
return;
Parent = parent;
if (Parent != null)
{
ParentPlayable = Parent.Playable;
KeepChildrenConnected = Parent.KeepChildrenConnected;
ChangeGraph(child.Graph);
_AssertGraphNextFrame = true;
}
}
/************************************************************************************************************************/
internal void ChangeGraph(AnimancerGraph graph)
{
if (Graph == graph)
return;
Graph?.CancelPreUpdate(this);
Graph = graph;
Graph?.RequirePreUpdate(this);
_AssertGraphNextFrame = true;
}
/************************************************************************************************************************/
private bool _AssertGraphNextFrame;
private void AssertGraph()
{
if (!_AssertGraphNextFrame)
return;
_AssertGraphNextFrame = false;
if (FadeIn.Node != null && !AssertNode(FadeIn.Node))
return;
for (int i = 0; i < FadeOutInternal.Count; i++)
if (!AssertNode(FadeOutInternal[i].Node))
return;
}
private bool AssertNode(AnimancerNode node)
{
string propertyName;
string nodeValue, myValue;
if (node.Graph == Graph)
{
if (node.Parent == Parent)
return true;
propertyName = nameof(node.Parent);
nodeValue = AnimancerUtilities.ToStringOrNull(node.Parent);
myValue = AnimancerUtilities.ToStringOrNull(Parent);
}
else
{
propertyName = nameof(node.Graph);
nodeValue = AnimancerUtilities.ToStringOrNull(node.Graph);
myValue = AnimancerUtilities.ToStringOrNull(Graph);
}
var graph = Graph ?? node.Graph;
Debug.LogWarning(
$"{nameof(AnimancerNode)}.{propertyName} doesn't match {nameof(FadeGroup)}.{propertyName}." +
$"\n• Node: {node.GetPath()}" +
$"\n• Node.{propertyName}: {nodeValue}" +
$"\n• This.{propertyName}: {myValue}" +
$"\n• Graph: {graph?.GetDescription("\n ")}");
return false;
}
/************************************************************************************************************************/
private void CopyDetailsFrom(FadeGroup copyFrom)
{
NormalizedTime = copyFrom.NormalizedTime;
NormalizedFadeSpeed = copyFrom.NormalizedFadeSpeed;
TargetWeight = copyFrom.TargetWeight;
_Easing = copyFrom._Easing;
}
/************************************************************************************************************************/
/// <summary>Creates a clone of this <see cref="FadeGroup"/> for a single target node (`copyTo`).</summary>
public FadeGroup CloneForSingleTarget(AnimancerNode copyFrom, AnimancerNode copyTo)
{
if (!IsValid)
return null;
var clone = Pool.Instance.Acquire();
if (copyFrom == FadeIn.Node)
{
clone.FadeIn = new(copyTo, FadeIn.StartingWeight);
}
else
{
for (int i = 0; i < FadeOutInternal.Count; i++)
{
var fadeOut = FadeOutInternal[i];
if (fadeOut.Node == copyFrom)
{
clone.FadeOutInternal.Add(new(copyTo, fadeOut.StartingWeight));
goto CopyDetails;
}
}
return null;
}
CopyDetails:
clone.ChangeParent(copyTo);
clone.CopyDetailsFrom(this);
clone.StartFade();
return clone;
}
/************************************************************************************************************************/
#endregion
/************************************************************************************************************************/
#region Pooling
/************************************************************************************************************************/
/// <summary>An <see cref="ObjectPool{T}"/> for <see cref="FadeGroup"/>.</summary>
/// https://kybernetik.com.au/animancer/api/Animancer/Pool
public class Pool : ObjectPool<FadeGroup>
{
/************************************************************************************************************************/
/// <summary>Singleton.</summary>
public static Pool Instance = new();
/************************************************************************************************************************/
/// <inheritdoc/>
protected override FadeGroup New()
=> new();
/************************************************************************************************************************/
#if UNITY_ASSERTIONS
/************************************************************************************************************************/
/// <inheritdoc/>
public override FadeGroup Acquire()
{
var fade = base.Acquire();
Debug.Assert(fade.FadeIn.Node == null, $"{nameof(fade.FadeIn)} is not null");
Debug.Assert(fade.FadeOutInternal.Count == 0, $"{nameof(fade.FadeOutInternal)} is not empty");
Debug.Assert(fade.Easing == null, $"{nameof(fade.Easing)} is not null");
return fade;
}
/// <inheritdoc/>
public override void Release(FadeGroup item)
{
Debug.Assert(((IUpdatable)item).UpdatableIndex < 0,
$"Releasing {nameof(FadeGroup)} which is still registered for updates.",
item.Graph?.Component as Object);
base.Release(item);
}
/************************************************************************************************************************/
#endif
/************************************************************************************************************************/
}
/************************************************************************************************************************/
#endregion
/************************************************************************************************************************/
}
}

View File

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

View File

@@ -0,0 +1,73 @@
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
using System.Text;
namespace Animancer
{
/// <summary>An <see cref="AnimancerNode"/> and its <see cref="StartingWeight"/>.</summary>
/// https://kybernetik.com.au/animancer/api/Animancer/NodeWeight
public readonly struct NodeWeight
{
/************************************************************************************************************************/
/// <summary>The <see cref="AnimancerNode"/>.</summary>
public readonly AnimancerNode Node;
/// <summary>The <see cref="AnimancerNode.Weight"/> from when this struct was captured.</summary>
public readonly float StartingWeight;
/************************************************************************************************************************/
/// <summary>Creates a new <see cref="NodeWeight"/>.</summary>
public NodeWeight(AnimancerNode node)
{
Node = node;
StartingWeight = node.Weight;
}
/************************************************************************************************************************/
/// <summary>Creates a new <see cref="NodeWeight"/>.</summary>
public NodeWeight(AnimancerNode node, float startingWeight)
{
Node = node;
StartingWeight = startingWeight;
}
/************************************************************************************************************************/
/// <summary>Creates a copy of `copyFrom`.</summary>
public NodeWeight(NodeWeight copyFrom, CloneContext context)
{
Node = context.GetOrCreateCloneOrOriginal(copyFrom.Node);
StartingWeight = copyFrom.StartingWeight;
}
/************************************************************************************************************************/
/// <summary>Appends a detailed descrption of this object.</summary>
public void AppendDescription(StringBuilder text, float targetWeight)
{
if (Node == null)
{
text.Append("Null: ")
.Append(StartingWeight)
.Append(" -> ")
.Append(targetWeight);
}
else
{
text.Append(Node.GetPath())
.Append(": ")
.Append(StartingWeight)
.Append(" -> ")
.Append(Node.Weight)
.Append(" -> ")
.Append(targetWeight);
}
}
/************************************************************************************************************************/
}
}

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

@@ -0,0 +1,819 @@
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
using System;
using System.Collections;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using System.Text;
using UnityEngine;
using UnityEngine.Playables;
using Object = UnityEngine.Object;
namespace Animancer
{
/// <summary>Base class for <see cref="Playable"/> wrapper objects in an <see cref="AnimancerGraph"/>.</summary>
/// <remarks>This is the base class of <see cref="AnimancerLayer"/> and <see cref="AnimancerState"/>.</remarks>
/// https://kybernetik.com.au/animancer/api/Animancer/AnimancerNode
public abstract class AnimancerNode : AnimancerNodeBase,
ICopyable<AnimancerNode>,
IEnumerable<AnimancerState>,
IEnumerator,
IHasDescription
{
/************************************************************************************************************************/
#region Playable
/************************************************************************************************************************/
#if UNITY_EDITOR
/// <summary>[Editor-Only] [Internal] Indicates whether the Inspector details for this node are expanded.</summary>
internal bool _IsInspectorExpanded;
#endif
/************************************************************************************************************************/
/// <summary>Creates and assigns the <see cref="Playable"/> managed by this node.</summary>
/// <remarks>This method also applies the <see cref="AnimancerNodeBase.Speed"/> if it was set beforehand.</remarks>
protected virtual void CreatePlayable()
{
#if UNITY_ASSERTIONS
if (Graph == null)
{
MarkAsUsed(this);
throw new InvalidOperationException($"{nameof(AnimancerNode)}.{nameof(Graph)}" +
$" is null when attempting to create its {nameof(Playable)}: {this}" +
$"\nThe {nameof(Graph)} is generally set when you first play a state," +
$" so you probably just need to play it before trying to access it.");
}
if (_Playable.IsValid())
Debug.LogWarning($"{nameof(AnimancerNode)}.{nameof(CreatePlayable)}" +
$" was called before destroying the previous {nameof(Playable)}: {this}", Graph?.Component as Object);
#endif
CreatePlayable(out _Playable);
#if UNITY_ASSERTIONS
if (!_Playable.IsValid())
throw new InvalidOperationException(
$"{nameof(AnimancerNode)}.{nameof(CreatePlayable)}" +
$" did not create a valid {nameof(Playable)} for {this}");
#endif
if (Speed != 1)
_Playable.SetSpeed(Speed);
}
/// <summary>Creates and assigns the <see cref="Playable"/> managed by this node.</summary>
protected abstract void CreatePlayable(out Playable playable);
/************************************************************************************************************************/
/// <summary>Destroys the <see cref="Playable"/>.</summary>
public void DestroyPlayable()
{
if (_Playable.IsValid())
Graph._PlayableGraph.DestroyPlayable(_Playable);
}
/************************************************************************************************************************/
/// <summary>Calls <see cref="DestroyPlayable"/> and <see cref="CreatePlayable()"/>.</summary>
public virtual void RecreatePlayable()
{
DestroyPlayable();
CreatePlayable();
}
/// <summary>Calls <see cref="RecreatePlayable"/> on this node and all its children recursively.</summary>
public void RecreatePlayableRecursive()
{
RecreatePlayable();
for (int i = ChildCount - 1; i >= 0; i--)
GetChild(i)?.RecreatePlayableRecursive();
}
/************************************************************************************************************************/
/// <summary>Copies the details of `copyFrom` into this node, replacing its previous contents.</summary>
public virtual void CopyFrom(AnimancerNode copyFrom, CloneContext context)
{
SetWeight(copyFrom._Weight);
FadeGroup = context.WillCloneUpdatables
? null
: copyFrom.FadeGroup?.CloneForSingleTarget(copyFrom, this);
Speed = copyFrom.Speed;
CopyIKFlags(copyFrom);
#if UNITY_ASSERTIONS
DebugName = context.GetCloneOrOriginal(copyFrom.DebugName);
#endif
}
/************************************************************************************************************************/
#endregion
/************************************************************************************************************************/
#region Graph
/************************************************************************************************************************/
/// <summary>The index of the port this node is connected to on the parent's <see cref="Playable"/>.</summary>
/// <remarks>
/// A negative value indicates that it is not assigned to a port.
/// <para></para>
/// Indices are generally assigned starting from 0, ascending in the order they are connected to their layer.
/// They will not usually change unless the <see cref="AnimancerNodeBase.Parent"/> changes or another state on
/// the same layer is destroyed so the last state is swapped into its place to avoid shuffling everything down
/// to cover the gap.
/// <para></para>
/// The setter is internal so user defined states cannot set it incorrectly. Ideally,
/// <see cref="AnimancerLayer"/> should be able to set the port in its constructor and
/// <see cref="AnimancerState.SetParent"/> should also be able to set it, but classes that further inherit from
/// there should not be able to change it without properly calling that method.
/// </remarks>
public int Index { get; internal set; } = int.MinValue;
/************************************************************************************************************************/
/// <summary>Creates a new <see cref="AnimancerNode"/>.</summary>
protected AnimancerNode()
{
#if UNITY_ASSERTIONS
if (TraceConstructor)
_ConstructorStackTrace = new(true);
#endif
}
/************************************************************************************************************************/
#if UNITY_ASSERTIONS
/************************************************************************************************************************/
/// <summary>[Assert-Only]
/// Should a <see cref="System.Diagnostics.StackTrace"/> be captured in the constructor of all new nodes so
/// <see cref="OptionalWarning.UnusedNode"/> can include it in the warning if that node ends up being unused?
/// </summary>
/// <remarks>This has a notable performance cost so it should only be used when trying to identify a problem.</remarks>
public static bool TraceConstructor { get; set; }
/************************************************************************************************************************/
/// <summary>[Assert-Only]
/// The stack trace of the constructor (or null if <see cref="TraceConstructor"/> was false).
/// </summary>
private System.Diagnostics.StackTrace _ConstructorStackTrace;
/// <summary>[Assert-Only]
/// Returns the stack trace of the constructor (or null if <see cref="TraceConstructor"/> was false).
/// </summary>
public static System.Diagnostics.StackTrace GetConstructorStackTrace(AnimancerNode node)
=> node._ConstructorStackTrace;
/************************************************************************************************************************/
/// <summary>[Assert-Only] Checks <see cref="OptionalWarning.UnusedNode"/>.</summary>
~AnimancerNode()
{
if (Graph != null ||
Parent != null ||
OptionalWarning.UnusedNode.IsDisabled())
return;
// ToString might throw an exception since finalizers arn't run on the main thread.
string name = null;
try { name = ToString(); }
catch { name = GetType().FullName; }
var message = $"The {nameof(Graph)} of '{name}'" +
$" is null during finalization (garbage collection)." +
$" This may have been caused by earlier exceptions, but otherwise it probably means" +
$" that this node was never used for anything and should not have been created.";
if (_ConstructorStackTrace != null)
message += "\n\nThis node was created at:\n" + _ConstructorStackTrace;
else
message += $"\n\nEnable {nameof(AnimancerNode)}.{nameof(TraceConstructor)} on startup" +
$" to allow this warning to include the {nameof(System.Diagnostics.StackTrace)}" +
$" of when the node was constructed.";
OptionalWarning.UnusedNode.Log(message);
}
/************************************************************************************************************************/
#endif
/************************************************************************************************************************/
/// <summary>Connects the `child`'s <see cref="Playable"/> to this node.</summary>
/// <remarks>This method is NOT safe to call if the child was already connected.</remarks>
protected internal void ConnectChildUnsafe(int index, AnimancerNode child)
{
#if UNITY_ASSERTIONS
if (index < 0)
{
MarkAsUsed(this);
throw new InvalidOperationException(
$"Invalid {nameof(index)} when attempting to connect to its parent:" +
"\n• Child: " + child +
"\n• Parent: " + this);
}
Validate.AssertPlayable(child);
#endif
Graph._PlayableGraph.Connect(_Playable, child._Playable, index, child._Weight);
}
/// <summary>Disconnects the <see cref="Playable"/> of the child at the specified `index` from this node.</summary>
/// <remarks>This method is safe to call if the child was already disconnected.</remarks>
protected void DisconnectChildSafe(int index)
{
if (_Playable.GetInput(index).IsValid())
Graph._PlayableGraph.Disconnect(_Playable, index);
}
/************************************************************************************************************************/
// IEnumerator for yielding in a coroutine to wait until animations have stopped.
/************************************************************************************************************************/
/// <summary>Is this node playing and not yet at its end?</summary>
/// <remarks>
/// This method is called by <see cref="IEnumerator.MoveNext"/> so this object can be used as a custom yield
/// instruction to wait until it finishes.
/// </remarks>
public abstract bool IsPlayingAndNotEnding();
bool IEnumerator.MoveNext()
=> IsPlayingAndNotEnding();
object IEnumerator.Current
=> null;
void IEnumerator.Reset() { }
/************************************************************************************************************************/
#endregion
/************************************************************************************************************************/
#region Children
/************************************************************************************************************************/
/// <inheritdoc/>
protected internal override AnimancerNode GetChildNode(int index)
=> GetChild(index);
/// <summary>Returns the state connected to the specified `index` as a child of this node.</summary>
/// <remarks>When overriding, don't call this base method because it throws an exception.</remarks>
/// <exception cref="NotSupportedException">This node can't have children.</exception>
public virtual AnimancerState GetChild(int index)
{
MarkAsUsed(this);
throw new NotSupportedException(this + " can't have children.");
}
/// <summary>Called when a child is connected with this node as its <see cref="AnimancerNodeBase.Parent"/>.</summary>
/// <remarks>When overriding, don't call this base method because it throws an exception.</remarks>
/// <exception cref="NotSupportedException">This node can't have children.</exception>
protected internal virtual void OnAddChild(AnimancerState child)
{
MarkAsUsed(this);
child.SetParentInternal(null);
throw new NotSupportedException(this + " can't have children.");
}
/************************************************************************************************************************/
// IEnumerable for 'foreach' statements.
/************************************************************************************************************************/
/// <summary>Gets an enumerator for all of this node's child states.</summary>
public virtual FastEnumerator<AnimancerState> GetEnumerator()
=> default;
IEnumerator<AnimancerState> IEnumerable<AnimancerState>.GetEnumerator()
=> GetEnumerator();
IEnumerator IEnumerable.GetEnumerator()
=> GetEnumerator();
/************************************************************************************************************************/
#endregion
/************************************************************************************************************************/
#region Weight
/************************************************************************************************************************/
/// <summary>[Internal] The current blend weight of this node. Accessed via <see cref="Weight"/>.</summary>
internal float _Weight;
/************************************************************************************************************************/
/// <summary>The current blend weight of this node which determines how much it affects the final output.</summary>
///
/// <remarks>
/// 0 has no effect while 1 applies the full effect and values inbetween apply a proportional effect.
/// <para></para>
/// Setting this property cancels any fade currently in progress. If you don't wish to do that, you can use
/// <see cref="SetWeight"/> instead.
/// <para></para>
/// <em>Animancer Lite only allows this value to be set to 0 or 1 in runtime builds.</em>
/// </remarks>
///
/// <example>
/// Calling <see cref="AnimancerLayer.Play(AnimationClip)"/> immediately sets the weight of all states to 0
/// and the new state to 1. Note that this is separate from other values like
/// <see cref="AnimancerState.IsPlaying"/> so a state can be paused at any point and still show its pose on the
/// character or it could be still playing at 0 weight if you want it to still trigger events (though states
/// are normally stopped when they reach 0 weight so you would need to explicitly set it to playing again).
/// <para></para>
/// Calling <see cref="AnimancerLayer.Play(AnimationClip, float, FadeMode)"/> doesn't immediately change
/// the weights, but instead calls <see cref="StartFade(float, float)"/> on every state to set their
/// <see cref="TargetWeight"/> and <see cref="FadeSpeed"/>. Then every update each state's weight will move
/// towards that target value at that speed.
/// </example>
public float Weight
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get => _Weight;
set
{
FadeGroup = null;
SetWeight(value);
}
}
/// <summary>
/// Sets the current blend weight of this node which determines how much it affects the final output.
/// 0 has no effect while 1 applies the full effect of this node.
/// </summary>
/// <remarks>
/// This method allows any fade currently in progress to continue. If you don't wish to do that, you can set
/// the <see cref="Weight"/> property instead.
/// <para></para>
/// <em>Animancer Lite only allows this value to be set to 0 or 1 in runtime builds.</em>
/// </remarks>
public virtual void SetWeight(float value)
=> SetWeightInternal(value);
/// <summary>The internal non-<c>virtual</c> implementation of <see cref="SetWeight"/>.</summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private void SetWeightInternal(float value)
{
if (_Weight == value)
return;
Validate.AssertSetWeight(this, value);
_Weight = value;
if (Graph != null)
Parent?.Playable.ApplyChildWeight(this);
}
/************************************************************************************************************************/
/// <inheritdoc/>
protected internal override float BaseWeight
=> Weight;
/// <summary>
/// The <see cref="Weight"/> of this state multiplied by the <see cref="Weight"/> of each of its parents down
/// the hierarchy to determine how much this state affects the final output.
/// </summary>
public float EffectiveWeight
{
get
{
var weight = Weight;
var parent = Parent;
while (parent != null)
{
weight *= parent.BaseWeight;
parent = parent.Parent;
}
return weight;
}
}
/************************************************************************************************************************/
#endregion
/************************************************************************************************************************/
#region Fading
/************************************************************************************************************************/
internal FadeGroup _FadeGroup;
/// <summary>The current fade being applied to this node (if any).</summary>
public FadeGroup FadeGroup
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get => _FadeGroup;
internal set
{
_FadeGroup?.Remove(this);
_FadeGroup = value;
}
}
/// <summary>
/// The desired <see cref="Weight"/> which this node is fading towards according to the
/// <see cref="FadeSpeed"/>.
/// </summary>
public float TargetWeight
=> FadeGroup != null
? FadeGroup.GetTargetWeight(this)
: Weight;
/// <summary>The speed at which this node is fading towards the <see cref="TargetWeight"/>.</summary>
/// <remarks>
/// This value isn't affected by this node's <see cref="AnimancerNodeBase.Speed"/>,
/// but is affected by its parents.
/// </remarks>
public float FadeSpeed
=> FadeGroup != null
? FadeGroup.FadeSpeed
: 0;
/************************************************************************************************************************/
/// <summary>
/// Calls <see cref="OnStartFade"/> and starts fading the <see cref="Weight"/> over the course
/// of the <see cref="AnimancerGraph.DefaultFadeDuration"/> (in seconds).
/// </summary>
/// <remarks>
/// If the `targetWeight` is 0 then <see cref="Stop"/> will be called when the fade is complete.
/// <para></para>
/// If the <see cref="Weight"/> is already equal to the `targetWeight` then the fade will end
/// immediately.
/// </remarks>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void StartFade(float targetWeight)
=> StartFade(targetWeight, AnimancerGraph.DefaultFadeDuration);
/// <summary>
/// Calls <see cref="OnStartFade"/> and starts fading the <see cref="Weight"/>
/// over the course of the `fadeDuration` (in seconds).
/// </summary>
/// <remarks>
/// If the `targetWeight` is 0 then <see cref="Stop"/> will be called when the fade is complete.
/// <para></para>
/// If the <see cref="Weight"/> is already equal to the `targetWeight`
/// then the fade will end immediately.
/// <para></para>
/// <em>Animancer Lite only allows a `targetWeight` of 0 or 1
/// and the default `fadeDuration` (0.25 seconds) in runtime builds.</em>
/// </remarks>
public void StartFade(float targetWeight, float fadeDuration)
{
if (Weight == targetWeight && FadeGroup == null)
{
OnStartFade();
}
else if (fadeDuration > 0)
{
var fade = FadeGroup.Pool.Instance.Acquire();
fade.SetFadeIn(this);
fade.StartFade(targetWeight, 1 / fadeDuration);
}
else
{
Weight = targetWeight;
}
}
/************************************************************************************************************************/
/// <summary>Called by <see cref="StartFade(float, float)"/>.</summary>
protected internal abstract void OnStartFade();
/************************************************************************************************************************/
/// <summary>Removes this node from the <see cref="FadeGroup"/>.</summary>
public void CancelFade()
=> _FadeGroup?.Remove(this);
/// <summary>[Internal] Called by <see cref="FadeGroup.Remove"/>.</summary>
/// <remarks>Not called when a fade fully completes.</remarks>
protected internal virtual void InternalClearFade()
{
_FadeGroup = null;
}
/************************************************************************************************************************/
/// <summary>Stops the animation and makes it inactive immediately so it no longer affects the output.</summary>
/// <remarks>
/// Sets <see cref="Weight"/> = 0 by default unless overridden.
/// <para></para>
/// Note that playing something new will automatically stop the old animation.
/// </remarks>
public void Stop()
{
FadeGroup = null;
SetWeightInternal(0);
StopWithoutWeight();
}
/// <summary>[Internal] Stops this node without setting its <see cref="Weight"/>.</summary>
protected internal virtual void StopWithoutWeight() { }
/************************************************************************************************************************/
#endregion
/************************************************************************************************************************/
#region Inverse Kinematics
/************************************************************************************************************************/
/// <summary>
/// Should setting the <see cref="AnimancerNodeBase.Parent"/>
/// also set this node's <see cref="ApplyAnimatorIK"/> to match it?
/// Default is true.
/// </summary>
public static bool ApplyParentAnimatorIK { get; set; } = true;
/// <summary>
/// Should setting the <see cref="AnimancerNodeBase.Parent"/>
/// also set this node's <see cref="ApplyFootIK"/> to match it?
/// Default is true.
/// </summary>
public static bool ApplyParentFootIK { get; set; } = true;
/************************************************************************************************************************/
/// <summary>
/// Copies the IK settings from `copyFrom` into this node:
/// <list type="bullet">
/// <item>If <see cref="ApplyParentAnimatorIK"/> is true, copy <see cref="ApplyAnimatorIK"/>.</item>
/// <item>If <see cref="ApplyParentFootIK"/> is true, copy <see cref="ApplyFootIK"/>.</item>
/// </list>
/// </summary>
public virtual void CopyIKFlags(AnimancerNodeBase copyFrom)
{
if (Graph == null)
return;
if (ApplyParentAnimatorIK)
ApplyAnimatorIK = copyFrom.ApplyAnimatorIK;
if (ApplyParentFootIK)
ApplyFootIK = copyFrom.ApplyFootIK;
}
/************************************************************************************************************************/
/// <inheritdoc/>
public override bool ApplyAnimatorIK
{
get
{
for (int i = ChildCount - 1; i >= 0; i--)
{
var state = GetChild(i);
if (state.ApplyAnimatorIK)
return true;
}
return false;
}
set
{
for (int i = ChildCount - 1; i >= 0; i--)
{
var state = GetChild(i);
state.ApplyAnimatorIK = value;
}
}
}
/************************************************************************************************************************/
/// <inheritdoc/>
public override bool ApplyFootIK
{
get
{
for (int i = ChildCount - 1; i >= 0; i--)
{
var state = GetChild(i);
if (state.ApplyFootIK)
return true;
}
return false;
}
set
{
for (int i = ChildCount - 1; i >= 0; i--)
{
var state = GetChild(i);
state.ApplyFootIK = value;
}
}
}
/************************************************************************************************************************/
#endregion
/************************************************************************************************************************/
#region Descriptions
/************************************************************************************************************************/
#if UNITY_ASSERTIONS
/// <summary>[Assert-Only] The Inspector display name of this node.</summary>
/// <remarks>Set using <see cref="SetDebugName"/>.</remarks>
public object DebugName { get; private set; }
#endif
/// <summary>[Assert-Conditional] Sets the <see cref="DebugName"/> to display in the Inspector.</summary>
[System.Diagnostics.Conditional(Strings.Assertions)]
public void SetDebugName(object name)
{
#if UNITY_ASSERTIONS
DebugName = name;
#endif
}
/// <summary>The Inspector display name of this node.</summary>
public override string ToString()
{
#if UNITY_ASSERTIONS
if (NameCache.TryToString(DebugName, out var name))
return name;
#endif
return base.ToString();
}
/************************************************************************************************************************/
/// <inheritdoc/>
public void AppendDescription(StringBuilder text, string separator = "\n")
{
text.Append(ToString());
AppendDetails(text, separator);
if (ChildCount > 0)
{
text.AppendField(separator, nameof(ChildCount), ChildCount);
var indentedSeparator = separator + Strings.Indent;
var i = 0;
foreach (var child in this)
{
text.Append(separator)
.Append('[')
.Append(i++)
.Append("] ")
.AppendDescription(child, indentedSeparator, true);
}
}
}
/************************************************************************************************************************/
/// <summary>Called by <see cref="AppendDescription"/> to append the details of this node.</summary>
protected virtual void AppendDetails(StringBuilder text, string separator)
{
text.AppendField(separator, "Playable", _Playable.IsValid()
? _Playable.GetPlayableType().ToString()
: "Invalid");
var parent = Parent;
var isConnected =
parent != null &&
parent.Playable.GetInput(Index).IsValid();
text.AppendField(separator, "Connected", isConnected);
text.AppendField(separator, nameof(Index), Index);
if (Index < 0)
text.Append(" (No Parent)");
text.AppendField(separator, nameof(Speed), Speed);
var realSpeed = _Playable.IsValid()
? _Playable.GetSpeed()
: Speed;
if (realSpeed != Speed)
text.Append(" (Real ").Append(realSpeed).Append(')');
text.AppendField(separator, nameof(Weight), Weight);
if (Weight != TargetWeight)
{
text.AppendField(separator, nameof(TargetWeight), TargetWeight);
text.AppendField(separator, nameof(FadeSpeed), FadeSpeed);
}
AppendIKDetails(text, separator, this);
#if UNITY_ASSERTIONS
if (_ConstructorStackTrace != null)
text.AppendField(separator, "ConstructorStackTrace", _ConstructorStackTrace);
#endif
}
/************************************************************************************************************************/
/// <summary>
/// Appends the details of <see cref="AnimancerNodeBase.ApplyAnimatorIK"/> and
/// <see cref="AnimancerNodeBase.ApplyFootIK"/>.
/// </summary>
public static void AppendIKDetails(StringBuilder text, string separator, AnimancerNodeBase node)
{
if (!node.Playable.IsValid())
return;
text.Append(separator)
.Append("InverseKinematics: ");
if (node.ApplyAnimatorIK)
{
text.Append("OnAnimatorIK");
if (node.ApplyFootIK)
text.Append(", FootIK");
}
else if (node.ApplyFootIK)
{
text.Append("FootIK");
}
else
{
text.Append("None");
}
}
/************************************************************************************************************************/
/// <summary>Returns the hierarchy path of this node through its <see cref="AnimancerNodeBase.Parent"/>s.</summary>
public string GetPath()
{
var path = StringBuilderPool.Instance.Acquire();
if (Parent is AnimancerNode parent)
{
AppendPath(path, parent);
AppendPortAndType(path);
}
else
{
AppendPortAndType(path);
}
return path.ReleaseToString();
}
/// <summary>Appends the hierarchy path of this state through its <see cref="AnimancerNodeBase.Parent"/>s.</summary>
private static void AppendPath(StringBuilder path, AnimancerNode parent)
{
if (parent != null)
{
if (parent.Parent is AnimancerNode grandParent)
{
AppendPath(path, grandParent);
}
else
{
var layer = parent.Layer;
if (layer != null)
{
path.Append("Layers[")
.Append(parent.Layer.Index)
.Append("].States");
}
else
{
path.Append("NoLayer -> ")
.Append(parent.ToString());
}
return;
}
}
if (parent is AnimancerState state)
{
state.AppendPortAndType(path);
}
else
{
path.Append(" -> ")
.Append(parent.GetType());
}
}
/// <summary>Appends "[Index] -> ToString()".</summary>
private void AppendPortAndType(StringBuilder path)
{
path.Append('[')
.Append(Index)
.Append("] -> ")
.Append(ToString());
}
/************************************************************************************************************************/
#endregion
/************************************************************************************************************************/
}
}

View File

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

View File

@@ -0,0 +1,282 @@
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
using System;
using UnityEngine.Playables;
namespace Animancer
{
/// <summary>Base class for objects that manage a <see cref="UnityEngine.Playables.Playable"/>.</summary>
/// <remarks>This is the base class of <see cref="AnimancerGraph"/> and <see cref="AnimancerNode"/>.</remarks>
/// https://kybernetik.com.au/animancer/api/Animancer/AnimancerNodeBase
public abstract class AnimancerNodeBase
{
/************************************************************************************************************************/
/// <summary>The <see cref="AnimancerGraph"/> containing this node.</summary>
public AnimancerGraph Graph { get; internal set; }
/************************************************************************************************************************/
/// <summary>The object which receives the output of the <see cref="Playable"/>.</summary>
/// <remarks>
/// This leads from <see cref="AnimancerState"/> to <see cref="AnimancerLayer"/> to
/// <see cref="AnimancerGraph"/> to <c>null</c>.
/// </remarks>
public AnimancerNodeBase Parent { get; protected set; }
/************************************************************************************************************************/
/// <summary>The root <see cref="AnimancerLayer"/> which this node is connected to (if any).</summary>
public virtual AnimancerLayer Layer
=> Parent?.Layer;
/************************************************************************************************************************/
/// <summary>The number of nodes using this as their <see cref="Parent"/>.</summary>
public virtual int ChildCount
=> 0;
/// <summary>Returns the node connected to the specified `index` as a child of this node.</summary>
/// <remarks>When overriding, don't call this base method because it throws an exception.</remarks>
/// <exception cref="NotSupportedException">This node can't have children.</exception>
protected internal virtual AnimancerNode GetChildNode(int index)
{
MarkAsUsed(this);
throw new NotSupportedException(this + " can't have children.");
}
/// <summary>Should child playables stay connected to the graph at all times?</summary>
/// <remarks>
/// If false, playables will be disconnected from the graph while they are inactive to stop it from
/// evaluating them every frame which usually improves performance.
/// </remarks>
/// <seealso cref="AnimancerGraph.KeepChildrenConnected"/>
public virtual bool KeepChildrenConnected
=> true;
/************************************************************************************************************************/
/// <summary>Called when a child's <see cref="AnimancerState.IsLooping"/> value changes.</summary>
protected virtual void OnChildIsLoopingChanged(bool value) { }
/// <summary>[Internal] Calls <see cref="OnChildIsLoopingChanged"/> for each <see cref="Parent"/> recursively.</summary>
protected internal void OnIsLoopingChangedRecursive(bool value)
{
var parent = Parent;
while (parent != null)
{
parent.OnChildIsLoopingChanged(value);
parent = parent.Parent;
}
}
/************************************************************************************************************************/
/// <summary>[Internal] Called when a child's <see cref="Parent"/> is changed from this node.</summary>
/// <remarks>When overriding, don't call this base method because it throws an exception.</remarks>
/// <exception cref="NotSupportedException">This node can't have children.</exception>
protected internal virtual void OnRemoveChild(AnimancerState child)
{
MarkAsUsed(this);
child.SetParentInternal(null);
throw new NotSupportedException(this + " can't have children.");
}
/************************************************************************************************************************/
/// <summary>[Internal] The <see cref="Playable"/>.</summary>
protected internal Playable _Playable;
/// <summary>The internal object this node manages in the <see cref="PlayableGraph"/>.</summary>
/// <remarks>
/// Must be set by <see cref="AnimancerNode.CreatePlayable()"/>. Failure to do so will throw the following
/// exception throughout the system when using this node: "<see cref="ArgumentException"/>: The playable passed
/// as an argument is invalid. To create a valid playable, please use the appropriate Create method".
/// </remarks>
public Playable Playable => _Playable;
/************************************************************************************************************************/
/// <summary>The current blend weight of this node which determines how much it affects the final output.</summary>
protected internal virtual float BaseWeight => 1;
/************************************************************************************************************************/
#region Speed
/************************************************************************************************************************/
private float _Speed = 1;
/// <summary>[Pro-Only] How fast the <see cref="AnimancerState.Time"/> is advancing every frame (default 1).</summary>
///
/// <remarks>
/// A negative value will play the animation backwards.
/// <para></para>
/// To pause an animation, consider setting <see cref="AnimancerState.IsPlaying"/> to false instead of setting
/// this value to 0.
/// <para></para>
/// <em>Animancer Lite doesn't allow this value to be changed in runtime builds.</em>
/// <para></para>
/// <strong>Example:</strong><code>
/// void SpeedExample(AnimancerComponent animancer, AnimationClip clip)
/// {
/// var state = animancer.Play(clip);
///
/// state.Speed = 1;// Normal speed.
/// state.Speed = 2;// Double speed.
/// state.Speed = 0.5f;// Half speed.
/// state.Speed = -1;// Normal speed playing backwards.
/// state.NormalizedTime = 1;// Start at the end to play backwards from there.
/// }
/// </code></remarks>
///
/// <exception cref="ArgumentOutOfRangeException">The value is not finite.</exception>
public virtual float Speed
{
get => _Speed;
set
{
if (_Speed == value)
return;
#if UNITY_ASSERTIONS
if (!value.IsFinite())
{
MarkAsUsed(this);
throw new ArgumentOutOfRangeException(
nameof(value),
value,
$"{nameof(Speed)} {Strings.MustBeFinite}");
}
#endif
_Speed = value;
if (_Playable.IsValid())
_Playable.SetSpeed(value);
}
}
/************************************************************************************************************************/
/// <summary>
/// The <see cref="Speed"/> of this node multiplied by the <see cref="Speed"/> of each of its parents to
/// determine the actual speed it's playing at.
/// </summary>
public float EffectiveSpeed
{
get => Speed * ParentEffectiveSpeed;
set => Speed = value / ParentEffectiveSpeed;
}
/************************************************************************************************************************/
/// <summary>
/// The multiplied <see cref="Speed"/> of each of the <see cref="Parent"/> down the hierarchy,
/// excluding the root <see cref="Speed"/>.
/// </summary>
private float ParentEffectiveSpeed
{
get
{
var parent = Parent;
if (parent == null)
return 1;
var speed = parent.Speed;
while ((parent = parent.Parent) != null)
{
speed *= parent.Speed;
}
return speed;
}
}
/************************************************************************************************************************/
#endregion
/************************************************************************************************************************/
/// <summary>
/// Should Unity call <c>OnAnimatorIK</c> on the animated object while this object and its children have any
/// <see cref="AnimancerNode.Weight"/>?
/// </summary>
/// <remarks>
/// This is equivalent to the "IK Pass" toggle in Animator Controller layers, except that due to limitations in
/// the Playables API the <c>layerIndex</c> will always be zero.
/// <para></para>
/// This value starts false by default, but can be automatically changed by
/// <see cref="AnimancerNode.CopyIKFlags"/> when the <see cref="Parent"/> is set.
/// <para></para>
/// IK only takes effect while at least one <see cref="ClipState"/> has a <see cref="AnimancerNode.Weight"/>
/// above zero. Other node types either store the value to apply to their children or don't support IK.
/// <para></para>
/// <strong>Documentation:</strong>
/// <see href="https://kybernetik.com.au/animancer/docs/manual/ik#ik-pass">
/// IK Pass</see>
/// </remarks>
public abstract bool ApplyAnimatorIK { get; set; }
/************************************************************************************************************************/
/// <summary>Should this object and its children apply IK to the character's feet?</summary>
/// <remarks>
/// This is equivalent to the "Foot IK" toggle in Animator Controller states.
/// <para></para>
/// This value starts true by default for <see cref="ClipState"/>s (false for others), but can be automatically
/// changed by <see cref="AnimancerNode.CopyIKFlags"/> when the <see cref="Parent"/> is set.
/// <para></para>
/// IK only takes effect while at least one <see cref="ClipState"/> has a <see cref="AnimancerNode.Weight"/>
/// above zero. Other node types either store the value to apply to their children or don't support IK.
/// <para></para>
/// <strong>Documentation:</strong>
/// <see href="https://kybernetik.com.au/animancer/docs/manual/ik#foot-ik">
/// Foot IK</see>
/// </remarks>
public abstract bool ApplyFootIK { get; set; }
/************************************************************************************************************************/
/// <summary>[Internal] Applies a change to a child's <see cref="AnimancerState.IsActive"/>.</summary>
protected internal virtual void ApplyChildActive(AnimancerState child, bool setActive)
=> child.ShouldBeActive = setActive;
/************************************************************************************************************************/
/// <summary>[Assert-Conditional] Prevents the `node` from causing <see cref="OptionalWarning.UnusedNode"/>.</summary>
[System.Diagnostics.Conditional(Strings.Assertions)]
public static void MarkAsUsed(AnimancerNodeBase node)
{
#if UNITY_ASSERTIONS
if (node.Graph == null)
GC.SuppressFinalize(node);
#endif
}
/************************************************************************************************************************/
#if UNITY_EDITOR
/************************************************************************************************************************/
/// <summary>[Editor-Only]
/// Adds functions to show and set <see cref="ApplyAnimatorIK"/> and
/// <see cref="ApplyFootIK"/>.
/// </summary>
public static void AddContextMenuIK(UnityEditor.GenericMenu menu, AnimancerNodeBase ik)
{
#if UNITY_IMGUI
menu.AddItem(new("Inverse Kinematics/Apply Animator IK ?"),
ik.ApplyAnimatorIK,
() => ik.ApplyAnimatorIK = !ik.ApplyAnimatorIK);
menu.AddItem(new("Inverse Kinematics/Apply Foot IK ?"),
ik.ApplyFootIK,
() => ik.ApplyFootIK = !ik.ApplyFootIK);
#endif
}
/************************************************************************************************************************/
#endif
/************************************************************************************************************************/
}
}

View File

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

View File

@@ -0,0 +1,67 @@
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
namespace Animancer
{
/// https://kybernetik.com.au/animancer/api/Animancer/AnimancerState
public abstract partial class AnimancerState
{
/************************************************************************************************************************/
#if UNITY_ASSERTIONS
private static bool _SkipNextExpectFade;
private bool _ExpectFade;
#endif
/************************************************************************************************************************/
/// <summary>[Internal] Sets a flag for <see cref="OptionalWarning.ExpectFade"/>.</summary>
[System.Diagnostics.Conditional(Strings.Assertions)]
public static void SetExpectFade(AnimancerState state, float fadeDuration)
{
#if UNITY_ASSERTIONS
state._ExpectFade = fadeDuration > 0;
#endif
}
/************************************************************************************************************************/
/// <summary>[Internal] Sets the next <see cref="AssertNotExpectingFade"/> call to be skipped.</summary>
[System.Diagnostics.Conditional(Strings.Assertions)]
internal static void SkipNextExpectFade()
{
#if UNITY_ASSERTIONS
_SkipNextExpectFade = true;
#endif
}
/************************************************************************************************************************/
/// <summary>[Internal] Call when playing a `state` without a fade to check <see cref="OptionalWarning.ExpectFade"/>.</summary>
[System.Diagnostics.Conditional(Strings.Assertions)]
internal static void AssertNotExpectingFade(AnimancerState state)
{
#if UNITY_ASSERTIONS
if (_SkipNextExpectFade)
{
_SkipNextExpectFade = false;
return;
}
if (state._ExpectFade)
{
state._ExpectFade = false;// Don't log again for the same state.
OptionalWarning.ExpectFade.Log(
"A state was created by a transition with a non-zero Fade Duration" +
" but is now being played without a fade, which may be unintentional." +
" In most cases, the transition should be played so that it can properly" +
" apply its settings, unlike if the state is played directly.",
state.Graph?.Component);
}
#endif
}
/************************************************************************************************************************/
}
}

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

@@ -0,0 +1,217 @@
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
using System;
using Unity.Collections;
using UnityEngine.Animations;
using UnityEngine.Playables;
namespace Animancer
{
/// <summary>[Pro-Only] A scripted animation for an <see cref="AnimationJobState{T}"/>.</summary>
/// <remarks>
/// <strong>Sample:</strong>
/// <see href="https://kybernetik.com.au/animancer/docs/samples/jobs/job-states">
/// Job States</see>
/// </remarks>
public interface IAnimancerStateJob : IDisposable
{
/************************************************************************************************************************/
/// <summary>The total time this job would take to play in seconds at normal speed.</summary>
float Length { get; }
/// <summary>Does this job loop back to the start when its time passes its <see cref="Length"/>?</summary>
bool IsLooping { get; }
/// <summary>Defines what do to when processing the root motion.</summary>
/// <remarks>
/// This is called by <see cref="IAnimationJob.ProcessRootMotion"/>
/// and receives the <see cref="AnimancerState.TimeD"/>.
/// </remarks>
void ProcessRootMotion(AnimationStream stream, double time);
/// <summary>Defines what do to when processing the animation.</summary>
/// <remarks>
/// This is called by <see cref="IAnimationJob.ProcessAnimation"/>
/// and receives the <see cref="AnimancerState.TimeD"/>.
/// </remarks>
void ProcessAnimation(AnimationStream stream, double time);
/************************************************************************************************************************/
}
/// <summary>[Pro-Only] An <see cref="AnimancerState"/> which plays an <see cref="IAnimancerStateJob"/>.</summary>
/// <remarks>
/// <strong>Sample:</strong>
/// <see href="https://kybernetik.com.au/animancer/docs/samples/jobs/job-states">
/// Job States</see>
/// </remarks>
/// https://kybernetik.com.au/animancer/api/Animancer/AnimationJobState_1
///
public class AnimationJobState<T> : AnimancerState, IUpdatable, IDisposable
where T : struct, IAnimancerStateJob
{
/************************************************************************************************************************/
/// <summary>
/// An <see cref="IAnimationJob"/> which wraps an <see cref="IAnimancerStateJob"/>
/// to provide its <see cref="Time"/> value.
/// </summary>
/// https://kybernetik.com.au/animancer/api/Animancer/TimedJob
public struct TimedJob : IAnimationJob, IDisposable
{
/// <summary>The <see cref="IAnimancerStateJob"/> data.</summary>
public T Job;
/// <summary>The <see cref="AnimancerState.TimeD"/> to be passed to the job.</summary>
public NativeArray<double> Time;
/// <summary>Cleans up the unmanaged resources used by this job.</summary>
public readonly void Dispose()
{
if (Time.IsCreated)
Time.Dispose();
Job.Dispose();
}
/// <summary>Defines what do to when processing the root motion.</summary>
public readonly void ProcessRootMotion(AnimationStream stream)
=> Job.ProcessRootMotion(stream, Time[0]);
/// <summary>Defines what do to when processing the animation.</summary>
public readonly void ProcessAnimation(AnimationStream stream)
=> Job.ProcessAnimation(stream, Time[0]);
}
/************************************************************************************************************************/
private new AnimationScriptPlayable _Playable;
private TimedJob _Job;
/// <summary>The data of the job to be executed by this state.</summary>
/// <remarks>
/// Setting this value has a minor performance cost. If it needs to be changed frequently,
/// consider using a single-item <c>NativeArray</c> in your job as demonstrated in the
/// <see href="https://kybernetik.com.au/animancer/docs/samples/jobs/hit-impacts#angle">
/// Hit Impacts</see> sample.
/// </remarks>
public T Job
{
get => _Job.Job;
set
{
_Job.Job = value;
_Playable.SetJobData(_Job);
}
}
/************************************************************************************************************************/
/// <inheritdoc/>
public override float Length
=> _Job.Job.Length;
/// <inheritdoc/>
public override bool IsLooping
=> _Job.Job.IsLooping;
/************************************************************************************************************************/
/// <inheritdoc/>
int IUpdatable.UpdatableIndex { get; set; } = IUpdatable.List.NotInList;
/************************************************************************************************************************/
/// <summary>Creates a new <see cref="AnimationJobState{T}"/>.</summary>
public AnimationJobState(T job)
{
_Job.Job = job;
}
/************************************************************************************************************************/
/// <inheritdoc/>
public override void SetGraph(AnimancerGraph graph)
{
Graph?.Disposables.Remove(this);
base.SetGraph(graph);
Graph?.Disposables.Add(this);
}
/************************************************************************************************************************/
/// <inheritdoc/>
protected override void CreatePlayable(out Playable playable)
{
if (!_Job.Time.IsCreated)
_Job.Time = AnimancerUtilities.CreateNativeReference<double>();
playable = _Playable = AnimationScriptPlayable.Create(Graph.PlayableGraph, _Job);
}
/************************************************************************************************************************/
/// <inheritdoc/>
protected override void OnSetIsPlaying()
{
base.OnSetIsPlaying();
if (IsPlaying)
Graph.RequirePreUpdate(this);
else
Graph.CancelPreUpdate(this);
}
/************************************************************************************************************************/
/// <summary>
/// Called every frame before the job is applied to send the
/// <see cref="AnimancerState.Time"/> to the <see cref="Job"/>.
/// </summary>
public virtual void Update()
{
var time = _Job.Time;
time[0] = TimeD;
}
/************************************************************************************************************************/
/// <inheritdoc/>
void IDisposable.Dispose()
=> _Job.Dispose();
/// <inheritdoc/>
public override void Destroy()
{
base.Destroy();
if (Graph != null)
{
Graph.CancelPreUpdate(this);
Graph.Disposables.Remove(this);
}
_Job.Dispose();
}
/************************************************************************************************************************/
/// <inheritdoc/>
public override string ToString()
=> _Job.Job.ToString();
/************************************************************************************************************************/
/// <inheritdoc/>
public override AnimancerState Clone(CloneContext context)
{
var clone = new AnimationJobState<T>(_Job.Job);
clone.CopyFrom(this, context);
return clone;
}
/************************************************************************************************************************/
}
}

View File

@@ -0,0 +1,13 @@
fileFormatVersion: 2
guid: 8a383b34eda0fe0408c12bc3a77108b5
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 UnityEngine;
using UnityEngine.Animations;
using UnityEngine.Playables;
using Object = UnityEngine.Object;
namespace Animancer
{
/// <summary>An <see cref="AnimancerState"/> which plays an <see cref="AnimationClip"/>.</summary>
/// <remarks>
/// <strong>Documentation:</strong>
/// <see href="https://kybernetik.com.au/animancer/docs/manual/playing/states">
/// States</see>
/// </remarks>
/// https://kybernetik.com.au/animancer/api/Animancer/ClipState
///
public class ClipState : AnimancerState
{
/************************************************************************************************************************/
#region Fields and Properties
/************************************************************************************************************************/
private AnimationClip _Clip;
/// <summary>The <see cref="AnimationClip"/> which this state plays.</summary>
public override AnimationClip Clip
{
get => _Clip;
set
{
Validate.AssertAnimationClip(value, true, $"set {nameof(ClipState)}.{nameof(Clip)}");
if (ChangeMainObject(ref _Clip, value))
{
_Length = value.length;
var isLooping = value.isLooping;
if (_IsLooping != isLooping)
{
_IsLooping = isLooping;
OnIsLoopingChangedRecursive(isLooping);
}
}
}
}
/// <summary>The <see cref="AnimationClip"/> which this state plays.</summary>
public override Object MainObject
{
get => _Clip;
set => Clip = (AnimationClip)value;
}
#if UNITY_EDITOR
/// <inheritdoc/>
public override Type MainObjectType
=> typeof(AnimationClip);
#endif
/************************************************************************************************************************/
private float _Length;
/// <summary>The <see cref="AnimationClip.length"/>.</summary>
public override float Length
=> _Length;
/************************************************************************************************************************/
private bool _IsLooping;
/// <summary>The <see cref="Motion.isLooping"/>.</summary>
public override bool IsLooping
=> _IsLooping;
/************************************************************************************************************************/
/// <inheritdoc/>
public override AnimancerEvent.DispatchInfo GetEventDispatchInfo()
{
var length = _Length;
return new(
length,
length != 0 ? Time / length : 0,
_IsLooping);
}
/************************************************************************************************************************/
/// <inheritdoc/>
public override Vector3 AverageVelocity
=> _Clip.averageSpeed;
/************************************************************************************************************************/
#region Inverse Kinematics
/************************************************************************************************************************/
/// <inheritdoc/>
public override bool ApplyAnimatorIK
{
get => _Playable.IsValid() && ((AnimationClipPlayable)_Playable).GetApplyPlayableIK();
set
{
Validate.AssertPlayable(this);
((AnimationClipPlayable)_Playable).SetApplyPlayableIK(value);
}
}
/************************************************************************************************************************/
/// <inheritdoc/>
public override bool ApplyFootIK
{
get => _Playable.IsValid() && ((AnimationClipPlayable)_Playable).GetApplyFootIK();
set
{
Validate.AssertPlayable(this);
((AnimationClipPlayable)_Playable).SetApplyFootIK(value);
}
}
/************************************************************************************************************************/
#endregion
/************************************************************************************************************************/
#endregion
/************************************************************************************************************************/
#region Methods
/************************************************************************************************************************/
/// <summary>Creates a new <see cref="ClipState"/> and sets its <see cref="Clip"/>.</summary>
/// <exception cref="ArgumentNullException">The `clip` is null.</exception>
public ClipState(AnimationClip clip)
{
Validate.AssertAnimationClip(clip, true, $"create {nameof(ClipState)}");
_Clip = clip;
_Length = clip.length;
_IsLooping = clip.isLooping;
}
/************************************************************************************************************************/
/// <summary>Creates and assigns the <see cref="AnimationClipPlayable"/> managed by this node.</summary>
protected override void CreatePlayable(out Playable playable)
{
playable = AnimationClipPlayable.Create(Graph._PlayableGraph, _Clip);
}
/************************************************************************************************************************/
/// <inheritdoc/>
public override void RecreatePlayable()
{
var playable = (AnimationClipPlayable)_Playable;
var footIK = playable.GetApplyFootIK();
var playableIK = playable.GetApplyPlayableIK();
base.RecreatePlayable();
playable = (AnimationClipPlayable)_Playable;
playable.SetApplyFootIK(footIK);
playable.SetApplyPlayableIK(playableIK);
}
/************************************************************************************************************************/
/// <inheritdoc/>
public override void Destroy()
{
_Clip = null;
base.Destroy();
}
/************************************************************************************************************************/
/// <inheritdoc/>
public override AnimancerState Clone(CloneContext context)
{
var clip = context.GetCloneOrOriginal(_Clip);
var clone = new ClipState(clip);
clone.CopyFrom(this, context);
return clone;
}
/************************************************************************************************************************/
#endregion
/************************************************************************************************************************/
}
}

View File

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

View File

@@ -0,0 +1,626 @@
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
#pragma warning disable CS0649 // Field is never assigned to, and will always have its default value.
using System;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Animations;
using UnityEngine.Playables;
using Object = UnityEngine.Object;
namespace Animancer
{
/// <summary>[Pro-Only]
/// An <see cref="AnimancerState"/> which contains other states.
/// </summary>
/// https://kybernetik.com.au/animancer/api/Animancer/ParentState
///
public abstract partial class ParentState : AnimancerState,
ICopyable<ParentState>
{
/************************************************************************************************************************/
#region Properties
/************************************************************************************************************************/
/// <summary>The children contained within this state.</summary>
/// <remarks>Only states up to the <see cref="ChildCount"/> should be assigned.</remarks>
protected AnimancerState[] ChildStates { get; private set; }
= Array.Empty<AnimancerState>();
/************************************************************************************************************************/
private int _ChildCount;
/// <inheritdoc/>
public sealed override int ChildCount
=> _ChildCount;
/************************************************************************************************************************/
/// <summary>The size of the internal array of <see cref="ChildStates"/>.</summary>
/// <remarks>
/// This value starts at 0 then expands to <see cref="ChildCapacity"/>
/// when the first child is added.
/// </remarks>
public int ChildCapacity
{
get => ChildStates.Length;
set
{
if (value == ChildStates.Length)
return;
#if UNITY_ASSERTIONS
if (value <= 1 && OptionalWarning.MixerMinChildren.IsEnabled())
OptionalWarning.MixerMinChildren.Log(
$"The {nameof(ChildCapacity)} of '{this}' is being set to {value}." +
$" The purpose of a mixer is to mix multiple child states so this may be a mistake.",
Graph?.Component);
#endif
var newChildStates = new AnimancerState[value];
if (value > _ChildCount)// Increase size.
{
Array.Copy(ChildStates, newChildStates, _ChildCount);
}
else// Decrease size.
{
for (int i = value; i < _ChildCount; i++)
ChildStates[i].Destroy();
Array.Copy(ChildStates, newChildStates, value);
_ChildCount = value;
}
ChildStates = newChildStates;
if (_Playable.IsValid())
{
_Playable.SetInputCount(value);
}
else if (Graph != null)
{
CreatePlayable();
}
OnChildCapacityChanged();
}
}
/// <summary>Called when the <see cref="ChildCapacity"/> is changed.</summary>
protected virtual void OnChildCapacityChanged() { }
/// <summary><see cref="ChildCapacity"/> starts at 0 then expands to this value when the first child is added.</summary>
/// <remarks>Default 8.</remarks>
public static int DefaultChildCapacity { get; set; } = 8;
/// <summary>
/// Ensures that the remaining unused <see cref="ChildCapacity"/>
/// is greater than or equal to the specified `minimumCapacity`.
/// </summary>
public void EnsureRemainingChildCapacity(int minimumCapacity)
{
minimumCapacity += _ChildCount;
if (ChildCapacity < minimumCapacity)
{
var capacity = Math.Max(ChildCapacity, DefaultChildCapacity);
while (capacity < minimumCapacity)
capacity *= 2;
ChildCapacity = capacity;
}
}
/************************************************************************************************************************/
/// <inheritdoc/>
public sealed override AnimancerState GetChild(int index)
=> ChildStates[index];
/// <inheritdoc/>
public sealed override FastEnumerator<AnimancerState> GetEnumerator()
=> new(ChildStates, _ChildCount);
/************************************************************************************************************************/
#endregion
/************************************************************************************************************************/
#region Initialization
/************************************************************************************************************************/
/// <summary>Creates and assigns the <see cref="AnimationMixerPlayable"/> managed by this state.</summary>
protected override void CreatePlayable(out Playable playable)
{
playable = AnimationMixerPlayable.Create(Graph._PlayableGraph, ChildCapacity);
}
/************************************************************************************************************************/
/// <summary>Connects the `child` to this mixer at its <see cref="AnimancerNode.Index"/>.</summary>
protected internal override void OnAddChild(AnimancerState child)
{
Validate.AssertGraph(child, Graph);
var capacity = ChildCapacity;
if (_ChildCount >= capacity)
ChildCapacity = Math.Max(DefaultChildCapacity, capacity * 2);
child.Index = _ChildCount;
ChildStates[_ChildCount] = child;
_ChildCount++;
child.IsPlaying = IsPlaying;
if (Graph != null)
ConnectChildUnsafe(child.Index, child);
#if UNITY_ASSERTIONS
_CachedToString = null;
#endif
}
/************************************************************************************************************************/
/// <summary>Disconnects the `child` from this mixer at its <see cref="AnimancerNode.Index"/>.</summary>
protected internal override void OnRemoveChild(AnimancerState child)
{
Validate.AssertCanRemoveChild(child, ChildStates, _ChildCount);
// Shuffle all subsequent children down one place.
if (Graph == null || !Graph._PlayableGraph.IsValid())
{
Array.Copy(
ChildStates, child.Index + 1,
ChildStates, child.Index,
_ChildCount - child.Index - 1);
for (int i = child.Index; i < _ChildCount - 1; i++)
ChildStates[i].Index = i;
}
else
{
Graph._PlayableGraph.Disconnect(_Playable, child.Index);
for (int i = child.Index + 1; i < _ChildCount; i++)
{
var otherChild = ChildStates[i];
Graph._PlayableGraph.Disconnect(_Playable, otherChild.Index);
otherChild.Index = i - 1;
ChildStates[i - 1] = otherChild;
ConnectChildUnsafe(i - 1, otherChild);
}
}
_ChildCount--;
ChildStates[_ChildCount] = null;
#if UNITY_ASSERTIONS
_CachedToString = null;
#endif
}
/************************************************************************************************************************/
/// <inheritdoc/>
public override void Destroy()
{
DestroyChildren();
base.Destroy();
}
/************************************************************************************************************************/
/// <inheritdoc/>
public sealed override void CopyFrom(AnimancerState copyFrom, CloneContext context)
=> this.CopyFromBase(copyFrom, context);
/// <inheritdoc/>
public virtual void CopyFrom(ParentState copyFrom, CloneContext context)
{
base.CopyFrom(copyFrom, context);
DestroyChildren();
var childCount = copyFrom.ChildCount;
EnsureRemainingChildCapacity(childCount);
for (int i = 0; i < childCount; i++)
{
var child = copyFrom.ChildStates[i];
child = context.Clone(child);
Add(child);
}
}
/************************************************************************************************************************/
#endregion
/************************************************************************************************************************/
#region Child Configuration
/************************************************************************************************************************/
/// <summary>Assigns the `state` as a child of this mixer.</summary>
/// <remarks>This is the same as calling <see cref="AnimancerState.SetParent"/>.</remarks>
public virtual void Add(AnimancerState child)
=> child.SetParent(this);
/// <summary>Creates and returns a new <see cref="ClipState"/> to play the `clip` as a child of this mixer.</summary>
public virtual ClipState Add(AnimationClip clip)
{
var child = new ClipState(clip);
Add(child);
return child;
}
/// <summary>Calls <see cref="AnimancerUtilities.CreateStateAndApply"/> then <see cref="Add(AnimancerState)"/>.</summary>
public virtual AnimancerState Add(ITransition transition)
{
var child = transition.CreateStateAndApply(Graph);
Add(child);
return child;
}
/// <summary>Calls one of the other <see cref="Add(object)"/> overloads as appropriate for the `child`.</summary>
public virtual AnimancerState Add(object child)
{
if (child is AnimationClip clip)
return Add(clip);
if (child is ITransition transition)
return Add(transition);
if (child is AnimancerState state)
{
Add(state);
return state;
}
MarkAsUsed(this);
throw new ArgumentException(
$"Failed to {nameof(Add)} '{AnimancerUtilities.ToStringOrNull(child)}'" +
$" as child of '{this}' because it isn't an" +
$" {nameof(AnimationClip)}, {nameof(ITransition)}, or {nameof(AnimancerState)}.");
}
/************************************************************************************************************************/
/// <summary>Calls <see cref="Add(AnimationClip)"/> for each of the `clips`.</summary>
public void AddRange(IList<AnimationClip> clips)
{
var count = clips.Count;
EnsureRemainingChildCapacity(count);
for (int i = 0; i < count; i++)
Add(clips[i]);
}
/// <summary>Calls <see cref="Add(AnimationClip)"/> for each of the `clips`.</summary>
public void AddRange(params AnimationClip[] clips)
=> AddRange((IList<AnimationClip>)clips);
/************************************************************************************************************************/
/// <summary>Calls <see cref="Add(ITransition)"/> for each of the `transitions`.</summary>
public void AddRange(IList<ITransition> transitions)
{
var count = transitions.Count;
EnsureRemainingChildCapacity(count);
for (int i = 0; i < count; i++)
Add(transitions[i]);
}
/// <summary>Calls <see cref="Add(ITransition)"/> for each of the `transitions`.</summary>
public void AddRange(params ITransition[] transitions)
=> AddRange((IList<ITransition>)transitions);
/************************************************************************************************************************/
/// <summary>Calls <see cref="Add(object)"/> for each of the `children`.</summary>
public void AddRange(IList<object> children)
{
var count = children.Count;
EnsureRemainingChildCapacity(count);
for (int i = 0; i < count; i++)
Add(children[i]);
}
/// <summary>Calls <see cref="Add(object)"/> for each of the `children`.</summary>
public void AddRange(params object[] children)
=> AddRange((IList<object>)children);
/************************************************************************************************************************/
/// <summary>Removes the child at the specified `index`.</summary>
public void Remove(int index, bool destroy)
=> Remove(ChildStates[index], destroy);
/// <summary>Removes the specified `child`.</summary>
public void Remove(AnimancerState child, bool destroy)
{
#if UNITY_ASSERTIONS
if (child.Parent != this)
Debug.LogWarning($"Attempting to remove a state which is not a child of this {GetType().Name}." +
$" This will remove the child from its actual parent so you should directly call" +
$" child.{nameof(child.Destroy)} or child.{nameof(child.SetParent)}(null, -1) instead." +
$"\n• Child: {child}" +
$"\n• Removing From: {this}" +
$"\n• Actual Parent: {child.Parent}",
Graph?.Component as Object);
#endif
if (destroy)
child.Destroy();
else
child.SetParent(null);
}
/************************************************************************************************************************/
/// <summary>Replaces the `child` at the specified `index`.</summary>
public virtual void Set(int index, AnimancerState child, bool destroyPrevious)
{
#if UNITY_ASSERTIONS
if ((uint)index >= _ChildCount)
{
MarkAsUsed(this);
MarkAsUsed(child);
throw new IndexOutOfRangeException(
$"Invalid child index. Must be 0 <= index < {nameof(ChildCount)} ({ChildCount}).");
}
#endif
if (child.Parent != null)
child.SetParent(null);
var previousChild = ChildStates[index];
previousChild.SetParentInternal(null);
child.SetGraph(Graph);
ChildStates[index] = child;
child.SetParentInternal(this, index);
child.IsPlaying = IsPlaying;
if (Graph != null)
{
Graph._PlayableGraph.Disconnect(_Playable, index);
ConnectChildUnsafe(index, child);
}
child.CopyIKFlags(this);
if (destroyPrevious)
previousChild.Destroy();
#if UNITY_ASSERTIONS
_CachedToString = null;
#endif
}
/// <summary>Replaces the child at the specified `index` with a new <see cref="ClipState"/>.</summary>
public ClipState Set(int index, AnimationClip clip, bool destroyPrevious)
{
var child = new ClipState(clip);
Set(index, child, destroyPrevious);
return child;
}
/// <summary>Replaces the child at the specified `index` with a <see cref="ITransition.CreateState"/>.</summary>
public AnimancerState Set(int index, ITransition transition, bool destroyPrevious)
{
var child = transition.CreateStateAndApply(Graph);
Set(index, child, destroyPrevious);
return child;
}
/// <summary>Calls one of the other <see cref="Set(int, object, bool)"/> overloads as appropriate for the `child`.</summary>
public AnimancerState Set(int index, object child, bool destroyPrevious)
{
if (child is AnimationClip clip)
return Set(index, clip, destroyPrevious);
if (child is ITransition transition)
return Set(index, transition, destroyPrevious);
if (child is AnimancerState state)
{
Set(index, state, destroyPrevious);
return state;
}
MarkAsUsed(this);
throw new ArgumentException(
$"Failed to {nameof(Set)} '{AnimancerUtilities.ToStringOrNull(child)}'" +
$" as child of '{this}' because it isn't an" +
$" {nameof(AnimationClip)}, {nameof(ITransition)}, or {nameof(AnimancerState)}.");
}
/************************************************************************************************************************/
/// <summary>Returns the index of the specified `child` state.</summary>
public int IndexOf(AnimancerState child)
=> Array.IndexOf(ChildStates, child, 0, _ChildCount);
/************************************************************************************************************************/
/// <summary>
/// Destroys all <see cref="ChildStates"/> connected to this mixer.
/// This operation cannot be undone.
/// </summary>
public void DestroyChildren()
{
for (int i = _ChildCount - 1; i >= 0; i--)
ChildStates[i].Destroy();
Array.Clear(ChildStates, 0, _ChildCount);
_ChildCount = 0;
}
/************************************************************************************************************************/
#endregion
/************************************************************************************************************************/
#region Updates
/************************************************************************************************************************/
/// <inheritdoc/>
protected internal override void UpdateEvents()
=> UpdateEventsRecursive(this);
/************************************************************************************************************************/
#endregion
/************************************************************************************************************************/
#region Inverse Kinematics
/************************************************************************************************************************/
private bool _ApplyAnimatorIK;
/// <inheritdoc/>
public override bool ApplyAnimatorIK
{
get => _ApplyAnimatorIK;
set => base.ApplyAnimatorIK = _ApplyAnimatorIK = value;
}
/************************************************************************************************************************/
private bool _ApplyFootIK;
/// <inheritdoc/>
public override bool ApplyFootIK
{
get => _ApplyFootIK;
set => base.ApplyFootIK = _ApplyFootIK = value;
}
/************************************************************************************************************************/
#endregion
/************************************************************************************************************************/
#region Other Methods
/************************************************************************************************************************/
#if UNITY_ASSERTIONS
/// <summary>[Assert-Only] A string built by <see cref="ToString"/> to describe this mixer.</summary>
private string _CachedToString;
#endif
/// <summary>
/// Returns a string describing the type of this mixer and the name of states connected to it.
/// </summary>
public override string ToString()
{
#if UNITY_ASSERTIONS
if (NameCache.TryToString(DebugName, out var name))
return name;
if (_CachedToString != null)
return _CachedToString;
#endif
// Gather child names.
var childNames = ListPool.Acquire<string>();
var allSimple = true;
for (int i = 0; i < _ChildCount; i++)
{
var state = ChildStates[i];
if (state == null)
continue;
if (state.MainObject != null)
{
childNames.Add(state.MainObject.name);
}
else
{
childNames.Add(state.ToString());
allSimple = false;
}
}
// If they all have a main object, check if they all have the same prefix so it doesn't need to be repeated.
int prefixLength = 0;
var count = childNames.Count;
if (count <= 1 || !allSimple)
{
prefixLength = 0;
}
else
{
var prefix = childNames[0];
var shortest = prefixLength = prefix.Length;
for (int iName = 0; iName < count; iName++)
{
var childName = childNames[iName];
if (shortest > childName.Length)
{
shortest = prefixLength = childName.Length;
}
for (int iCharacter = 0; iCharacter < prefixLength; iCharacter++)
{
if (childName[iCharacter] != prefix[iCharacter])
{
prefixLength = iCharacter;
break;
}
}
}
if (prefixLength < 3 ||// Less than 3 characters probably isn't an intentional prefix.
prefixLength >= shortest)
prefixLength = 0;
}
// Build the parent name.
var parentName = StringBuilderPool.Instance.Acquire();
var type = GetType().Name;
if (type.EndsWith("State"))
parentName.Append(type, 0, type.Length - 5);
else
parentName.Append(type);
parentName.Append('(');
if (count > 0)
{
if (prefixLength > 0)
parentName.Append(childNames[0], 0, prefixLength).Append('[');
for (int i = 0; i < count; i++)
{
if (i > 0)
parentName.Append(", ");
var childName = childNames[i];
parentName.Append(childName, prefixLength, childName.Length - prefixLength);
}
parentName.Append(']');
}
ListPool.Release(childNames);
parentName.Append(')');
var result = parentName.ReleaseToString();
#if UNITY_ASSERTIONS
_CachedToString = result;
#endif
return result;
}
/************************************************************************************************************************/
/// <inheritdoc/>
public override void GatherAnimationClips(ICollection<AnimationClip> clips)
=> clips.GatherFromSource(ChildStates);
/************************************************************************************************************************/
#endregion
/************************************************************************************************************************/
}
}

View File

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

View File

@@ -0,0 +1,442 @@
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
using System;
using System.Collections.Generic;
using System.Text;
using UnityEngine;
using UnityEngine.Animations;
using UnityEngine.Audio;
using UnityEngine.Playables;
using Object = UnityEngine.Object;
namespace Animancer
{
/// <summary>[Pro-Only] An <see cref="AnimancerState"/> which plays a <see cref="PlayableAsset"/>.</summary>
/// <remarks>
/// <strong>Documentation:</strong>
/// <see href="https://kybernetik.com.au/animancer/docs/manual/timeline">
/// Timeline</see>
/// </remarks>
/// https://kybernetik.com.au/animancer/api/Animancer/PlayableAssetState
public class PlayableAssetState : AnimancerState
{
/************************************************************************************************************************/
#region Fields and Properties
/************************************************************************************************************************/
/// <summary>The <see cref="PlayableAsset"/> which this state plays.</summary>
private PlayableAsset _Asset;
/// <summary>The <see cref="PlayableAsset"/> which this state plays.</summary>
public PlayableAsset Asset
{
get => _Asset;
set => ChangeMainObject(ref _Asset, value);
}
/// <summary>The <see cref="PlayableAsset"/> which this state plays.</summary>
public override Object MainObject
{
get => _Asset;
set => _Asset = (PlayableAsset)value;
}
#if UNITY_EDITOR
/// <inheritdoc/>
public override Type MainObjectType
=> typeof(PlayableAsset);
#endif
/************************************************************************************************************************/
private float _Length;
/// <summary>The <see cref="PlayableAsset.duration"/> (cached on initialization).</summary>
public override float Length
=> _Length;
/************************************************************************************************************************/
/// <inheritdoc/>
public override float Speed
{
get => base.Speed;
set
{
base.Speed = value;
for (int i = Outputs.Count - 1; i >= 0; i--)
Outputs[i].GetSourcePlayable().SetSpeed(value);
}
}
/************************************************************************************************************************/
/// <inheritdoc/>
public override AnimancerEvent.DispatchInfo GetEventDispatchInfo()
{
var length = _Length;
return new(
length,
length != 0 ? Time / length : 0,
false);
}
/************************************************************************************************************************/
/// <inheritdoc/>
protected override void OnSetIsPlaying()
{
if (!_Playable.IsValid())
return;
var inputCount = _Playable.GetInputCount();
for (int i = 0; i < inputCount; i++)
{
var playable = _Playable.GetInput(i);
if (!playable.IsValid())
continue;
if (IsPlaying)
playable.Play();
else
playable.Pause();
}
}
/************************************************************************************************************************/
/// <summary>IK cannot be dynamically enabled on a <see cref="PlayableAssetState"/>.</summary>
public override void CopyIKFlags(AnimancerNodeBase copyFrom) { }
/************************************************************************************************************************/
/// <summary>IK cannot be dynamically enabled on a <see cref="PlayableAssetState"/>.</summary>
public override bool ApplyAnimatorIK
{
get => false;
set
{
#if UNITY_ASSERTIONS
if (value)
OptionalWarning.UnsupportedIK.Log(
$"IK cannot be dynamically enabled on a {nameof(PlayableAssetState)}.", Graph?.Component);
#endif
}
}
/************************************************************************************************************************/
/// <summary>IK cannot be dynamically enabled on a <see cref="PlayableAssetState"/>.</summary>
public override bool ApplyFootIK
{
get => false;
set
{
#if UNITY_ASSERTIONS
if (value)
OptionalWarning.UnsupportedIK.Log(
$"IK cannot be dynamically enabled on a {nameof(PlayableAssetState)}.", Graph?.Component);
#endif
}
}
/************************************************************************************************************************/
#endregion
/************************************************************************************************************************/
#region Methods
/************************************************************************************************************************/
/// <summary>Creates a new <see cref="PlayableAssetState"/> to play the `asset`.</summary>
/// <exception cref="ArgumentNullException">The `asset` is null.</exception>
public PlayableAssetState(PlayableAsset asset)
{
if (asset == null)
throw new ArgumentNullException(nameof(asset));
_Asset = asset;
}
/************************************************************************************************************************/
/// <inheritdoc/>
protected override void CreatePlayable(out Playable playable)
{
playable = _Asset.CreatePlayable(Graph._PlayableGraph, Graph.Component.gameObject);
playable.SetDuration(9223372.03685477);// https://github.com/KybernetikGames/animancer/issues/111
_Length = (float)_Asset.duration;
if (!_HasInitializedBindings)
InitializeBindings();
}
/************************************************************************************************************************/
private readonly List<PlayableOutput>
Outputs = new();
private IList<Object> _Bindings;
private bool _HasInitializedBindings;
/************************************************************************************************************************/
/// <summary>The objects controlled by each track in the asset.</summary>
public IList<Object> Bindings
{
get => _Bindings;
set
{
DestroyBoundOutputs(false);
_Bindings = value;
InitializeBindings();
}
}
/************************************************************************************************************************/
/// <summary>Sets the <see cref="Bindings"/>.</summary>
public void SetBindings(params Object[] bindings)
{
Bindings = bindings;
}
/************************************************************************************************************************/
private void InitializeBindings()
{
if (Graph == null)
return;
_HasInitializedBindings = true;
Validate.AssertPlayable(this);
var graph = Graph._PlayableGraph;
var bindableIndex = 0;
var bindableCount = _Bindings != null
? _Bindings.Count
: 0;
var speed = Speed;
foreach (var binding in _Asset.outputs)
{
GetBindingDetails(binding, out var trackName, out var trackType, out var isMarkers);
var bindable = bindableIndex < bindableCount
? _Bindings[bindableIndex]
: null;
#if UNITY_ASSERTIONS
if (!isMarkers &&
trackType != null &&
bindable != null &&
!trackType.IsAssignableFrom(bindable.GetType()))
{
Debug.LogError(
$"Binding Type Mismatch: bindings[{bindableIndex}] is '{bindable}'" +
$" but should be a {trackType.FullName} for {trackName}",
Graph.Component as Object);
bindableIndex++;
continue;
}
#endif
var playable = _Playable.GetInput(bindableIndex);
if (speed != 1)
playable.SetSpeed(speed);
if (trackType == typeof(Animator))// AnimationTrack.
{
if (bindable != null)
{
#if UNITY_ASSERTIONS
if (bindable == Graph.Component?.Animator)
Debug.LogError(
$"{nameof(PlayableAsset)} tracks should not be bound to the same {nameof(Animator)} as" +
$" Animancer. Leaving the binding of the first Animation Track empty will automatically" +
$" apply its animation to the object being controlled by Animancer.",
Graph.Component as Object);
#endif
var playableOutput = AnimationPlayableOutput.Create(graph, trackName, (Animator)bindable);
playableOutput.SetReferenceObject(binding.sourceObject);
playableOutput.SetSourcePlayable(playable);
playableOutput.SetWeight(1);
Outputs.Add(playableOutput);
}
}
#if UNITY_AUDIO
else if (trackType == typeof(AudioSource))// AudioTrack.
{
if (bindable != null)
{
var playableOutput = AudioPlayableOutput.Create(graph, trackName, (AudioSource)bindable);
playableOutput.SetReferenceObject(binding.sourceObject);
playableOutput.SetSourcePlayable(playable);
playableOutput.SetWeight(1);
Outputs.Add(playableOutput);
}
}
#endif
else if (isMarkers)// Markers.
{
var animancer = Graph.Component as Component;
var playableOutput = ScriptPlayableOutput.Create(graph, trackName);
playableOutput.SetReferenceObject(binding.sourceObject);
playableOutput.SetSourcePlayable(playable);
playableOutput.SetWeight(1);
playableOutput.SetUserData(animancer);
Outputs.Add(playableOutput);
var receivers = ListPool.Acquire<INotificationReceiver>();
animancer.GetComponents(receivers);
for (int i = 0; i < receivers.Count; i++)
playableOutput.AddNotificationReceiver(receivers[i]);
ListPool.Release(receivers);
continue;// Don't increment the bindingIndex.
}
else// ActivationTrack, ControlTrack, PlayableTrack, SignalTrack.
{
var playableOutput = ScriptPlayableOutput.Create(graph, trackName);
playableOutput.SetReferenceObject(binding.sourceObject);
playableOutput.SetSourcePlayable(playable);
playableOutput.SetWeight(1);
playableOutput.SetUserData(bindable);
if (bindable is INotificationReceiver receiver)
playableOutput.AddNotificationReceiver(receiver);
Outputs.Add(playableOutput);
}
bindableIndex++;
}
}
/************************************************************************************************************************/
/// <summary>Gathers details about the `binding`.</summary>
public static void GetBindingDetails(
PlayableBinding binding,
out string name,
out Type type,
out bool isMarkers)
{
name = binding.streamName;
type = binding.outputTargetType;
isMarkers = type == typeof(GameObject) && name == "Markers";
}
/************************************************************************************************************************/
/// <inheritdoc/>
public override void SetWeight(float value)
{
base.SetWeight(value);
for (int i = Outputs.Count - 1; i >= 0; i--)
Outputs[i].SetWeight(value);
}
/************************************************************************************************************************/
/// <inheritdoc/>
public override void Destroy()
{
_Asset = null;
DestroyBoundOutputs(true);
base.Destroy();
}
/************************************************************************************************************************/
/// <summary>
/// Destroys all of the outputs created for the <see cref="Bindings"/>
/// and optionally the state playable itself.
/// </summary>
public void DestroyBoundOutputs(bool destroyStatePlayable)
{
if (Graph == null)
return;
var graph = Graph._PlayableGraph;
if (!graph.IsValid())
return;
for (int i = Outputs.Count - 1; i >= 0; i--)
{
var output = Outputs[i];
var playable = output.GetSourcePlayable();
if (playable.IsValid())
graph.DestroySubgraph(playable);
graph.DestroyOutput(output);
}
Outputs.Clear();
if (destroyStatePlayable && _Playable.IsValid())
graph.DestroySubgraph(_Playable);
}
/************************************************************************************************************************/
/// <inheritdoc/>
public override AnimancerState Clone(CloneContext context)
{
var asset = context.GetCloneOrOriginal(_Asset);
var clone = new PlayableAssetState(asset);
clone.CopyFrom(this, context);
return clone;
}
/************************************************************************************************************************/
/// <inheritdoc/>
protected override void AppendDetails(StringBuilder text, string separator)
{
base.AppendDetails(text, separator);
text.Append(separator)
.Append($"{nameof(Bindings)}: ");
int count;
if (_Bindings == null)
{
text.Append("Null");
count = 0;
}
else
{
count = _Bindings.Count;
text.Append('[')
.Append(count)
.Append(']');
}
text.Append(_HasInitializedBindings
? " (Initialized)"
: " (Not Initialized)");
for (int i = 0; i < count; i++)
{
text.Append(separator)
.Append($"{nameof(Bindings)}[")
.Append(i)
.Append("] = ")
.Append(AnimancerUtilities.ToStringOrNull(_Bindings[i]));
}
}
/************************************************************************************************************************/
#endregion
/************************************************************************************************************************/
}
}

View File

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

View File

@@ -0,0 +1,375 @@
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
#pragma warning disable CS0649 // Field is never assigned to, and will always have its default value.
using System;
using System.Collections.Generic;
using UnityEngine;
namespace Animancer
{
/// <summary>[Pro-Only]
/// An <see cref="AnimancerState"/> which plays a sequence of other states.
/// </summary>
/// https://kybernetik.com.au/animancer/api/Animancer/SequenceState
///
public partial class SequenceState : ParentState,
ICopyable<SequenceState>,
IUpdatable
{
/************************************************************************************************************************/
#region Fields and Properties
/************************************************************************************************************************/
private double[] _TimeOffsets = Array.Empty<double>();
private double[] _FadeEndTimes = Array.Empty<double>();
private double[] _StateEndTimes = Array.Empty<double>();
/// <summary>The index of the child state which is active at the current time.</summary>
private int _ActiveChildIndex;
/************************************************************************************************************************/
/// <inheritdoc/>
public override float Length
=> ChildCount > 0
? (float)_StateEndTimes[ChildCount - 1]
: 0;
/************************************************************************************************************************/
/// <inheritdoc/>
public override double RawTime
{
get => base.RawTime;
set
{
base.RawTime = value;
if (ChildCount == 0)
return;
var activeChildIndex = GetActiveChildIndex(value);
SetActiveChildIndex(activeChildIndex);
for (int i = 0; i < ChildCount; i++)
{
var child = ChildStates[i];
var childTime = value;
if (i > 0)
childTime -= _StateEndTimes[i - 1];
childTime *= child.Speed;
child.TimeD = childTime + _TimeOffsets[i];
}
}
}
/************************************************************************************************************************/
/// <inheritdoc/>
public override void MoveTime(double time, bool normalized)
{
base.MoveTime(time, normalized);
for (int i = 0; i < ChildCount; i++)
{
var child = ChildStates[i];
var childTime = time;
if (i > 0)
childTime -= _StateEndTimes[i - 1];
childTime *= child.Speed;
child.MoveTime(childTime + _TimeOffsets[i], normalized);
}
}
/************************************************************************************************************************/
/// <summary>Sequences don't loop.</summary>
/// <remarks>
/// If the last state in the sequence is set to loop it will do so,
/// but the rest of the sequence won't replay automatically.
/// </remarks>
public override bool IsLooping
=> false;
/************************************************************************************************************************/
#endregion
/************************************************************************************************************************/
#region Initialisation
/************************************************************************************************************************/
/// <summary>Replaces the child states with new ones created from the `transitions`.</summary>
public void Set(params ITransition[] transitions)
=> Set((IList<ITransition>)transitions);
/// <summary>Replaces the child states with new ones created from the `transitions`.</summary>
public void Set(IList<ITransition> transitions)
{
var oldChildCount = ChildCount;
var newChildCount = transitions.Count;
ChildCapacity = newChildCount;
for (int i = 0; i < newChildCount; i++)
{
var transition = transitions[i];
var state = transition.CreateStateAndApply(Graph);
state.IsPlaying = IsPlaying;
if (i < oldChildCount)
Set(i, state, true);
else
Add(state);
_FadeEndTimes[i] += transition.FadeDuration;
}
while (oldChildCount > newChildCount)
Remove(--oldChildCount, true);
}
/************************************************************************************************************************/
/// <inheritdoc/>
protected internal override void OnAddChild(AnimancerState child)
{
base.OnAddChild(child);
GatherChildDetails(child);
}
/************************************************************************************************************************/
/// <summary>Gathers the timing details of a newly added `child` state.</summary>
private void GatherChildDetails(AnimancerState child)
{
var index = child.Index;
if (index == 0)
child.Weight = 1;
_TimeOffsets[index] = child.TimeD;
var startTime = GetStartTime(index);
_FadeEndTimes[index] = startTime;
var length = child.RemainingDuration;
if (length < 0)
length = 0;
startTime += length;
_StateEndTimes[index] = startTime;
}
/************************************************************************************************************************/
/// <inheritdoc/>
public override AnimancerState Add(ITransition transition)
{
var child = base.Add(transition);
_FadeEndTimes[child.Index] += transition.FadeDuration;
return child;
}
/************************************************************************************************************************/
/// <summary>Adds the `child` to the end of this sequence.</summary>
public void Add(AnimancerState child, float fadeDuration)
{
Add(child);
_FadeEndTimes[child.Index] += fadeDuration;
}
/// <summary>Adds the `clip` to the end of this sequence.</summary>
public void Add(AnimationClip clip, float fadeDuration)
{
var child = Add(clip);
_FadeEndTimes[child.Index] += fadeDuration;
}
/************************************************************************************************************************/
#endregion
/************************************************************************************************************************/
#region Execution
/************************************************************************************************************************/
/// <inheritdoc/>
protected override void OnChildCapacityChanged()
{
base.OnChildCapacityChanged();
var capacity = ChildCapacity;
Array.Resize(ref _TimeOffsets, capacity);
Array.Resize(ref _FadeEndTimes, capacity);
Array.Resize(ref _StateEndTimes, capacity);
}
/************************************************************************************************************************/
/// <inheritdoc/>
int IUpdatable.UpdatableIndex { get; set; } = IUpdatable.List.NotInList;
/************************************************************************************************************************/
/// <inheritdoc/>
protected override void OnSetIsPlaying()
{
base.OnSetIsPlaying();
var isPlaying = IsPlaying;
var childStates = ChildStates;
for (int i = 0; i < ChildCount; i++)
childStates[i].IsPlaying = isPlaying;
if (IsPlaying)
Graph.RequirePreUpdate(this);
else
Graph.CancelPreUpdate(this);
}
/************************************************************************************************************************/
/// <inheritdoc/>
public override void Destroy()
{
base.Destroy();
Graph.CancelPreUpdate(this);
}
/************************************************************************************************************************/
/// <summary>
/// Called every frame while this state is playing to control its children.
/// </summary>
public virtual void Update()
{
var time = Time;
var activeChildIndex = _ActiveChildIndex;
if (Speed >= 0)
{
while (activeChildIndex < ChildCount - 1 && time > _StateEndTimes[activeChildIndex])
activeChildIndex++;
}
else
{
while (activeChildIndex > 0 && time > _StateEndTimes[activeChildIndex - 1])
activeChildIndex--;
}
SetActiveChildIndex(activeChildIndex);
var startTime = GetStartTime(activeChildIndex);
var endFadeTime = _FadeEndTimes[activeChildIndex];
if (activeChildIndex == 0 || time > endFadeTime)
{
ChildStates[activeChildIndex].Weight = 1;
if (activeChildIndex > 0)
ChildStates[activeChildIndex - 1].Weight = 0;
}
else
{
var weight = Mathf.InverseLerp((float)startTime, (float)endFadeTime, time);
ChildStates[activeChildIndex].Weight = weight;
ChildStates[activeChildIndex - 1].Weight = 1 - weight;
}
}
/************************************************************************************************************************/
/// <summary>
/// Calculates the indices of the `first` and `last` child states
/// which should be active at the specified `time`.
/// </summary>
public int GetActiveChildIndex(double time)
{
var index = Array.BinarySearch(_StateEndTimes, time);
if (index < 0)
index = ~index;
if (index >= ChildCount)
index = ChildCount - 1;
return index;
}
/************************************************************************************************************************/
/// <summary>Clears the weights of any active chhildren and sets the newly active child.</summary>
private void SetActiveChildIndex(int index)
{
if (_ActiveChildIndex == index)
return;
ChildStates[_ActiveChildIndex].Weight = 0;
if (_ActiveChildIndex > 0)
ChildStates[_ActiveChildIndex - 1].Weight = 0;
_ActiveChildIndex = index;
}
/************************************************************************************************************************/
/// <summary>Gets the time when the specified child starts relative to the start of this sequence.</summary>
public double GetStartTime(int childIndex)
=> childIndex > 0
? _StateEndTimes[childIndex - 1]
: 0;
/************************************************************************************************************************/
#endregion
/************************************************************************************************************************/
#region Copying
/************************************************************************************************************************/
/// <inheritdoc/>
public override AnimancerState Clone(CloneContext context)
{
var clone = new SequenceState();
clone.CopyFrom(this, context);
return clone;
}
/************************************************************************************************************************/
/// <inheritdoc/>
public sealed override void CopyFrom(ParentState copyFrom, CloneContext context)
=> this.CopyFromBase(copyFrom, context);
/// <inheritdoc/>
public virtual void CopyFrom(SequenceState copyFrom, CloneContext context)
{
base.CopyFrom(copyFrom, context);
var childCount = Math.Min(copyFrom.ChildCount, ChildCount);
Array.Copy(copyFrom._TimeOffsets, 0, _TimeOffsets, 0, childCount);
Array.Copy(copyFrom._FadeEndTimes, 0, _FadeEndTimes, 0, childCount);
Array.Copy(copyFrom._StateEndTimes, 0, _StateEndTimes, 0, childCount);
_ActiveChildIndex = copyFrom._ActiveChildIndex;
}
/************************************************************************************************************************/
#endregion
/************************************************************************************************************************/
}
}

View File

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

View File

@@ -0,0 +1,60 @@
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
using UnityEngine.Playables;
namespace Animancer
{
/// <summary>
/// A <see cref="PlayableBehaviour"/> which executes <see cref="IUpdatable.Update"/>
/// on each item in an <see cref="IUpdatable.List"/> every frame.
/// </summary>
public class UpdatableListPlayable : PlayableBehaviour
{
/************************************************************************************************************************/
/// <summary>
/// Since <see cref="ScriptPlayable{T}.Create(PlayableGraph, int)"/> needs to clone an existing instance,
/// we keep a static template to avoid allocating an extra garbage one every time.
/// This also means the fields can't be readonly because field initializers don't run on the clone.
/// </summary>
private static readonly UpdatableListPlayable Template = new();
/************************************************************************************************************************/
/// <summary>The <see cref="AnimancerGraph"/> this behaviour is connected to.</summary>
private AnimancerGraph _Graph;
/// <summary>Objects to be updated before time advances.</summary>
private IUpdatable.List _Updatables;
/************************************************************************************************************************/
/// <summary>Creates a new <see cref="UpdatableListPlayable"/>.</summary>
public static ScriptPlayable<UpdatableListPlayable> Create(
AnimancerGraph graph,
int inputCount,
IUpdatable.List updatables)
{
var playable = ScriptPlayable<UpdatableListPlayable>.Create(graph._PlayableGraph, Template, inputCount);
var instance = playable.GetBehaviour();
instance._Graph = graph;
instance._Updatables = updatables;
return playable;
}
/************************************************************************************************************************/
/// <summary>[Internal] Calls <see cref="IUpdatable.Update"/> on everything added to this list.</summary>
/// <remarks>
/// Called by the <see cref="PlayableGraph"/> after the rest of the <see cref="Playable"/>s are evaluated.
/// </remarks>
public override void PrepareFrame(Playable playable, FrameData info)
=> _Graph.UpdateAll(
_Updatables,
info.deltaTime * info.effectiveParentSpeed,
info.frameId);
/************************************************************************************************************************/
}
}

View File

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

View File

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

View File

@@ -0,0 +1,128 @@
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
using System;
using Unity.Collections;
using UnityEngine;
using UnityEngine.Animations;
namespace Animancer
{
/// <summary>
/// A replacement for the default <see cref="AnimationLayerMixerPlayable"/> which uses custom
/// <see cref="BoneWeights"/> for each individual bone instead of just using an <see cref="AvatarMask"/>
/// to include or exclude them entirely.
/// </summary>
/// https://kybernetik.com.au/animancer/api/Animancer/WeightedMaskLayerList
public class WeightedMaskLayerList : AnimancerLayerList, IDisposable
{
/************************************************************************************************************************/
/// <summary>The objects being masked.</summary>
public readonly Transform[] Bones;
/// <summary>The job data.</summary>
private readonly WeightedMaskMixerJob _Job;
/************************************************************************************************************************/
/// <summary>The root motion weight of each layer.</summary>
public NativeArray<float> RootMotionWeights
=> _Job.rootMotionWeights;
/************************************************************************************************************************/
/// <summary>The number of objects being masked (excluding the root bone).</summary>
public int BoneCount
=> Bones.Length - 1;
/************************************************************************************************************************/
/// <summary>The blend weight of each of the <see cref="Bones"/>.</summary>
public NativeArray<float> BoneWeights
=> _Job.boneWeights;
/************************************************************************************************************************/
/// <summary>Returns the index of the value corresponding to the 'bone' in the <see cref="BoneWeights"/> array.</summary>
public int IndexOf(Transform bone)
=> Array.IndexOf(Bones, bone) - 1;// Index - 1 since the root is ignored.
/************************************************************************************************************************/
/// <summary>Creates a new <see cref="AnimancerGraph"/> and <see cref="WeightedMaskLayerList"/>.</summary>
/// <remarks>
/// This method can't be a constructor because it would need to
/// assign itself to the graph before being fully constructed.
/// </remarks>
public static WeightedMaskLayerList Create(Animator animator, int layerCount)
{
var graph = new AnimancerGraph();
var layers = new WeightedMaskLayerList(graph, animator, layerCount);
graph.Layers = layers;
return layers;
}
/// <summary>Creates a new <see cref="WeightedMaskLayerList"/>.</summary>
public WeightedMaskLayerList(AnimancerGraph graph, Animator animator, int layerCount)
: base(graph, layerCount)
{
if (layerCount < 2)
throw new ArgumentOutOfRangeException(
nameof(layerCount),
"Layer count must be at least 2 (Base + 1).");
graph.Layers = this;
Bones = animator.GetComponentsInChildren<Transform>(true);
var boneCount = BoneCount;
_Job = new WeightedMaskMixerJob()
{
boneTransforms = new(boneCount, Allocator.Persistent, NativeArrayOptions.UninitializedMemory),
boneWeights = new(boneCount * (layerCount - 1), Allocator.Persistent, NativeArrayOptions.ClearMemory),
layerCount = layerCount,
rootMotionWeights = new(layerCount, Allocator.Persistent, NativeArrayOptions.UninitializedMemory),
};
graph.Disposables.Add(this);
for (var i = 0; i < layerCount; i++)
_Job.rootMotionWeights[i] = 1;
for (var i = 0; i < boneCount; i++)
{
_Job.boneTransforms[i] = animator.BindStreamTransform(Bones[i + 1]);
_Job.boneWeights[i] = 1;
}
var playable = AnimationScriptPlayable.Create(graph, _Job, Capacity);
playable.SetProcessInputs(false);
Playable = playable;
}
/************************************************************************************************************************/
/// <inheritdoc/>
void IDisposable.Dispose()
{
_Job.rootMotionWeights.Dispose();
_Job.boneTransforms.Dispose();
_Job.boneWeights.Dispose();
}
/************************************************************************************************************************/
/// <inheritdoc/>
public override AnimancerLayer Add()
{
if (Count >= Capacity)
throw new InvalidOperationException(
$"{nameof(WeightedMaskLayerList)} doesn't support dynamically changing its layer count.");
return base.Add();
}
/************************************************************************************************************************/
}
}

View File

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

View File

@@ -0,0 +1,264 @@
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
using System;
using Unity.Collections;
using UnityEngine;
namespace Animancer
{
/// <summary>
/// Replaces the default <see cref="AnimancerLayerMixerList"/>
/// with a <see cref="WeightedMaskLayerList"/>.
/// </summary>
/// https://kybernetik.com.au/animancer/api/Animancer/WeightedMaskLayers
[AddComponentMenu(Strings.MenuPrefix + "Weighted Mask Layers")]
[AnimancerHelpUrl(typeof(WeightedMaskLayers))]
[DefaultExecutionOrder(-10000)]// Awake before anything else initializes Animancer.
public class WeightedMaskLayers : MonoBehaviour
{
/************************************************************************************************************************/
[SerializeField] private AnimancerComponent _Animancer;
/// <summary>[<see cref="SerializeField"/>] The component to apply the layers to.</summary>
public AnimancerComponent Animancer
=> _Animancer;
/************************************************************************************************************************/
[SerializeField] private WeightedMaskLayersDefinition _Definition;
/// <summary>[<see cref="SerializeField"/>]
/// The definition of transforms to control and weights to apply to them.
/// </summary>
public ref WeightedMaskLayersDefinition Definition
=> ref _Definition;
/************************************************************************************************************************/
[SerializeField] private int _LayerCount = 2;
/// <summary>[<see cref="SerializeField"/>] The number of layers (minimum 2).</summary>
public ref int LayerCount
=> ref _LayerCount;
/************************************************************************************************************************/
/// <summary>The layer list created at runtime and assigned to <see cref="AnimancerGraph.Layers"/>.</summary>
public WeightedMaskLayerList Layers { get; protected set; }
/************************************************************************************************************************/
/// <summary>The index of each of the <see cref="WeightedMaskLayersDefinition.Transforms"/>.</summary>
public int[] Indices { get; protected set; }
/************************************************************************************************************************/
/// <summary>Finds the <see cref="Animancer"/> reference if it was missing.</summary>
protected virtual void OnValidate()
{
gameObject.GetComponentInParentOrChildren(ref _Animancer);
if (LayerCount < 2)
LayerCount = 2;
}
/************************************************************************************************************************/
/// <summary>Initializes the <see cref="Layers"/> and applies the default group weights.</summary>
protected virtual void Awake()
{
if (Definition == null ||
!Definition.IsValid)
return;
if (_Animancer == null)
TryGetComponent(out _Animancer);
Layers = WeightedMaskLayerList.Create(_Animancer.Animator, LayerCount);
_Animancer.InitializeGraph(Layers.Graph);
Indices = Definition.CalculateIndices(Layers);
for (int i = 1; i < LayerCount; i++)// Start at 1.
SetWeights(i, 0);
}
/************************************************************************************************************************/
/// <summary>Applies the weights of the specified group to the specified layer.</summary>
public void SetWeights(int layerIndex, int groupIndex)
{
Definition.AssertGroupIndex(groupIndex);
var boneWeights = Layers.BoneWeights;
var definitionWeights = Definition.Weights;
var layerIndexOffset = (layerIndex - 1) * Layers.BoneCount;
var groupDefinitionStart = groupIndex * Indices.Length;
for (int i = 0; i < Indices.Length; i++)
{
var index = Indices[i];
if (index < 0)
continue;
var weight = definitionWeights[groupDefinitionStart + i];
boneWeights[layerIndexOffset + index] = weight;
}
var rootMotionWeights = Layers.RootMotionWeights;
rootMotionWeights[layerIndex] = Definition.RootMotionWeights[groupIndex];
}
/************************************************************************************************************************/
private Fade _Fade;
/// <summary>Fades the weights towards the specified group.</summary>
public void FadeWeights(
int layerIndex,
int groupIndex,
float fadeDuration,
Func<float, float> easing = null)
{
if (fadeDuration > 0)
{
_Fade ??= new();
_Fade.Start(this, layerIndex, groupIndex, fadeDuration, easing);
}
else
{
SetWeights(layerIndex, groupIndex);
}
}
/************************************************************************************************************************/
/// <summary>An <see cref="IUpdatable"/> which fades <see cref="WeightedMaskLayers"/> over time.</summary>
/// https://kybernetik.com.au/animancer/api/Animancer/Fade
public class Fade : Updatable
{
/************************************************************************************************************************/
private NativeArray<float> _CurrentWeights;
private NativeArray<float> _CurrentRootMotionWeights;
private float[] _OriginalWeights;
private WeightedMaskLayers _Layers;
private int _LayerIndex;
private int _LayerIndexOffset;
private int _TargetWeightIndex;
private float _OriginalRootMotionWeight;
private float _TargetRootMotionWeight;
private Func<float, float> _Easing;
/// <summary>The amount of time that has passed since the start of this fade (in seconds).</summary>
public float ElapsedTime;
/// <summary>The total amount of time this fade will take (in seconds).</summary>
public float Duration;
/************************************************************************************************************************/
/// <summary>Initializes this fade and registers it to receive updates.</summary>
public void Start(
WeightedMaskLayers layers,
int layerIndex,
int groupIndex,
float duration,
Func<float, float> easing = null)
{
layers.Definition.AssertGroupIndex(groupIndex);
_CurrentWeights = layers.Layers.BoneWeights;
_CurrentRootMotionWeights = layers.Layers.RootMotionWeights;
_OriginalRootMotionWeight = _CurrentRootMotionWeights[layerIndex];
_TargetRootMotionWeight = layers.Definition.RootMotionWeights[groupIndex];
_Easing = easing;
_Layers = layers;
_LayerIndex = layerIndex;
_TargetWeightIndex = layers.Definition.IndexOf(groupIndex, 0);
Duration = duration;
_LayerIndexOffset = (layerIndex - 1) * layers.Layers.BoneCount;
var indices = _Layers.Indices;
AnimancerUtilities.SetLength(ref _OriginalWeights, indices.Length);
for (int i = 0; i < indices.Length; i++)
{
var index = _LayerIndexOffset + indices[i];
_OriginalWeights[i] = _CurrentWeights[index];
}
ElapsedTime = 0;
layers.Layers.Graph.RequirePreUpdate(this);
}
/************************************************************************************************************************/
/// <inheritdoc/>
public override void Update()
{
ElapsedTime += AnimancerGraph.DeltaTime;
if (ElapsedTime < Duration)
{
ApplyFade(ElapsedTime / Duration);
}
else
{
ApplyTargetWeights();
AnimancerGraph.Current.CancelPreUpdate(this);
}
}
/************************************************************************************************************************/
/// <summary>Recalculates the weights by interpolating based on `t`.</summary>
private void ApplyFade(float t)
{
if (_Easing != null)
t = _Easing(t);
var targetWeights = _Layers.Definition.Weights;
var indices = _Layers.Indices;
var boneWeights = _CurrentWeights;
for (int i = 0; i < indices.Length; i++)
{
var index = _LayerIndexOffset + indices[i];
var from = _OriginalWeights[i];
var to = targetWeights[_TargetWeightIndex + i];
boneWeights[index] = Mathf.LerpUnclamped(from, to, t);
}
_CurrentRootMotionWeights[_LayerIndex] = Mathf.LerpUnclamped(
_OriginalRootMotionWeight,
_TargetRootMotionWeight,
t);
}
/// <summary>Recalculates the target weights.</summary>
private void ApplyTargetWeights()
{
var targetWeights = _Layers.Definition.Weights;
var indices = _Layers.Indices;
var boneWeights = _CurrentWeights;
for (int i = 0; i < indices.Length; i++)
{
var index = _LayerIndexOffset + indices[i];
var to = targetWeights[_TargetWeightIndex + i];
boneWeights[index] = to;
}
_CurrentRootMotionWeights[_LayerIndex] = _TargetRootMotionWeight;
}
/************************************************************************************************************************/
}
/************************************************************************************************************************/
}
}

View File

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

View File

@@ -0,0 +1,386 @@
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
using System;
using System.Runtime.CompilerServices;
using UnityEngine;
namespace Animancer
{
/// <summary>Serializable data which defines how to control a <see cref="WeightedMaskLayerList"/>.</summary>
/// https://kybernetik.com.au/animancer/api/Animancer/WeightedMaskLayersDefinition
[Serializable]
public class WeightedMaskLayersDefinition :
ICopyable<WeightedMaskLayersDefinition>,
IEquatable<WeightedMaskLayersDefinition>
#if UNITY_EDITOR
, ISerializationCallbackReceiver
#endif
{
/************************************************************************************************************************/
/// <summary>The name of the serialized backing field of <see cref="Transforms"/>.</summary>
public const string
TransformsField = nameof(_Transforms);
[SerializeField]
private Transform[] _Transforms;
/// <summary><see cref="Transform"/>s being controlled by this definition.</summary>
public ref Transform[] Transforms
=> ref _Transforms;
/************************************************************************************************************************/
/// <summary>The name of the serialized backing field of <see cref="Weights"/>.</summary>
public const string
WeightsField = nameof(_Weights);
[SerializeField]
private float[] _Weights;
/// <summary>Groups of weights which will be applied to the <see cref="Transforms"/>.</summary>
/// <remarks>
/// This is a flattened 2D array containing groups of target weights corresponding to the transforms.
/// With n transforms, indices 0 to n-1 are Group 0, n to n*2-1 are Group 1, etc.
/// </remarks>
public ref float[] Weights
=> ref _Weights;
/************************************************************************************************************************/
[SerializeField]
private float[] _RootMotionWeights;
/// <summary>Each group has a multiplier for the Root Motion output of any layer the group is applied to.</summary>
public ref float[] RootMotionWeights
=> ref _RootMotionWeights;
/************************************************************************************************************************/
/// <summary>The number of weight groups in this definition.</summary>
public int GroupCount
{
get => _Transforms == null || _Transforms.Length == 0 || _Weights == null
? 0
: _Weights.Length / _Transforms.Length;
set
{
if (_Transforms != null && value > 0)
{
Array.Resize(ref _Weights, _Transforms.Length * value);
Array.Resize(ref _RootMotionWeights, value);
}
else
{
_Weights = Array.Empty<float>();
_RootMotionWeights = Array.Empty<float>();
}
}
}
/************************************************************************************************************************/
/// <summary>[Assert-Conditional] Asserts that the `groupIndex` is valid.</summary>
[System.Diagnostics.Conditional(Strings.Assertions)]
public void AssertGroupIndex(int groupIndex)
{
if ((uint)groupIndex >= (uint)GroupCount)
throw new ArgumentOutOfRangeException(
nameof(groupIndex),
groupIndex,
$"Must be 0 <= {nameof(groupIndex)} < Group Count ({GroupCount})");
}
/************************************************************************************************************************/
/// <summary>Calculates the index of each of the <see cref="Transforms"/>.</summary>
public int[] CalculateIndices(WeightedMaskLayerList layers)
{
var indices = new int[_Transforms.Length];
for (int i = 0; i < _Transforms.Length; i++)
{
indices[i] = layers.IndexOf(_Transforms[i]);
#if UNITY_ASSERTIONS
if (indices[i] < 0)
Debug.LogWarning(
$"Unable to find index of {_Transforms[i]} in {nameof(WeightedMaskLayerList)}",
_Transforms[i]);
#endif
}
return indices;
}
/************************************************************************************************************************/
/// <summary>
/// Adds the `transform` at the specified `index`
/// along with any associated <see cref="_Weights"/>.
/// </summary>
public void AddTransform(Transform transform)
{
var index = _Transforms.Length;
AnimancerUtilities.InsertAt(ref _Transforms, index, transform);
if (_Transforms.Length == 1 && _Weights.IsNullOrEmpty())
{
_Weights = new float[1];
return;
}
while (index <= _Weights.Length)
{
AnimancerUtilities.InsertAt(ref _Weights, index, 0);
index += _Transforms.Length;
}
}
/// <summary>
/// Removes the `index` from the <see cref="_Transforms"/>
/// along with any associated <see cref="_Weights"/>.
/// </summary>
public void RemoveTransform(int index)
{
AnimancerUtilities.RemoveAt(ref _Transforms, index);
while (index < _Weights.Length)
{
AnimancerUtilities.RemoveAt(ref _Weights, index);
index += _Transforms.Length;
}
}
/************************************************************************************************************************/
/// <summary>Calculates the index in the <see cref="Weights"/> corresponding to the specified values.</summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public int IndexOfGroup(int groupIndex)
=> groupIndex * _Transforms.Length;
/// <summary>Calculates the index in the <see cref="Weights"/> corresponding to the specified values.</summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public int IndexOf(int groupIndex, int transformIndex)
=> groupIndex * _Transforms.Length + transformIndex;
/************************************************************************************************************************/
/// <summary>Gets the specified weight.</summary>
/// <remarks>Returns <see cref="float.NaN"/> if the indices are outside the <see cref="Weights"/>.</remarks>
public float GetWeight(int groupIndex, int transformIndex)
{
if (Weights == null)
return float.NaN;
var index = IndexOf(groupIndex, transformIndex);
return (uint)index < (uint)Weights.Length
? Weights[index]
: float.NaN;
}
/// <summary>Sets the specified weight.</summary>
/// <remarks>Returns false if the indices are outside the <see cref="Weights"/>.</remarks>
public bool SetWeight(int groupIndex, int transformIndex, float value)
{
if (Weights == null)
return false;
var index = IndexOf(groupIndex, transformIndex);
if ((uint)index < (uint)Weights.Length)
{
Weights[index] = value;
return true;
}
return false;
}
/************************************************************************************************************************/
/// <summary>Gets the specified RootMotion weight.</summary>
/// <remarks>Returns <see cref="float.NaN"/> if the indices are outside the <see cref="RootMotionWeights"/>.</remarks>
public float GetRmWeight(int groupIndex)
{
if (RootMotionWeights == null)
return float.NaN;
return (uint)groupIndex < (uint)RootMotionWeights.Length
? RootMotionWeights[groupIndex]
: float.NaN;
}
/// <summary>Sets the specified RootMotion weight.</summary>
/// <remarks>Returns false if the indices are outside the <see cref="RootMotionWeights"/>.</remarks>
public bool SetRmWeight(int groupIndex, float value)
{
if (RootMotionWeights == null)
return false;
if ((uint)groupIndex < (uint)RootMotionWeights.Length)
{
RootMotionWeights[groupIndex] = value;
return true;
}
return false;
}
/************************************************************************************************************************/
/// <inheritdoc/>
public void CopyFrom(WeightedMaskLayersDefinition copyFrom, CloneContext context)
{
AnimancerUtilities.CopyExactArray(copyFrom._Transforms, ref _Transforms);
AnimancerUtilities.CopyExactArray(copyFrom._Weights, ref _Weights);
AnimancerUtilities.CopyExactArray(copyFrom._RootMotionWeights, ref _RootMotionWeights);
}
/************************************************************************************************************************/
/// <summary>Does this definition contain valid data?</summary>
public bool IsValid
=> !_Transforms.IsNullOrEmpty()
&& _Weights != null && _Weights.Length >= _Transforms.Length
&& _RootMotionWeights != null && _RootMotionWeights.Length == GroupCount;
/************************************************************************************************************************/
/// <summary>Ensures that the data in this definition is value.</summary>
public void Validate()
{
ValidateArraySizes();
RemoveMissingAndDuplicate();
}
/// <summary>Ensures that all the arrays have valid sizes.</summary>
public void ValidateArraySizes()
{
if (_Transforms.IsNullOrEmpty())
{
_Transforms = Array.Empty<Transform>();
_Weights = Array.Empty<float>();
_RootMotionWeights = Array.Empty<float>();
}
if (_Weights == null ||
_Weights.Length < _Transforms.Length)
{
AnimancerUtilities.SetLength(ref _Weights, _Transforms.Length);
}
else
{
var expectedWeightCount = (int)Math.Ceiling(_Weights.Length / (double)_Transforms.Length);
expectedWeightCount *= _Transforms.Length;
AnimancerUtilities.SetLength(ref _Weights, expectedWeightCount);
}
var rootMotionWeightCount = _RootMotionWeights == null
? 0
: _RootMotionWeights.Length;
var groupCount = GroupCount;
if (rootMotionWeightCount != groupCount)
{
AnimancerUtilities.SetLength(ref _RootMotionWeights, groupCount);
for (int i = rootMotionWeightCount; i < groupCount; i++)
_RootMotionWeights[i] = 1;
}
}
/// <summary>Removes any missing or identical <see cref="_Transforms"/>.</summary>
public bool RemoveMissingAndDuplicate()
{
var removedAny = false;
for (int i = 0; i < _Transforms.Length; i++)
{
var transform = _Transforms[i];
if (transform == null)
{
RemoveTransform(i);
removedAny = true;
}
else
{
var nextIndex = i + 1;
RemoveDuplicates:
nextIndex = Array.IndexOf(_Transforms, transform, nextIndex);
if (nextIndex > i)
{
RemoveTransform(nextIndex);
removedAny = true;
goto RemoveDuplicates;
}
}
}
return removedAny;
}
/************************************************************************************************************************/
#if UNITY_EDITOR
/************************************************************************************************************************/
/// <inheritdoc/>
void ISerializationCallbackReceiver.OnBeforeSerialize()
=> Validate();
/// <inheritdoc/>
void ISerializationCallbackReceiver.OnAfterDeserialize()
=> Validate();
/************************************************************************************************************************/
#endif
/************************************************************************************************************************/
/// <summary>Returns a summary of this definition.</summary>
public override string ToString()
=> $"{nameof(WeightedMaskLayersDefinition)}(" +
$"{nameof(Transforms)}={(Transforms != null ? Transforms.Length : 0)}, " +
$"{nameof(Weights)}={(Weights != null ? Weights.Length : 0)}, " +
$"{nameof(RootMotionWeights)}={(RootMotionWeights != null ? RootMotionWeights.Length : 0)})";
/************************************************************************************************************************/
#region Equality
/************************************************************************************************************************/
/// <summary>Are all fields in this object equal to the equivalent in `obj`?</summary>
public override bool Equals(object obj)
=> Equals(obj as WeightedMaskLayersDefinition);
/// <summary>Are all fields in this object equal to the equivalent fields in `other`?</summary>
public bool Equals(WeightedMaskLayersDefinition other)
=> other != null
&& AnimancerUtilities.ContentsAreEqual(_Transforms, other._Transforms)
&& AnimancerUtilities.ContentsAreEqual(_Weights, other._Weights)
&& AnimancerUtilities.ContentsAreEqual(_RootMotionWeights, other._RootMotionWeights);
/// <summary>Are all fields in `a` equal to the equivalent fields in `b`?</summary>
public static bool operator ==(WeightedMaskLayersDefinition a, WeightedMaskLayersDefinition b)
=> a is null
? b is null
: a.Equals(b);
/// <summary>Are any fields in `a` not equal to the equivalent fields in `b`?</summary>
public static bool operator !=(WeightedMaskLayersDefinition a, WeightedMaskLayersDefinition b)
=> !(a == b);
/************************************************************************************************************************/
/// <summary>Returns a hash code based on the values of this object's fields.</summary>
public override int GetHashCode()
=> AnimancerUtilities.Hash(-871379578,
_Transforms.SafeGetHashCode(),
_Weights.SafeGetHashCode(),
_RootMotionWeights.SafeGetHashCode());
/************************************************************************************************************************/
#endregion
/************************************************************************************************************************/
}
}

View File

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

View File

@@ -0,0 +1,199 @@
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik //
using Unity.Collections;
using UnityEngine;
using UnityEngine.Animations;
#if UNITY_BURST
using Unity.Burst;
#endif
namespace Animancer
{
/// <summary>
/// An <see cref="IAnimationJob"/> which mixes its inputs based on individual <see cref="boneWeights"/>.
/// </summary>
#if UNITY_BURST
[BurstCompile(FloatPrecision.Low, FloatMode.Fast
#if UNITY_BURST_1_6_0
, OptimizeFor = OptimizeFor.Performance
#endif
)]
#endif
public struct WeightedMaskMixerJob : IAnimationJob
{
/************************************************************************************************************************/
/// <summary>The number of layers being mixed.</summary>
public int layerCount;
/// <summary>The root motion weight of each layer.</summary>
public NativeArray<float> rootMotionWeights;
/// <summary>The handles for each bone being mixed.</summary>
/// <remarks>
/// All animated bones must be included,
/// even if their individual weight isn't modified.
/// </remarks>
public NativeArray<TransformStreamHandle> boneTransforms;
/// <summary>The blend weight of each bone.</summary>
/// <remarks>
/// This array corresponds to the <see cref="boneTransforms"/>,
/// repeated for each layer after the first and excluding the base layer.
/// For example, if there are 3 layers and 10 bones, then this array will have 20 elements
/// with the first 10 being for Layer 1 and the next 10 being for Layer 2.
/// </remarks>
public NativeArray<float> boneWeights;
/************************************************************************************************************************/
/// <inheritdoc/>
readonly void IAnimationJob.ProcessRootMotion(AnimationStream output)
{
var input = output.GetInputStream(0);
var velocity = input.velocity;
var angularVelocity = input.angularVelocity;
var hasRootMotionWeights = rootMotionWeights.IsCreated;
if (hasRootMotionWeights)
{
var baseLayerWeight = rootMotionWeights[0];
velocity *= baseLayerWeight;
angularVelocity *= baseLayerWeight;
}
for (int i = 1; i < layerCount; i++)// Start at 1.
{
input = output.GetInputStream(i);
if (!input.isValid)
continue;
var layerWeight = output.GetInputWeight(i);
if (hasRootMotionWeights)
layerWeight *= rootMotionWeights[i];
velocity = Vector3.LerpUnclamped(
velocity,
input.velocity,
layerWeight);
angularVelocity = Vector3.LerpUnclamped(
angularVelocity,
input.angularVelocity,
layerWeight);
}
output.velocity = velocity;
output.angularVelocity = angularVelocity;
}
/************************************************************************************************************************/
/// <inheritdoc/>
readonly void IAnimationJob.ProcessAnimation(AnimationStream output)
{
if (layerCount == 2)
{
ProcessAnimation2Layers(output);
return;
}
// Blending more than 2 layers is less efficient because we need to use these temporary arrays.
var transformCount = boneTransforms.Length;
var localPositions = new NativeArray<Vector3>(transformCount, Allocator.Temp, NativeArrayOptions.UninitializedMemory);
var localRotations = new NativeArray<Quaternion>(transformCount, Allocator.Temp, NativeArrayOptions.UninitializedMemory);
var input = output.GetInputStream(0);
for (var i = 0; i < transformCount; i++)
{
var transform = boneTransforms[i];
localPositions[i] = transform.GetLocalPosition(input);
localRotations[i] = transform.GetLocalRotation(input);
}
for (int iLayer = 1; iLayer < layerCount; iLayer++)// Start at 1.
{
input = output.GetInputStream(iLayer);
if (!input.isValid)
break;
var layerWeight = output.GetInputWeight(iLayer);
if (layerWeight == 0)
continue;
var weightOffset = (iLayer - 1) * transformCount;
for (var iTransform = 0; iTransform < transformCount; iTransform++)
{
var transform = boneTransforms[iTransform];
var weight = layerWeight * boneWeights[weightOffset + iTransform];
if (weight == 0)
continue;
localPositions[iTransform] = Vector3.LerpUnclamped(
localPositions[iTransform],
transform.GetLocalPosition(input),
weight);
localRotations[iTransform] = Quaternion.SlerpUnclamped(
localRotations[iTransform],
transform.GetLocalRotation(input),
weight);
}
}
for (var i = 0; i < transformCount; i++)
{
var transform = boneTransforms[i];
transform.SetLocalPosition(output, localPositions[i]);
transform.SetLocalRotation(output, localRotations[i]);
}
localPositions.Dispose();
localRotations.Dispose();
}
/************************************************************************************************************************/
/// <summary>Blends the layers in an optimized way when there are only 2.</summary>
private readonly void ProcessAnimation2Layers(AnimationStream output)
{
var input0 = output.GetInputStream(0);
var input1 = output.GetInputStream(1);
if (input1.isValid)
{
var layerWeight = output.GetInputWeight(1);
var transformCount = boneTransforms.Length;
for (var i = 0; i < transformCount; i++)
{
var transform = boneTransforms[i];
var weight = layerWeight * boneWeights[i];
var position0 = transform.GetLocalPosition(input0);
var position1 = transform.GetLocalPosition(input1);
transform.SetLocalPosition(output, Vector3.LerpUnclamped(position0, position1, weight));
var rotation0 = transform.GetLocalRotation(input0);
var rotation1 = transform.GetLocalRotation(input1);
transform.SetLocalRotation(output, Quaternion.SlerpUnclamped(rotation0, rotation1, weight));
}
}
else
{
var transformCount = boneTransforms.Length;
for (var i = 0; i < transformCount; i++)
{
var transform = boneTransforms[i];
transform.SetLocalPosition(output, transform.GetLocalPosition(input0));
transform.SetLocalRotation(output, transform.GetLocalRotation(input0));
}
}
}
/************************************************************************************************************************/
}
}

View File

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