// 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
{
///
/// A layer on which animations can play with their states managed independantly of other layers while blending the
/// output with those layers.
///
///
///
/// This class can be used as a custom yield instruction to wait until all animations finish playing.
///
/// Documentation:
///
/// Layers
///
/// https://kybernetik.com.au/animancer/api/Animancer/AnimancerLayer
///
public class AnimancerLayer : AnimancerNode,
IAnimationClipCollection,
ICopyable
{
/************************************************************************************************************************/
#region Fields and Properties
/************************************************************************************************************************/
/// [Internal] Creates a new .
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));
}
/************************************************************************************************************************/
/// Creates and assigns the managed by this layer.
protected override void CreatePlayable(out Playable playable)
=> playable = AnimationMixerPlayable.Create(Graph._PlayableGraph, _Capacity);
/************************************************************************************************************************/
/// A layer is its own root.
public override AnimancerLayer Layer
=> this;
///
public override bool KeepChildrenConnected
=> Graph.KeepChildrenConnected;
/************************************************************************************************************************/
/// The animation states connected to this layer.
private readonly List
States = new();
/************************************************************************************************************************/
private readonly AnimancerState.ActiveList
ActiveStatesInternal;
/// The states connected to this layer which are .
public IReadOnlyIndexedList ActiveStates
=> ActiveStatesInternal;
/************************************************************************************************************************/
/// The default is 8 unless changed.
///
/// This value only affects newly created layers.
///
/// 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.
///
public static int DefaultCapacity = 8;
private int _Capacity = DefaultCapacity;
///
/// The number of states that can be connected to this layer before its needs to
/// allocate more inputs.
///
/// Starts at the and doubles each time it needs to expand.
/// This value cannot be set lower than the .
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;
/// The state of the animation currently being played.
///
/// 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 itself will
/// not register in this property.
///
/// Each time this property changes, the is incremented.
///
public AnimancerState CurrentState
{
get => _CurrentState;
private set
{
_CurrentState = value;
CommandCount++;
}
}
///
/// The number of times the 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.
///
public int CommandCount { get; private set; }
#if UNITY_EDITOR
/// [Editor-Only] [Internal] Increases the by 1.
internal void IncrementCommandCount() => CommandCount++;
#endif
/************************************************************************************************************************/
/// [Pro-Only]
/// Determines whether this layer is set to additive blending. Otherwise it will override any earlier layers.
///
public bool IsAdditive
{
get => Graph.Layers.IsAdditive(Index);
set => Graph.Layers.SetAdditive(Index, value);
}
/************************************************************************************************************************/
/// [Internal] [Pro-Only] The mask that determines which bones this layer will affect.
internal AvatarMask _Mask;
/// [Pro-Only] The mask that determines which bones this layer will affect.
///
/// 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.
///
public AvatarMask Mask
{
get => _Mask;
set => Graph.Layers.SetMask(Index, value);
}
/************************************************************************************************************************/
///
/// The average velocity of the root motion of all currently playing animations, taking their current
/// into account.
///
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
/************************************************************************************************************************/
///
public override int ChildCount
=> States.Count;
/// Returns the state connected to the specified `index` as a child of this layer.
/// This method is identical to .
public override AnimancerState GetChild(int index)
=> States[index];
/// Returns the state connected to the specified `index` as a child of this layer.
/// This indexer is identical to .
public AnimancerState this[int index]
=> States[index];
/************************************************************************************************************************/
/// Connects the `state` to this layer at its .
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);
}
/************************************************************************************************************************/
/// Disconnects the `state` from this layer at its .
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);
}
/************************************************************************************************************************/
///
protected internal override void ApplyChildActive(AnimancerState state, bool setActive)
{
if (setActive)
ActiveStatesInternal.Add(state);
else
ActiveStatesInternal.Remove(state);
}
/************************************************************************************************************************/
/// [Internal] Connects all states in this layer.
internal void ConnectAllStates()
{
for (int i = ChildCount - 1; i >= 0; i--)
if (!_Playable.GetInput(i).IsValid())
ConnectChildUnsafe(i, States[i]);
}
/// [Internal] Disconnects all states which are not .
internal void DisconnectInactiveStates()
{
for (int i = ChildCount - 1; i >= 0; i--)
if (!States[i].IsActive)
DisconnectChildSafe(i);
}
/************************************************************************************************************************/
/// [Internal]
/// Checks if any events should be invoked on any of the .
///
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();
}
/************************************************************************************************************************/
/// [Internal] Cancels the current so it can be object pooled.
internal void OnGraphDestroyed()
{
CurrentState?.FadeGroup?.Cancel();
}
/************************************************************************************************************************/
///
public override FastEnumerator GetEnumerator()
=> new(States);
/************************************************************************************************************************/
///
public sealed override void CopyFrom(AnimancerNode copyFrom, CloneContext context)
=> this.CopyFromBase(copyFrom, context);
/// Copies the details of `copyFrom` into this layer.
/// Call (as well) if you want to copy the states.
public virtual void CopyFrom(AnimancerLayer copyFrom, CloneContext context)
{
base.CopyFrom(copyFrom, context);
IsAdditive = copyFrom.IsAdditive;
Mask = copyFrom.Mask;
}
/************************************************************************************************************************/
/// Copies the details of all states in `copyFrom` to their equivalent states in this layer.
///
/// Any states which do not have an equivalent in this layer will be cloned into this layer.
///
/// Call (as well)
/// if you want to copy the details of the layer itself.
///
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 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
/************************************************************************************************************************/
/// Creates and returns a new to play the `clip`.
///
/// is used to determine the .
///
public ClipState CreateState(AnimationClip clip)
=> CreateState(Graph.GetKey(clip), clip);
///
/// Creates and returns a new to play the `clip` and registers it with the `key`.
///
public ClipState CreateState(object key, AnimationClip clip)
{
var state = new ClipState(clip)
{
_Key = key,
};
state.SetParent(this);
return state;
}
/************************************************************************************************************************/
/// Returns a state registered with the `key` and attached to this layer or null if none exist.
/// The `key` is null.
///
/// 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.
///
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;
}
}
}
/************************************************************************************************************************/
///
/// Calls for each of the specified clips.
///
/// If you only want to create a single state, use .
///
public void CreateIfNew(AnimationClip clip0, AnimationClip clip1)
{
GetOrCreateState(clip0);
GetOrCreateState(clip1);
}
///
/// Calls for each of the specified clips.
///
/// If you only want to create a single state, use .
///
public void CreateIfNew(AnimationClip clip0, AnimationClip clip1, AnimationClip clip2)
{
GetOrCreateState(clip0);
GetOrCreateState(clip1);
GetOrCreateState(clip2);
}
///
/// Calls for each of the specified clips.
///
/// If you only want to create a single state, use .
///
public void CreateIfNew(AnimationClip clip0, AnimationClip clip1, AnimationClip clip2, AnimationClip clip3)
{
GetOrCreateState(clip0);
GetOrCreateState(clip1);
GetOrCreateState(clip2);
GetOrCreateState(clip3);
}
///
/// Calls for each of the specified clips.
///
/// If you only want to create a single state, use .
///
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);
}
}
/************************************************************************************************************************/
///
/// Calls and returns the state registered with that key or
/// creates one if it doesn't exist.
///
/// If the state already exists but has the wrong , the `allowSetClip`
/// parameter determines what will happen. False causes it to throw an while
/// true allows it to change the . Note that the change is somewhat costly to
/// performance to use with caution.
///
///
public AnimancerState GetOrCreateState(AnimationClip clip, bool allowSetClip = false)
{
return GetOrCreateState(Graph.GetKey(clip), clip, allowSetClip);
}
///
/// Returns the state registered with the if there is one. Otherwise
/// this method uses to create a new one and registers it with
/// that key before returning it.
///
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;
}
/// Returns the state registered with the `key` or creates one if it doesn't exist.
///
/// The `key` is null.
///
/// If the state already exists but has the wrong , the `allowSetClip`
/// parameter determines what will happen. False causes it to throw an while
/// true allows it to change the . Note that the change is somewhat costly to
/// performance to use with caution.
///
/// See also: .
///
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;
}
/// Returns the `state` if it's a child of this layer. Otherwise makes a clone of it.
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;
}
/************************************************************************************************************************/
///
/// The maximum that
/// will treat as being weightless. Default = 0.1.
///
/// This allows states with very small weights to be reused instead of needing to create new ones.
public static float WeightlessThreshold { get; set; } = 0.1f;
///
/// 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.
///
public static int MaxCloneCount { get; set; } = 3;
///
/// If the `state`'s is not currently low,
/// this method finds or creates a copy of it which is low.
/// The returned is also set to 0.
///
///
/// If this method would exceed the , it returns the clone with the lowest weight.
///
/// "Low" weight is defined as less than or equal to the .
///
/// The Fade Modes page
/// explains why clones are created.
///
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;
}
/************************************************************************************************************************/
/// Destroys all states connected to this layer.
/// This operation cannot be undone.
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
/************************************************************************************************************************/
///
protected internal override void OnStartFade()
{
CommandCount++;
}
/************************************************************************************************************************/
// Play Immediately.
/************************************************************************************************************************/
/// Stops all other animations on this layer, plays the `clip`, and returns its state.
///
/// The animation will continue playing from its current .
/// To restart it from the beginning you can use ...Play(clip).Time = 0;.
///
/// This method is safe to call repeatedly without checking whether the `clip` was already playing.
///
public AnimancerState Play(
AnimationClip clip)
=> Play(GetOrCreateState(clip));
/// Stops all other animations on the same layer, plays the `state`, and returns it.
///
/// The animation will continue playing from its current .
/// To restart it from the beginning you can use ...Play(state).Time = 0;.
///
/// This method is safe to call repeatedly without checking whether the `state` was already playing.
///
///
/// The is another state (likely a ).
/// It must be either null or a layer.
///
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.
/************************************************************************************************************************/
///
/// Starts fading in the `clip` over the course of the `fadeDuration`
/// while fading out all others in the same layer. Returns its state.
///
///
/// 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.
///
/// If the layer currently has 0 ,
/// this method will fade in the layer itself and simply the `state`.
///
/// This method is safe to call repeatedly without checking whether the `state` was already playing.
///
/// Animancer Lite only allows the default `fadeDuration` (0.25 seconds) in runtime builds.
///
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);
}
///
/// Starts fading in the `state` over the course of the `fadeDuration` while fading out all others in this
/// layer. Returns the `state`.
///
///
/// 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.
///
/// If the layer currently has 0 , this method will fade in the layer itself
/// and simply the `state`.
///
/// This method is safe to call repeatedly without checking whether the `state` was already playing.
///
/// Animancer Lite only allows the default `fadeDuration` (0.25 seconds) in runtime builds.
///
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;
}
/************************************************************************************************************************/
/// Is the `state` already faded in or fading in with shorter than the given `fadeDuration`?
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;
}
/************************************************************************************************************************/
/// Clears and reuses the existing fade if there is one. Otherwise gets one from the object pool.
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.
/************************************************************************************************************************/
///
/// Creates a state for the `transition` if it didn't already exist, then calls
/// or
/// depending on the .
///
///
/// This method is safe to call repeatedly without checking whether the `transition` was already playing.
///
public AnimancerState Play(
ITransition transition)
=> Graph.Transitions != null
? Graph.Transitions.Play(this, transition)
: Play(transition, transition.FadeDuration, transition.FadeMode);
///
/// Creates a state for the `transition` if it didn't already exist, then calls
/// or
/// depending on the .
///
///
/// This method is safe to call repeatedly without checking whether the `transition` was already playing.
///
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.
/************************************************************************************************************************/
///
/// Stops all other animations on this layer,
/// plays the animation registered with the `key`,
/// and returns the animation's state.
///
///
/// If no state is registered with the `key`, this method does nothing and returns null.
///
/// The animation will continue playing from its current .
/// To restart it from the beginning you can simply set the returned state's time to 0.
///
/// This method is safe to call repeatedly without checking whether the animation was already playing.
///
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;
}
///
/// Stops all other animations on this layer,
/// plays the animation registered with the `key`,
/// and returns the animation's state.
///
///
/// If no state is registered with the `key`, this method does nothing and returns null.
///
/// The animation will continue playing from its current .
/// To restart it from the beginning you can simply set the returned state's time to 0.
///
/// This method is safe to call repeatedly without checking whether the animation was already playing.
///
public AnimancerState TryPlay(
IHasKey hasKey)
=> TryPlay(hasKey.Key);
///
/// 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.
///
///
/// If no state is registered with the `key`, this method does nothing and returns null.
///
/// 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.
///
/// If the layer currently has 0 , this method will
/// fade in the layer itself and simply the `state`.
///
/// This method is safe to call repeatedly without checking whether the animation was already playing.
///
/// Animancer Lite only allows the default `fadeDuration` (0.25 seconds) in runtime builds.
///
public AnimancerState TryPlay(
object key,
float fadeDuration,
FadeMode mode = default)
=> Graph.States.TryGet(key, out var state)
? Play(state, fadeDuration, mode)
: null;
///
/// 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.
///
///
/// If no state is registered with the `key`, this method does nothing and returns null.
///
/// 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.
///
/// If the layer currently has 0 , this method will
/// fade in the layer itself and simply the `state`.
///
/// This method is safe to call repeatedly without checking whether the animation was already playing.
///
/// Animancer Lite only allows the default `fadeDuration` (0.25 seconds) in runtime builds.
///
public AnimancerState TryPlay(
IHasKey hasKey,
float fadeDuration,
FadeMode mode = default)
=> TryPlay(hasKey.Key, fadeDuration, mode);
/************************************************************************************************************************/
/// Manipulates the other parameters according to the `mode`.
///
/// The is null when using or
/// .
///
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
/************************************************************************************************************************/
///
/// Sets = 0 and calls
/// on all animations to stop them from playing and rewind them to the start.
///
protected internal override void StopWithoutWeight()
{
CurrentState = null;
for (int i = ActiveStatesInternal.Count - 1; i >= 0; i--)
ActiveStatesInternal[i].Stop();
}
/************************************************************************************************************************/
// Checking
/************************************************************************************************************************/
///
/// Returns true if the `clip` is currently being played by at least one state.
///
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;
}
///
/// Returns true if at least one animation is being played.
///
public bool IsAnyStatePlaying()
{
for (int i = ActiveStatesInternal.Count - 1; i >= 0; i--)
if (ActiveStatesInternal[i].IsPlaying)
return true;
return false;
}
///
/// Returns true if the is playing and hasn't yet reached its end.
///
/// This method is called by so this object can be used as a custom yield
/// instruction to wait until it finishes.
///
public override bool IsPlayingAndNotEnding()
=> _CurrentState != null && _CurrentState.IsPlayingAndNotEnding();
/************************************************************************************************************************/
///
/// Calculates the total of all states in this layer.
///
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;
///
public override bool ApplyAnimatorIK
{
get => _ApplyAnimatorIK;
set => base.ApplyAnimatorIK = _ApplyAnimatorIK = value;
}
/************************************************************************************************************************/
private bool _ApplyFootIK;
///
public override bool ApplyFootIK
{
get => _ApplyFootIK;
set => base.ApplyFootIK = _ApplyFootIK = value;
}
/************************************************************************************************************************/
#endregion
/************************************************************************************************************************/
#region Other
/************************************************************************************************************************/
/// []
/// Gathers all the animations in this layer.
///
public void GatherAnimationClips(ICollection clips)
=> clips.GatherFromSource(States);
/************************************************************************************************************************/
/// The Inspector display name of this layer.
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
}
/************************************************************************************************************************/
///
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
/************************************************************************************************************************/
}
}