// 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 /************************************************************************************************************************/ } }