// 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 toString = null) { if (collection == null) return "null"; else return DeepToString(collection.GetEnumerator(), separator, toString); } /// Returns a string containing the value of each element in `collection` (each on a new line). public static string DeepToString( this IEnumerable collection, Func toString = null) => DeepToString(collection, Environment.NewLine, toString); /// Returns a string containing the value of each element in `enumerator`. public static string DeepToString( this IEnumerator enumerator, string separator, Func toString = null) { var text = StringBuilderPool.Instance.Acquire(); AppendDeepToString(text, enumerator, separator, toString); return text.ReleaseToString(); } /// Returns a string containing the value of each element in `enumerator` (each on a new line). public static string DeepToString( this IEnumerator enumerator, Func toString = null) => DeepToString(enumerator, Environment.NewLine, toString); /************************************************************************************************************************/ /// Each element returned by `enumerator` is appended to `text`. public static void AppendDeepToString( StringBuilder text, IEnumerator enumerator, string separator, Func toString = null) { text.Append("[]"); var countIndex = text.Length - 1; var count = 0; while (enumerator.MoveNext()) { text.Append(separator); text.Append('['); text.Append(count); text.Append("] = "); var value = enumerator.Current; if (toString != null) value = toString(value); text.Append(ToStringOrNull(value)); count++; } text.Insert(countIndex, count); } /************************************************************************************************************************/ /// Returns the value registered in the `dictionary` using the `key`. /// Returns default() if nothing was registered. [MethodImpl(MethodImplOptions.AggressiveInlining)] public static TValue Get( this Dictionary dictionary, TKey key) { dictionary.TryGetValue(key, out var value); return value; } /// Registers the `value` in the `dictionary` using the `key`, replacing any previous value. /// /// This is identical to setting dictionary[key] = value; /// except the syntax matches dictionary.Add(key, value);. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void Set( this Dictionary dictionary, TKey key, TValue value) => dictionary[key] = value; /************************************************************************************************************************/ /// Removes any items from the `dictionary` that use destroyed objects as their key. public static void RemoveDestroyedObjects(Dictionary dictionary) where TKey : Object { using (ListPool.Instance.Acquire(out var oldObjects)) { foreach (var obj in dictionary.Keys) if (obj == null) oldObjects.Add(obj); for (int i = 0; i < oldObjects.Count; i++) dictionary.Remove(oldObjects[i]); } } /// /// Creates a new dictionary and returns true if it was null or calls and /// returns false if it wasn't. /// public static bool InitializeCleanDictionary(ref Dictionary dictionary) where TKey : Object { if (dictionary == null) { dictionary = new(); return true; } else { RemoveDestroyedObjects(dictionary); return false; } } /************************************************************************************************************************/ #endregion /************************************************************************************************************************/ #region Animator Controllers /************************************************************************************************************************/ /// 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) { return parameter.type switch { AnimatorControllerParameterType.Float => animator.GetFloat(parameter.nameHash), AnimatorControllerParameterType.Int => animator.GetInteger(parameter.nameHash), AnimatorControllerParameterType.Bool or AnimatorControllerParameterType.Trigger => animator.GetBool(parameter.nameHash), _ => throw CreateUnsupportedArgumentException(parameter.type), }; } /// Gets the value of the `parameter` in the `playable`. public static object GetParameterValue(AnimatorControllerPlayable playable, AnimatorControllerParameter parameter) { return parameter.type switch { AnimatorControllerParameterType.Float => playable.GetFloat(parameter.nameHash), AnimatorControllerParameterType.Int => playable.GetInteger(parameter.nameHash), AnimatorControllerParameterType.Bool or AnimatorControllerParameterType.Trigger => playable.GetBool(parameter.nameHash), _ => 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); } } /************************************************************************************************************************/ #endregion /************************************************************************************************************************/ #region Math /************************************************************************************************************************/ /// 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; /************************************************************************************************************************/ /// The opposite of . public static float InverseLerpUnclamped(float a, float b, float value) { if (a == b) return 0; else return (value - a) / (b - a); } /************************************************************************************************************************/ /// Are the given values equal or both (which wouldn't normally be equal)? public static bool IsEqualOrBothNaN(this float a, float b) => a == b || (float.IsNaN(a) && float.IsNaN(b)); /************************************************************************************************************************/ /// [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(); /************************************************************************************************************************/ #endregion /************************************************************************************************************************/ #region Hashing /************************************************************************************************************************/ /// Returns a hash value from the given parameters. [MethodImpl(MethodImplOptions.AggressiveInlining)] public static int Hash(int seed, int hash1, int hash2) { AddHash(ref seed, hash1); AddHash(ref seed, hash2); return seed; } /// Returns a hash value from the given parameters. [MethodImpl(MethodImplOptions.AggressiveInlining)] public static int Hash(int seed, int hash1, int hash2, int hash3) { AddHash(ref seed, hash1); AddHash(ref seed, hash2); AddHash(ref seed, hash3); return seed; } /// Returns a hash value from the given parameters. [MethodImpl(MethodImplOptions.AggressiveInlining)] public static int Hash(int seed, int hash1, int hash2, int hash3, int hash4) { AddHash(ref seed, hash1); AddHash(ref seed, hash2); AddHash(ref seed, hash3); AddHash(ref seed, hash4); return seed; } /************************************************************************************************************************/ /// Includes `add` in the `hash`. [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void AddHash(ref int hash, int add) => hash = hash * -1521134295 + add; /************************************************************************************************************************/ /// Uses to get a hash code. [MethodImpl(MethodImplOptions.AggressiveInlining)] public static int SafeGetHashCode(this T value) => EqualityComparer.Default.GetHashCode(value); /************************************************************************************************************************/ #endregion /************************************************************************************************************************/ #region Components /************************************************************************************************************************/ /// Is the `obj` null or a destroyed ? public static bool IsNullOrDestroyed(this object obj) => obj == null || (obj is Object unityObject && unityObject == null); /************************************************************************************************************************/ /// [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 { if (gameObject == null) return null; 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 (gameObject == null) return false; if (component != null && (component is not Object obj || obj != null)) return false; component = gameObject.GetComponentInParentOrChildren(); return component is not null; } /************************************************************************************************************************/ /// Creates a new and `singleton` instance if it was null. /// Calls on the instance. public static T InitializeSingleton(ref T singleton) where T : Behaviour { if (singleton != null) return singleton; #if UNITY_EDITOR // In Edit Mode or if we enter Play Mode without a Domain Reload // there might already be an existing instance. // Object.FindObjectOfType won't find it for whatever reason. var instances = Resources.FindObjectsOfTypeAll(); for (int i = 0; i < instances.Length; i++) { singleton = instances[i]; // Ignore prefabs if an instance gets saved in one. if (string.IsNullOrEmpty(singleton.gameObject.scene.path)) continue; singleton.enabled = true; return singleton; } // In Edit Mode, create a hidden object so we don't dirty the scene. if (!UnityEditor.EditorApplication.isPlayingOrWillChangePlaymode) { var gameObject = UnityEditor.EditorUtility.CreateGameObjectWithHideFlags( typeof(T).Name, HideFlags.HideAndDontSave); singleton = gameObject.AddComponent(); return singleton; } #endif // Otherwise, just create a regular instance. { var gameObject = new GameObject(typeof(T).Name); singleton = gameObject.AddComponent(); Object.DontDestroyOnLoad(gameObject); return singleton; } } /************************************************************************************************************************/ #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?.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; if (component is not IAnimancerComponent animancer) animancer = component.gameObject.GetComponentInParentOrChildren(); if (!ShouldEditModePlay(animancer, clip)) return; // If it's already initialized, play immediately. if (animancer.IsGraphInitialized) { animancer.Graph.Layers[0].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.Graph.Layers[0].Play(clip); }; #endif } #if UNITY_EDITOR private static bool ShouldEditModePlay(IAnimancerComponent animancer, AnimationClip clip) => ShouldEditModeSample(clip, animancer?.Animator) && (animancer is not Object obj || obj != null); #endif /************************************************************************************************************************/ #endregion /************************************************************************************************************************/ } }