chore: initial commit
This commit is contained in:
1342
Packages/com.kybernetik.animancer/Runtime/Core/AnimancerGraph.cs
Normal file
1342
Packages/com.kybernetik.animancer/Runtime/Core/AnimancerGraph.cs
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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:
|
||||
@@ -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
|
||||
/************************************************************************************************************************/
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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:
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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:
|
||||
@@ -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
|
||||
/************************************************************************************************************************/
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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:
|
||||
1444
Packages/com.kybernetik.animancer/Runtime/Core/AnimancerUtilities.cs
Normal file
1444
Packages/com.kybernetik.animancer/Runtime/Core/AnimancerUtilities.cs
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 5951f73829d4fe5478c69d982a08e77e
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 89969125b8d531b45b2e18a548ac6b92
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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:
|
||||
@@ -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
|
||||
/************************************************************************************************************************/
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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:
|
||||
@@ -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
|
||||
/************************************************************************************************************************/
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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:
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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:
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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:
|
||||
@@ -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
|
||||
/************************************************************************************************************************/
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
@@ -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:
|
||||
@@ -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
|
||||
/************************************************************************************************************************/
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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:
|
||||
@@ -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
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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:
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: bfafec2a7eda5e149b200fa583c51e15
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 5927c425f3f0c0e4baeb0cd737707442
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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
|
||||
/************************************************************************************************************************/
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 30eb95e632f815a49a7ac0f1d68cb006
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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> { }
|
||||
|
||||
/************************************************************************************************************************/
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: ddc1c2d634269b94891d46befabcb48b
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
@@ -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 { }
|
||||
}
|
||||
|
||||
@@ -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:
|
||||
@@ -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
|
||||
/************************************************************************************************************************/
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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:
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 0d10e76a0343a30418b3cbc71bf313e3
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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
|
||||
/************************************************************************************************************************/
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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:
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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:
|
||||
@@ -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
@@ -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:
|
||||
@@ -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
|
||||
/************************************************************************************************************************/
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 54f55b421ce0c584ab6bcb47947126e4
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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
|
||||
/************************************************************************************************************************/
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 8abfd735619067c40b7e6e10bcc0b559
|
||||
labels:
|
||||
- Interface
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -0,0 +1,67 @@
|
||||
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-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
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
@@ -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:
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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:
|
||||
@@ -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
|
||||
/************************************************************************************************************************/
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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:
|
||||
@@ -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
|
||||
/************************************************************************************************************************/
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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:
|
||||
@@ -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
|
||||
/************************************************************************************************************************/
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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:
|
||||
@@ -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
|
||||
/************************************************************************************************************************/
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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:
|
||||
@@ -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);
|
||||
|
||||
/************************************************************************************************************************/
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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:
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: db2c18cf91057604fba518cb84714b94
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
}
|
||||
}
|
||||
@@ -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:
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
}
|
||||
}
|
||||
@@ -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:
|
||||
@@ -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
|
||||
/************************************************************************************************************************/
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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:
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/************************************************************************************************************************/
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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:
|
||||
Reference in New Issue
Block a user