// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2023 Kybernetik // using System; using System.Collections; using System.Collections.Generic; 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; /************************************************************************************************************************/ /// Loops the `value` so that 0 <= value < 1. /// This is more efficient than using with a length of 1. public static float Wrap01(float value) { var valueAsDouble = (double)value; value = (float)(valueAsDouble - Math.Floor(valueAsDouble)); return value < 1 ? value : 0; } /// Loops the `value` so that 0 <= value < length. /// Unike , this method will never return the `length`. public static float Wrap(float value, float length) { var valueAsDouble = (double)value; var lengthAsDouble = (double)length; value = (float)(valueAsDouble - Math.Floor(valueAsDouble / lengthAsDouble) * lengthAsDouble); return value < length ? value : 0; } /************************************************************************************************************************/ /// /// Rounds the `value` to the nearest integer using . /// public static float Round(float value) => (float)Math.Round(value, MidpointRounding.AwayFromZero); /// /// Rounds the `value` to be a multiple of the `multiple` using . /// public static float Round(float value, float multiple) => Round(value / multiple) * multiple; /************************************************************************************************************************/ /// [Animancer Extension] Is the `value` not NaN or Infinity? /// Newer versions of the .NET framework apparently have a float.IsFinite method. public static bool IsFinite(this float value) => !float.IsNaN(value) && !float.IsInfinity(value); /// [Animancer Extension] Is the `value` not NaN or Infinity? /// Newer versions of the .NET framework apparently have a double.IsFinite method. public static bool IsFinite(this double value) => !double.IsNaN(value) && !double.IsInfinity(value); /// [Animancer Extension] Are all components of the `value` not NaN or Infinity? public static bool IsFinite(this Vector2 value) => value.x.IsFinite() && value.y.IsFinite(); /************************************************************************************************************************/ /// /// 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 is null) return "Null"; if (obj is Object unityObject && unityObject == null) return $"Null ({obj.GetType()})"; return obj.ToString(); } /************************************************************************************************************************/ /// 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) { var temp = array[a]; array[a] = array[b]; array[b] = temp; } /************************************************************************************************************************/ /// [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. /// /// /// 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) { array = new T[length]; return true; } else return false; } /************************************************************************************************************************/ /// [Animancer Extension] Is the `node` is not null and ? public static bool IsValid(this AnimancerNode node) => node != null && node.IsValid; /// [Animancer Extension] Is the `transition` not null and ? public static bool IsValid(this ITransitionDetailed transition) => transition != null && transition.IsValid; /************************************************************************************************************************/ /// [Animancer Extension] Calls and . public static AnimancerState CreateStateAndApply(this ITransition transition, AnimancerPlayable root = null) { var state = transition.CreateState(); state.SetRoot(root); transition.Apply(state); return state; } /************************************************************************************************************************/ /// [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. { Assert(graph.GetOutput(0).GetSourcePlayable().Equals(playable), $"{nameof(RemovePlayable)} can only be used on playables connected to another playable or to the graph output."); if (destroy) playable.Destroy(); else graph.Disconnect(playable, 0); graph.GetOutput(0).SetSourcePlayable(input); } } /************************************************************************************************************************/ /// /// Checks if any in the `source` has an animation event with the specified /// `functionName`. /// public static bool HasEvent(IAnimationClipCollection source, string functionName) { var clips = ObjectPool.AcquireSet(); source.GatherAnimationClips(clips); foreach (var clip in clips) { if (HasEvent(clip, functionName)) { ObjectPool.Release(clips); return true; } } ObjectPool.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 Vector2(averageVelocity.x, averageVelocity.z)); } } /************************************************************************************************************************/ /// Copies the value of the `parameter` from `copyFrom` to `copyTo`. public static void CopyParameterValue(Animator copyFrom, Animator copyTo, AnimatorControllerParameter parameter) { switch (parameter.type) { case AnimatorControllerParameterType.Float: copyTo.SetFloat(parameter.nameHash, copyFrom.GetFloat(parameter.nameHash)); break; case AnimatorControllerParameterType.Int: copyTo.SetInteger(parameter.nameHash, copyFrom.GetInteger(parameter.nameHash)); break; case AnimatorControllerParameterType.Bool: case AnimatorControllerParameterType.Trigger: copyTo.SetBool(parameter.nameHash, copyFrom.GetBool(parameter.nameHash)); break; default: throw CreateUnsupportedArgumentException(parameter.type); } } /// Copies the value of the `parameter` from `copyFrom` to `copyTo`. public static void CopyParameterValue(AnimatorControllerPlayable copyFrom, AnimatorControllerPlayable copyTo, AnimatorControllerParameter parameter) { switch (parameter.type) { case AnimatorControllerParameterType.Float: copyTo.SetFloat(parameter.nameHash, copyFrom.GetFloat(parameter.nameHash)); break; case AnimatorControllerParameterType.Int: copyTo.SetInteger(parameter.nameHash, copyFrom.GetInteger(parameter.nameHash)); break; case AnimatorControllerParameterType.Bool: case AnimatorControllerParameterType.Trigger: copyTo.SetBool(parameter.nameHash, copyFrom.GetBool(parameter.nameHash)); break; default: throw CreateUnsupportedArgumentException(parameter.type); } } /************************************************************************************************************************/ /// Gets the value of the `parameter` in the `animator`. public static object GetParameterValue(Animator animator, AnimatorControllerParameter parameter) { switch (parameter.type) { case AnimatorControllerParameterType.Float: return animator.GetFloat(parameter.nameHash); case AnimatorControllerParameterType.Int: return animator.GetInteger(parameter.nameHash); case AnimatorControllerParameterType.Bool: case AnimatorControllerParameterType.Trigger: return animator.GetBool(parameter.nameHash); default: throw CreateUnsupportedArgumentException(parameter.type); } } /// Gets the value of the `parameter` in the `playable`. public static object GetParameterValue(AnimatorControllerPlayable playable, AnimatorControllerParameter parameter) { switch (parameter.type) { case AnimatorControllerParameterType.Float: return playable.GetFloat(parameter.nameHash); case AnimatorControllerParameterType.Int: return playable.GetInteger(parameter.nameHash); case AnimatorControllerParameterType.Bool: case AnimatorControllerParameterType.Trigger: return playable.GetBool(parameter.nameHash); default: throw CreateUnsupportedArgumentException(parameter.type); } } /************************************************************************************************************************/ /// Sets the `value` of the `parameter` in the `animator`. public static void SetParameterValue(Animator animator, AnimatorControllerParameter parameter, object value) { switch (parameter.type) { case AnimatorControllerParameterType.Float: animator.SetFloat(parameter.nameHash, (float)value); break; case AnimatorControllerParameterType.Int: animator.SetInteger(parameter.nameHash, (int)value); break; case AnimatorControllerParameterType.Bool: animator.SetBool(parameter.nameHash, (bool)value); break; case AnimatorControllerParameterType.Trigger: if ((bool)value) animator.SetTrigger(parameter.nameHash); else animator.ResetTrigger(parameter.nameHash); break; default: throw CreateUnsupportedArgumentException(parameter.type); } } /// Sets the `value` of the `parameter` in the `playable`. public static void SetParameterValue(AnimatorControllerPlayable playable, AnimatorControllerParameter parameter, object value) { switch (parameter.type) { case AnimatorControllerParameterType.Float: playable.SetFloat(parameter.nameHash, (float)value); break; case AnimatorControllerParameterType.Int: playable.SetInteger(parameter.nameHash, (int)value); break; case AnimatorControllerParameterType.Bool: playable.SetBool(parameter.nameHash, (bool)value); break; case AnimatorControllerParameterType.Trigger: if ((bool)value) playable.SetTrigger(parameter.nameHash); else playable.ResetTrigger(parameter.nameHash); break; default: throw CreateUnsupportedArgumentException(parameter.type); } } /************************************************************************************************************************/ /// /// 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 { return new NativeArray(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 ArgumentException(GetUnsupportedMessage(value)); /************************************************************************************************************************/ #endregion /************************************************************************************************************************/ #region Components /************************************************************************************************************************/ /// [Animancer Extension] /// Adds the specified type of , links it to the `animator`, and returns it. /// public static T AddAnimancerComponent(this Animator animator) where T : Component, IAnimancerComponent { var animancer = animator.gameObject.AddComponent(); animancer.Animator = animator; return animancer; } /************************************************************************************************************************/ /// [Animancer Extension] /// Returns the on the same as the `animator` if /// there is one. Otherwise this method adds a new one and returns it. /// public static T GetOrAddAnimancerComponent(this Animator animator) where T : Component, IAnimancerComponent { if (animator.TryGetComponent(out var component)) return component; else return animator.AddAnimancerComponent(); } /************************************************************************************************************************/ /// /// Returns the first component on the `gameObject` or its parents or children (in /// that order). /// public static T GetComponentInParentOrChildren(this GameObject gameObject) where T : class { var component = gameObject.GetComponentInParent(); if (component != null) return component; return gameObject.GetComponentInChildren(); } /// /// If the `component` is null, this method tries to find one on the `gameObject` or its parents or /// children (in that order). /// public static bool GetComponentInParentOrChildren(this GameObject gameObject, ref T component) where T : class { if (component != null && (!(component is Object obj) || obj != null)) return false; component = gameObject.GetComponentInParentOrChildren(); return !(component is null); } /************************************************************************************************************************/ #endregion /************************************************************************************************************************/ #region Editor /************************************************************************************************************************/ /// [Assert-Conditional] /// Throws an if the `condition` is false. /// /// /// This method is similar to , but it throws an exception instead of /// just logging the `message`. /// [System.Diagnostics.Conditional(Strings.Assertions)] public static void Assert(bool condition, object message) { #if UNITY_ASSERTIONS if (!condition) throw new UnityEngine.Assertions.AssertionException(message != null ? message.ToString() : "Assertion failed.", null); #endif } /************************************************************************************************************************/ /// [Editor-Conditional] Indicates that the `target` needs to be re-serialized. [System.Diagnostics.Conditional(Strings.UnityEditor)] public static void SetDirty(Object target) { #if UNITY_EDITOR UnityEditor.EditorUtility.SetDirty(target); #endif } /************************************************************************************************************************/ /// [Editor-Conditional] /// Applies the effects of the animation `clip` to the . /// /// This method is safe to call during .OnValidate. /// The animation to apply. If null, this method does nothing. /// /// The animation will be applied to an or component on the same /// object as this or on any of its parents or children. If null, this method does nothing. /// /// Determines which part of the animation to apply (in seconds). /// [System.Diagnostics.Conditional(Strings.UnityEditor)] public static void EditModeSampleAnimation(this AnimationClip clip, Component component, float time = 0) { #if UNITY_EDITOR if (!ShouldEditModeSample(clip, component)) return; var gameObject = component.gameObject; component = gameObject.GetComponentInParentOrChildren(); if (component == null) { component = gameObject.GetComponentInParentOrChildren(); if (component == null) return; } UnityEditor.EditorApplication.delayCall += () => { if (!ShouldEditModeSample(clip, component)) return; clip.SampleAnimation(component.gameObject, time); }; } private static bool ShouldEditModeSample(AnimationClip clip, Component component) { return !UnityEditor.EditorApplication.isPlayingOrWillChangePlaymode && clip != null && component != null && !UnityEditor.EditorUtility.IsPersistent(component); #endif } /************************************************************************************************************************/ /// [Editor-Conditional] Plays the specified `clip` if called in Edit Mode. /// This method is safe to call during .OnValidate. /// The animation to apply. If null, this method does nothing. /// /// The animation will be played on an on the same object as this or on any /// of its parents or children. If null, this method does nothing. /// /// [System.Diagnostics.Conditional(Strings.UnityEditor)] public static void EditModePlay(this AnimationClip clip, Component component) { #if UNITY_EDITOR if (!ShouldEditModeSample(clip, component)) return; var animancer = component as IAnimancerComponent; if (animancer == null) animancer = component.gameObject.GetComponentInParentOrChildren(); if (!ShouldEditModePlay(animancer, clip)) return; // If it's already initialized, play immediately. if (animancer.IsPlayableInitialized) { animancer.Playable.Play(clip); return; } // Otherwise, delay it in case this was called at a bad time (such as during OnValidate). UnityEditor.EditorApplication.delayCall += () => { if (ShouldEditModePlay(animancer, clip)) animancer.Playable.Play(clip); }; } private static bool ShouldEditModePlay(IAnimancerComponent animancer, AnimationClip clip) { return ShouldEditModeSample(clip, animancer?.Animator) && (!(animancer is Object obj) || obj != null); #endif } /************************************************************************************************************************/ #if UNITY_ASSERTIONS /************************************************************************************************************************/ private static System.Reflection.FieldInfo _DelegatesField; private static bool _GotDelegatesField; /// [Assert-Only] /// Uses reflection to achieve the same as without allocating /// garbage every time. /// /// If the delegate is null or , this method returns false and outputs null. /// If the underlying delegate field was not found, this method returns false and outputs null. /// If the delegate is not multicast, this method this method returns true and outputs null. /// If the delegate is multicast, this method this method returns true and outputs its invocation list. /// /// public static bool TryGetInvocationListNonAlloc(MulticastDelegate multicast, out Delegate[] delegates) { if (multicast == null) { delegates = null; return false; } if (!_GotDelegatesField) { const string FieldName = "delegates"; _GotDelegatesField = true; _DelegatesField = typeof(MulticastDelegate).GetField("delegates", System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance); if (_DelegatesField != null && _DelegatesField.FieldType != typeof(Delegate[])) _DelegatesField = null; if (_DelegatesField == null) Debug.LogError($"Unable to find {nameof(MulticastDelegate)}.{FieldName} field."); } if (_DelegatesField == null) { delegates = null; return false; } else { delegates = (Delegate[])_DelegatesField.GetValue(multicast); return true; } } /************************************************************************************************************************/ #endif /************************************************************************************************************************/ #endregion /************************************************************************************************************************/ } }