// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2024 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]
/// 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,
IParametizedState,
IUpdatable
{
/************************************************************************************************************************/
#region Properties
/************************************************************************************************************************/
/// 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.",
Graph?.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 (Graph != 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.
/// Default 8.
public static int DefaultChildCapacity { get; set; } = 8;
///
/// Ensures that the remaining unused
/// is greater than or equal to the specified `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(ChildStates, _ChildCount);
/************************************************************************************************************************/
///
protected override void OnSetIsPlaying()
{
var isPlaying = IsPlaying;
for (int i = _ChildCount - 1; i >= 0; i--)
ChildStates[i].IsPlaying = isPlaying;
}
/************************************************************************************************************************/
/// If greater than 0 then is true.
private int _LoopingChildCount;
/// 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);
/************************************************************************************************************************/
/// 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
{
GetTimeDetails(out var totalWeight, out var normalizedTime, out var length);
if (totalWeight == 0)
return base.RawTime;
totalWeight *= totalWeight;
return normalizedTime * length / totalWeight;
}
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 void GetEventDispatchInfo(
out float length,
out float normalizedTime,
out bool isLooping)
{
GetTimeDetails(out _, out normalizedTime, out length);
isLooping = _LoopingChildCount > 0;
}
/************************************************************************************************************************/
///
/// Gets the time details based on the synchronized child states if any are active,
/// otherwise recalculates based on all child states.
///
private void GetTimeDetails(out float totalWeight, out float normalizedTime, out float length)
{
if (_SynchronizedChildren != null)
{
GetTimeDetails(
_SynchronizedChildren,
_SynchronizedChildren.Count,
out totalWeight,
out normalizedTime,
out length);
if (totalWeight > MinimumSynchronizeChildrenWeight)
return;
}
GetTimeDetails(
ChildStates,
_ChildCount,
out totalWeight,
out normalizedTime,
out length);
}
/// Gets the time details based on the `states`.
private void GetTimeDetails(
IList states,
int count,
out float totalWeight,
out float normalizedTime,
out float length)
{
totalWeight = 0;
normalizedTime = 0;
length = 0;
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;
totalWeight += weight;
normalizedTime += state.Time / stateLength * weight;
length += stateLength * weight;
}
}
/************************************************************************************************************************/
/// 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;
}
}
/************************************************************************************************************************/
#endregion
/************************************************************************************************************************/
#region Initialization
/************************************************************************************************************************/
/// Creates and assigns the managed by this state.
protected override void CreatePlayable(out Playable playable)
{
playable = AnimationMixerPlayable.Create(Graph._PlayableGraph, ChildCapacity);
}
/************************************************************************************************************************/
/// Connects the `state` to this mixer at its .
protected internal override void OnAddChild(AnimancerState state)
{
Validate.AssertGraph(state, Graph);
var capacity = ChildCapacity;
if (_ChildCount >= capacity)
ChildCapacity = Math.Max(DefaultChildCapacity, capacity * 2);
state.Index = _ChildCount;
ChildStates[_ChildCount] = state;
_ChildCount++;
state.IsPlaying = IsPlaying;
if (Graph != null)
ConnectChildUnsafe(state.Index, state);
if (SynchronizeNewChildren)
Synchronize(state);
if (state.IsLooping)
AddIsLooping(1);
#if UNITY_ASSERTIONS
_CachedToString = 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 (Graph == null || !Graph._PlayableGraph.IsValid())
{
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
{
Graph._PlayableGraph.Disconnect(_Playable, state.Index);
for (int i = state.Index + 1; i < _ChildCount; i++)
{
var otherChild = ChildStates[i];
Graph._PlayableGraph.Disconnect(_Playable, otherChild.Index);
otherChild.Index = i - 1;
ChildStates[i - 1] = otherChild;
ConnectChildUnsafe(i - 1, otherChild);
}
}
_ChildCount--;
ChildStates[_ChildCount] = null;
if (state.IsLooping)
AddIsLooping(-1);
#if UNITY_ASSERTIONS
_CachedToString = null;
#endif
}
/************************************************************************************************************************/
///
public override void Destroy()
{
DestroyChildren();
base.Destroy();
}
/************************************************************************************************************************/
///
public override AnimancerState Clone(CloneContext context)
{
var clone = new ManualMixerState();
clone.CopyFrom(this, context);
return clone;
}
/************************************************************************************************************************/
///
public sealed override void CopyFrom(AnimancerState copyFrom, CloneContext context)
=> this.CopyFromBase(copyFrom, context);
///
public virtual void CopyFrom(ManualMixerState copyFrom, CloneContext context)
{
base.CopyFrom(copyFrom, context);
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 = context.Clone(child);
Add(child);
}
SynchronizeNewChildren = synchronizeNewChildren;
}
/************************************************************************************************************************/
#endregion
/************************************************************************************************************************/
#region Child Configuration
/************************************************************************************************************************/
/// Assigns the `state` as a child of this mixer.
/// This is the same as calling .
public void Add(AnimancerState state)
=> state.SetParent(this);
/// 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(ITransition transition)
{
var state = transition.CreateStateAndApply(Graph);
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 ITransition transition)
return Add(transition);
if (child is AnimancerState state)
{
Add(state);
return state;
}
MarkAsUsed(this);
throw new ArgumentException($"Failed to {nameof(Add)} '{AnimancerUtilities.ToStringOrNull(child)}'" +
$" as child of '{this}' because it isn't an" +
$" {nameof(AnimationClip)}, {nameof(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 `transitions`.
public void AddRange(params ITransition[] transitions)
=> AddRange((IList)transitions);
/************************************************************************************************************************/
/// Calls for each of the `children`.
public void AddRange(IList