// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 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 General /************************************************************************************************************************/ /// This is Animancer Pro. public const bool IsAnimancerPro = true; /************************************************************************************************************************/ /// /// If `obj` exists, this method returns . /// Or if it's null, this method returns "Null". /// Or if it's 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 exactly 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)); /************************************************************************************************************************/ /// [Animancer Extension] Returns the `url` in a HTML <a> tag. public static string AsHtmlLink(this string url, string text = null) => $"{text ?? url}"; /************************************************************************************************************************/ /// /// Attempts to interpret the `name` as a direction /// based on the presence of the characters RLUD /// corresponding to Right, Left, Up, and Down. /// public static Vector2Int GetDirection(string name) => new( GetDirection(name, 'R', 'L'), GetDirection(name, 'U', 'D')); /************************************************************************************************************************/ /// /// Returns 1 if the `name` contains the `positive` value /// or -1 for `negative`. Otherwise returns 0. /// public static int GetDirection(string name, char positive, char negative) { var isPositive = ContainsCaseInsensitive(name, positive); var isNegative = ContainsCaseInsensitive(name, negative); if (isPositive) { if (isNegative) return 0; else return 1; } else { if (isNegative) return -1; else return 0; } } /************************************************************************************************************************/ /// Does the `text` contain the `character` (ignoring case)? public static bool ContainsCaseInsensitive(string text, char character) { var upper = char.ToUpper(character); var lower = char.ToLower(character); for (int i = text.Length - 1; i >= 0; i--) { var c = text[i]; if (c == upper || c == lower) return true; } return false; } /************************************************************************************************************************/ #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 the `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 the `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` /// and returns true as long as the `value` is the appropriate type. /// public static bool TrySetParameterValue( Animator animator, AnimatorControllerParameter parameter, object value) { switch (parameter.type) { case AnimatorControllerParameterType.Float: if (value is not float floatValue) return false; animator.SetFloat(parameter.nameHash, floatValue); return true; case AnimatorControllerParameterType.Int: if (value is not int intValue) return false; animator.SetInteger(parameter.nameHash, intValue); return true; case AnimatorControllerParameterType.Bool: if (value is not bool boolValue) return false; animator.SetBool(parameter.nameHash, boolValue); return true; case AnimatorControllerParameterType.Trigger: if (value is not bool triggerValue) return false; if (triggerValue) animator.SetTrigger(parameter.nameHash); else animator.ResetTrigger(parameter.nameHash); return true; default: throw CreateUnsupportedArgumentException(parameter.type); } } /// /// Sets the `value` of the `parameter` in the `playable` /// and returns true as long as the `value` is the appropriate type. /// public static bool TrySetParameterValue( AnimatorControllerPlayable playable, AnimatorControllerParameter parameter, object value) { switch (parameter.type) { case AnimatorControllerParameterType.Float: if (value is not float floatValue) return false; playable.SetFloat(parameter.nameHash, floatValue); return true; case AnimatorControllerParameterType.Int: if (value is not int intValue) return false; playable.SetInteger(parameter.nameHash, intValue); return true; case AnimatorControllerParameterType.Bool: if (value is not bool boolValue) return false; playable.SetBool(parameter.nameHash, boolValue); return true; case AnimatorControllerParameterType.Trigger: if (value is not bool triggerValue) return false; if (triggerValue) playable.SetTrigger(parameter.nameHash); else playable.ResetTrigger(parameter.nameHash); return true; 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 Transitions /************************************************************************************************************************/ /// Is the `transition` not null and ? public static bool IsValid(this ITransition transition) => transition != null && transition.IsValid; /************************************************************************************************************************/ /// /// Returns the /// or if it's null or throws an exception. /// public static float TryGetFadeDuration(this ITransition transition) { if (transition == null) return float.NaN; try { return transition.FadeDuration; } catch { return float.NaN; } } /************************************************************************************************************************/ /// /// Returns the /// or if it's null or throws an exception. /// public static float TryGetNormalizedStartTime(this ITransition transition) { if (transition == null) return float.NaN; try { return transition.NormalizedStartTime; } catch { return float.NaN; } } /************************************************************************************************************************/ /// Outputs the or . /// Returns false if the `motionOrTransition` is null or an unsupported type. public static bool TryGetIsLooping(object motionOrTransition, out bool isLooping) { if (motionOrTransition is Motion motion) { if (motion != null) { isLooping = motion.isLooping; return true; } } else if (motionOrTransition is ITransition transition) { isLooping = transition.IsLooping; return true; } isLooping = false; return false; } /************************************************************************************************************************/ /// /// Outputs the /// or . /// /// Returns false if the `clipOrTransition` is null or an unsupported type. public static bool TryGetLength(object clipOrTransition, out float length) { if (clipOrTransition is AnimationClip clip) { if (clip != null) { length = clip.length; return true; } } else if (clipOrTransition is ITransition transition) { length = transition.MaximumLength; return true; } length = 0; return false; } /************************************************************************************************************************/ /// /// Tries to calculate the amount of time (in seconds) /// from the /// too the , /// including the /// public static bool TryCalculateDuration(ITransition transition, out float duration) { var speed = transition.Speed; duration = transition.MaximumLength; var normalizedStartTime = transition.NormalizedStartTime; if (!float.IsNaN(normalizedStartTime)) duration *= 1 - normalizedStartTime; var normalizedEndTime = transition.Events.NormalizedEndTime; if (!float.IsNaN(normalizedEndTime)) duration *= normalizedEndTime; if (speed.IsFinite() && speed != 0) duration /= speed; return true; } /************************************************************************************************************************/ #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 /************************************************************************************************************************/ } }