// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2024 Kybernetik // using System; using System.Collections; using System.Collections.Generic; using System.Runtime.CompilerServices; using System.Text; using UnityEngine; using UnityEngine.Playables; using Object = UnityEngine.Object; namespace Animancer { /// Base class for wrapper objects in an . /// This is the base class of and . /// https://kybernetik.com.au/animancer/api/Animancer/AnimancerNode public abstract class AnimancerNode : AnimancerNodeBase, ICopyable, IEnumerable, IEnumerator, IHasDescription { /************************************************************************************************************************/ #region Playable /************************************************************************************************************************/ #if UNITY_EDITOR /// [Editor-Only] [Internal] Indicates whether the Inspector details for this node are expanded. internal bool _IsInspectorExpanded; #endif /************************************************************************************************************************/ /// Creates and assigns the managed by this node. /// This method also applies the if it was set beforehand. protected virtual void CreatePlayable() { #if UNITY_ASSERTIONS if (Graph == null) { MarkAsUsed(this); throw new InvalidOperationException($"{nameof(AnimancerNode)}.{nameof(Graph)}" + $" is null when attempting to create its {nameof(Playable)}: {this}" + $"\nThe {nameof(Graph)} is generally set when you first play a state," + $" so you probably just need to play it before trying to access it."); } if (_Playable.IsValid()) Debug.LogWarning($"{nameof(AnimancerNode)}.{nameof(CreatePlayable)}" + $" was called before destroying the previous {nameof(Playable)}: {this}", Graph?.Component as Object); #endif CreatePlayable(out _Playable); #if UNITY_ASSERTIONS if (!_Playable.IsValid()) throw new InvalidOperationException( $"{nameof(AnimancerNode)}.{nameof(CreatePlayable)}" + $" did not create a valid {nameof(Playable)} for {this}"); #endif if (Speed != 1) _Playable.SetSpeed(Speed); } /// Creates and assigns the managed by this node. protected abstract void CreatePlayable(out Playable playable); /************************************************************************************************************************/ /// Destroys the . public void DestroyPlayable() { if (_Playable.IsValid()) Graph._PlayableGraph.DestroyPlayable(_Playable); } /************************************************************************************************************************/ /// Calls and . public virtual void RecreatePlayable() { DestroyPlayable(); CreatePlayable(); } /// Calls on this node and all its children recursively. public void RecreatePlayableRecursive() { RecreatePlayable(); for (int i = ChildCount - 1; i >= 0; i--) GetChild(i)?.RecreatePlayableRecursive(); } /************************************************************************************************************************/ /// Copies the details of `copyFrom` into this node, replacing its previous contents. public virtual void CopyFrom(AnimancerNode copyFrom, CloneContext context) { SetWeight(copyFrom._Weight); FadeGroup = context.WillCloneUpdatables ? null : copyFrom.FadeGroup?.CloneForSingleTarget(copyFrom, this); Speed = copyFrom.Speed; CopyIKFlags(copyFrom); #if UNITY_ASSERTIONS DebugName = context.GetCloneOrOriginal(copyFrom.DebugName); #endif } /************************************************************************************************************************/ #endregion /************************************************************************************************************************/ #region Graph /************************************************************************************************************************/ /// The index of the port this node is connected to on the parent's . /// /// A negative value indicates that it is not assigned to a port. /// /// Indices are generally assigned starting from 0, ascending in the order they are connected to their layer. /// They will not usually change unless the changes or another state on /// the same layer is destroyed so the last state is swapped into its place to avoid shuffling everything down /// to cover the gap. /// /// The setter is internal so user defined states cannot set it incorrectly. Ideally, /// should be able to set the port in its constructor and /// should also be able to set it, but classes that further inherit from /// there should not be able to change it without properly calling that method. /// public int Index { get; internal set; } = int.MinValue; /************************************************************************************************************************/ /// Creates a new . protected AnimancerNode() { #if UNITY_ASSERTIONS if (TraceConstructor) _ConstructorStackTrace = new(true); #endif } /************************************************************************************************************************/ #if UNITY_ASSERTIONS /************************************************************************************************************************/ /// [Assert-Only] /// Should a be captured in the constructor of all new nodes so /// can include it in the warning if that node ends up being unused? /// /// This has a notable performance cost so it should only be used when trying to identify a problem. public static bool TraceConstructor { get; set; } /************************************************************************************************************************/ /// [Assert-Only] /// The stack trace of the constructor (or null if was false). /// private System.Diagnostics.StackTrace _ConstructorStackTrace; /// [Assert-Only] /// Returns the stack trace of the constructor (or null if was false). /// public static System.Diagnostics.StackTrace GetConstructorStackTrace(AnimancerNode node) => node._ConstructorStackTrace; /************************************************************************************************************************/ /// [Assert-Only] Checks . ~AnimancerNode() { if (Graph != null || Parent != null || OptionalWarning.UnusedNode.IsDisabled()) return; // ToString might throw an exception since finalizers arn't run on the main thread. string name = null; try { name = ToString(); } catch { name = GetType().FullName; } var message = $"The {nameof(Graph)} of '{name}'" + $" is null during finalization (garbage collection)." + $" This may have been caused by earlier exceptions, but otherwise it probably means" + $" that this node was never used for anything and should not have been created."; if (_ConstructorStackTrace != null) message += "\n\nThis node was created at:\n" + _ConstructorStackTrace; else message += $"\n\nEnable {nameof(AnimancerNode)}.{nameof(TraceConstructor)} on startup" + $" to allow this warning to include the {nameof(System.Diagnostics.StackTrace)}" + $" of when the node was constructed."; OptionalWarning.UnusedNode.Log(message); } /************************************************************************************************************************/ #endif /************************************************************************************************************************/ /// Connects the `child`'s to this node. /// This method is NOT safe to call if the child was already connected. protected internal void ConnectChildUnsafe(int index, AnimancerNode child) { #if UNITY_ASSERTIONS if (index < 0) { MarkAsUsed(this); throw new InvalidOperationException( $"Invalid {nameof(index)} when attempting to connect to its parent:" + "\n• Child: " + child + "\n• Parent: " + this); } Validate.AssertPlayable(child); #endif Graph._PlayableGraph.Connect(_Playable, child._Playable, index, child._Weight); } /// Disconnects the of the child at the specified `index` from this node. /// This method is safe to call if the child was already disconnected. protected void DisconnectChildSafe(int index) { if (_Playable.GetInput(index).IsValid()) Graph._PlayableGraph.Disconnect(_Playable, index); } /************************************************************************************************************************/ // IEnumerator for yielding in a coroutine to wait until animations have stopped. /************************************************************************************************************************/ /// Is this node playing and not yet at its end? /// /// This method is called by so this object can be used as a custom yield /// instruction to wait until it finishes. /// public abstract bool IsPlayingAndNotEnding(); bool IEnumerator.MoveNext() => IsPlayingAndNotEnding(); object IEnumerator.Current => null; void IEnumerator.Reset() { } /************************************************************************************************************************/ #endregion /************************************************************************************************************************/ #region Children /************************************************************************************************************************/ /// protected internal override AnimancerNode GetChildNode(int index) => GetChild(index); /// Returns the state connected to the specified `index` as a child of this node. /// When overriding, don't call this base method because it throws an exception. /// This node can't have children. public virtual AnimancerState GetChild(int index) { MarkAsUsed(this); throw new NotSupportedException(this + " can't have children."); } /// Called when a child is connected with this node as its . /// When overriding, don't call this base method because it throws an exception. /// This node can't have children. protected internal virtual void OnAddChild(AnimancerState state) { MarkAsUsed(this); state.SetParentInternal(null); throw new NotSupportedException(this + " can't have children."); } /************************************************************************************************************************/ // IEnumerable for 'foreach' statements. /************************************************************************************************************************/ /// Gets an enumerator for all of this node's child states. public virtual FastEnumerator GetEnumerator() => default; IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); /************************************************************************************************************************/ #endregion /************************************************************************************************************************/ #region Weight /************************************************************************************************************************/ /// [Internal] The current blend weight of this node. Accessed via . internal float _Weight; /************************************************************************************************************************/ /// The current blend weight of this node which determines how much it affects the final output. /// /// /// 0 has no effect while 1 applies the full effect and values inbetween apply a proportional effect. /// /// Setting this property cancels any fade currently in progress. If you don't wish to do that, you can use /// instead. /// /// Animancer Lite only allows this value to be set to 0 or 1 in runtime builds. /// /// /// /// Calling immediately sets the weight of all states to 0 /// and the new state to 1. Note that this is separate from other values like /// so a state can be paused at any point and still show its pose on the /// character or it could be still playing at 0 weight if you want it to still trigger events (though states /// are normally stopped when they reach 0 weight so you would need to explicitly set it to playing again). /// /// Calling doesn't immediately change /// the weights, but instead calls on every state to set their /// and . Then every update each state's weight will move /// towards that target value at that speed. /// public float Weight { [MethodImpl(MethodImplOptions.AggressiveInlining)] get => _Weight; set { FadeGroup = null; SetWeight(value); } } /// /// Sets the current blend weight of this node which determines how much it affects the final output. /// 0 has no effect while 1 applies the full effect of this node. /// /// /// This method allows any fade currently in progress to continue. If you don't wish to do that, you can set /// the property instead. /// /// Animancer Lite only allows this value to be set to 0 or 1 in runtime builds. /// public virtual void SetWeight(float value) => SetWeightInternal(value); /// The internal non-virtual implementation of . [MethodImpl(MethodImplOptions.AggressiveInlining)] private void SetWeightInternal(float value) { if (_Weight == value) return; Validate.AssertSetWeight(this, value); _Weight = value; if (Graph != null) Parent?.Playable.ApplyChildWeight(this); } /************************************************************************************************************************/ /// protected internal override float BaseWeight => Weight; /// /// The of this state multiplied by the of each of its parents down /// the hierarchy to determine how much this state affects the final output. /// public float EffectiveWeight { get { var weight = Weight; var parent = Parent; while (parent != null) { weight *= parent.BaseWeight; parent = parent.Parent; } return weight; } } /************************************************************************************************************************/ #endregion /************************************************************************************************************************/ #region Fading /************************************************************************************************************************/ internal FadeGroup _FadeGroup; /// The current fade being applied to this node (if any). public FadeGroup FadeGroup { [MethodImpl(MethodImplOptions.AggressiveInlining)] get => _FadeGroup; internal set { _FadeGroup?.Remove(this); _FadeGroup = value; } } /// /// The desired which this node is fading towards according to the /// . /// public float TargetWeight => FadeGroup != null ? FadeGroup.GetTargetWeight(this) : Weight; /// The speed at which this node is fading towards the . /// /// This value isn't affected by this node's , /// but is affected by its parents. /// public float FadeSpeed => FadeGroup != null ? FadeGroup.FadeSpeed : 0; /************************************************************************************************************************/ /// /// Calls and starts fading the over the course /// of the (in seconds). /// /// /// If the `targetWeight` is 0 then will be called when the fade is complete. /// /// If the is already equal to the `targetWeight` then the fade will end /// immediately. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public void StartFade(float targetWeight) => StartFade(targetWeight, AnimancerGraph.DefaultFadeDuration); /// /// Calls and starts fading the /// over the course of the `fadeDuration` (in seconds). /// /// /// If the `targetWeight` is 0 then will be called when the fade is complete. /// /// If the is already equal to the `targetWeight` /// then the fade will end immediately. /// /// Animancer Lite only allows a `targetWeight` of 0 or 1 /// and the default `fadeDuration` (0.25 seconds) in runtime builds. /// public void StartFade(float targetWeight, float fadeDuration) { if (Weight == targetWeight && FadeGroup == null) { OnStartFade(); } else if (fadeDuration > 0) { var fadeSpeed = Math.Abs(targetWeight - Weight) / fadeDuration; var fade = FadeGroup.Pool.Instance.Acquire(); fade.SetFadeIn(this); fade.StartFade(targetWeight, fadeSpeed); } else { Weight = targetWeight; } } /************************************************************************************************************************/ /// Called by . protected internal abstract void OnStartFade(); /************************************************************************************************************************/ /// Removes this node from the . public void CancelFade() => _FadeGroup?.Remove(this); /// [Internal] Called by . /// Not called when a fade fully completes. protected internal virtual void InternalClearFade() { _FadeGroup = null; } /************************************************************************************************************************/ /// Stops the animation and makes it inactive immediately so it no longer affects the output. /// /// Sets = 0 by default unless overridden. /// /// Note that playing something new will automatically stop the old animation. /// public void Stop() { FadeGroup = null; SetWeightInternal(0); StopWithoutWeight(); } /// [Internal] Stops this node without setting its . protected internal virtual void StopWithoutWeight() { } /************************************************************************************************************************/ #endregion /************************************************************************************************************************/ #region Inverse Kinematics /************************************************************************************************************************/ /// /// Should setting the /// also set this node's to match it? /// Default is true. /// public static bool ApplyParentAnimatorIK { get; set; } = true; /// /// Should setting the /// also set this node's to match it? /// Default is true. /// public static bool ApplyParentFootIK { get; set; } = true; /************************************************************************************************************************/ /// /// Copies the IK settings from `copyFrom` into this node: /// /// If is true, copy . /// If is true, copy . /// /// public virtual void CopyIKFlags(AnimancerNodeBase copyFrom) { if (Graph == null) return; if (ApplyParentAnimatorIK) ApplyAnimatorIK = copyFrom.ApplyAnimatorIK; if (ApplyParentFootIK) ApplyFootIK = copyFrom.ApplyFootIK; } /************************************************************************************************************************/ /// public override bool ApplyAnimatorIK { get { for (int i = ChildCount - 1; i >= 0; i--) { var state = GetChild(i); if (state.ApplyAnimatorIK) return true; } return false; } set { for (int i = ChildCount - 1; i >= 0; i--) { var state = GetChild(i); state.ApplyAnimatorIK = value; } } } /************************************************************************************************************************/ /// public override bool ApplyFootIK { get { for (int i = ChildCount - 1; i >= 0; i--) { var state = GetChild(i); if (state.ApplyFootIK) return true; } return false; } set { for (int i = ChildCount - 1; i >= 0; i--) { var state = GetChild(i); state.ApplyFootIK = value; } } } /************************************************************************************************************************/ #endregion /************************************************************************************************************************/ #region Descriptions /************************************************************************************************************************/ #if UNITY_ASSERTIONS /// [Assert-Only] The Inspector display name of this node. /// Set using . public object DebugName { get; private set; } #endif /// [Assert-Conditional] Sets the to display in the Inspector. [System.Diagnostics.Conditional(Strings.Assertions)] public void SetDebugName(object name) { #if UNITY_ASSERTIONS DebugName = name; #endif } /// The Inspector display name of this node. public override string ToString() { #if UNITY_ASSERTIONS if (NameCache.TryToString(DebugName, out var name)) return name; #endif return base.ToString(); } /************************************************************************************************************************/ /// public void AppendDescription(StringBuilder text, string separator = "\n") { text.Append(ToString()); AppendDetails(text, separator); if (ChildCount > 0) { text.AppendField(separator, nameof(ChildCount), ChildCount); var indentedSeparator = separator + Strings.Indent; var i = 0; foreach (var child in this) { text.Append(separator) .Append('[') .Append(i++) .Append("] ") .AppendDescription(child, indentedSeparator, true); } } } /************************************************************************************************************************/ /// Called by to append the details of this node. protected virtual void AppendDetails(StringBuilder text, string separator) { text.AppendField(separator, "Playable", _Playable.IsValid() ? _Playable.GetPlayableType().ToString() : "Invalid"); var parent = Parent; var isConnected = parent != null && parent.Playable.GetInput(Index).IsValid(); text.AppendField(separator, "Connected", isConnected); text.AppendField(separator, nameof(Index), Index); if (Index < 0) text.Append(" (No Parent)"); text.AppendField(separator, nameof(Speed), Speed); var realSpeed = _Playable.IsValid() ? _Playable.GetSpeed() : Speed; if (realSpeed != Speed) text.Append(" (Real ").Append(realSpeed).Append(')'); text.AppendField(separator, nameof(Weight), Weight); if (Weight != TargetWeight) { text.AppendField(separator, nameof(TargetWeight), TargetWeight); text.AppendField(separator, nameof(FadeSpeed), FadeSpeed); } AppendIKDetails(text, separator, this); #if UNITY_ASSERTIONS if (_ConstructorStackTrace != null) text.AppendField(separator, "ConstructorStackTrace", _ConstructorStackTrace); #endif } /************************************************************************************************************************/ /// /// Appends the details of and /// . /// public static void AppendIKDetails(StringBuilder text, string separator, AnimancerNodeBase node) { if (!node.Playable.IsValid()) return; text.Append(separator) .Append("InverseKinematics: "); if (node.ApplyAnimatorIK) { text.Append("OnAnimatorIK"); if (node.ApplyFootIK) text.Append(", FootIK"); } else if (node.ApplyFootIK) { text.Append("FootIK"); } else { text.Append("None"); } } /************************************************************************************************************************/ /// Returns the hierarchy path of this node through its s. public string GetPath() { var path = StringBuilderPool.Instance.Acquire(); if (Parent is AnimancerNode parent) { AppendPath(path, parent); AppendPortAndType(path); } else { AppendPortAndType(path); } return path.ReleaseToString(); } /// Appends the hierarchy path of this state through its s. private static void AppendPath(StringBuilder path, AnimancerNode parent) { if (parent != null) { if (parent.Parent is AnimancerNode grandParent) { AppendPath(path, grandParent); } else { var layer = parent.Layer; if (layer != null) { path.Append("Layers[") .Append(parent.Layer.Index) .Append("].States"); } else { path.Append("NoLayer -> ") .Append(parent.ToString()); } return; } } if (parent is AnimancerState state) { state.AppendPortAndType(path); } else { path.Append(" -> ") .Append(parent.GetType()); } } /// Appends "[Index] -> ToString()". private void AppendPortAndType(StringBuilder path) { path.Append('[') .Append(Index) .Append("] -> ") .Append(ToString()); } /************************************************************************************************************************/ #endregion /************************************************************************************************************************/ } }