// 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