// 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
{
/// A group of s which are cross-fading.
///
///
/// Documentation:
///
/// Custom Easing
///
///
/// https://kybernetik.com.au/animancer/api/Animancer/FadeGroup
///
public partial class FadeGroup : Updatable,
ICloneable,
ICopyable,
IHasDescription
{
/************************************************************************************************************************/
#region Fields and Properties
/************************************************************************************************************************/
// Parameters.
/************************************************************************************************************************/
/// The 0-1 progress of this fade.
public float NormalizedTime { get; set; }
/// The which the is moving towards.
public float TargetWeight { get; set; }
/// The speed at which the increases.
public float FadeSpeed { get; set; }
/// The total amount of time this fade will take to complete (in seconds).
public float FadeDuration
{
get => FadeSpeed != 0
? 1 / FadeSpeed
: float.PositiveInfinity;
set => FadeSpeed = value != 0
? 1 / value
: float.PositiveInfinity;
}
/// The remaining amount of time this fade will take to complete (in seconds).
public float RemainingFadeDuration
{
get => FadeSpeed != 0
? (1 - NormalizedTime) / FadeSpeed
: float.PositiveInfinity;
set => FadeSpeed = value != 0
? (1 - NormalizedTime) / value
: float.PositiveInfinity;
}
/************************************************************************************************************************/
// Parent.
/************************************************************************************************************************/
/// The .
public AnimancerGraph Graph { get; private set; }
/// The .
public AnimancerNodeBase Parent { get; private set; }
/// The of the .
public Playable ParentPlayable { get; private set; }
/// Should the fading nodes always be connected to the ?
public bool KeepChildrenConnected { get; private set; }
/************************************************************************************************************************/
// Nodes.
/************************************************************************************************************************/
/// The node which is fading towards the .
public NodeWeight FadeIn { get; private set; }
internal readonly List FadeOutInternal = new();
/// The nodes which are fading out.
public IReadOnlyList FadeOut => FadeOutInternal;
/************************************************************************************************************************/
// Custom Fade.
/************************************************************************************************************************/
private Func _Easing;
/// [Pro-Only] An optional function for modifying the fade curve.
///
/// The is passed in and the return value is multiplied by the
/// to set the of the .
///
/// has various common functions that could be used here.
///
/// Note that the may be null
/// right after playing something if it was already playing, so
///
///
/// can be used to avoid needing to null-check it.
///
/// Animancer Lite ignores this property in runtime builds.
///
public Func Easing
{
get => _Easing;
set
{
_Easing = value;
AssertNormalizedBounds(value, nameof(Easing));
}
}
/************************************************************************************************************************/
#endregion
/************************************************************************************************************************/
#region Initialization
/************************************************************************************************************************/
/// Assigns the target nodes that will be faded.
public void SetNodes(
AnimancerNode parent,
AnimancerNode fadeIn,
IReadOnlyList 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;
}
}
}
/************************************************************************************************************************/
/// Assigns the with no .
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;
}
/************************************************************************************************************************/
/// Adds a node to the list.
public void AddFadeOut(AnimancerNode fadeOut)
{
FadeOutInternal.Add(new(fadeOut));
fadeOut.FadeGroup = this;
}
/************************************************************************************************************************/
/// Sets the starting values and registers this fade to be updated.
public void StartFade(
float targetWeight,
float fadeSpeed)
{
NormalizedTime = 0;
TargetWeight = targetWeight;
FadeSpeed = fadeSpeed;
StartFade();
}
/// Registers this fade to be updated.
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
/************************************************************************************************************************/
/// Should this fade continue?
public bool IsValid
=> FadeSpeed > 0;
/************************************************************************************************************************/
/// Does this fade affect the `node`?
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;
}
/************************************************************************************************************************/
///
/// Returns the if the `node` is the .
/// Otherwise, returns 0.
///
public float GetTargetWeight(AnimancerNode node)
{
return FadeIn.Node == node
? TargetWeight
: 0;
}
/************************************************************************************************************************/
#endregion
/************************************************************************************************************************/
#region Methods
/************************************************************************************************************************/
///
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();
}
/************************************************************************************************************************/
///
/// Recalculates the node weights based on the .
///
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);
}
/************************************************************************************************************************/
/// Interrupts this fade and releases it to the .
public void Cancel()
{
Graph.CancelPreUpdate(this);
Release();
}
/************************************************************************************************************************/
/// Removes the `node` from this and returns true if successful.
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;
}
/************************************************************************************************************************/
///
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);
}
}
/************************************************************************************************************************/
/// [Assert-Conditional] Checks .
[System.Diagnostics.Conditional(Strings.Assertions)]
public static void AssertNormalizedBounds(Func 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
/************************************************************************************************************************/
///
public virtual FadeGroup Clone(CloneContext context)
{
if (!IsValid)
return null;
var clone = new FadeGroup();
clone.CopyFrom(this, context);
return clone;
}
/************************************************************************************************************************/
///
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;
}
/************************************************************************************************************************/
/// Creates a clone of this for a single target node (`copyTo`).
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
/************************************************************************************************************************/
/// An for .
/// https://kybernetik.com.au/animancer/api/Animancer/Pool
public class Pool : ObjectPool
{
/************************************************************************************************************************/
/// Singleton.
public static Pool Instance = new();
/************************************************************************************************************************/
///
protected override FadeGroup New()
=> new();
/************************************************************************************************************************/
#if UNITY_ASSERTIONS
/************************************************************************************************************************/
///
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;
}
///
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
/************************************************************************************************************************/
}
}