// Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2023 Kybernetik //
#if UNITY_EDITOR
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using Object = UnityEngine.Object;
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 Recursion Guard
/************************************************************************************************************************/
private const int MaxFieldDepth = 7;
/************************************************************************************************************************/
private static readonly HashSet
RecursionGuard = new HashSet();
private static int _CallCount;
private static bool BeginRecursionGuard(object obj)
{
if (RecursionGuard.Contains(obj))
return false;
RecursionGuard.Add(obj);
return true;
}
private static void EndCall()
{
if (_CallCount == 0)
RecursionGuard.Clear();
}
/************************************************************************************************************************/
#endregion
/************************************************************************************************************************/
#region Fields and Accessors
/************************************************************************************************************************/
/// All the s that have been gathered.
public readonly HashSet Clips = new HashSet();
/// All the s that have been gathered.
public readonly HashSet Transitions = new HashSet();
/************************************************************************************************************************/
///
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 Dictionary();
/************************************************************************************************************************/
static AnimationGatherer()
{
UnityEditor.EditorApplication.hierarchyChanged += ClearCache;
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;
/// 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)
{
if (!BeginRecursionGuard(gameObject))
return null;
try
{
_CallCount++;
if (!ObjectToGatherer.TryGetValue(gameObject, out var gatherer))
{
gatherer = new AnimationGatherer();
ObjectToGatherer.Add(gameObject, gatherer);
gatherer.GatherFromComponents(gameObject);
}
return gatherer;
}
catch (Exception exception)
{
HandleException(exception);
return null;
}
finally
{
_CallCount--;
EndCall();
}
}
///
/// 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 (ObjectPool.Disposable.AcquireSet(out var clipSet))
{
gatherer.GatherAnimationClips(clipSet);
AnimancerUtilities.SetLength(ref clips, clipSet.Count);
clipSet.CopyTo(clips);
}
if (sort)
Array.Sort(clips, (a, b) => a.name.CompareTo(b.name));
}
/************************************************************************************************************************/
private void GatherFromComponents(GameObject gameObject)
{
var root = AnimancerEditorUtilities.FindRoot(gameObject);
using (ObjectPool.Disposable.AcquireList(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 == null)
return;
if (source is AnimationClip clip)
{
Clips.Add(clip);
return;
}
if (!MightContainAnimations(source.GetType()))
return;
if (!BeginRecursionGuard(source))
return;
try
{
if (Clips.GatherFromSource(source))
return;
}
catch (Exception exception)
{
HandleException(exception);
}
finally
{
RecursionGuard.Remove(source);
}
GatherFromFields(source, depth);
}
/************************************************************************************************************************/
/// Types mapped to a delegate that can quickly gather their clips.
private static readonly Dictionary>
TypeToGathererDelegate = new Dictionary>();
///
/// Uses reflection to gather s from fields on the `source` object.
///
private void GatherFromFields(object source, int depth)
{
if (depth >= MaxFieldDepth ||
source == null ||
!BeginRecursionGuard(source))
return;
var type = source.GetType();
if (!TypeToGathererDelegate.TryGetValue(type, out var gatherClips))
{
gatherClips = BuildClipGathererDelegate(type, depth);
TypeToGathererDelegate.Add(type, gatherClips);
}
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(AnimancerEditorUtilities.InstanceBindings);
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);
gatherer.Clips.Gather(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 == null ||
(source is Object sourceObject && sourceObject == null))
return;
gatherer.GatherFromObject(source, depth + 1);
};
}
}
type = type.BaseType;
}
return gathererDelegate;
}
/************************************************************************************************************************/
private static bool MightContainAnimations(Type type)
{
return
!type.IsPrimitive &&
!type.IsEnum &&
!type.IsAutoClass &&
!type.IsPointer;
}
/************************************************************************************************************************/
}
}
#endif