You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

1305 lines
50 KiB

// 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
{
/// <summary>[Pro-Only] Base class for <see cref="AnimancerState"/>s which blend other states together.</summary>
/// <remarks>
/// Documentation: <see href="https://kybernetik.com.au/animancer/docs/manual/blending/mixers">Mixers</see>
/// </remarks>
/// https://kybernetik.com.au/animancer/api/Animancer/MixerState
///
public abstract partial class MixerState : AnimancerState
{
/************************************************************************************************************************/
/// <summary>An <see cref="ITransition{TState}"/> that creates a <see cref="MixerState{TParameter}"/> for <see cref="Vector2"/>.</summary>
public interface ITransition2D : ITransition<MixerState<Vector2>> { }
/************************************************************************************************************************/
#region Properties
/************************************************************************************************************************/
/// <summary>Mixers should keep child playables connected to the graph at all times.</summary>
public override bool KeepChildrenConnected => true;
/// <summary>A <see cref="MixerState"/> has no <see cref="AnimationClip"/>.</summary>
public override AnimationClip Clip => null;
/************************************************************************************************************************/
/// <summary>Returns the collection of states connected to this mixer. Note that some elements may be null.</summary>
/// <remarks>
/// 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.
/// </remarks>
public abstract IList<AnimancerState> ChildStates { get; }
/// <inheritdoc/>
public override int ChildCount => ChildStates.Count;
/// <inheritdoc/>
public override AnimancerState GetChild(int index) => ChildStates[index];
/// <inheritdoc/>
public override FastEnumerator<AnimancerState> GetEnumerator()
=> new FastEnumerator<AnimancerState>(ChildStates);
/************************************************************************************************************************/
/// <inheritdoc/>
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;
}
}
/************************************************************************************************************************/
/// <summary>Are any child states looping?</summary>
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;
}
}
/************************************************************************************************************************/
/// <summary>
/// The weighted average <see cref="AnimancerState.Time"/> of each child state according to their
/// <see cref="AnimancerNode.Weight"/>.
/// </summary>
/// <remarks>
/// If there are any <see cref="SynchronizedChildren"/>, only those states will be included in the getter
/// calculation.
/// </remarks>
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;
}
}
}
/************************************************************************************************************************/
/// <inheritdoc/>
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);
}
/************************************************************************************************************************/
/// <summary>Gets the time details based on the <see cref="SynchronizedChildren"/>.</summary>
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;
}
/// <summary>Gets the time details based on all child states.</summary>
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;
}
}
/************************************************************************************************************************/
/// <summary>
/// The weighted average <see cref="AnimancerState.Length"/> of each child state according to their
/// <see cref="AnimancerNode.Weight"/>.
/// </summary>
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
/************************************************************************************************************************/
/// <summary>Creates and assigns the <see cref="AnimationMixerPlayable"/> managed by this state.</summary>
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();
}
/************************************************************************************************************************/
/// <summary>
/// Creates and returns a new <see cref="ClipState"/> to play the `clip` with this mixer as its parent.
/// </summary>
public ClipState CreateChild(int index, AnimationClip clip)
{
var state = new ClipState(clip);
state.SetParent(this, index);
state.IsPlaying = IsPlaying;
return state;
}
/// <summary>
/// Calls <see cref="AnimancerUtilities.CreateStateAndApply"/> and sets this mixer as the state's parent.
/// </summary>
public AnimancerState CreateChild(int index, ITransition transition)
{
var state = transition.CreateStateAndApply(Root);
state.SetParent(this, index);
state.IsPlaying = IsPlaying;
return state;
}
/// <summary>Calls <see cref="CreateChild(int, AnimationClip)"/> or <see cref="CreateChild(int, ITransition)"/>.</summary>
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;
}
/************************************************************************************************************************/
/// <summary>Assigns the `state` as a child of this mixer.</summary>
public void SetChild(int index, AnimancerState state) => state.SetParent(this, index);
/************************************************************************************************************************/
/// <summary>Connects the `state` to this mixer at its <see cref="AnimancerNode.Index"/>.</summary>
protected internal override void OnAddChild(AnimancerState state)
{
OnAddChild(ChildStates, state);
if (AutoSynchronizeChildren)
Synchronize(state);
#if UNITY_ASSERTIONS
if (_IsGeneratedName)
{
_IsGeneratedName = false;
SetDebugName(null);
}
#endif
}
/************************************************************************************************************************/
/// <summary>Disconnects the `state` from this mixer at its <see cref="AnimancerNode.Index"/>.</summary>
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
}
/************************************************************************************************************************/
/// <inheritdoc/>
public override void Destroy()
{
DestroyChildren();
base.Destroy();
}
/************************************************************************************************************************/
/// <summary>
/// Destroys all <see cref="ChildStates"/> connected to this mixer. This operation cannot be undone.
/// </summary>
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
/************************************************************************************************************************/
/// <summary>
/// Creates an <see cref="AnimationScriptPlayable"/> to run the specified Animation Job instead of the usual
/// <see cref="AnimationMixerPlayable"/>.
/// </summary>
/// <example><code>
/// 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.
/// </code>
/// See also: <seealso cref="CreatePlayable{T}(out Playable, T, bool)"/>
/// </example>
public AnimationScriptPlayable CreatePlayable<T>(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;
}
/************************************************************************************************************************/
/// <summary>
/// Creates an <see cref="AnimationScriptPlayable"/> to run the specified Animation Job instead of the usual
/// <see cref="AnimationMixerPlayable"/>.
/// </summary>
///
/// <remarks>
/// Documentation: <see href="https://kybernetik.com.au/animancer/docs/source/creating-custom-states">Creating Custom States</see>
/// </remarks>
///
/// <example><code>
/// 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)
/// {
/// }
/// }
/// }
/// </code>
/// See also: <seealso cref="CreatePlayable{T}(AnimancerPlayable, T, bool)"/>
/// </example>
protected void CreatePlayable<T>(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;
}
/************************************************************************************************************************/
/// <summary>
/// Gets the Animation Job data from the <see cref="AnimationScriptPlayable"/>.
/// </summary>
/// <exception cref="InvalidCastException">
/// This mixer was not initialized using <see cref="CreatePlayable{T}(AnimancerPlayable, T, bool)"/>
/// or <see cref="CreatePlayable{T}(out Playable, T, bool)"/>.
/// </exception>
public T GetJobData<T>()
where T : struct, IAnimationJob
=> ((AnimationScriptPlayable)_Playable).GetJobData<T>();
/// <summary>
/// Sets the Animation Job data in the <see cref="AnimationScriptPlayable"/>.
/// </summary>
/// <exception cref="InvalidCastException">
/// This mixer was not initialized using <see cref="CreatePlayable{T}(AnimancerPlayable, T, bool)"/>
/// or <see cref="CreatePlayable{T}(out Playable, T, bool)"/>.
/// </exception>
public void SetJobData<T>(T value)
where T : struct, IAnimationJob
=> ((AnimationScriptPlayable)_Playable).SetJobData<T>(value);
/************************************************************************************************************************/
#endregion
/************************************************************************************************************************/
#region Updates
/************************************************************************************************************************/
/// <summary>Updates the time of this mixer and all of its child states.</summary>
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);
}
/************************************************************************************************************************/
/// <summary>Indicates whether the weights of all child states should be recalculated.</summary>
public bool WeightsAreDirty { get; set; }
/************************************************************************************************************************/
/// <summary>
/// If <see cref="WeightsAreDirty"/> this method recalculates the weights of all child states and returns true.
/// </summary>
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;
}
/************************************************************************************************************************/
/// <summary>
/// Recalculates the weights of all child states based on the current value of the
/// <see cref="MixerState{TParameter}.Parameter"/> and the thresholds.
/// <para></para>
/// Overrides of this method must set <see cref="WeightsAreDirty"/> = false.
/// </summary>
protected virtual void ForceRecalculateWeights() { }
/************************************************************************************************************************/
#endregion
/************************************************************************************************************************/
#region Synchronize Children
/************************************************************************************************************************/
/// <summary>Should newly added children be automatically added to the synchronization list? Default true.</summary>
public static bool AutoSynchronizeChildren { get; set; } = true;
/// <summary>The minimum total weight of all children for their times to be synchronized (default 0.01).</summary>
public static float MinimumSynchronizeChildrenWeight { get; set; } = 0.01f;
/************************************************************************************************************************/
private List<AnimancerState> _SynchronizedChildren;
/// <summary>A copy of the internal list of child states that will have their times synchronized.</summary>
/// <remarks>
/// If this mixer is a child of another mixer, its synchronized children will be managed by the parent.
/// <para></para>
/// The getter allocates a new array if <see cref="SynchronizedChildCount"/> is greater than zero.
/// </remarks>
public AnimancerState[] SynchronizedChildren
{
get => SynchronizedChildCount > 0 ? _SynchronizedChildren.ToArray() : Array.Empty<AnimancerState>();
set
{
if (_SynchronizedChildren == null)
_SynchronizedChildren = new List<AnimancerState>();
else
_SynchronizedChildren.Clear();
for (int i = 0; i < value.Length; i++)
Synchronize(value[i]);
}
}
/// <summary>The number of <see cref="SynchronizedChildren"/>.</summary>
public int SynchronizedChildCount => _SynchronizedChildren != null ? _SynchronizedChildren.Count : 0;
/************************************************************************************************************************/
/// <summary>Is the `state` in the <see cref="SynchronizedChildren"/>?</summary>
public bool IsSynchronized(AnimancerState state)
{
var synchronizer = GetParentMixer();
return
synchronizer._SynchronizedChildren != null &&
synchronizer._SynchronizedChildren.Contains(state);
}
/************************************************************************************************************************/
/// <summary>Adds the `state` to the <see cref="SynchronizedChildren"/>.</summary>
/// <remarks>
/// The `state` must be a child of this mixer.
/// <para></para>
/// If this mixer is a child of another mixer, the `state` will be added to the parent's
/// <see cref="SynchronizedChildren"/> instead.
/// </remarks>
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);
}
/// <summary>The internal implementation of <see cref="Synchronize"/>.</summary>
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<AnimancerState>();
#if UNITY_ASSERTIONS
if (_SynchronizedChildren.Contains(state))
Debug.LogError($"{state} is already in the {nameof(SynchronizedChildren)} list.");
#endif
_SynchronizedChildren.Add(state);
RequireUpdate();
}
/************************************************************************************************************************/
/// <summary>Removes the `state` from the <see cref="SynchronizedChildren"/>.</summary>
public void DontSynchronize(AnimancerState state)
{
var synchronizer = GetParentMixer();
if (synchronizer._SynchronizedChildren != null &&
synchronizer._SynchronizedChildren.Remove(state) &&
state._Playable.IsValid())
state._Playable.SetSpeed(state.Speed);
}
/************************************************************************************************************************/
/// <summary>Removes all children of this mixer from the <see cref="SynchronizedChildren"/>.</summary>
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);
}
}
}
}
/************************************************************************************************************************/
/// <summary>Initializes the internal <see cref="SynchronizedChildren"/> list.</summary>
/// <remarks>
/// The array can be null or empty. Any elements not in the array will be treated as <c>true</c>.
/// <para></para>
/// This method can only be called before any <see cref="SynchronizedChildren"/> are added and also before this
/// mixer is made the child of another mixer.
/// </remarks>
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));
}
/************************************************************************************************************************/
/// <summary>
/// Returns the highest <see cref="MixerState"/> in the hierarchy above this mixer or this mixer itself if
/// there are none above it.
/// </summary>
public MixerState GetParentMixer()
{
var mixer = this;
var parent = Parent;
while (parent != null)
{
if (parent is MixerState parentMixer)
mixer = parentMixer;
parent = parent.Parent;
}
return mixer;
}
/// <summary>Returns the highest <see cref="MixerState"/> in the hierarchy above the `state` (inclusive).</summary>
public static MixerState GetParentMixer(IPlayableWrapper node)
{
MixerState mixer = null;
while (node != null)
{
if (node is MixerState parentMixer)
mixer = parentMixer;
node = node.Parent;
}
return mixer;
}
/************************************************************************************************************************/
/// <summary>Is the `child` a child of the `parent`?</summary>
public static bool IsChildOf(IPlayableWrapper child, IPlayableWrapper parent)
{
while (true)
{
child = child.Parent;
if (child == parent)
return true;
else if (child == null)
return false;
}
}
/************************************************************************************************************************/
/// <summary>
/// Synchronizes the <see cref="AnimancerState.NormalizedTime"/>s of the <see cref="SynchronizedChildren"/> by
/// modifying their internal playable speeds.
/// </summary>
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.
}
/************************************************************************************************************************/
/// <summary>
/// The multiplied <see cref="PlayableExtensions.GetSpeed"/> of this mixer and its parents down the
/// hierarchy to determine the actual speed its output is being played at.
/// </summary>
/// <remarks>
/// This can be different from the <see cref="AnimancerNode.EffectiveSpeed"/> because the
/// <see cref="SynchronizedChildren"/> have their playable speed modified without setting their
/// <see cref="AnimancerNode.Speed"/>.
/// </remarks>
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;
/// <inheritdoc/>
public override bool ApplyAnimatorIK
{
get => _ApplyAnimatorIK;
set => base.ApplyAnimatorIK = _ApplyAnimatorIK = value;
}
/************************************************************************************************************************/
private bool _ApplyFootIK;
/// <inheritdoc/>
public override bool ApplyFootIK
{
get => _ApplyFootIK;
set => base.ApplyFootIK = _ApplyFootIK = value;
}
/************************************************************************************************************************/
#endregion
/************************************************************************************************************************/
#region Other Methods
/************************************************************************************************************************/
/// <summary>Calculates the sum of the <see cref="AnimancerNode.Weight"/> of all `states`.</summary>
public float CalculateTotalWeight(IList<AnimancerState> 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;
}
/************************************************************************************************************************/
/// <summary>
/// Sets <see cref="AnimancerState.Time"/> for all <see cref="ChildStates"/>.
/// </summary>
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;
}
}
/************************************************************************************************************************/
/// <summary>
/// Sets the weight of all states after the `previousIndex` to 0.
/// </summary>
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;
}
}
/************************************************************************************************************************/
/// <summary>
/// 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 <see cref="ChildStates"/>.
/// </summary>
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;
}
/************************************************************************************************************************/
/// <summary>Divides the weight of all child states by the `totalWeight`.</summary>
/// <remarks>
/// If the `totalWeight` is equal to the total <see cref="AnimancerNode.Weight"/> of all child states, then the
/// new total will become 1.
/// </remarks>
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;
}
}
/************************************************************************************************************************/
/// <summary>Gets a user-friendly key to identify the `state` in the Inspector.</summary>
public virtual string GetDisplayKey(AnimancerState state) => $"[{state.Index}]";
/************************************************************************************************************************/
/// <inheritdoc/>
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;
}
}
/************************************************************************************************************************/
/// <summary>
/// Recalculates the <see cref="AnimancerState.Duration"/> of all child states so that they add up to 1.
/// </summary>
/// <exception cref="NullReferenceException">There are any states with no <see cref="Clip"/>.</exception>
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
/// <summary>Has the <see cref="AnimancerNode.DebugName"/> been generated from the child states?</summary>
private bool _IsGeneratedName;
#endif
/// <summary>
/// Returns a string describing the type of this mixer and the name of <see cref="Clip"/>s connected to it.
/// </summary>
public override string ToString()
{
#if UNITY_ASSERTIONS
if (!string.IsNullOrEmpty(DebugName))
return DebugName;
#endif
// Gather child names.
var childNames = ObjectPool.AcquireList<string>();
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;
}
/************************************************************************************************************************/
/// <inheritdoc/>
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]);
}
}
}
/************************************************************************************************************************/
/// <inheritdoc/>
public override void GatherAnimationClips(ICollection<AnimationClip> clips) => clips.GatherFromSource(ChildStates);
/************************************************************************************************************************/
#endregion
/************************************************************************************************************************/
}
}