// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2024 Kybernetik //
using System;
using System.Collections;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using System.Text;
using Unity.Collections;
using UnityEngine;
using UnityEngine.Animations;
using UnityEngine.Playables;
using Object = UnityEngine.Object;
namespace Animancer
{
/// Various extension methods and utilities.
/// https://kybernetik.com.au/animancer/api/Animancer/AnimancerUtilities
///
public static partial class AnimancerUtilities
{
/************************************************************************************************************************/
#region Misc
/************************************************************************************************************************/
/// This is Animancer Pro.
public const bool IsAnimancerPro = true;
/************************************************************************************************************************/
///
/// If `obj` exists, this method returns .
/// Or if it is null, this method returns "Null".
/// Or if it is an that has been destroyed, this method returns "Null (ObjectType)".
///
public static string ToStringOrNull(object obj)
{
if (obj == null)
return "Null";
if (obj is Object unityObject && unityObject == null)
return $"Null ({obj.GetType()})";
return obj.ToString();
}
/************************************************************************************************************************/
/// [Animancer Extension]
/// Is the `node` is not null and its valid?
///
public static bool IsValid(this AnimancerNode node)
=> node != null
&& node.Playable.IsValid();
/************************************************************************************************************************/
/// [Animancer Extension] Calls and .
public static AnimancerState CreateStateAndApply(this ITransition transition, AnimancerGraph graph = null)
{
var state = transition.CreateState();
state.SetGraph(graph);
transition.Apply(state);
return state;
}
/************************************************************************************************************************/
///
/// If the `key` is an ,
/// this method gets its
/// and repeats that check until it finds another kind of key, which it returns.
///
public static object GetRootKey(object key)
{
while (key is AnimancerState state)
{
var stateKey = state.Key;
if (stateKey == null)
break;
key = stateKey;
}
return key;
}
///
/// If a state is registered with the `key`, this method gets it and repeats that check then returns the last
/// state found.
///
public static object GetLastKey(AnimancerStateDictionary states, object key)
{
while (states.TryGet(key, out var state))
key = state;
return key;
}
/************************************************************************************************************************/
///
/// Calls using output 0 from the `child` and
/// .
///
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static void Connect(
this PlayableGraph graph,
TParent parent,
TChild child,
int parentInputIndex,
float weight)
where TParent : struct, IPlayable
where TChild : struct, IPlayable
{
graph.Connect(child, 0, parent, parentInputIndex);
parent.SetInputWeight(parentInputIndex, weight);
}
/************************************************************************************************************************/
/// Applies the `child`'s current .
public static void ApplyChildWeight(this Playable parent, AnimancerNode child)
=> parent.SetInputWeight(child.Index, child.Weight);
///
/// Sets and applies the `child`'s
/// and .
///
public static void SetChildWeight(this Playable parent, AnimancerState child, float weight)
{
if (child._Weight == weight)
return;
Validate.AssertSetWeight(child, weight);
child._Weight = weight;
child.ShouldBeActive = weight > 0 || child.IsPlaying;
parent.SetInputWeight(child.Index, weight);
}
/************************************************************************************************************************/
/// [Pro-Only] Reconnects the input of the specified `playable` to its output.
public static void RemovePlayable(Playable playable, bool destroy = true)
{
if (!playable.IsValid())
return;
Assert(playable.GetInputCount() == 1,
$"{nameof(RemovePlayable)} can only be used on playables with 1 input.");
Assert(playable.GetOutputCount() == 1,
$"{nameof(RemovePlayable)} can only be used on playables with 1 output.");
var input = playable.GetInput(0);
if (!input.IsValid())
{
if (destroy)
playable.Destroy();
return;
}
var graph = playable.GetGraph();
var output = playable.GetOutput(0);
if (output.IsValid())// Connected to another Playable.
{
if (destroy)
{
playable.Destroy();
}
else
{
Assert(output.GetInputCount() == 1,
$"{nameof(RemovePlayable)} can only be used on playables connected to a playable with 1 input.");
graph.Disconnect(output, 0);
graph.Disconnect(playable, 0);
}
graph.Connect(input, 0, output, 0);
}
else// Connected to the graph output.
{
var playableOutput = graph.FindOutput(playable);
if (playableOutput.IsOutputValid())
playableOutput.SetSourcePlayable(input);
if (destroy)
playable.Destroy();
else
graph.Disconnect(playable, 0);
}
}
/************************************************************************************************************************/
/// Returns the output connected to the `sourcePlayable` (if any).
public static PlayableOutput FindOutput(this PlayableGraph graph, Playable sourcePlayable)
{
var handle = sourcePlayable.GetHandle();
var outputCount = graph.GetOutputCount();
for (int i = outputCount - 1; i >= 0; i--)
{
var output = graph.GetOutput(i);
if (output.GetSourcePlayable().GetHandle() == handle)
return output;
}
return default;
}
/************************************************************************************************************************/
///
/// Checks if any in the `source` has an animation event with the specified
/// `functionName`.
///
public static bool HasEvent(IAnimationClipCollection source, string functionName)
{
var clips = SetPool.Acquire();
source.GatherAnimationClips(clips);
foreach (var clip in clips)
{
if (HasEvent(clip, functionName))
{
SetPool.Release(clips);
return true;
}
}
SetPool.Release(clips);
return false;
}
/// Checks if the `clip` has an animation event with the specified `functionName`.
public static bool HasEvent(AnimationClip clip, string functionName)
{
var events = clip.events;
for (int i = events.Length - 1; i >= 0; i--)
{
if (events[i].functionName == functionName)
return true;
}
return false;
}
/************************************************************************************************************************/
/// [Animancer Extension] [Pro-Only]
/// Calculates all thresholds in the `mixer` using the of each
/// state on the X and Z axes.
///
/// Note that this method requires the Root Transform Position (XZ) -> Bake Into Pose toggle to be
/// disabled in the Import Settings of each in the mixer.
///
public static void CalculateThresholdsFromAverageVelocityXZ(this MixerState mixer)
{
mixer.ValidateThresholdCount();
for (int i = mixer.ChildCount - 1; i >= 0; i--)
{
var state = mixer.GetChild(i);
if (state == null)
continue;
var averageVelocity = state.AverageVelocity;
mixer.SetThreshold(i, new(averageVelocity.x, averageVelocity.z));
}
}
/************************************************************************************************************************/
///
/// Creates a containing a single element so that it can be used like a reference
/// in Unity's C# Job system which does not allow regular reference types.
///
/// Note that you must call when you're done with the array.
public static NativeArray CreateNativeReference()
where T : struct
=> new(1, Allocator.Persistent, NativeArrayOptions.ClearMemory);
/************************************************************************************************************************/
///
/// Creates a of s for each of the `transforms`.
///
/// Note that you must call when you're done with the array.
public static NativeArray ConvertToTransformStreamHandles(
IList transforms, Animator animator)
{
var count = transforms.Count;
var boneHandles = new NativeArray(
count, Allocator.Persistent, NativeArrayOptions.UninitializedMemory);
for (int i = 0; i < count; i++)
boneHandles[i] = animator.BindStreamTransform(transforms[i]);
return boneHandles;
}
/************************************************************************************************************************/
/// Returns a string stating that the `value` is unsupported.
public static string GetUnsupportedMessage(T value)
=> $"Unsupported {typeof(T).FullName}: {value}";
/// Returns an exception stating that the `value` is unsupported.
public static ArgumentException CreateUnsupportedArgumentException(T value)
=> new(GetUnsupportedMessage(value));
/************************************************************************************************************************/
#endregion
/************************************************************************************************************************/
#region Collections
/************************************************************************************************************************/
///
/// If the `index` is within the `list`,
/// this method outputs the `item` at that `index` and returns true.
///
public static bool TryGet(this IList list, int index, out T item)
{
if ((uint)index < (uint)list.Count)
{
item = list[index];
return true;
}
else
{
item = default;
return false;
}
}
/************************************************************************************************************************/
///
/// If the `index` is within the `list` and that `item` is not null,
/// this method outputs it and returns true.
///
public static bool TryGetObject(this IList list, int index, out T item)
where T : Object
{
if (list.TryGet(index, out item) &&
item != null)
return true;
item = default;
return false;
}
/************************************************************************************************************************/
///
/// If the `obj` is a or ,
/// this method outputs its `transform` and returns true.
///
public static bool TryGetTransform(Object obj, out Transform transform)
{
if (obj is Component component)
{
transform = component.transform;
return true;
}
else if (obj is GameObject gameObject)
{
transform = gameObject.transform;
return true;
}
else
{
transform = null;
return false;
}
}
/************************************************************************************************************************/
/// Ensures that the length and contents of `copyTo` match `copyFrom`.
public static void CopyExactArray(T[] copyFrom, ref T[] copyTo)
{
if (copyFrom == null)
{
copyTo = null;
return;
}
var length = copyFrom.Length;
SetLength(ref copyTo, length);
Array.Copy(copyFrom, copyTo, length);
}
/************************************************************************************************************************/
/// [Animancer Extension] Swaps array[a] with array[b].
public static void Swap(this T[] array, int a, int b)
=> (array[b], array[a]) = (array[a], array[b]);
/************************************************************************************************************************/
/// Are both lists the same size with the same items in the same order?
public static bool ContentsAreEqual(IList a, IList b)
{
if (a == null)
return b == null;
if (b == null)
return false;
var aCount = a.Count;
var bCount = b.Count;
if (aCount != bCount)
return false;
for (int i = 0; i < aCount; i++)
if (!EqualityComparer.Default.Equals(a[i], b[i]))
return false;
return true;
}
/************************************************************************************************************************/
/// [Animancer Extension]
/// Is the `array` null or its 0?
///
public static bool IsNullOrEmpty(this T[] array)
=> array == null
|| array.Length == 0;
/************************************************************************************************************************/
///
/// If the `array` is null or its isn't equal to the specified `length`, this
/// method creates a new array with that `length` and returns true. Otherwise, it returns false
/// and the array us unchanged.
///
///
/// Unlike , this method doesn't copy over the contents of the old
/// `array` into the new one.
///
public static bool SetLength(ref T[] array, int length)
{
if (array != null && array.Length == length)
return false;
array = new T[length];
return true;
}
/************************************************************************************************************************/
/// Resizes the `array` to be at least 1 larger and inserts the `item` at the specified `index`.
/// If the `index` is beyond the end of the array, it will be resized large enough to fit.
public static void InsertAt(ref T[] array, int index, T item)
{
if (array == null)
{
array = new T[] { item };
}
else if (index >= array.Length)
{
Array.Resize(ref array, index + 1);
array[index] = item;
}
else
{
var newArray = new T[array.Length + 1];
Array.Copy(array, 0, newArray, 0, index);
Array.Copy(array, index, newArray, index + 1, array.Length - index);
newArray[index] = item;
array = newArray;
}
}
/************************************************************************************************************************/
/// Removes the item at the specified `index` and resizes the `array` to be 1 smaller.
public static void RemoveAt(ref T[] array, int index)
{
if (array == null ||
array.Length == 0)
return;
var newArray = new T[array.Length - 1];
Array.Copy(array, 0, newArray, 0, index);
Array.Copy(array, index + 1, newArray, index, array.Length - index - 1);
array = newArray;
}
/************************************************************************************************************************/
/// Returns the `array`, or if it was null.
public static T[] NullIsEmpty(this T[] array)
=> array
?? Array.Empty();
/************************************************************************************************************************/
/// Returns a string containing the value of each element in `collection`.
public static string DeepToString(
this IEnumerable collection,
string separator,
Func