// Animancer // Copyright 2020 Kybernetik // using System; using System.Collections; using System.Collections.Generic; using System.Text; using UnityEngine; using UnityEngine.Playables; namespace Animancer { /// /// Base class for wrapper objects in . /// public abstract class AnimancerNode : Key, IEnumerable, IEnumerator, IPlayableWrapper { /************************************************************************************************************************/ #region Graph /************************************************************************************************************************/ /// /// The internal struct this state manages in the . /// /// Should be set in the child class constructor. Failure to do so will throw the following exception /// throughout the system when using this node: ": The playable passed as an /// argument is invalid. To create a valid playable, please use the appropriate Create method". /// protected internal Playable _Playable; /// [Internal] The managed by this object. Playable IPlayableWrapper.Playable { get { return _Playable; } } /************************************************************************************************************************/ /// The at the root of the graph. public readonly AnimancerPlayable Root; /// The root which this node is connected to. public abstract AnimancerLayer Layer { get; } /// The object which receives the output of this node. public abstract IPlayableWrapper Parent { get; } /************************************************************************************************************************/ /// /// 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 won't 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 can't 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; } /************************************************************************************************************************/ /// Constructs a new . protected AnimancerNode(AnimancerPlayable root) { if (root == null) throw new ArgumentNullException("root"); Index = -1; Root = root; } /************************************************************************************************************************/ /// The number of states using this node as their . public virtual int ChildCount { get { return 0; } } /// /// Returns the state connected to the specified `index` as a child of this node. /// /// Thrown if this node can't have children. public virtual AnimancerState GetChild(int index) { throw new NotSupportedException(); } /// /// Called when a child is connected with this node as its . /// /// Thrown if this node can't have children. protected internal virtual void OnAddChild(AnimancerState state) { throw new NotSupportedException(); } /// /// Called when a child's is changed from this node to something else. /// /// Thrown if this node can't have children. protected internal virtual void OnRemoveChild(AnimancerState state) { throw new NotSupportedException(); } /************************************************************************************************************************/ /// Connects the `state` to the `mixer` at its . /// Thrown if the was already occupied. protected void OnAddChild(IList states, AnimancerState state) { var index = state.Index; if (states[index] != null) { state.ClearParent(); throw new InvalidOperationException( "Tried to add a state to an already occupied port on " + this + ":" + "\n Port: " + index + "\n Old State: " + states[index] + "\n New State: " + state); } states[index] = state; if (KeepChildrenConnected) { state.ConnectToGraph(); } else { state.SetWeightDirty(); } } /************************************************************************************************************************/ /// [Internal] /// Called by for any states connected to this mixer. /// Adds the `state`s port to a list of spares to be reused by another state and notifies the root /// . /// protected internal virtual void OnChildDestroyed(AnimancerState state) { } /************************************************************************************************************************/ /// /// Connects the to the . /// public void ConnectToGraph() { var parent = Parent; if (parent == null) return; Root._Graph.Connect(_Playable, 0, parent.Playable, Index); if (_IsWeightDirty) Root.RequireUpdate(this); } /// /// Disconnects the from the . /// public void DisconnectFromGraph() { var parent = Parent; if (parent == null) return; var parentMixer = parent.Playable; if (parentMixer.GetInput(Index).IsValid()) Root._Graph.Disconnect(parentMixer, Index); } /************************************************************************************************************************/ /// /// Indicates whether child playables should stay connected to this mixer at all times (default false). /// public virtual bool KeepChildrenConnected { get { return false; } } /// /// Ensures that all children of this node are connected to the . /// internal void ConnectAllChildrenToGraph() { if (!Parent.Playable.GetInput(Index).IsValid()) ConnectToGraph(); var count = ChildCount; for (int i = 0; i < count; i++) GetChild(i).ConnectAllChildrenToGraph(); } /// /// Ensures that all children of this node which have zero weight are disconnected from the /// . /// internal void DisconnectWeightlessChildrenFromGraph() { if (Weight == 0) DisconnectFromGraph(); var count = ChildCount; for (int i = 0; i < count; i++) GetChild(i).DisconnectWeightlessChildrenFromGraph(); } /************************************************************************************************************************/ /// /// Indicates whether the is usable (properly initialised and not destroyed). /// public bool IsValid { get { return _Playable.IsValid(); } } /************************************************************************************************************************/ // IEnumerable for 'foreach' statements. /************************************************************************************************************************/ /// Gets an enumerator for all of this node's child states. public virtual IEnumerator GetEnumerator() { yield break; } IEnumerator IEnumerable.GetEnumerator() { return GetEnumerator(); } /************************************************************************************************************************/ // IEnumerator for yielding in a coroutine to wait until animations have stopped. /************************************************************************************************************************/ /// /// Returns true if the animation 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. /// protected internal abstract bool IsPlayingAndNotEnding(); /// Calls . bool IEnumerator.MoveNext() { return IsPlayingAndNotEnding(); } /// Returns null. object IEnumerator.Current { get { return null; } } /// Does nothing. void IEnumerator.Reset() { } /************************************************************************************************************************/ #endregion /************************************************************************************************************************/ #region Weight /************************************************************************************************************************/ /// The current blend weight of this node. Accessed via . private float _Weight; /// Indicates whether the weight has changed and should be applied to the parent mixer. private bool _IsWeightDirty = true; /************************************************************************************************************************/ /// /// 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 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 a runtime build. /// /// /// /// 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 does not 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 { get { return _Weight; } set { SetWeight(value); TargetWeight = value; FadeSpeed = 0; } } /// /// 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 a runtime build. /// public void SetWeight(float value) { if (_Weight == value) return; Debug.Assert(!float.IsNaN(value), "Weight must not be NaN"); _Weight = value; _IsWeightDirty = true; Root.RequireUpdate(this); } /// /// Flags this node as having a dirty weight that needs to be applied next update. /// protected internal void SetWeightDirty() { _IsWeightDirty = true; Root.RequireUpdate(this); } /************************************************************************************************************************/ /// [Internal] /// Applies the to the connection between this node and its . /// internal void ApplyWeight() { if (!_IsWeightDirty) return; _IsWeightDirty = false; var parent = Parent; if (parent == null) return; Playable parentMixer; if (!parent.KeepChildrenConnected) { if (_Weight == 0) { DisconnectFromGraph(); return; } parentMixer = parent.Playable; if (!parentMixer.GetInput(Index).IsValid()) ConnectToGraph(); } else parentMixer = parent.Playable; parentMixer.SetInputWeight(Index, _Weight); } /************************************************************************************************************************/ #endregion /************************************************************************************************************************/ #region Fading /************************************************************************************************************************/ /// /// The desired which this node is fading towards according to the /// . /// public float TargetWeight { get; set; } /// /// The speed at which this node is fading towards the . /// public float FadeSpeed { get; set; } /************************************************************************************************************************/ /// /// 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` in a runtime build. /// public void StartFade(float targetWeight, float fadeDuration = AnimancerPlayable.DefaultFadeDuration) { TargetWeight = targetWeight; if (targetWeight == Weight) { if (targetWeight == 0) { Stop(); } else { FadeSpeed = 0; OnStartFade(); } return; } // Duration 0 = Instant. if (fadeDuration <= 0) { FadeSpeed = float.PositiveInfinity; } else// Otherwise determine how fast we need to go to cover the distance in the specified time. { FadeSpeed = Math.Abs(Weight - targetWeight) / fadeDuration; } OnStartFade(); Root.RequireUpdate(this); } /************************************************************************************************************************/ /// /// Called by . /// protected internal abstract void OnStartFade(); /************************************************************************************************************************/ /// /// Stops the animation and makes it inactive immediately so it no longer affects the output. /// Sets = 0 by default. /// public virtual void Stop() { Weight = 0; } /************************************************************************************************************************/ /// /// Moves the towards the according to the /// . /// private void UpdateFade(out bool needsMoreUpdates) { var fadeSpeed = FadeSpeed; if (fadeSpeed == 0) { needsMoreUpdates = false; return; } _IsWeightDirty = true; fadeSpeed *= ParentEffectiveSpeed * AnimancerPlayable.DeltaTime; if (fadeSpeed < 0) fadeSpeed = -fadeSpeed; var target = TargetWeight; var current = _Weight; var delta = target - current; if (delta > 0) { if (delta > fadeSpeed) { _Weight = current + fadeSpeed; needsMoreUpdates = true; return; } } else { if (-delta > fadeSpeed) { _Weight = current - fadeSpeed; needsMoreUpdates = true; return; } } _Weight = target; needsMoreUpdates = false; if (target == 0) { Stop(); } else { FadeSpeed = 0; } } /************************************************************************************************************************/ #endregion /************************************************************************************************************************/ /// /// Updates the for fading, applies it to this state's port on the parent mixer, and plays /// or pauses the if its state is dirty. /// /// If the 's is set to false, this method will /// also connect/disconnect this node from the in the playable graph. /// protected internal virtual void Update(out bool needsMoreUpdates) { UpdateFade(out needsMoreUpdates); ApplyWeight(); } /************************************************************************************************************************/ #region Misc /************************************************************************************************************************/ #if UNITY_EDITOR /// [Editor-Only] [Internal] Indicates whether the Inspector details for this node are expanded. internal bool _IsInspectorExpanded; #endif /************************************************************************************************************************/ private float _Speed = 1; /// /// How fast the is advancing every frame. /// /// 1 is the normal speed. /// /// A negative value will play the animation backwards. /// /// Animancer Lite does not allow this value to be changed in a runtime build. /// /// /// /// /// void PlayAnimation(AnimancerComponent animancer, AnimationClip clip) /// { /// var state = animancer.Play(clip); /// /// state.Speed = 1;// Normal speed. /// state.Speed = 2;// Double speed. /// state.Speed = 0.5f;// Half speed. /// state.Speed = -1;// Normal speed playing backwards. /// } /// /// public float Speed { get { return _Speed; } set { Debug.Assert(!float.IsNaN(value), "Speed must not be NaN"); _Speed = value; _Playable.SetSpeed(value); } } /************************************************************************************************************************/ /// /// The of each of this node's parents down the hierarchy, including the root /// . /// private float ParentEffectiveSpeed { get { var speed = Root.Speed; var parent = Parent; while (parent != null) { speed *= parent.Speed; parent = parent.Parent; } return speed; } } /// /// The of this node multiplied by the of each of its parents down the /// hierarchy (including the root ) to determine the actual speed its output is /// being played at. /// public float EffectiveSpeed { get { return Speed * ParentEffectiveSpeed; } set { Speed = value / ParentEffectiveSpeed; } } /************************************************************************************************************************/ #region Descriptions /************************************************************************************************************************/ /// Returns a detailed descrption of the current details of this node. public string GetDescription(int maxChildDepth = 10, string delimiter = "\n") { var text = new StringBuilder(); AppendDescription(text, maxChildDepth, delimiter); return text.ToString(); } /************************************************************************************************************************/ /// Appends a detailed descrption of the current details of this node. public void AppendDescription(StringBuilder text, int maxChildDepth = 10, string delimiter = "\n") { if (text.Length > 0) text.Append(delimiter); text.Append(ToString()); delimiter += " "; AppendDetails(text, delimiter); if (maxChildDepth-- > 0 && ChildCount > 0) { text.Append(delimiter).Append("ChildCount: ").Append(ChildCount); var indentedDelimiter = delimiter + " "; foreach (var childState in this) { text.Append(delimiter).Append("[").Append(childState.Index).Append("] "); childState.AppendDescription(text, maxChildDepth, indentedDelimiter); } } } /************************************************************************************************************************/ /// /// Called by to append the details of this node. /// protected virtual void AppendDetails(StringBuilder text, string delimiter) { text.Append(delimiter).Append("Index: ").Append(Index); text.Append(delimiter).Append("Speed: ").Append(Speed); text.Append(delimiter).Append("Weight: ").Append(Weight); if (Weight != TargetWeight) { text.Append(delimiter).Append("TargetWeight: ").Append(TargetWeight); text.Append(delimiter).Append("FadeSpeed: ").Append(FadeSpeed); } } /************************************************************************************************************************/ #endregion /************************************************************************************************************************/ #endregion /************************************************************************************************************************/ } }