// Animancer // https://kybernetik.com.au/animancer // Copyright 2021 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; using Object = UnityEngine.Object; namespace Animancer { /// [Pro-Only] Base class for s which blend other states together. /// /// Documentation: Mixers /// /// https://kybernetik.com.au/animancer/api/Animancer/MixerState /// public abstract partial class MixerState : AnimancerState { /************************************************************************************************************************/ /// An that creates a for . public interface ITransition2D : ITransition> { } /************************************************************************************************************************/ #region Properties /************************************************************************************************************************/ /// Mixers should keep child playables connected to the graph at all times. public override bool KeepChildrenConnected => true; /// A has no . public override AnimationClip Clip => null; /************************************************************************************************************************/ /// Returns the collection of states connected to this mixer. Note that some elements may be null. /// /// Getting an enumerator that automatically skips over null states is slower and creates garbage, so /// internally we use this property and perform null checks manually even though it increases the code /// complexity a bit. /// public abstract IList ChildStates { get; } /// public override int ChildCount => ChildStates.Count; /// public override AnimancerState GetChild(int index) => ChildStates[index]; /// public override FastEnumerator GetEnumerator() => new FastEnumerator(ChildStates); /************************************************************************************************************************/ /// protected override void OnSetIsPlaying() { var childStates = ChildStates; for (int i = childStates.Count - 1; i >= 0; i--) { var state = childStates[i]; if (state == null) continue; state.IsPlaying = IsPlaying; } } /************************************************************************************************************************/ /// Are any child states looping? public override bool IsLooping { get { var childStates = ChildStates; for (int i = childStates.Count - 1; i >= 0; i--) { var state = childStates[i]; if (state == null) continue; if (state.IsLooping) return true; } return false; } } /************************************************************************************************************************/ /// /// The weighted average of each child state according to their /// . /// /// /// If there are any , only those states will be included in the getter /// calculation. /// protected override float RawTime { get { RecalculateWeights(); if (!GetSynchronizedTimeDetails(out var totalWeight, out var normalizedTime, out var length)) GetTimeDetails(out totalWeight, out normalizedTime, out length); if (totalWeight == 0) return base.RawTime; totalWeight *= totalWeight; return normalizedTime * length / totalWeight; } set { var states = ChildStates; var childCount = states.Count; if (value == 0) goto ZeroTime; var length = Length; if (length == 0) goto ZeroTime; value /= length;// Normalize. while (--childCount >= 0) { var state = states[childCount]; if (state != null) state.NormalizedTime = value; } return; // If the value is 0, we can set the child times slightly more efficiently. ZeroTime: while (--childCount >= 0) { var state = states[childCount]; if (state != null) state.Time = 0; } } } /************************************************************************************************************************/ /// public override void MoveTime(float time, bool normalized) { base.MoveTime(time, normalized); var states = ChildStates; var count = states.Count; for (int i = 0; i < count; i++) states[i].MoveTime(time, normalized); } /************************************************************************************************************************/ /// Gets the time details based on the . private bool GetSynchronizedTimeDetails(out float totalWeight, out float normalizedTime, out float length) { totalWeight = 0; normalizedTime = 0; length = 0; 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; totalWeight += weight; normalizedTime += state.Time / stateLength * weight; length += stateLength * weight; } } return totalWeight > MinimumSynchronizeChildrenWeight; } /// Gets the time details based on all child states. private void GetTimeDetails(out float totalWeight, out float normalizedTime, out float length) { totalWeight = 0; normalizedTime = 0; length = 0; var states = ChildStates; for (int i = states.Count - 1; i >= 0; i--) { var state = states[i]; if (state == null) continue; var weight = state.Weight; if (weight == 0) continue; var stateLength = state.Length; if (stateLength == 0) continue; totalWeight += weight; normalizedTime += state.Time / stateLength * weight; length += stateLength * weight; } } /************************************************************************************************************************/ /// /// The weighted average of each child state according to their /// . /// public override float Length { get { RecalculateWeights(); 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; var states = ChildStates; totalChildWeight = CalculateTotalWeight(states); if (totalChildWeight <= 0) return 0; for (int i = states.Count - 1; i >= 0; i--) { var state = states[i]; if (state != null) length += state.Length * state.Weight; } return length / totalChildWeight; } } /************************************************************************************************************************/ #endregion /************************************************************************************************************************/ #region Initialisation /************************************************************************************************************************/ /// Creates and assigns the managed by this state. protected override void CreatePlayable(out Playable playable) { #if UNITY_2021_2_OR_NEWER playable = AnimationMixerPlayable.Create(Root._Graph, ChildStates.Count); #else playable = AnimationMixerPlayable.Create(Root._Graph, ChildStates.Count, false); #endif RecalculateWeights(); } /************************************************************************************************************************/ /// /// Creates and returns a new to play the `clip` with this mixer as its parent. /// public ClipState CreateChild(int index, AnimationClip clip) { var state = new ClipState(clip); state.SetParent(this, index); state.IsPlaying = IsPlaying; return state; } /// /// Calls and sets this mixer as the state's parent. /// public AnimancerState CreateChild(int index, ITransition transition) { var state = transition.CreateStateAndApply(Root); state.SetParent(this, index); state.IsPlaying = IsPlaying; return state; } /// Calls or . public AnimancerState CreateChild(int index, Object state) { if (state is AnimationClip clip) { return CreateChild(index, clip); } else if (state is ITransition transition) { return CreateChild(index, transition); } else return null; } /************************************************************************************************************************/ /// Assigns the `state` as a child of this mixer. public void SetChild(int index, AnimancerState state) => state.SetParent(this, index); /************************************************************************************************************************/ /// Connects the `state` to this mixer at its . protected internal override void OnAddChild(AnimancerState state) { OnAddChild(ChildStates, state); if (AutoSynchronizeChildren) Synchronize(state); #if UNITY_ASSERTIONS if (_IsGeneratedName) { _IsGeneratedName = false; SetDebugName(null); } #endif } /************************************************************************************************************************/ /// Disconnects the `state` from this mixer at its . protected internal override void OnRemoveChild(AnimancerState state) { if (_SynchronizedChildren != null) _SynchronizedChildren.Remove(state); var states = ChildStates; Validate.AssertCanRemoveChild(state, states); states[state.Index] = null; Root?._Graph.Disconnect(_Playable, state.Index); #if UNITY_ASSERTIONS if (_IsGeneratedName) { _IsGeneratedName = false; SetDebugName(null); } #endif } /************************************************************************************************************************/ /// public override void Destroy() { DestroyChildren(); base.Destroy(); } /************************************************************************************************************************/ /// /// Destroys all connected to this mixer. This operation cannot be undone. /// public void DestroyChildren() { var states = ChildStates; for (int i = states.Count - 1; i >= 0; i--) { var state = states[i]; if (state != null) state.Destroy(); } } /************************************************************************************************************************/ #endregion /************************************************************************************************************************/ #region Jobs /************************************************************************************************************************/ /// /// Creates an to run the specified Animation Job instead of the usual /// . /// /// /// 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(AnimancerPlayable root, T job, bool processInputs = false) where T : struct, IAnimationJob { // Can't just use SetRoot normally because it would call the regular CreatePlayable method. SetRoot(null); Root = root; root.States.Register(this); #if UNITY_ASSERTIONS if (HasEvents) Debug.LogWarning($"{nameof(CreatePlayable)} should be called before configuring any Animancer Events on this state."); #endif var playable = AnimationScriptPlayable.Create(root._Graph, job, ChildCount); if (!processInputs) playable.SetProcessInputs(false); for (int i = ChildCount - 1; i >= 0; i--) GetChild(i)?.SetRoot(root); return playable; } /************************************************************************************************************************/ /// /// Creates an to run the specified Animation Job instead of the usual /// . /// /// /// /// Documentation: Creating Custom States /// /// /// /// 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(Root._Graph, 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 /************************************************************************************************************************/ /// Updates the time of this mixer and all of its child states. protected internal override void Update(out bool needsMoreUpdates) { base.Update(out needsMoreUpdates); if (RecalculateWeights()) { // Apply the child weights immediately to ensure they are all in sync. Otherwise some of them might // have already updated before the mixer and would not apply it until next frame. var childStates = ChildStates; for (int i = childStates.Count - 1; i >= 0; i--) { var state = childStates[i]; if (state == null) continue; state.ApplyWeight(); } } ApplySynchronizeChildren(ref needsMoreUpdates); } /************************************************************************************************************************/ /// Indicates whether the weights of all child states should be recalculated. public bool WeightsAreDirty { get; set; } /************************************************************************************************************************/ /// /// If this method recalculates the weights of all child states and returns true. /// public bool RecalculateWeights() { if (WeightsAreDirty) { ForceRecalculateWeights(); Debug.Assert(!WeightsAreDirty, $"{nameof(MixerState)}.{nameof(WeightsAreDirty)} was not set to false by {nameof(ForceRecalculateWeights)}()."); return true; } else return false; } /************************************************************************************************************************/ /// /// Recalculates the weights of all child states based on the current value of the /// and the thresholds. /// /// Overrides of this method must set = false. /// protected virtual void ForceRecalculateWeights() { } /************************************************************************************************************************/ #endregion /************************************************************************************************************************/ #region Synchronize Children /************************************************************************************************************************/ /// Should newly added children be automatically added to the synchronization list? Default true. public static bool AutoSynchronizeChildren { 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 List(); 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)) 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 (state is MixerState mixer) { 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.", Root?.Component); #endif if (_SynchronizedChildren == null) _SynchronizedChildren = new List(); #if UNITY_ASSERTIONS if (_SynchronizedChildren.Contains(state)) Debug.LogError($"{state} is already in the {nameof(SynchronizedChildren)} list."); #endif _SynchronizedChildren.Add(state); RequireUpdate(); } /************************************************************************************************************************/ /// 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(GetChild(i)); } else flagCount = 0; for (int i = flagCount; i < ChildCount; i++) SynchronizeDirect(GetChild(i)); } /************************************************************************************************************************/ /// /// Returns the highest in the hierarchy above this mixer or this mixer itself if /// there are none above it. /// public MixerState GetParentMixer() { var mixer = this; var parent = Parent; while (parent != null) { if (parent is MixerState parentMixer) mixer = parentMixer; parent = parent.Parent; } return mixer; } /// Returns the highest in the hierarchy above the `state` (inclusive). public static MixerState GetParentMixer(IPlayableWrapper node) { MixerState mixer = null; while (node != null) { if (node is MixerState parentMixer) mixer = parentMixer; node = node.Parent; } return mixer; } /************************************************************************************************************************/ /// Is the `child` a child of the `parent`? public static bool IsChildOf(IPlayableWrapper child, IPlayableWrapper 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. /// protected void ApplySynchronizeChildren(ref bool needsMoreUpdates) { if (_SynchronizedChildren == null || _SynchronizedChildren.Count <= 1) return; needsMoreUpdates = true; var deltaTime = AnimancerPlayable.DeltaTime * CalculateRealEffectiveSpeed(); if (deltaTime == 0) return; 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 = state.Weight; 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. throw new ArgumentOutOfRangeException(nameof(totalWeight), totalWeight, "Total weight must be a finite positive value"); if (!weightedNormalizedTime.IsFinite()) throw new ArgumentOutOfRangeException(nameof(weightedNormalizedTime), weightedNormalizedTime, "Time must be finite"); if (!weightedNormalizedSpeed.IsFinite()) throw new ArgumentOutOfRangeException(nameof(weightedNormalizedSpeed), weightedNormalizedSpeed, "Speed must be finite"); #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. } /************************************************************************************************************************/ /// /// 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 Inverse Kinematics /************************************************************************************************************************/ private bool _ApplyAnimatorIK; /// public override bool ApplyAnimatorIK { get => _ApplyAnimatorIK; set => base.ApplyAnimatorIK = _ApplyAnimatorIK = value; } /************************************************************************************************************************/ private bool _ApplyFootIK; /// public override bool ApplyFootIK { get => _ApplyFootIK; set => base.ApplyFootIK = _ApplyFootIK = value; } /************************************************************************************************************************/ #endregion /************************************************************************************************************************/ #region Other Methods /************************************************************************************************************************/ /// Calculates the sum of the of all `states`. public float CalculateTotalWeight(IList states) { var total = 0f; for (int i = states.Count - 1; i >= 0; i--) { var state = states[i]; if (state != null) total += state.Weight; } return total; } /************************************************************************************************************************/ /// /// Sets for all . /// public void SetChildrenTime(float value, bool normalized = false) { var states = ChildStates; for (int i = states.Count - 1; i >= 0; i--) { var state = states[i]; if (state == null) continue; 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) { var states = ChildStates; var childCount = states.Count; while (++previousIndex < childCount) { var state = states[previousIndex]; if (state == null) continue; state.Weight = 0; } } /************************************************************************************************************************/ /// /// Returns the state at the specified `index` if it is not null, otherwise increments the index and checks /// again. Returns null if no state is found by the end of the . /// protected AnimancerState GetNextState(ref int index) { var states = ChildStates; var childCount = states.Count; while (index < childCount) { var state = states[index]; if (state != null) return state; index++; } return null; } /************************************************************************************************************************/ /// Divides the weight of all child states by the `totalWeight`. /// /// If the `totalWeight` is equal to the total of all child states, then the /// new total will become 1. /// public void NormalizeWeights(float totalWeight) { if (totalWeight == 1) return; totalWeight = 1f / totalWeight; var states = ChildStates; for (int i = states.Count - 1; i >= 0; i--) { var state = states[i]; if (state == null) continue; state.Weight *= totalWeight; } } /************************************************************************************************************************/ /// 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); RecalculateWeights(); var childStates = ChildStates; for (int i = childStates.Count - 1; i >= 0; i--) { var state = childStates[i]; if (state == null) continue; velocity += state.AverageVelocity * state.Weight; } return velocity; } } /************************************************************************************************************************/ /// /// Recalculates the of all child states so that they add up to 1. /// /// There are any states with no . public void NormalizeDurations() { var childStates = ChildStates; int divideBy = 0; float totalDuration = 0f; // Count the number of states that exist and their total duration. var count = childStates.Count; for (int i = 0; i < count; i++) { var state = childStates[i]; if (state == null) continue; divideBy++; totalDuration += state.Duration; } // Calculate the average duration. totalDuration /= divideBy; // Set all states to that duration. for (int i = 0; i < count; i++) { var state = childStates[i]; if (state == null) continue; state.Duration = totalDuration; } } /************************************************************************************************************************/ #if UNITY_ASSERTIONS /// Has the been generated from the child states? private bool _IsGeneratedName; #endif /// /// Returns a string describing the type of this mixer and the name of s connected to it. /// public override string ToString() { #if UNITY_ASSERTIONS if (!string.IsNullOrEmpty(DebugName)) return DebugName; #endif // Gather child names. var childNames = ObjectPool.AcquireList(); var allSimple = true; var states = ChildStates; var count = states.Count; for (int i = 0; i < count; i++) { var state = states[i]; if (state == null) continue; if (state.MainObject != null) { childNames.Add(state.MainObject.name); } else { childNames.Add(state.ToString()); allSimple = false; } } // If they all have a main object, check if they all have the same prefix so it doesn't need to be repeated. int prefixLength = 0; count = childNames.Count; if (count <= 1 || !allSimple) { prefixLength = 0; } else { var prefix = childNames[0]; var shortest = prefixLength = prefix.Length; for (int iName = 0; iName < count; iName++) { var childName = childNames[iName]; if (shortest > childName.Length) { shortest = prefixLength = childName.Length; } for (int iCharacter = 0; iCharacter < prefixLength; iCharacter++) { if (childName[iCharacter] != prefix[iCharacter]) { prefixLength = iCharacter; break; } } } if (prefixLength < 3 ||// Less than 3 characters probably isn't an intentional prefix. prefixLength >= shortest) prefixLength = 0; } // Build the mixer name. var mixerName = ObjectPool.AcquireStringBuilder(); if (count > 0) { if (prefixLength > 0) mixerName.Append(childNames[0], 0, prefixLength).Append('['); for (int i = 0; i < count; i++) { if (i > 0) mixerName.Append(", "); var childName = childNames[i]; mixerName.Append(childName, prefixLength, childName.Length - prefixLength); } mixerName.Append(prefixLength > 0 ? "] (" : " ("); } ObjectPool.Release(childNames); var type = GetType().FullName; if (type.EndsWith("State")) mixerName.Append(type, 0, type.Length - 5); else mixerName.Append(type); if (count > 0) mixerName.Append(')'); var result = mixerName.ReleaseToString(); #if UNITY_ASSERTIONS _IsGeneratedName = true; SetDebugName(result); #endif return result; } /************************************************************************************************************************/ /// 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 override void GatherAnimationClips(ICollection clips) => clips.GatherFromSource(ChildStates); /************************************************************************************************************************/ #endregion /************************************************************************************************************************/ } }