// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2023 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 : AnimancerState, ICopyable
{
/************************************************************************************************************************/
/// An that creates a .
public interface ITransition : ITransition { }
///
/// An that creates a for
/// .
///
public interface ITransition2D : ITransition> { }
/************************************************************************************************************************/
#region Properties
/************************************************************************************************************************/
/// Returns true because mixers should always keep child playables connected to the graph.
public override bool KeepChildrenConnected => true;
/// A has no .
public override AnimationClip Clip => null;
/************************************************************************************************************************/
/// The states connected to this mixer.
/// Only states up to the should be assigned.
protected AnimancerState[] ChildStates { get; private set; } = Array.Empty();
/************************************************************************************************************************/
private int _ChildCount;
///
public sealed override int ChildCount
=> _ChildCount;
/************************************************************************************************************************/
/// The size of the internal array of .
/// This value starts at 0 then expands to when the first child is added.
public int ChildCapacity
{
get => ChildStates.Length;
set
{
if (value == ChildStates.Length)
return;
#if UNITY_ASSERTIONS
if (value <= 1 && OptionalWarning.MixerMinChildren.IsEnabled())
OptionalWarning.MixerMinChildren.Log(
$"The {nameof(ChildCapacity)} of '{this}' is being set to {value}." +
$" The purpose of a mixer is to mix multiple child states so this may be a mistake.",
Root?.Component);
#endif
var newChildStates = new AnimancerState[value];
if (value > _ChildCount)// Increase size.
{
Array.Copy(ChildStates, newChildStates, _ChildCount);
}
else// Decrease size.
{
for (int i = value; i < _ChildCount; i++)
ChildStates[i].Destroy();
Array.Copy(ChildStates, newChildStates, value);
_ChildCount = value;
}
ChildStates = newChildStates;
if (_Playable.IsValid())
{
_Playable.SetInputCount(value);
}
else if (Root != null)
{
CreatePlayable();
}
OnChildCapacityChanged();
}
}
/// Called when the is changed.
protected virtual void OnChildCapacityChanged() { }
/// starts at 0 then expands to this value when the first child is added.
public static int DefaultChildCapacity { get; set; } = 8;
///
/// Ensures that the remaining unused is greater than or equal to the
/// `minimumCapacity`.
///
public void EnsureRemainingChildCapacity(int minimumCapacity)
{
minimumCapacity += _ChildCount;
if (ChildCapacity < minimumCapacity)
{
var capacity = Math.Max(ChildCapacity, DefaultChildCapacity);
while (capacity < minimumCapacity)
capacity *= 2;
ChildCapacity = capacity;
}
}
/************************************************************************************************************************/
///
public sealed override AnimancerState GetChild(int index)
=> ChildStates[index];
///
public sealed override FastEnumerator GetEnumerator()
=> new FastEnumerator(ChildStates, _ChildCount);
/************************************************************************************************************************/
///
protected override void OnSetIsPlaying()
{
#if UNITY_ASSERTIONS
if (_ChildCount <= 0 && IsPlaying)
throw new InvalidOperationException($"Unable to play {this} because it has no child states.");
#endif
for (int i = _ChildCount - 1; i >= 0; i--)
ChildStates[i].IsPlaying = IsPlaying;
}
/************************************************************************************************************************/
/// Are any child states looping?
public override bool IsLooping
{
get
{
for (int i = _ChildCount - 1; i >= 0; i--)
if (ChildStates[i].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.
///
public override double 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
{
if (value == 0)
goto ZeroTime;
var length = Length;
if (length == 0)
goto ZeroTime;
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.
ZeroTime:
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);
}
/************************************************************************************************************************/
/// 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;
for (int i = _ChildCount - 1; i >= 0; i--)
{
var state = ChildStates[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;
}
}
/************************************************************************************************************************/
///
/// 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;
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;
}
}
/************************************************************************************************************************/
#endregion
/************************************************************************************************************************/
#region Initialization
/************************************************************************************************************************/
/// 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, ChildCapacity);
#else
playable = AnimationMixerPlayable.Create(Root._Graph, ChildCapacity, false);
#endif
RecalculateWeights();
}
/************************************************************************************************************************/
/// Connects the `state` to this mixer at its .
protected internal override void OnAddChild(AnimancerState state)
{
if (state.Index != _ChildCount)
throw new ArgumentException("Mixer child index out of order." +
" Mixer children must be added in sequence starting from 0 to ensure that they contain no nulls.");
var capacity = ChildCapacity;
if (_ChildCount >= capacity)
ChildCapacity = Math.Max(DefaultChildCapacity, capacity * 2);
OnAddChild(ChildStates, state);
_ChildCount++;
if (SynchronizeNewChildren)
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)
{
DontSynchronize(state);
Validate.AssertCanRemoveChild(state, ChildStates, _ChildCount);
// Shuffle all subsequent children down one place.
if (Root == null)
{
Array.Copy(ChildStates, state.Index + 1, ChildStates, state.Index, _ChildCount - state.Index - 1);
for (int i = state.Index; i < _ChildCount - 1; i++)
ChildStates[i].Index = i;
}
else
{
Root._Graph.Disconnect(_Playable, state.Index);
for (int i = state.Index + 1; i < _ChildCount; i++)
{
var otherChild = ChildStates[i];
Root._Graph.Disconnect(_Playable, otherChild.Index);
otherChild.Index = i - 1;
ChildStates[i - 1] = otherChild;
otherChild.ConnectToGraph();
}
}
_ChildCount--;
ChildStates[_ChildCount] = null;
#if UNITY_ASSERTIONS
if (_IsGeneratedName)
{
_IsGeneratedName = false;
SetDebugName(null);
}
#endif
}
/************************************************************************************************************************/
///
public override void Destroy()
{
DestroyChildren();
base.Destroy();
}
/************************************************************************************************************************/
///
public override AnimancerState Clone(AnimancerPlayable root)
{
var clone = new ManualMixerState();
clone.SetNewCloneRoot(root);
((ICopyable)clone).CopyFrom(this);
return clone;
}
///
void ICopyable.CopyFrom(ManualMixerState copyFrom)
{
((ICopyable)this).CopyFrom(copyFrom);
DestroyChildren();
var synchronizeNewChildren = SynchronizeNewChildren;
var childCount = copyFrom.ChildCount;
EnsureRemainingChildCapacity(childCount);
for (int i = 0; i < childCount; i++)
{
var child = copyFrom.ChildStates[i];
SynchronizeNewChildren = copyFrom.IsSynchronized(child);
child = child.Clone(Root);
Add(child);
}
SynchronizeNewChildren = synchronizeNewChildren;
}
/************************************************************************************************************************/
#endregion
/************************************************************************************************************************/
#region Child Configuration
/************************************************************************************************************************/
/// Assigns the `state` as a child of this mixer.
/// The `state` must not be null. To remove a child, call instead.
public void Add(AnimancerState state)
{
state.SetParent(this, _ChildCount);// Count will get incremented by OnAddChild.
state.IsPlaying = IsPlaying;
}
/// Creates and returns a new to play the `clip` as a child of this mixer.
public ClipState Add(AnimationClip clip)
{
var state = new ClipState(clip);
Add(state);
return state;
}
/// Calls then .
public AnimancerState Add(Animancer.ITransition transition)
{
var state = transition.CreateStateAndApply(Root);
Add(state);
return state;
}
/// Calls one of the other overloads as appropriate for the `child`.
public AnimancerState Add(object child)
{
if (child is AnimationClip clip)
return Add(clip);
if (child is Animancer.ITransition transition)
return Add(transition);
if (child is AnimancerState state)
{
Add(state);
return state;
}
throw new ArgumentException($"Failed to {nameof(Add)} '{AnimancerUtilities.ToStringOrNull(child)}'" +
$" as child of '{this}' because it isn't an" +
$" {nameof(AnimationClip)}, {nameof(Animancer.ITransition)}, or {nameof(AnimancerState)}.");
}
/************************************************************************************************************************/
/// Calls for each of the `clips`.
public void AddRange(IList clips)
{
var count = clips.Count;
EnsureRemainingChildCapacity(count);
for (int i = 0; i < count; i++)
Add(clips[i]);
}
/// Calls for each of the `clips`.
public void AddRange(params AnimationClip[] clips)
=> AddRange((IList)clips);
/************************************************************************************************************************/
/// Calls for each of the `transitions`.
public void AddRange(IList transitions)
{
var count = transitions.Count;
EnsureRemainingChildCapacity(count);
for (int i = 0; i < count; i++)
Add(transitions[i]);
}
/// Calls for each of the `clips`.
public void AddRange(params Animancer.ITransition[] clips)
=> AddRange((IList)clips);
/************************************************************************************************************************/
/// Calls for each of the `children`.
public void AddRange(IList