// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik // #pragma warning disable CS0649 // Field is never assigned to, and will always have its default value. using System; using System.Collections.Generic; using System.Text; using UnityEngine; using UnityEngine.Animations; using UnityEngine.Playables; namespace Animancer { /// [Pro-Only] /// An which blends multiple child states /// by allowing you to control their manually. /// /// /// This mixer type is similar to the Direct Blend Type in Mecanim Blend Trees. /// The official Direct Blend Trees /// tutorial explains their general concepts and purpose which apply to s as well. /// /// Documentation: /// /// Mixers /// /// https://kybernetik.com.au/animancer/api/Animancer/ManualMixerState /// public partial class ManualMixerState : ParentState, ICopyable, IParametizedState, IUpdatable { /************************************************************************************************************************/ #region Properties /************************************************************************************************************************/ /// protected override void OnSetIsPlaying() { var isPlaying = IsPlaying; for (int i = ChildCount - 1; i >= 0; i--) ChildStates[i].IsPlaying = isPlaying; } /************************************************************************************************************************/ /// The weighted average of each child state. /// /// If there are any , /// only those states will be included in the getter calculation. /// public override double RawTime { get { var details = GetTimeDetails(); if (details.TotalWeight == 0) return base.RawTime; return details.NormalizedTime * details.Length; } set { if (value == 0) goto SetToZero; var length = Length; if (length == 0) goto SetToZero; value /= length;// Normalize. for (int i = ChildCount - 1; i >= 0; i--) ChildStates[i].NormalizedTimeD = value; return; // If the value is 0, we can set the child times more efficiently. SetToZero: for (int i = ChildCount - 1; i >= 0; i--) ChildStates[i].TimeD = 0; } } /************************************************************************************************************************/ /// public override void MoveTime(double time, bool normalized) { base.MoveTime(time, normalized); for (int i = ChildCount - 1; i >= 0; i--) ChildStates[i].MoveTime(time, normalized); } /************************************************************************************************************************/ /// public override AnimancerEvent.DispatchInfo GetEventDispatchInfo() { var details = GetTimeDetails(); return new( details.Length, details.NormalizedTime, LoopingChildCount > 0); } /************************************************************************************************************************/ /// /// Gets the time details based on the synchronized child states if any are active, /// otherwise recalculates based on all child states. /// private TimeDetails GetTimeDetails() { if (_SynchronizedChildren != null) { var details = GetTimeDetails(_SynchronizedChildren, _SynchronizedChildren.Count); if (details.TotalWeight > MinimumSynchronizeChildrenWeight) return details; } return GetTimeDetails(ChildStates, ChildCount); } /// Gets the time details based on the `states`. private TimeDetails GetTimeDetails(IList states, int count) { var details = default(TimeDetails); for (int i = count - 1; i >= 0; i--) { var state = states[i]; var weight = state.Weight; if (weight == 0) continue; var stateLength = state.Length; if (stateLength == 0) continue; details.Length += stateLength * weight; details.NormalizedTime += state.Time / stateLength * weight; details.TotalWeight += weight; } if (details.TotalWeight != 0) { var inverseTotalWeight = 1f / details.TotalWeight; details.Length *= inverseTotalWeight; details.NormalizedTime *= inverseTotalWeight; } return details; } /************************************************************************************************************************/ /// The weighted average of each child state. public override float Length { get { var length = 0f; var totalChildWeight = 0f; if (_SynchronizedChildren != null) { for (int i = _SynchronizedChildren.Count - 1; i >= 0; i--) { var state = _SynchronizedChildren[i]; var weight = state.Weight; if (weight == 0) continue; var stateLength = state.Length; if (stateLength == 0) continue; totalChildWeight += weight; length += stateLength * weight; } } if (totalChildWeight > 0) return length / totalChildWeight; totalChildWeight = CalculateTotalWeight(ChildStates, ChildCount); if (totalChildWeight <= 0) return 0; for (int i = ChildCount - 1; i >= 0; i--) { var state = ChildStates[i]; length += state.Length * state.Weight; } return length / totalChildWeight; } } /************************************************************************************************************************/ /// Details used for managing the timing of a mixer. /// https://kybernetik.com.au/animancer/api/Animancer/TimeDetails public struct TimeDetails { /************************************************************************************************************************/ /// public float Length; /// public float NormalizedTime; /// The total weight of all child states. public float TotalWeight; /************************************************************************************************************************/ /// Returns a description of these details. public override readonly string ToString() => $"{nameof(TimeDetails)}(" + $"{nameof(Length)}={Length}, " + $"{nameof(NormalizedTime)}={NormalizedTime}, " + $"{nameof(TotalWeight)}={TotalWeight})"; /************************************************************************************************************************/ } /************************************************************************************************************************/ #endregion /************************************************************************************************************************/ #region Initialization /************************************************************************************************************************/ /// Connects the `child` to this mixer at its . protected internal override void OnAddChild(AnimancerState child) { base.OnAddChild(child); if (child.IsLooping) AddIsLooping(1); if (SynchronizeNewChildren) Synchronize(child); } /************************************************************************************************************************/ /// Disconnects the `state` from this mixer at its . protected internal override void OnRemoveChild(AnimancerState child) { DontSynchronize(child); if (child.IsLooping) AddIsLooping(-1); base.OnRemoveChild(child); } /************************************************************************************************************************/ /// public override void Set(int index, AnimancerState child, bool destroyPrevious) { #if UNITY_ASSERTIONS if ((uint)index >= ChildCount) { MarkAsUsed(this); MarkAsUsed(child); throw new IndexOutOfRangeException( $"Invalid child index. Must be 0 <= index < {nameof(ChildCount)} ({ChildCount})."); } #endif var previousChild = ChildStates[index]; DontSynchronize(previousChild); var loopingOffset = 0; if (previousChild.IsLooping) loopingOffset--; if (child.IsLooping) loopingOffset++; if (loopingOffset != 0) AddIsLooping(loopingOffset); base.Set(index, child, destroyPrevious); if (SynchronizeNewChildren) Synchronize(child); } /************************************************************************************************************************/ /// public override void Destroy() { DestroyChildren(); base.Destroy(); } /************************************************************************************************************************/ /// public override AnimancerState Clone(CloneContext context) { var clone = new ManualMixerState(); clone.CopyFrom(this, context); return clone; } /************************************************************************************************************************/ /// public virtual void CopyFrom(ManualMixerState copyFrom, CloneContext context) { var synchronizeNewChildren = SynchronizeNewChildren; SynchronizeNewChildren = false; base.CopyFrom(copyFrom, context); SynchronizeNewChildren = synchronizeNewChildren; var childCount = copyFrom.ChildCount; for (int i = 0; i < childCount; i++) if (copyFrom.IsSynchronized(copyFrom.ChildStates[i])) Synchronize(ChildStates[i]); } /************************************************************************************************************************/ #endregion /************************************************************************************************************************/ #region Looping /************************************************************************************************************************/ /// If greater than 0 then is true. public int LoopingChildCount { get; private set; } /************************************************************************************************************************/ /// Are any child states looping? public override bool IsLooping => LoopingChildCount > 0; /************************************************************************************************************************/ /// Sets and informs the s. private void AddIsLooping(int offset) { var wasLooping = IsLooping; LoopingChildCount += offset; var isLooping = IsLooping; if (wasLooping != isLooping) OnIsLoopingChangedRecursive(isLooping); } /************************************************************************************************************************/ /// protected override void OnChildIsLoopingChanged(bool value) => AddIsLooping(value ? 1 : -1); /************************************************************************************************************************/ #endregion /************************************************************************************************************************/ #region Jobs /************************************************************************************************************************/ /// /// Creates an to run the specified Animation Job instead of the usual /// . /// /// /// /// Example: /// void CreatePlayableExample(AnimancerComponent animancer) /// } /// var job = new MyJob();// A struct that implements IAnimationJob. /// var mixer = new WhateverMixerState();// e.g. LinearMixerState. /// mixer.CreatePlayable(animancer, job); /// // Use mixer.Initialize, CreateChild, and SetChild to configure the children as normal. /// } /// /// See also: /// public AnimationScriptPlayable CreatePlayable( AnimancerGraph graph, T job, bool processInputs = false) where T : struct, IAnimationJob { // Can't just use SetGraph normally because it would call the regular CreatePlayable method. SetGraph(null); Graph = graph; graph.States.Register(this); var playable = AnimationScriptPlayable.Create(graph._PlayableGraph, job, ChildCount); if (!processInputs) playable.SetProcessInputs(false); for (int i = ChildCount - 1; i >= 0; i--) ChildStates[i].SetGraph(graph); return playable; } /************************************************************************************************************************/ /// /// Creates an to run the specified Animation Job instead of the usual /// . /// /// /// /// /// Documentation: /// /// Creating Custom States /// /// Example: /// public class MyMixer : LinearMixerState /// { /// protected override void CreatePlayable(out Playable playable) /// { /// CreatePlayable(out playable, new MyJob()); /// } /// /// private struct MyJob : IAnimationJob /// { /// public void ProcessAnimation(AnimationStream stream) /// { /// } /// /// public void ProcessRootMotion(AnimationStream stream) /// { /// } /// } /// } /// /// See also: /// protected void CreatePlayable( out Playable playable, T job, bool processInputs = false) where T : struct, IAnimationJob { var scriptPlayable = AnimationScriptPlayable.Create(Graph._PlayableGraph, job, ChildCount); if (!processInputs) scriptPlayable.SetProcessInputs(false); playable = scriptPlayable; } /************************************************************************************************************************/ /// /// Gets the Animation Job data from the . /// /// /// This mixer was not initialized using /// or . /// public T GetJobData() where T : struct, IAnimationJob => ((AnimationScriptPlayable)_Playable).GetJobData(); /// /// Sets the Animation Job data in the . /// /// /// This mixer was not initialized using /// or . /// public void SetJobData(T value) where T : struct, IAnimationJob => ((AnimationScriptPlayable)_Playable).SetJobData(value); /************************************************************************************************************************/ #endregion /************************************************************************************************************************/ #region Updates /************************************************************************************************************************/ /// public int UpdatableIndex { get; set; } = IUpdatable.List.NotInList; /// Recalculates the weights of child states and synchronizes their times if necessary. public virtual void Update() { if (!ApplySynchronizeChildren()) Graph.CancelPreUpdate(this); } /************************************************************************************************************************/ /// public override void SetGraph(AnimancerGraph graph) { if (Graph == graph) return; Graph?.CancelPreUpdate(this); base.SetGraph(graph); if (SynchronizedChildCount > 0) graph?.RequirePreUpdate(this); } /************************************************************************************************************************/ #endregion /************************************************************************************************************************/ #region Synchronize Children /************************************************************************************************************************/ /// Should newly added children be automatically added to the synchronization list? Default true. public static bool SynchronizeNewChildren { get; set; } = true; /// The minimum total weight of all children for their times to be synchronized. Default 0.01. public static float MinimumSynchronizeChildrenWeight { get; set; } = 0.01f; /************************************************************************************************************************/ private List _SynchronizedChildren; /// A copy of the internal list of child states that will have their times synchronized. /// /// If this mixer is a child of another mixer, its synchronized children will be managed by the parent. /// /// The getter allocates a new array if is greater than zero. /// public AnimancerState[] SynchronizedChildren { get => SynchronizedChildCount > 0 ? _SynchronizedChildren.ToArray() : Array.Empty(); set { if (_SynchronizedChildren == null) _SynchronizedChildren = new(); else _SynchronizedChildren.Clear(); for (int i = 0; i < value.Length; i++) Synchronize(value[i]); } } /// The number of . public int SynchronizedChildCount => _SynchronizedChildren != null ? _SynchronizedChildren.Count : 0; /************************************************************************************************************************/ /// Is the `state` in the ? public bool IsSynchronized(AnimancerState state) { var synchronizer = GetParentMixer(); return synchronizer._SynchronizedChildren != null && synchronizer._SynchronizedChildren.Contains(state); } /************************************************************************************************************************/ /// Adds the `state` to the . /// /// The `state` must be a child of this mixer. /// /// If this mixer is a child of another mixer, the `state` will be added to the parent's /// instead. /// public void Synchronize(AnimancerState state) { if (state == null) return; #if UNITY_ASSERTIONS if (!IsChildOf(state, this)) { MarkAsUsed(this); throw new ArgumentException( $"State is not a child of the mixer." + $"\n• State: {state}" + $"\n• Mixer: {this}", nameof(state)); } #endif var synchronizer = GetParentMixer(); synchronizer.SynchronizeDirect(state); } /// The internal implementation of . private void SynchronizeDirect(AnimancerState state) { if (state == null) return; // If the state is a mixer, steal all its synchronized children instead of synchronizing the mixer itself. if (state is ManualMixerState mixer) { if (mixer._SynchronizedChildren != null) { for (int i = 0; i < mixer._SynchronizedChildren.Count; i++) Synchronize(mixer._SynchronizedChildren[i]); mixer._SynchronizedChildren.Clear(); } return; } #if UNITY_ASSERTIONS if (OptionalWarning.MixerSynchronizeZeroLength.IsEnabled() && state.Length == 0) OptionalWarning.MixerSynchronizeZeroLength.Log( $"Adding a state with zero {nameof(AnimancerState.Length)} to the synchronization list: '{state}'." + $"\n\nSynchronization is based on the {nameof(NormalizedTime)}" + $" which can't be calculated if the {nameof(Length)} is 0." + $" Some state types can change their {nameof(Length)}, in which case you can just disable this warning." + $" But otherwise, the indicated state probably shouldn't be added to the synchronization list.", Graph?.Component); #endif _SynchronizedChildren ??= new(); #if UNITY_ASSERTIONS if (_SynchronizedChildren.Contains(state)) Debug.LogError($"{state} is already in the {nameof(SynchronizedChildren)} list."); #endif _SynchronizedChildren.Add(state); Graph?.RequirePreUpdate(this); } /************************************************************************************************************************/ /// Removes the `state` from the . public void DontSynchronize(AnimancerState state) { var synchronizer = GetParentMixer(); if (synchronizer._SynchronizedChildren != null && synchronizer._SynchronizedChildren.Remove(state) && state._Playable.IsValid()) state._Playable.SetSpeed(state.Speed); } /************************************************************************************************************************/ /// Removes all children of this mixer from the . public void DontSynchronizeChildren() { var synchronizer = GetParentMixer(); var synchronizedChildren = synchronizer._SynchronizedChildren; if (synchronizedChildren == null) return; if (synchronizer == this) { for (int i = synchronizedChildren.Count - 1; i >= 0; i--) { var state = synchronizedChildren[i]; if (state._Playable.IsValid()) state._Playable.SetSpeed(state.Speed); } synchronizedChildren.Clear(); } else { for (int i = synchronizedChildren.Count - 1; i >= 0; i--) { var state = synchronizedChildren[i]; if (IsChildOf(state, this)) { if (state._Playable.IsValid()) state._Playable.SetSpeed(state.Speed); synchronizedChildren.RemoveAt(i); } } } } /************************************************************************************************************************/ /// Initializes the internal list. /// /// The array can be null or empty. Any elements not in the array will be treated as true. /// /// This method can only be called before any are added and also before this /// mixer is made the child of another mixer. /// public void InitializeSynchronizedChildren(params bool[] synchronizeChildren) { AnimancerUtilities.Assert(GetParentMixer() == this, $"{nameof(InitializeSynchronizedChildren)} cannot be used on a mixer that is a child of another mixer."); AnimancerUtilities.Assert(_SynchronizedChildren == null, $"{nameof(InitializeSynchronizedChildren)} cannot be used on a mixer already has synchronized children."); int flagCount; if (synchronizeChildren != null) { flagCount = synchronizeChildren.Length; for (int i = 0; i < flagCount; i++) if (synchronizeChildren[i]) SynchronizeDirect(ChildStates[i]); } else flagCount = 0; for (int i = flagCount; i < ChildCount; i++) SynchronizeDirect(ChildStates[i]); } /************************************************************************************************************************/ /// /// Returns the highest in the hierarchy above this mixer /// or this mixer itself if there are none above it. /// public ManualMixerState GetParentMixer() { var mixer = this; var parent = Parent; while (parent != null) { if (parent is ManualMixerState parentMixer) mixer = parentMixer; parent = parent.Parent; } return mixer; } /// Returns the highest in the hierarchy above the `state` (inclusive). public static ManualMixerState GetParentMixer(AnimancerNodeBase node) { ManualMixerState mixer = null; while (node != null) { if (node is ManualMixerState parentMixer) mixer = parentMixer; node = node.Parent; } return mixer; } /************************************************************************************************************************/ /// Is the `child` a child of the `parent`? public static bool IsChildOf(AnimancerNodeBase child, AnimancerNodeBase parent) { while (true) { child = child.Parent; if (child == parent) return true; else if (child == null) return false; } } /************************************************************************************************************************/ /// /// Synchronizes the s of the by /// modifying their internal playable speeds. /// private bool ApplySynchronizeChildren() { if (Weight == 0 || !IsPlaying || _SynchronizedChildren == null || _SynchronizedChildren.Count <= 1) return false; var deltaTime = AnimancerGraph.DeltaTime * CalculateRealEffectiveSpeed(); if (deltaTime == 0) return true; var count = _SynchronizedChildren.Count; // Calculate the weighted average normalized time and normalized speed of all children. var totalWeight = 0f; var weightedNormalizedTime = 0f; var weightedNormalizedSpeed = 0f; for (int i = 0; i < count; i++) { var state = _SynchronizedChildren[i]; var weight = CalculateRelativeWeight(state); if (weight == 0) continue; var length = state.Length; if (length == 0) continue; totalWeight += weight; weight /= length; weightedNormalizedTime += state.Time * weight; weightedNormalizedSpeed += state.Speed * weight; } #if UNITY_ASSERTIONS if (!(totalWeight >= 0) || totalWeight == float.PositiveInfinity)// Reversed comparison includes NaN. { MarkAsUsed(this); throw new ArgumentOutOfRangeException(nameof(totalWeight), totalWeight, $"Total weight {Strings.MustBeFinite} and must be positive"); } if (!weightedNormalizedTime.IsFinite()) { MarkAsUsed(this); throw new ArgumentOutOfRangeException(nameof(weightedNormalizedTime), weightedNormalizedTime, $"Time {Strings.MustBeFinite}"); } if (!weightedNormalizedSpeed.IsFinite()) { MarkAsUsed(this); throw new ArgumentOutOfRangeException(nameof(weightedNormalizedSpeed), weightedNormalizedSpeed, $"Speed {Strings.MustBeFinite}"); } #endif // If the total weight is too small, pretend they are all at Weight = 1. if (totalWeight < MinimumSynchronizeChildrenWeight) { weightedNormalizedTime = 0; weightedNormalizedSpeed = 0; var nonZeroCount = 0; for (int i = 0; i < count; i++) { var state = _SynchronizedChildren[i]; var length = state.Length; if (length == 0) continue; length = 1f / length; weightedNormalizedTime += state.Time * length; weightedNormalizedSpeed += state.Speed * length; nonZeroCount++; } totalWeight = nonZeroCount; } // Increment that time value according to delta time. weightedNormalizedTime += deltaTime * weightedNormalizedSpeed; weightedNormalizedTime /= totalWeight; var inverseDeltaTime = 1f / deltaTime; // Modify the speed of all children to go from their current normalized time to the average in one frame. for (int i = 0; i < count; i++) { var state = _SynchronizedChildren[i]; var length = state.Length; if (length == 0) continue; var normalizedTime = state.Time / length; var speed = (weightedNormalizedTime - normalizedTime) * length * inverseDeltaTime; state._Playable.SetSpeed(speed); } // After this, all the playables will update and advance according to their new speeds this frame. return true; } /************************************************************************************************************************/ /// Calculates the weight of the `child` multiplied by its parents up to this mixer. private float CalculateRelativeWeight(AnimancerState child) { var weight = child.Weight; var parent = child.Parent; while (parent != this && parent != null) { weight *= parent.BaseWeight; parent = parent.Parent; } return weight; } /************************************************************************************************************************/ /// /// The multiplied of this mixer and its parents down the /// hierarchy to determine the actual speed its output is being played at. /// /// /// This can be different from the because the /// have their playable speed modified without setting their /// . /// public float CalculateRealEffectiveSpeed() { var speed = _Playable.GetSpeed(); var parent = Parent; while (parent != null) { speed *= parent.Playable.GetSpeed(); parent = parent.Parent; } return (float)speed; } /************************************************************************************************************************/ #endregion /************************************************************************************************************************/ #region Other Methods /************************************************************************************************************************/ /// Calculates the sum of the of all `states`. public static float CalculateTotalWeight(AnimancerState[] states, int count) { var total = 0f; for (int i = count - 1; i >= 0; i--) total += states[i].Weight; return total; } /************************************************************************************************************************/ /// /// Sets for all . /// public void SetChildrenTime(float value, bool normalized = false) { for (int i = ChildCount - 1; i >= 0; i--) { var state = ChildStates[i]; if (normalized) state.NormalizedTime = value; else state.Time = value; } } /************************************************************************************************************************/ /// Sets the weight of all states after the `previousIndex` to 0. protected void DisableRemainingStates(int previousIndex) { for (int i = previousIndex + 1; i < ChildCount; i++) Playable.SetChildWeight(ChildStates[i], 0); } /************************************************************************************************************************/ private static float[] _TemporaryWeights = Array.Empty(); /// Returns an array at least as large as the `count`. /// /// The same array is returned by subsequent calls as long as it's large enough /// and it isn't cleared between calls so it will contain the previous data. /// public static float[] GetTemporaryFloatArray(int count) { if (_TemporaryWeights.Length < count) { if (count <= 16) count = 16; else count = Mathf.NextPowerOfTwo(count); _TemporaryWeights = new float[count]; } return _TemporaryWeights; } /************************************************************************************************************************/ /// Divides `weights` by the `totalWeight` and applies them to the child states. public void NormalizeAndApplyWeights(float totalWeight, float[] weights) { totalWeight = 1f / totalWeight; for (int i = ChildCount - 1; i >= 0; i--) { var state = ChildStates[i]; var weight = weights[i] * totalWeight; Playable.SetChildWeight(state, weight); } } /************************************************************************************************************************/ /// Gets a user-friendly key to identify the `state` in the Inspector. public virtual string GetDisplayKey(AnimancerState state) => $"[{state.Index}]"; /************************************************************************************************************************/ /// public override Vector3 AverageVelocity { get { var velocity = default(Vector3); for (int i = ChildCount - 1; i >= 0; i--) { var state = ChildStates[i]; velocity += state.AverageVelocity * state.Weight; } return velocity; } } /************************************************************************************************************************/ /// /// Recalculates the of all child states so that they add up to 1. /// public void NormalizeDurations() { int divideBy = 0; float totalDuration = 0f; // Count the number of states that exist and their total duration. for (int i = 0; i < ChildCount; i++) { divideBy++; totalDuration += ChildStates[i].Duration; } // Calculate the average duration. totalDuration /= divideBy; // Set all states to that duration. for (int i = 0; i < ChildCount; i++) { ChildStates[i].Duration = totalDuration; } } /************************************************************************************************************************/ /// protected override void AppendDetails(StringBuilder text, string separator) { base.AppendDetails(text, separator); text.Append(separator) .Append("SynchronizedChildren: "); if (SynchronizedChildCount == 0) { text.Append("0"); } else { text.Append(_SynchronizedChildren.Count); separator += Strings.Indent; for (int i = 0; i < _SynchronizedChildren.Count; i++) { text.Append(separator) .Append(_SynchronizedChildren[i]); } } } /************************************************************************************************************************/ /// public virtual void GetParameters(List parameters) { } /// public virtual void SetParameters(List parameters) { } /************************************************************************************************************************/ #endregion /************************************************************************************************************************/ } }