// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2026 Kybernetik // #if UNITY_EDITOR using System; using System.Collections; using System.Collections.Generic; using System.Reflection; using UnityEngine; namespace Animancer.Editor { /// [Editor-Only] /// A system that procedurally gathers animations throughout the hierarchy without needing explicit references. /// /// https://kybernetik.com.au/animancer/api/Animancer.Editor/AnimationGatherer /// public class AnimationGatherer : IAnimationClipCollection { /************************************************************************************************************************/ #region Fields and Accessors /************************************************************************************************************************/ /// All the s that have been gathered. public readonly HashSet Clips = new(); /// All the s that have been gathered. public readonly HashSet Transitions = new(); /************************************************************************************************************************/ /// public void GatherAnimationClips(ICollection clips) { try { foreach (var clip in Clips) clips.Add(clip); foreach (var transition in Transitions) clips.GatherFromSource(transition); } catch (Exception exception) { HandleException(exception); } } /************************************************************************************************************************/ #endregion /************************************************************************************************************************/ #region Cache /************************************************************************************************************************/ private static readonly Dictionary ObjectToGatherer = new(); /************************************************************************************************************************/ static AnimationGatherer() { UnityEditor.Selection.selectionChanged += ClearCache; } /************************************************************************************************************************/ /// Clears all cached gatherers. public static void ClearCache() => ObjectToGatherer.Clear(); /************************************************************************************************************************/ #endregion /************************************************************************************************************************/ /// /// Should exceptions thrown while gathering animations be logged? /// Default is false to ignore them. /// public static bool LogExceptions { get; set; } /// /// Logs the `exception` if is true. /// Otherwise does nothing. /// private static void HandleException(Exception exception) { if (LogExceptions) Debug.LogException(exception); } /************************************************************************************************************************/ /// /// Returns a cached containing any s /// referenced by components in the same hierarchy as the `gameObject`. /// See for details. /// public static AnimationGatherer GatherFromGameObject(GameObject gameObject) { using var _ = AnimationGathererRecursionGuard.Begin(); if (AnimationGathererRecursionGuard.HasCheckedObject(gameObject)) return null; try { if (!ObjectToGatherer.TryGetValue(gameObject, out var gatherer)) { gatherer = new(); ObjectToGatherer.Add(gameObject, gatherer); gatherer.GatherFromComponents(gameObject); } return gatherer; } catch (Exception exception) { HandleException(exception); return null; } } /// /// Fills the `clips` with any s /// referenced by components in the same hierarchy as the `gameObject`. /// See for details. /// public static void GatherFromGameObject(GameObject gameObject, ICollection clips) { var gatherer = GatherFromGameObject(gameObject); gatherer?.GatherAnimationClips(clips); } /// /// Fills the `clips` with any s /// referenced by components in the same hierarchy as the `gameObject`. /// See for details. /// public static void GatherFromGameObject(GameObject gameObject, ref AnimationClip[] clips, bool sort) { var gatherer = GatherFromGameObject(gameObject); if (gatherer == null) return; using (SetPool.Instance.Acquire(out var clipSet)) { gatherer.GatherAnimationClips(clipSet); AnimancerUtilities.SetLength(ref clips, clipSet.Count); clipSet.CopyTo(clips); } if (sort) Array.Sort(clips, (a, b) => a.GetCachedName().CompareTo(b.GetCachedName())); } /************************************************************************************************************************/ private void GatherFromComponents(GameObject gameObject) { var root = AnimancerUtilities.FindRoot(gameObject); using (ListPool.Instance.Acquire(out var components)) { root.GetComponentsInChildren(true, components); GatherFromComponents(components); } } /************************************************************************************************************************/ private void GatherFromComponents(List components) { var i = components.Count; GatherClips: try { while (--i >= 0) { GatherFromObject(components[i], 0); } } catch (Exception exception) { HandleException(exception); goto GatherClips; } } /************************************************************************************************************************/ /// Gathers all animations from the `source`s fields. private void GatherFromObject(object source, int depth) { if (source.IsNullOrDestroyed()) return; if (AnimationGathererRecursionGuard.HasCheckedObject(source)) return; if (source is AnimationClip clip) { Clips.Add(clip); return; } if (!MightContainAnimations(source.GetType())) return; try { if (Clips.GatherFromSource(source)) return; } catch (Exception exception) { HandleException(exception); } GatherFromFields(source, depth); } /************************************************************************************************************************/ /// Types mapped to a delegate that can quickly gather their clips. private static readonly Dictionary> TypeToGathererDelegate = new(); /// /// Uses reflection to gather s from fields on the `source` object. /// private void GatherFromFields(object source, int depth) { if (depth >= AnimationGathererRecursionGuard.MaxFieldDepth || source.IsNullOrDestroyed()) return; var type = source.GetType(); if (!TypeToGathererDelegate.TryGetValue(type, out var gatherClips)) { gatherClips = BuildClipGathererDelegate(type, depth); TypeToGathererDelegate.Add(type, gatherClips); if (gatherClips == null) AnimationGathererRecursionGuard.DontGatherFrom.Add(type); } gatherClips?.Invoke(source, this); } /************************************************************************************************************************/ /// /// Creates a delegate to gather s /// from all relevant fields in a given `type`. /// private static Action BuildClipGathererDelegate(Type type, int depth) { if (!MightContainAnimations(type)) return null; Action gathererDelegate = null; while (type != null) { var fields = type.GetFields(AnimancerReflection.InstanceBindings | BindingFlags.DeclaredOnly); for (int i = 0; i < fields.Length; i++) { var field = fields[i]; var fieldType = field.FieldType; if (!MightContainAnimations(fieldType)) continue; if (fieldType == typeof(AnimationClip)) { gathererDelegate += (obj, gatherer) => { var clip = (AnimationClip)field.GetValue(obj); if (clip != null) gatherer.Clips.Add(clip); }; } else if (typeof(IAnimationClipSource).IsAssignableFrom(fieldType) || typeof(IAnimationClipCollection).IsAssignableFrom(fieldType)) { gathererDelegate += (obj, gatherer) => { var source = field.GetValue(obj); gatherer.Clips.GatherFromSource(source); }; } else if (typeof(ICollection).IsAssignableFrom(fieldType)) { gathererDelegate += (obj, gatherer) => { var collection = (ICollection)field.GetValue(obj); if (collection != null) { foreach (var item in collection) { gatherer.GatherFromObject(item, depth + 1); } } }; } else { gathererDelegate += (obj, gatherer) => { var source = field.GetValue(obj); if (source.IsNullOrDestroyed()) return; gatherer.GatherFromObject(source, depth + 1); }; } } type = type.BaseType; } return gathererDelegate; } /************************************************************************************************************************/ private static bool MightContainAnimations(Type type) => !type.IsPrimitive && !type.IsEnum && !type.IsAutoClass && !type.IsPointer && !AnimationGathererRecursionGuard.DontGatherFrom.Contains(type); /************************************************************************************************************************/ } } #endif