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