1405 lines
57 KiB
C#
1405 lines
57 KiB
C#
// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2024 Kybernetik //
|
|
|
|
using System;
|
|
using System.Collections;
|
|
using System.Collections.Generic;
|
|
using System.Text;
|
|
using UnityEngine;
|
|
using UnityEngine.Animations;
|
|
using UnityEngine.Playables;
|
|
|
|
namespace Animancer
|
|
{
|
|
/// <summary>
|
|
/// A layer on which animations can play with their states managed independantly of other layers while blending the
|
|
/// output with those layers.
|
|
/// </summary>
|
|
///
|
|
/// <remarks>
|
|
/// This class can be used as a custom yield instruction to wait until all animations finish playing.
|
|
/// <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/AnimancerLayer
|
|
///
|
|
public class AnimancerLayer : AnimancerNode,
|
|
IAnimationClipCollection,
|
|
ICopyable<AnimancerLayer>
|
|
{
|
|
/************************************************************************************************************************/
|
|
#region Fields and Properties
|
|
/************************************************************************************************************************/
|
|
|
|
/// <summary>[Internal] Creates a new <see cref="AnimancerLayer"/>.</summary>
|
|
protected internal AnimancerLayer(AnimancerGraph graph, int index)
|
|
{
|
|
Graph = graph;
|
|
Parent = graph;
|
|
Index = index;
|
|
|
|
if (ApplyParentAnimatorIK)
|
|
_ApplyAnimatorIK = graph.ApplyAnimatorIK;
|
|
if (ApplyParentFootIK)
|
|
_ApplyFootIK = graph.ApplyFootIK;
|
|
|
|
CreatePlayable();
|
|
|
|
ActiveStatesInternal = new(new(Graph, Playable));
|
|
}
|
|
|
|
/************************************************************************************************************************/
|
|
|
|
/// <summary>Creates and assigns the <see cref="AnimationMixerPlayable"/> managed by this layer.</summary>
|
|
protected override void CreatePlayable(out Playable playable)
|
|
=> playable = AnimationMixerPlayable.Create(Graph._PlayableGraph, _Capacity);
|
|
|
|
/************************************************************************************************************************/
|
|
|
|
/// <summary>A layer is its own root.</summary>
|
|
public override AnimancerLayer Layer
|
|
=> this;
|
|
|
|
/// <inheritdoc/>
|
|
public override bool KeepChildrenConnected
|
|
=> Graph.KeepChildrenConnected;
|
|
|
|
/************************************************************************************************************************/
|
|
|
|
/// <summary>The animation states connected to this layer.</summary>
|
|
private readonly List<AnimancerState>
|
|
States = new();
|
|
|
|
/************************************************************************************************************************/
|
|
|
|
private readonly AnimancerState.ActiveList
|
|
ActiveStatesInternal;
|
|
|
|
/// <summary>The states connected to this layer which are <see cref="AnimancerState.IsActive"/>.</summary>
|
|
public IReadOnlyIndexedList<AnimancerState> ActiveStates
|
|
=> ActiveStatesInternal;
|
|
|
|
/************************************************************************************************************************/
|
|
|
|
/// <summary>The default <see cref="Capacity"/> is 8 unless changed.</summary>
|
|
/// <remarks>
|
|
/// This value only affects newly created layers.
|
|
/// <para></para>
|
|
/// This value should be set high enough to include all states a layer is likely to have. It's generally not
|
|
/// particularly important though since expanding the capacity is fairly fast.
|
|
/// </remarks>
|
|
public static int DefaultCapacity = 8;
|
|
|
|
private int _Capacity = DefaultCapacity;
|
|
|
|
/// <summary>
|
|
/// The number of states that can be connected to this layer before its <see cref="Playable"/> needs to
|
|
/// allocate more inputs.
|
|
/// </summary>
|
|
/// <remarks>Starts at the <see cref="DefaultCapacity"/> and doubles each time it needs to expand.</remarks>
|
|
/// <exception cref="ArgumentException">This value cannot be set lower than the <see cref="ChildCount"/>.</exception>
|
|
public int Capacity
|
|
{
|
|
get => _Capacity;
|
|
set
|
|
{
|
|
if (value < ChildCount)
|
|
throw new ArgumentException(
|
|
$"{nameof(Capacity)} ({value}) cannot be smaller than {nameof(ChildCount)} ({ChildCount}).");
|
|
|
|
_Capacity = value;
|
|
_Playable.SetInputCount(value);
|
|
}
|
|
}
|
|
|
|
/************************************************************************************************************************/
|
|
|
|
private AnimancerState _CurrentState;
|
|
|
|
/// <summary>The state of the animation currently being played.</summary>
|
|
/// <remarks>
|
|
/// Specifically, this is the state that was most recently started using any of the Play or CrossFade methods
|
|
/// on this layer. States controlled individually via methods in the <see cref="AnimancerState"/> itself will
|
|
/// not register in this property.
|
|
/// <para></para>
|
|
/// Each time this property changes, the <see cref="CommandCount"/> is incremented.
|
|
/// </remarks>
|
|
public AnimancerState CurrentState
|
|
{
|
|
get => _CurrentState;
|
|
private set
|
|
{
|
|
_CurrentState = value;
|
|
CommandCount++;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// The number of times the <see cref="CurrentState"/> has changed. By storing this value and later comparing
|
|
/// the stored value to the current value, you can determine whether the state has been changed since then,
|
|
/// even it has changed back to the same state.
|
|
/// </summary>
|
|
public int CommandCount { get; private set; }
|
|
|
|
#if UNITY_EDITOR
|
|
/// <summary>[Editor-Only] [Internal] Increases the <see cref="CommandCount"/> by 1.</summary>
|
|
internal void IncrementCommandCount() => CommandCount++;
|
|
#endif
|
|
|
|
/************************************************************************************************************************/
|
|
|
|
/// <summary>[Pro-Only]
|
|
/// Determines whether this layer is set to additive blending. Otherwise it will override any earlier layers.
|
|
/// </summary>
|
|
public bool IsAdditive
|
|
{
|
|
get => Graph.Layers.IsAdditive(Index);
|
|
set => Graph.Layers.SetAdditive(Index, value);
|
|
}
|
|
|
|
/************************************************************************************************************************/
|
|
|
|
/// <summary>[Internal] [Pro-Only] The mask that determines which bones this layer will affect.</summary>
|
|
internal AvatarMask _Mask;
|
|
|
|
/// <summary>[Pro-Only] The mask that determines which bones this layer 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 AvatarMask Mask
|
|
{
|
|
get => _Mask;
|
|
set => Graph.Layers.SetMask(Index, value);
|
|
}
|
|
|
|
/************************************************************************************************************************/
|
|
|
|
/// <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 = ActiveStatesInternal.Count - 1; i >= 0; i--)
|
|
{
|
|
var state = ActiveStatesInternal[i];
|
|
velocity += state.AverageVelocity * state.Weight;
|
|
}
|
|
|
|
return velocity;
|
|
}
|
|
}
|
|
|
|
/************************************************************************************************************************/
|
|
#endregion
|
|
/************************************************************************************************************************/
|
|
#region Child States
|
|
/************************************************************************************************************************/
|
|
|
|
/// <inheritdoc/>
|
|
public override int ChildCount
|
|
=> States.Count;
|
|
|
|
/// <summary>Returns the state connected to the specified `index` as a child of this layer.</summary>
|
|
/// <remarks>This method is identical to <see cref="this[int]"/>.</remarks>
|
|
public override AnimancerState GetChild(int index)
|
|
=> States[index];
|
|
|
|
/// <summary>Returns the state connected to the specified `index` as a child of this layer.</summary>
|
|
/// <remarks>This indexer is identical to <see cref="GetChild(int)"/>.</remarks>
|
|
public AnimancerState this[int index]
|
|
=> States[index];
|
|
|
|
/************************************************************************************************************************/
|
|
|
|
/// <summary>Connects the `state` to this layer at its <see cref="AnimancerNode.Index"/>.</summary>
|
|
protected internal override void OnAddChild(AnimancerState state)
|
|
{
|
|
Validate.AssertGraph(state, Graph);
|
|
|
|
var index = States.Count;
|
|
state.Index = index;
|
|
States.Add(state);
|
|
|
|
if (_Capacity <= index)
|
|
{
|
|
_Capacity *= 2;
|
|
_Playable.SetInputCount(_Capacity);
|
|
}
|
|
|
|
// If the state should be active, deactivate and properly add it to the active list.
|
|
if (state.TryDeactivate())
|
|
ActiveStatesInternal.Add(state);
|
|
|
|
if (Graph.KeepChildrenConnected)
|
|
ConnectChildUnsafe(state.Index, state);
|
|
}
|
|
|
|
/************************************************************************************************************************/
|
|
|
|
/// <summary>Disconnects the `state` from this layer at its <see cref="AnimancerNode.Index"/>.</summary>
|
|
protected internal override void OnRemoveChild(AnimancerState state)
|
|
{
|
|
var index = state.Index;
|
|
Validate.AssertCanRemoveChild(state, States, States.Count);
|
|
|
|
if (ActiveStatesInternal.Remove(state))
|
|
state._ActiveIndex = 0;
|
|
|
|
if (Graph._PlayableGraph.IsValid() &&
|
|
_Playable.GetInput(index).IsValid())
|
|
Graph._PlayableGraph.Disconnect(_Playable, index);
|
|
|
|
// Swap the last state into the place of the one that was just removed.
|
|
var last = States.Count - 1;
|
|
if (index < last)
|
|
{
|
|
state = States[last];
|
|
|
|
DisconnectChildSafe(last);
|
|
|
|
States[index] = state;
|
|
state.Index = index;
|
|
|
|
if (state.IsActive || Graph.KeepChildrenConnected)
|
|
ConnectChildUnsafe(index, state);
|
|
}
|
|
|
|
States.RemoveAt(last);
|
|
}
|
|
|
|
/************************************************************************************************************************/
|
|
|
|
/// <inheritdoc/>
|
|
protected internal override void ApplyChildActive(AnimancerState state, bool setActive)
|
|
{
|
|
if (setActive)
|
|
ActiveStatesInternal.Add(state);
|
|
else
|
|
ActiveStatesInternal.Remove(state);
|
|
}
|
|
|
|
/************************************************************************************************************************/
|
|
|
|
/// <summary>[Internal] Connects all states in this layer.</summary>
|
|
internal void ConnectAllStates()
|
|
{
|
|
for (int i = ChildCount - 1; i >= 0; i--)
|
|
if (!_Playable.GetInput(i).IsValid())
|
|
ConnectChildUnsafe(i, States[i]);
|
|
}
|
|
|
|
/// <summary>[Internal] Disconnects all states which are not <see cref="AnimancerState.IsActive"/>.</summary>
|
|
internal void DisconnectInactiveStates()
|
|
{
|
|
for (int i = ChildCount - 1; i >= 0; i--)
|
|
if (!States[i].IsActive)
|
|
DisconnectChildSafe(i);
|
|
}
|
|
|
|
/************************************************************************************************************************/
|
|
|
|
/// <summary>[Internal]
|
|
/// Checks if any events should be invoked on any of the <see cref="ActiveStates"/>.
|
|
/// </summary>
|
|
internal void UpdateEvents()
|
|
{
|
|
if (Weight <= 0)
|
|
return;
|
|
|
|
if (FadeGroup != null &&
|
|
FadeGroup.GetTargetWeight(this) == 0 &&
|
|
!AnimancerState.RaiseEventsDuringFadeOut)
|
|
return;
|
|
|
|
for (int i = ActiveStatesInternal.Count - 1; i >= 0; i--)
|
|
ActiveStatesInternal[i].UpdateEvents();
|
|
}
|
|
|
|
/************************************************************************************************************************/
|
|
|
|
/// <summary>[Internal] Cancels the current <see cref="FadeGroup"/> so it can be object pooled.</summary>
|
|
internal void OnGraphDestroyed()
|
|
{
|
|
CurrentState?.FadeGroup?.Cancel();
|
|
}
|
|
|
|
/************************************************************************************************************************/
|
|
|
|
/// <inheritdoc/>
|
|
public override FastEnumerator<AnimancerState> GetEnumerator()
|
|
=> new(States);
|
|
|
|
/************************************************************************************************************************/
|
|
|
|
/// <inheritdoc/>
|
|
public sealed override void CopyFrom(AnimancerNode copyFrom, CloneContext context)
|
|
=> this.CopyFromBase(copyFrom, context);
|
|
|
|
/// <summary>Copies the details of `copyFrom` into this layer.</summary>
|
|
/// <remarks>Call <see cref="CopyStatesFrom"/> (as well) if you want to copy the states.</remarks>
|
|
public virtual void CopyFrom(AnimancerLayer copyFrom, CloneContext context)
|
|
{
|
|
base.CopyFrom(copyFrom, context);
|
|
|
|
IsAdditive = copyFrom.IsAdditive;
|
|
Mask = copyFrom.Mask;
|
|
}
|
|
|
|
/************************************************************************************************************************/
|
|
|
|
/// <summary>Copies the details of all states in `copyFrom` to their equivalent states in this layer.</summary>
|
|
/// <remarks>
|
|
/// Any states which do not have an equivalent in this layer will be cloned into this layer.
|
|
/// <para></para>
|
|
/// Call <see cref="CopyFrom(AnimancerLayer, CloneContext)"/> (as well)
|
|
/// if you want to copy the details of the layer itself.
|
|
/// </remarks>
|
|
public void CopyStatesFrom(AnimancerLayer copyFrom, CloneContext context, bool includeInactive = false)
|
|
{
|
|
if (copyFrom == this)
|
|
return;
|
|
|
|
Debug.Assert(context.TryGetValue(copyFrom.Graph, out var thisGraph));
|
|
Debug.Assert(thisGraph == Graph);
|
|
|
|
for (int i = ActiveStatesInternal.Count - 1; i >= 0; i--)
|
|
ActiveStatesInternal[i].Stop();
|
|
|
|
CommandCount++;
|
|
|
|
if (!includeInactive && Weight == 0)
|
|
return;
|
|
|
|
IReadOnlyList<AnimancerState> copyFromStates = includeInactive
|
|
? copyFrom.States
|
|
: copyFrom.ActiveStatesInternal;
|
|
|
|
var stateCount = copyFromStates.Count;
|
|
for (int i = 0; i < stateCount; i++)
|
|
{
|
|
var state = copyFromStates[i];
|
|
|
|
// If the clone already exists, copy over the state details.
|
|
if (context.TryGetClone(state, out var clone))
|
|
clone.CopyFrom(state, context);
|
|
else// Otherwise, create a new clone.
|
|
clone = context.Clone(state);
|
|
|
|
if (clone.Parent != this)
|
|
clone.SetParent(this);
|
|
|
|
if (copyFrom.CurrentState == state)
|
|
CurrentState = clone;
|
|
|
|
// This should prevent the clones from being one frame behind after the next animation update
|
|
// but it seems to only work for some states and not others.
|
|
// clone.Time += clone.EffectiveSpeed * Time.deltaTime;
|
|
}
|
|
}
|
|
|
|
/************************************************************************************************************************/
|
|
#endregion
|
|
/************************************************************************************************************************/
|
|
#region Create State
|
|
/************************************************************************************************************************/
|
|
|
|
/// <summary>Creates and returns a new <see cref="ClipState"/> to play the `clip`.</summary>
|
|
/// <remarks>
|
|
/// <see cref="AnimancerGraph.GetKey"/> is used to determine the <see cref="AnimancerState.Key"/>.
|
|
/// </remarks>
|
|
public ClipState CreateState(AnimationClip clip)
|
|
=> CreateState(Graph.GetKey(clip), clip);
|
|
|
|
/// <summary>
|
|
/// Creates and returns a new <see cref="ClipState"/> to play the `clip` and registers it with the `key`.
|
|
/// </summary>
|
|
public ClipState CreateState(object key, AnimationClip clip)
|
|
{
|
|
var state = new ClipState(clip)
|
|
{
|
|
_Key = key,
|
|
};
|
|
state.SetParent(this);
|
|
return state;
|
|
}
|
|
|
|
/************************************************************************************************************************/
|
|
|
|
/// <summary>Returns a state registered with the `key` and attached to this layer or null if none exist.</summary>
|
|
/// <exception cref="ArgumentNullException">The `key` is null.</exception>
|
|
/// <remarks>
|
|
/// If a state is registered with the `key` but on a different layer, this method will use that state as the
|
|
/// key and try to look up another state with it. This allows it to associate multiple states with the same
|
|
/// original key.
|
|
/// </remarks>
|
|
public AnimancerState GetState(ref object key)
|
|
{
|
|
if (key == null)
|
|
throw new ArgumentNullException(nameof(key));
|
|
|
|
// Check through any states backwards in the key chain.
|
|
var earlierKey = key;
|
|
while (earlierKey is AnimancerState keyState)
|
|
{
|
|
if (keyState.Parent == this)// If the state is on this layer, return it.
|
|
{
|
|
key = keyState.Key;
|
|
return keyState;
|
|
}
|
|
else if (keyState.Parent == null)// If the state is on no layer, attach it to this one and return it.
|
|
{
|
|
key = keyState.Key;
|
|
keyState.SetParent(this);
|
|
return keyState;
|
|
}
|
|
else// Otherwise the state is on a different layer.
|
|
{
|
|
earlierKey = keyState.Key;
|
|
}
|
|
}
|
|
|
|
while (true)
|
|
{
|
|
// If no state is registered with the key, return null.
|
|
if (!Graph.States.TryGet(key, out var state))
|
|
return null;
|
|
|
|
if (state.Parent == this)// If the state is on this layer, return it.
|
|
{
|
|
return state;
|
|
}
|
|
else if (state.Parent == null)// If the state is on no layer, attach it to this one and return it.
|
|
{
|
|
state.SetParent(this);
|
|
return state;
|
|
}
|
|
else// Otherwise the state is on a different layer.
|
|
{
|
|
// Use it as the key and try to look up the next state in a chain.
|
|
key = state;
|
|
}
|
|
}
|
|
}
|
|
|
|
/************************************************************************************************************************/
|
|
|
|
/// <summary>
|
|
/// Calls <see cref="GetOrCreateState(AnimationClip, bool)"/> for each of the specified clips.
|
|
/// <para></para>
|
|
/// If you only want to create a single state, use <see cref="CreateState(AnimationClip)"/>.
|
|
/// </summary>
|
|
public void CreateIfNew(AnimationClip clip0, AnimationClip clip1)
|
|
{
|
|
GetOrCreateState(clip0);
|
|
GetOrCreateState(clip1);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Calls <see cref="GetOrCreateState(AnimationClip, bool)"/> for each of the specified clips.
|
|
/// <para></para>
|
|
/// If you only want to create a single state, use <see cref="CreateState(AnimationClip)"/>.
|
|
/// </summary>
|
|
public void CreateIfNew(AnimationClip clip0, AnimationClip clip1, AnimationClip clip2)
|
|
{
|
|
GetOrCreateState(clip0);
|
|
GetOrCreateState(clip1);
|
|
GetOrCreateState(clip2);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Calls <see cref="GetOrCreateState(AnimationClip, bool)"/> for each of the specified clips.
|
|
/// <para></para>
|
|
/// If you only want to create a single state, use <see cref="CreateState(AnimationClip)"/>.
|
|
/// </summary>
|
|
public void CreateIfNew(AnimationClip clip0, AnimationClip clip1, AnimationClip clip2, AnimationClip clip3)
|
|
{
|
|
GetOrCreateState(clip0);
|
|
GetOrCreateState(clip1);
|
|
GetOrCreateState(clip2);
|
|
GetOrCreateState(clip3);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Calls <see cref="GetOrCreateState(AnimationClip, bool)"/> for each of the specified clips.
|
|
/// <para></para>
|
|
/// If you only want to create a single state, use <see cref="CreateState(AnimationClip)"/>.
|
|
/// </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)
|
|
GetOrCreateState(clip);
|
|
}
|
|
}
|
|
|
|
/************************************************************************************************************************/
|
|
|
|
/// <summary>
|
|
/// Calls <see cref="AnimancerGraph.GetKey"/> and returns the state registered with that 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 to use with caution.
|
|
/// </summary>
|
|
/// <exception cref="ArgumentException"/>
|
|
public AnimancerState GetOrCreateState(AnimationClip clip, bool allowSetClip = false)
|
|
{
|
|
return GetOrCreateState(Graph.GetKey(clip), clip, allowSetClip);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns the state registered with the <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 GetOrCreateState(ITransition transition)
|
|
{
|
|
var key = transition.Key;
|
|
var state = GetState(ref key);
|
|
|
|
if (state == null)
|
|
{
|
|
state = transition.CreateState();
|
|
state._Key = key;
|
|
state.SetParent(this);
|
|
}
|
|
|
|
return state;
|
|
}
|
|
|
|
/// <summary>Returns the state registered with the `key` or creates one if it doesn't exist.</summary>
|
|
/// <exception cref="ArgumentException"/>
|
|
/// <exception cref="ArgumentNullException">The `key` is null.</exception>
|
|
/// <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 to use with caution.
|
|
/// <para></para>
|
|
/// See also: <see cref="AnimancerStateDictionary.GetOrCreate(object, AnimationClip, bool)"/>.
|
|
/// </remarks>
|
|
public AnimancerState GetOrCreateState(object key, AnimationClip clip, bool allowSetClip = false)
|
|
{
|
|
var state = GetState(ref key);
|
|
if (state == null)
|
|
return CreateState(key, clip);
|
|
|
|
// If a state exists but has the wrong clip, either change it or complain.
|
|
if (!ReferenceEquals(state.Clip, clip))
|
|
{
|
|
if (allowSetClip)
|
|
{
|
|
state.Clip = clip;
|
|
}
|
|
else
|
|
{
|
|
throw new ArgumentException(
|
|
AnimancerStateDictionary.GetClipMismatchError(key, state.Clip, clip));
|
|
}
|
|
}
|
|
|
|
return state;
|
|
}
|
|
|
|
/// <summary>Returns the `state` if it's a child of this layer. Otherwise makes a clone of it.</summary>
|
|
public AnimancerState GetOrCreateState(AnimancerState state)
|
|
{
|
|
var parent = state.Parent;
|
|
if (parent == this)
|
|
return state;
|
|
|
|
if (parent == null)
|
|
{
|
|
state.SetParent(this);
|
|
return state;
|
|
}
|
|
|
|
var key = state.Key;
|
|
key ??= state;
|
|
|
|
var stateOnThisLayer = GetState(ref key);
|
|
|
|
if (stateOnThisLayer == null)
|
|
{
|
|
stateOnThisLayer = state.Clone();
|
|
stateOnThisLayer._Weight = 0;
|
|
stateOnThisLayer.SetParent(this);
|
|
stateOnThisLayer.Key = key;
|
|
}
|
|
|
|
return stateOnThisLayer;
|
|
}
|
|
|
|
/************************************************************************************************************************/
|
|
|
|
/// <summary>
|
|
/// The maximum <see cref="AnimancerNode.Weight"/> that <see cref="GetOrCreateWeightlessState"/>
|
|
/// will treat as being weightless. Default = 0.1.
|
|
/// </summary>
|
|
/// <remarks>This allows states with very small weights to be reused instead of needing to create new ones.</remarks>
|
|
public static float WeightlessThreshold { get; set; } = 0.1f;
|
|
|
|
/// <summary>
|
|
/// The maximum number of duplicate states that can be created for a single clip when trying to get a
|
|
/// weightless state. Exceeding this limit will cause it to just use the state with the lowest weight.
|
|
/// Default = 3.
|
|
/// </summary>
|
|
public static int MaxCloneCount { get; set; } = 3;
|
|
|
|
/// <summary>
|
|
/// If the `state`'s <see cref="AnimancerNode.Weight"/> is not currently low,
|
|
/// this method finds or creates a copy of it which is low.
|
|
/// The returned <see cref="AnimancerState.Time"/> is also set to 0.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// If this method would exceed the <see cref="MaxCloneCount"/>, it returns the clone with the lowest weight.
|
|
/// <para></para>
|
|
/// "Low" weight is defined as less than or equal to the <see cref="WeightlessThreshold"/>.
|
|
/// <para></para>
|
|
/// The <see href="https://kybernetik.com.au/animancer/docs/manual/blending/fading/modes">Fade Modes</see> page
|
|
/// explains why clones are created.
|
|
/// </remarks>
|
|
public AnimancerState GetOrCreateWeightlessState(AnimancerState state)
|
|
{
|
|
if (state.Parent == null)
|
|
{
|
|
state.Weight = 0;
|
|
goto GotState;
|
|
}
|
|
|
|
if (state.Parent == this &&
|
|
state.Weight <= WeightlessThreshold)
|
|
goto GotState;
|
|
|
|
float lowestWeight = float.PositiveInfinity;
|
|
AnimancerState lowestWeightState = null;
|
|
|
|
int cloneCount = 0;
|
|
|
|
// Use any earlier state that is weightless.
|
|
var keyState = state;
|
|
while (true)
|
|
{
|
|
keyState = keyState.Key as AnimancerState;
|
|
if (keyState == null)
|
|
{
|
|
break;
|
|
}
|
|
else if (keyState.Parent == this)
|
|
{
|
|
if (keyState.Weight <= WeightlessThreshold)
|
|
{
|
|
state = keyState;
|
|
goto GotState;
|
|
}
|
|
else if (lowestWeight > keyState.Weight)
|
|
{
|
|
lowestWeight = keyState.Weight;
|
|
lowestWeightState = keyState;
|
|
}
|
|
}
|
|
else if (keyState.Parent == null)
|
|
{
|
|
keyState.SetParent(this);
|
|
goto GotState;
|
|
}
|
|
|
|
cloneCount++;
|
|
}
|
|
|
|
if (state.Parent == this)
|
|
{
|
|
lowestWeight = state.Weight;
|
|
lowestWeightState = state;
|
|
}
|
|
|
|
keyState = state;
|
|
|
|
// If that state is not at low weight,
|
|
// get or create another state registered using the previous state as a key.
|
|
// Keep going through states in this manner until you find one at low weight.
|
|
while (true)
|
|
{
|
|
var key = (object)state;
|
|
if (!Graph.States.TryGet(key, out state))
|
|
{
|
|
if (cloneCount >= MaxCloneCount && lowestWeightState != null)
|
|
{
|
|
state = lowestWeightState;
|
|
goto GotState;
|
|
}
|
|
else
|
|
{
|
|
#if UNITY_ASSERTIONS
|
|
var cloneTimer = OptionalWarning.CloneComplexState.IsEnabled() && keyState is not ClipState
|
|
? SimpleTimer.Start()
|
|
: SimpleTimer.Default;
|
|
#endif
|
|
|
|
state = keyState.Clone();
|
|
state.SetDebugName($"[{cloneCount + 1}] {keyState}");
|
|
state.Weight = 0;
|
|
state.Key = key;
|
|
if (state.Parent != this)
|
|
state.SetParent(this);
|
|
|
|
#if UNITY_ASSERTIONS
|
|
if (cloneTimer.Count() > 0)
|
|
{
|
|
var milliseconds = cloneTimer.TotalTimeSeconds * 1000;
|
|
OptionalWarning.CloneComplexState.Log(
|
|
$"A {keyState.GetType().Name} was cloned in {milliseconds} milliseconds." +
|
|
$" This performance cost may be notable and complex states generally have parameters" +
|
|
$" that need to be controlled which may result in undesired behaviour if your scripts" +
|
|
$" are only expecting to have one state to control so you may wish to avoid cloning." +
|
|
$"\n\nThe Fade Modes page explains why these clones are created:" +
|
|
$" {Strings.DocsURLs.FadeModes}",
|
|
Graph?.Component);
|
|
}
|
|
#endif
|
|
|
|
goto GotState;
|
|
}
|
|
}
|
|
else if (state.Parent == this)
|
|
{
|
|
if (state.Weight <= WeightlessThreshold)
|
|
{
|
|
goto GotState;
|
|
}
|
|
else if (lowestWeight > state.Weight)
|
|
{
|
|
lowestWeight = state.Weight;
|
|
lowestWeightState = state;
|
|
}
|
|
}
|
|
else if (state.Parent == null)
|
|
{
|
|
state.SetParent(this);
|
|
goto GotState;
|
|
}
|
|
|
|
cloneCount++;
|
|
}
|
|
|
|
GotState:
|
|
|
|
state.TimeD = 0;
|
|
|
|
return state;
|
|
}
|
|
|
|
/************************************************************************************************************************/
|
|
|
|
/// <summary>Destroys all states connected to this layer.</summary>
|
|
/// <remarks>This operation cannot be undone.</remarks>
|
|
public void DestroyStates()
|
|
{
|
|
CurrentState?.FadeGroup?.Cancel();
|
|
|
|
for (int i = States.Count - 1; i >= 0; i--)
|
|
States[i].Destroy();
|
|
|
|
States.Clear();
|
|
CurrentState = null;
|
|
}
|
|
|
|
/************************************************************************************************************************/
|
|
#endregion
|
|
/************************************************************************************************************************/
|
|
#region Play Management
|
|
/************************************************************************************************************************/
|
|
|
|
/// <inheritdoc/>
|
|
protected internal override void OnStartFade()
|
|
{
|
|
CommandCount++;
|
|
}
|
|
|
|
/************************************************************************************************************************/
|
|
// Play Immediately.
|
|
/************************************************************************************************************************/
|
|
|
|
/// <summary>Stops all other animations on this layer, plays the `clip`, and returns its state.</summary>
|
|
/// <remarks>
|
|
/// The animation will continue playing from its current <see cref="AnimancerState.Time"/>.
|
|
/// To restart it from the beginning you can use <c>...Play(clip).Time = 0;</c>.
|
|
/// <para></para>
|
|
/// This method is safe to call repeatedly without checking whether the `clip` was already playing.
|
|
/// </remarks>
|
|
public AnimancerState Play(
|
|
AnimationClip clip)
|
|
=> Play(GetOrCreateState(clip));
|
|
|
|
/// <summary>Stops all other animations on the same layer, plays the `state`, and returns it.</summary>
|
|
/// <remarks>
|
|
/// The animation will continue playing from its current <see cref="AnimancerState.Time"/>.
|
|
/// To restart it from the beginning you can use <c>...Play(state).Time = 0;</c>.
|
|
/// <para></para>
|
|
/// This method is safe to call repeatedly without checking whether the `state` was already playing.
|
|
/// </remarks>
|
|
/// <exception cref="InvalidOperationException">
|
|
/// The <see cref="AnimancerNodeBase.Parent"/> is another state (likely a <see cref="ManualMixerState"/>).
|
|
/// It must be either null or a layer.
|
|
/// </exception>
|
|
public AnimancerState Play(
|
|
AnimancerState state)
|
|
{
|
|
#if UNITY_ASSERTIONS
|
|
AnimancerEvent.AssertEventPlayMismatch(Graph);
|
|
AnimancerState.AssertNotExpectingFade(state);
|
|
|
|
if (state.Parent is AnimancerState)
|
|
throw new InvalidOperationException(
|
|
$"A layer can't Play a state which is the child of another state." +
|
|
$"\n• State: {state}" +
|
|
$"\n• Parent: {state.Parent}" +
|
|
$"\n• Layer: {this}");
|
|
#endif
|
|
|
|
// If the layer is at 0 weight and not fading, set it to 1.
|
|
if (Weight == 0 && FadeGroup == null)
|
|
Weight = 1;
|
|
|
|
CurrentState?.FadeGroup?.Cancel();
|
|
|
|
state = GetOrCreateState(state);
|
|
|
|
CurrentState = state;
|
|
|
|
for (int i = ActiveStatesInternal.Count - 1; i >= 0; i--)
|
|
{
|
|
var otherState = ActiveStatesInternal[i];
|
|
if (otherState != state)
|
|
otherState.Stop();
|
|
}
|
|
|
|
// Similar to state.Play but more optimized.
|
|
state.SetIsPlaying(true);
|
|
state._Weight = 1;
|
|
if (!state.IsActive)
|
|
ActiveStatesInternal.Add(state);
|
|
_Playable.ApplyChildWeight(state);
|
|
|
|
return state;
|
|
}
|
|
|
|
/************************************************************************************************************************/
|
|
// Cross Fade.
|
|
/************************************************************************************************************************/
|
|
|
|
/// <summary>
|
|
/// Starts fading in the `clip` over the course of the `fadeDuration`
|
|
/// while fading out all others in the same layer. Returns its state.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// If the `state` was already playing and fading in with less time remaining than the `fadeDuration`,
|
|
/// this method will allow it to complete the existing fade rather than starting a slower one.
|
|
/// <para></para>
|
|
/// If the layer currently has 0 <see cref="AnimancerNode.Weight"/>,
|
|
/// this method will fade in the layer itself and simply <see cref="AnimancerState.Play"/> the `state`.
|
|
/// <para></para>
|
|
/// This method is safe to call repeatedly without checking whether the `state` was already playing.
|
|
/// <para></para>
|
|
/// <em>Animancer Lite only allows the default `fadeDuration` (0.25 seconds) in runtime builds.</em>
|
|
/// </remarks>
|
|
public AnimancerState Play(
|
|
AnimationClip clip,
|
|
float fadeDuration,
|
|
FadeMode mode = default)
|
|
{
|
|
var key = Graph.GetKey(clip);
|
|
var state = GetOrCreateState(key, clip);
|
|
|
|
if (Graph.Transitions != null)
|
|
fadeDuration = Graph.Transitions.GetFadeDuration(this, key, fadeDuration);
|
|
|
|
return Play(state, fadeDuration, mode);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Starts fading in the `state` over the course of the `fadeDuration` while fading out all others in this
|
|
/// layer. Returns the `state`.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// If the `state` was already playing and fading in with less time remaining than the `fadeDuration`, this
|
|
/// method will allow it to complete the existing fade rather than starting a slower one.
|
|
/// <para></para>
|
|
/// If the layer currently has 0 <see cref="AnimancerNode.Weight"/>, this method will fade in the layer itself
|
|
/// and simply <see cref="AnimancerState.Play"/> the `state`.
|
|
/// <para></para>
|
|
/// This method is safe to call repeatedly without checking whether the `state` was already playing.
|
|
/// <para></para>
|
|
/// <em>Animancer Lite only allows the default `fadeDuration` (0.25 seconds) in runtime builds.</em>
|
|
/// </remarks>
|
|
public AnimancerState Play(
|
|
AnimancerState state,
|
|
float fadeDuration,
|
|
FadeMode mode = default)
|
|
{
|
|
|
|
// Skip the fade if:
|
|
if (fadeDuration <= 0 ||// There is no duration.
|
|
(Graph.SkipFirstFade && Index == 0 && Weight == 0))// Or this is Layer 0 and it has no weight.
|
|
{
|
|
Weight = 1;
|
|
AnimancerState.SkipNextExpectFade();
|
|
state = Play(state);
|
|
|
|
if (mode == FadeMode.FromStart ||
|
|
mode == FadeMode.NormalizedFromStart)
|
|
state.TimeD = 0;
|
|
|
|
return state;
|
|
}
|
|
|
|
AnimancerEvent.AssertEventPlayMismatch(Graph);
|
|
|
|
EvaluateFadeMode(mode, ref state, fadeDuration, out var stateFadeSpeed, out var layerFadeDuration);
|
|
|
|
StartFade(1, layerFadeDuration);
|
|
|
|
// If the layer has to fade in, play the state immediately.
|
|
if (Weight == 0)
|
|
{
|
|
AnimancerState.SkipNextExpectFade();
|
|
return Play(state);
|
|
}
|
|
|
|
state = GetOrCreateState(state);
|
|
|
|
CurrentState = state;
|
|
|
|
// If the state is already playing or will finish fading in faster than this new fade,
|
|
// continue the existing fade.
|
|
if (IsAlreadyFadingIn(state, fadeDuration))
|
|
{
|
|
CommandCount++;// Still pretend the fade was restarted.
|
|
}
|
|
else// Otherwise fade in the target state and fade out all others.
|
|
{
|
|
|
|
state.IsPlaying = true;
|
|
|
|
var fade = GetFade();
|
|
fade.SetNodes(this, state, ActiveStatesInternal, Graph.KeepChildrenConnected);
|
|
fade.StartFade(1, stateFadeSpeed);
|
|
}
|
|
|
|
return state;
|
|
}
|
|
|
|
/************************************************************************************************************************/
|
|
|
|
/// <summary>Is the `state` already faded in or fading in with shorter than the given `fadeDuration`?</summary>
|
|
private static bool IsAlreadyFadingIn(
|
|
AnimancerState state,
|
|
float fadeDuration)
|
|
{
|
|
if (!state.IsPlaying)
|
|
return false;
|
|
|
|
var fadeGroup = state.FadeGroup;
|
|
if (fadeGroup == null)
|
|
return state.Weight == 1;
|
|
|
|
return
|
|
fadeGroup.FadeIn.Node == state &&
|
|
fadeGroup.TargetWeight == 1 &&
|
|
fadeGroup.RemainingFadeDuration <= fadeDuration;
|
|
}
|
|
|
|
/************************************************************************************************************************/
|
|
|
|
/// <summary>Clears and reuses the existing fade if there is one. Otherwise gets one from the object pool.</summary>
|
|
private FadeGroup GetFade()
|
|
{
|
|
if (CurrentState != null)
|
|
{
|
|
var fade = CurrentState.FadeGroup;
|
|
if (fade != null)
|
|
{
|
|
fade.FadeOutInternal.Clear();
|
|
fade.Easing = null;
|
|
return fade;
|
|
}
|
|
}
|
|
|
|
return FadeGroup.Pool.Instance.Acquire();
|
|
}
|
|
|
|
/************************************************************************************************************************/
|
|
// Transition.
|
|
/************************************************************************************************************************/
|
|
|
|
/// <summary>
|
|
/// Creates a state for the `transition` if it didn't already exist, then calls
|
|
/// <see cref="Play(AnimancerState)"/> or <see cref="Play(AnimancerState, float, FadeMode)"/>
|
|
/// depending on the <see cref="ITransition.FadeDuration"/>.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// This method is safe to call repeatedly without checking whether the `transition` was already playing.
|
|
/// </remarks>
|
|
public AnimancerState Play(
|
|
ITransition transition)
|
|
=> Graph.Transitions != null
|
|
? Graph.Transitions.Play(this, transition)
|
|
: Play(transition, transition.FadeDuration, transition.FadeMode);
|
|
|
|
/// <summary>
|
|
/// Creates a state for the `transition` if it didn't already exist, then calls
|
|
/// <see cref="Play(AnimancerState)"/> or <see cref="Play(AnimancerState, float, FadeMode)"/>
|
|
/// depending on the <see cref="ITransition.FadeDuration"/>.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// This method is safe to call repeatedly without checking whether the `transition` was already playing.
|
|
/// </remarks>
|
|
public AnimancerState Play(
|
|
ITransition transition,
|
|
float fadeDuration,
|
|
FadeMode mode = default)
|
|
{
|
|
var state = GetOrCreateState(transition);
|
|
state = Play(state, fadeDuration, mode);
|
|
transition.Apply(state);
|
|
return state;
|
|
}
|
|
|
|
/************************************************************************************************************************/
|
|
// Try Play.
|
|
/************************************************************************************************************************/
|
|
|
|
/// <summary>
|
|
/// Stops all other animations on this layer,
|
|
/// plays the animation registered with the `key`,
|
|
/// and returns the animation's state.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// If no state is registered with the `key`, this method does nothing and returns null.
|
|
/// <para></para>
|
|
/// The animation will continue playing from its current <see cref="AnimancerState.Time"/>.
|
|
/// To restart it from the beginning you can simply set the returned state's time to 0.
|
|
/// <para></para>
|
|
/// This method is safe to call repeatedly without checking whether the animation was already playing.
|
|
/// </remarks>
|
|
public AnimancerState TryPlay(
|
|
object key)
|
|
{
|
|
if (Graph.Transitions != null)
|
|
{
|
|
var transitionState = Graph.Transitions.TryPlay(this, key);
|
|
if (transitionState != null)
|
|
return transitionState;
|
|
}
|
|
|
|
return Graph.States.TryGet(key, out var state)
|
|
? Play(state)
|
|
: null;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Stops all other animations on this layer,
|
|
/// plays the animation registered with the `key`,
|
|
/// and returns the animation's state.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// If no state is registered with the `key`, this method does nothing and returns null.
|
|
/// <para></para>
|
|
/// The animation will continue playing from its current <see cref="AnimancerState.Time"/>.
|
|
/// To restart it from the beginning you can simply set the returned state's time to 0.
|
|
/// <para></para>
|
|
/// This method is safe to call repeatedly without checking whether the animation was already playing.
|
|
/// </remarks>
|
|
public AnimancerState TryPlay(
|
|
IHasKey hasKey)
|
|
=> TryPlay(hasKey.Key);
|
|
|
|
/// <summary>
|
|
/// Starts fading in the animation registered with the `key`
|
|
/// while fading out all others in the same layer over the course of the `fadeDuration`
|
|
/// and returns the animation's state.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// If no state is registered with the `key`, this method does nothing and returns null.
|
|
/// <para></para>
|
|
/// If the `state` was already playing and fading in with less time remaining than the `fadeDuration`,
|
|
/// this method allows it to continue the existing fade rather than starting a slower one.
|
|
/// <para></para>
|
|
/// If the layer currently has 0 <see cref="AnimancerNode.Weight"/>, this method will
|
|
/// fade in the layer itself and simply <see cref="AnimancerState.Play"/> the `state`.
|
|
/// <para></para>
|
|
/// This method is safe to call repeatedly without checking whether the animation was already playing.
|
|
/// <para></para>
|
|
/// <em>Animancer Lite only allows the default `fadeDuration` (0.25 seconds) in runtime builds.</em>
|
|
/// </remarks>
|
|
public AnimancerState TryPlay(
|
|
object key,
|
|
float fadeDuration,
|
|
FadeMode mode = default)
|
|
=> Graph.States.TryGet(key, out var state)
|
|
? Play(state, fadeDuration, mode)
|
|
: null;
|
|
|
|
/// <summary>
|
|
/// Starts fading in the animation registered with the `key`
|
|
/// while fading out all others in the same layer over the course of the `fadeDuration`
|
|
/// and returns the animation's state.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// If no state is registered with the `key`, this method does nothing and returns null.
|
|
/// <para></para>
|
|
/// If the `state` was already playing and fading in with less time remaining than the `fadeDuration`,
|
|
/// this method allows it to continue the existing fade rather than starting a slower one.
|
|
/// <para></para>
|
|
/// If the layer currently has 0 <see cref="AnimancerNode.Weight"/>, this method will
|
|
/// fade in the layer itself and simply <see cref="AnimancerState.Play"/> the `state`.
|
|
/// <para></para>
|
|
/// This method is safe to call repeatedly without checking whether the animation was already playing.
|
|
/// <para></para>
|
|
/// <em>Animancer Lite only allows the default `fadeDuration` (0.25 seconds) in runtime builds.</em>
|
|
/// </remarks>
|
|
public AnimancerState TryPlay(
|
|
IHasKey hasKey,
|
|
float fadeDuration,
|
|
FadeMode mode = default)
|
|
=> TryPlay(hasKey.Key, fadeDuration, mode);
|
|
|
|
/************************************************************************************************************************/
|
|
|
|
/// <summary>Manipulates the other parameters according to the `mode`.</summary>
|
|
/// <exception cref="ArgumentException">
|
|
/// The <see cref="AnimancerState.Clip"/> is null when using <see cref="FadeMode.FromStart"/> or
|
|
/// <see cref="FadeMode.NormalizedFromStart"/>.
|
|
/// </exception>
|
|
private void EvaluateFadeMode(
|
|
FadeMode mode,
|
|
ref AnimancerState state,
|
|
float fadeDuration,
|
|
out float stateFadeSpeed,
|
|
out float layerFadeDuration)
|
|
{
|
|
layerFadeDuration = fadeDuration;
|
|
|
|
float fadeDistance;
|
|
|
|
switch (mode)
|
|
{
|
|
case FadeMode.FixedSpeed:
|
|
fadeDistance = 1;
|
|
layerFadeDuration *= Math.Abs(1 - Weight);
|
|
break;
|
|
|
|
case FadeMode.FixedDuration:
|
|
fadeDistance = Math.Abs(1 - state.Weight);
|
|
break;
|
|
|
|
case FadeMode.FromStart:
|
|
state = GetOrCreateWeightlessState(state);
|
|
fadeDistance = 1;
|
|
break;
|
|
|
|
case FadeMode.NormalizedSpeed:
|
|
{
|
|
var length = state.Length;
|
|
fadeDistance = 1;
|
|
fadeDuration *= length;
|
|
layerFadeDuration *= Math.Abs(1 - Weight) * length;
|
|
}
|
|
break;
|
|
|
|
case FadeMode.NormalizedDuration:
|
|
{
|
|
var length = state.Length;
|
|
fadeDistance = Math.Abs(1 - state.Weight);
|
|
fadeDuration *= length;
|
|
layerFadeDuration *= length;
|
|
}
|
|
break;
|
|
|
|
case FadeMode.NormalizedFromStart:
|
|
{
|
|
state = GetOrCreateWeightlessState(state);
|
|
|
|
var length = state.Length;
|
|
fadeDistance = 1;
|
|
fadeDuration *= length;
|
|
layerFadeDuration *= length;
|
|
}
|
|
break;
|
|
|
|
default:
|
|
throw AnimancerUtilities.CreateUnsupportedArgumentException(mode);
|
|
}
|
|
|
|
stateFadeSpeed = fadeDistance / fadeDuration;
|
|
}
|
|
|
|
/************************************************************************************************************************/
|
|
// Stopping
|
|
/************************************************************************************************************************/
|
|
|
|
/// <summary>
|
|
/// Sets <see cref="AnimancerNode.Weight"/> = 0 and calls <see cref="AnimancerNode.Stop"/>
|
|
/// on all animations to stop them from playing and rewind them to the start.
|
|
/// </summary>
|
|
protected internal override void StopWithoutWeight()
|
|
{
|
|
CurrentState = null;
|
|
|
|
for (int i = ActiveStatesInternal.Count - 1; i >= 0; i--)
|
|
ActiveStatesInternal[i].Stop();
|
|
}
|
|
|
|
/************************************************************************************************************************/
|
|
// Checking
|
|
/************************************************************************************************************************/
|
|
|
|
/// <summary>
|
|
/// Returns true if the `clip` is currently being played by at least one state.
|
|
/// </summary>
|
|
public bool IsPlayingClip(AnimationClip clip)
|
|
{
|
|
for (int i = ActiveStatesInternal.Count - 1; i >= 0; i--)
|
|
{
|
|
var state = ActiveStatesInternal[i];
|
|
if (state.Clip == clip && state.IsPlaying)
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns true if at least one animation is being played.
|
|
/// </summary>
|
|
public bool IsAnyStatePlaying()
|
|
{
|
|
for (int i = ActiveStatesInternal.Count - 1; i >= 0; i--)
|
|
if (ActiveStatesInternal[i].IsPlaying)
|
|
return true;
|
|
|
|
return false;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns true if the <see cref="CurrentState"/> is playing and hasn't yet reached its end.
|
|
/// <para></para>
|
|
/// 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.
|
|
/// </summary>
|
|
public override bool IsPlayingAndNotEnding()
|
|
=> _CurrentState != null && _CurrentState.IsPlayingAndNotEnding();
|
|
|
|
/************************************************************************************************************************/
|
|
|
|
/// <summary>
|
|
/// Calculates the total <see cref="AnimancerNode.Weight"/> of all states in this layer.
|
|
/// </summary>
|
|
public float GetTotalChildWeight()
|
|
{
|
|
float weight = 0;
|
|
|
|
for (int i = ActiveStatesInternal.Count - 1; i >= 0; i--)
|
|
{
|
|
weight += ActiveStatesInternal[i].Weight;
|
|
}
|
|
|
|
return weight;
|
|
}
|
|
|
|
/************************************************************************************************************************/
|
|
#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
|
|
/************************************************************************************************************************/
|
|
|
|
/// <summary>[<see cref="IAnimationClipCollection"/>]
|
|
/// Gathers all the animations in this layer.
|
|
/// </summary>
|
|
public void GatherAnimationClips(ICollection<AnimationClip> clips)
|
|
=> clips.GatherFromSource(States);
|
|
|
|
/************************************************************************************************************************/
|
|
|
|
/// <summary>The Inspector display name of this layer.</summary>
|
|
public override string ToString()
|
|
{
|
|
#if UNITY_ASSERTIONS
|
|
if (DebugName == null)
|
|
{
|
|
if (_Mask != null)
|
|
return _Mask.GetCachedName();
|
|
|
|
SetDebugName(Index == 0
|
|
? "Base Layer"
|
|
: "Layer " + Index);
|
|
}
|
|
|
|
return base.ToString();
|
|
#else
|
|
return "Layer " + Index;
|
|
#endif
|
|
}
|
|
|
|
/************************************************************************************************************************/
|
|
|
|
/// <inheritdoc/>
|
|
protected override void AppendDetails(StringBuilder text, string separator)
|
|
{
|
|
base.AppendDetails(text, separator);
|
|
|
|
text.AppendField(separator, nameof(CurrentState), CurrentState?.GetPath());
|
|
text.AppendField(separator, nameof(CommandCount), CommandCount);
|
|
text.AppendField(separator, nameof(IsAdditive), IsAdditive);
|
|
text.AppendField(separator, nameof(Mask), Mask);
|
|
}
|
|
|
|
/************************************************************************************************************************/
|
|
#endregion
|
|
/************************************************************************************************************************/
|
|
}
|
|
}
|
|
|