You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
290 lines
10 KiB
C#
290 lines
10 KiB
C#
3 months ago
|
// Animancer // Copyright 2020 Kybernetik //
|
||
|
|
||
|
#if UNITY_EDITOR
|
||
|
|
||
|
using System;
|
||
|
using System.Collections;
|
||
|
using System.Collections.Generic;
|
||
|
using System.Reflection;
|
||
|
using UnityEngine;
|
||
|
using Object = UnityEngine.Object;
|
||
|
|
||
|
namespace Animancer.Editor
|
||
|
{
|
||
|
/// <summary>[Editor-Only]
|
||
|
/// Allows animations to be procedurally gathered throughout the hierarchy without needing explicit references.
|
||
|
/// </summary>
|
||
|
/// <remarks>
|
||
|
/// This class is [Editor-Only] because it uses reflection and is not particularly efficient, but it does not
|
||
|
/// actually use any Editor Only functionality so it could be made usable at runtime by simply removing the
|
||
|
/// <c>#if UNITY_EDITOR</c> at the top of the file and <c>#endif</c> at the bottom.
|
||
|
/// </remarks>
|
||
|
public static class AnimationGatherer
|
||
|
{
|
||
|
/************************************************************************************************************************/
|
||
|
|
||
|
private const int MaxFieldDepth = 7;
|
||
|
|
||
|
/************************************************************************************************************************/
|
||
|
|
||
|
private static readonly HashSet<object>
|
||
|
RecursionGuard = new HashSet<object>();
|
||
|
|
||
|
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();
|
||
|
}
|
||
|
|
||
|
/************************************************************************************************************************/
|
||
|
|
||
|
/// <summary>
|
||
|
/// Fills the `clips` with any <see cref="AnimationClip"/>s referenced by components in the same hierarchy as
|
||
|
/// the `gameObject`. See <see cref="ICharacterRoot"/> for details.
|
||
|
/// </summary>
|
||
|
public static void GatherFromGameObject(GameObject gameObject, ICollection<AnimationClip> clips)
|
||
|
{
|
||
|
if (!BeginRecursionGuard(gameObject))
|
||
|
return;
|
||
|
|
||
|
try
|
||
|
{
|
||
|
_CallCount++;
|
||
|
|
||
|
var clipSet = clips as HashSet<AnimationClip>;
|
||
|
if (clipSet == null)
|
||
|
clipSet = ObjectPool.AcquireSet<AnimationClip>();
|
||
|
|
||
|
GatherFromComponents(gameObject, clipSet);
|
||
|
|
||
|
if (clipSet != clips)
|
||
|
{
|
||
|
clips.Gather(clipSet);
|
||
|
ObjectPool.Release(clipSet);
|
||
|
}
|
||
|
}
|
||
|
finally
|
||
|
{
|
||
|
_CallCount--;
|
||
|
EndCall();
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/// <summary>
|
||
|
/// Fills the `clips` with any <see cref="AnimationClip"/>s referenced by components in the same hierarchy as
|
||
|
/// the `gameObject`. See <see cref="ICharacterRoot"/> for details.
|
||
|
/// </summary>
|
||
|
public static void GatherFromGameObject(GameObject gameObject, ref AnimationClip[] clips, bool sort)
|
||
|
{
|
||
|
if (!BeginRecursionGuard(gameObject))
|
||
|
return;
|
||
|
|
||
|
try
|
||
|
{
|
||
|
_CallCount++;
|
||
|
|
||
|
var clipSet = ObjectPool.AcquireSet<AnimationClip>();
|
||
|
|
||
|
GatherFromComponents(gameObject, clipSet);
|
||
|
|
||
|
if (clips == null || clips.Length != clipSet.Count)
|
||
|
clips = new AnimationClip[clipSet.Count];
|
||
|
|
||
|
clipSet.CopyTo(clips);
|
||
|
ObjectPool.Release(clipSet);
|
||
|
|
||
|
if (sort)
|
||
|
Array.Sort(clips, (a, b) => a.name.CompareTo(b.name));
|
||
|
}
|
||
|
finally
|
||
|
{
|
||
|
_CallCount--;
|
||
|
EndCall();
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/************************************************************************************************************************/
|
||
|
|
||
|
private static void GatherFromComponents(GameObject gameObject, HashSet<AnimationClip> clips)
|
||
|
{
|
||
|
var root = AnimancerEditorUtilities.FindRoot(gameObject);
|
||
|
|
||
|
var components = ObjectPool.AcquireList<MonoBehaviour>();
|
||
|
root.GetComponentsInChildren(true, components);
|
||
|
GatherFromComponents(components, clips);
|
||
|
ObjectPool.Release(components);
|
||
|
}
|
||
|
|
||
|
/************************************************************************************************************************/
|
||
|
|
||
|
private static void GatherFromComponents(List<MonoBehaviour> components, HashSet<AnimationClip> clips)
|
||
|
{
|
||
|
var i = components.Count;
|
||
|
GatherClips:
|
||
|
try
|
||
|
{
|
||
|
while (--i >= 0)
|
||
|
{
|
||
|
GatherFromObject(components[i], clips, 0);
|
||
|
}
|
||
|
}
|
||
|
catch (Exception ex)
|
||
|
{
|
||
|
// If something throws an exception, log it and go to the next object.
|
||
|
Debug.LogException(ex);
|
||
|
goto GatherClips;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/************************************************************************************************************************/
|
||
|
|
||
|
/// <summary>
|
||
|
/// Gathers all animations from the `source`s fields.
|
||
|
/// </summary>
|
||
|
private static void GatherFromObject(object source, ICollection<AnimationClip> clips, int depth)
|
||
|
{
|
||
|
if (!BeginRecursionGuard(source))
|
||
|
return;
|
||
|
|
||
|
try
|
||
|
{
|
||
|
if (clips.GatherFromSource(source))
|
||
|
return;
|
||
|
}
|
||
|
finally
|
||
|
{
|
||
|
RecursionGuard.Remove(source);
|
||
|
}
|
||
|
|
||
|
GatherFromFields(source, clips, depth);
|
||
|
}
|
||
|
|
||
|
/************************************************************************************************************************/
|
||
|
|
||
|
/// <summary>Types mapped to a delegate that can quickly gather their clips.</summary>
|
||
|
private static readonly Dictionary<Type, Action<object, ICollection<AnimationClip>>>
|
||
|
TypeToGatherer = new Dictionary<Type, Action<object, ICollection<AnimationClip>>>();
|
||
|
|
||
|
/// <summary>
|
||
|
/// Uses reflection to gather <see cref="AnimationClip"/>s from fields on the `source` object.
|
||
|
/// </summary>
|
||
|
private static void GatherFromFields(object source, ICollection<AnimationClip> clips, int depth)
|
||
|
{
|
||
|
if (depth >= MaxFieldDepth ||
|
||
|
source == null ||
|
||
|
!BeginRecursionGuard(source))
|
||
|
return;
|
||
|
|
||
|
var type = source.GetType();
|
||
|
Action<object, ICollection<AnimationClip>> gatherClips;
|
||
|
|
||
|
if (!TypeToGatherer.TryGetValue(type, out gatherClips))
|
||
|
{
|
||
|
gatherClips = BuildClipGatherer(type, depth);
|
||
|
TypeToGatherer.Add(type, gatherClips);
|
||
|
}
|
||
|
|
||
|
if (gatherClips != null)
|
||
|
gatherClips(source, clips);
|
||
|
}
|
||
|
|
||
|
/************************************************************************************************************************/
|
||
|
|
||
|
/// <summary>
|
||
|
/// Creates a delegate to gather <see cref="AnimationClip"/>s from all relevant fields in a given `type`.
|
||
|
/// </summary>
|
||
|
private static Action<object, ICollection<AnimationClip>> BuildClipGatherer(Type type, int depth)
|
||
|
{
|
||
|
if (type.IsPrimitive ||
|
||
|
type.IsEnum ||
|
||
|
type.IsAutoClass ||
|
||
|
type.IsPointer)
|
||
|
return null;
|
||
|
|
||
|
Action<object, ICollection<AnimationClip>> gatherer = null;
|
||
|
|
||
|
while (type != null)
|
||
|
{
|
||
|
var fields = type.GetFields(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance);
|
||
|
for (int i = 0; i < fields.Length; i++)
|
||
|
{
|
||
|
var field = fields[i];
|
||
|
var fieldType = field.FieldType;
|
||
|
if (fieldType.IsPrimitive ||
|
||
|
fieldType.IsEnum ||
|
||
|
fieldType.IsAutoClass ||
|
||
|
fieldType.IsPointer)
|
||
|
continue;
|
||
|
|
||
|
if (fieldType == typeof(AnimationClip))
|
||
|
{
|
||
|
gatherer += (obj, clips) =>
|
||
|
{
|
||
|
var clip = (AnimationClip)field.GetValue(obj);
|
||
|
clips.Gather(clip);
|
||
|
};
|
||
|
}
|
||
|
else if (typeof(IAnimationClipSource).IsAssignableFrom(fieldType) ||
|
||
|
typeof(IAnimationClipCollection).IsAssignableFrom(fieldType))
|
||
|
{
|
||
|
gatherer += (obj, clips) =>
|
||
|
{
|
||
|
var source = field.GetValue(obj);
|
||
|
clips.GatherFromSource(source);
|
||
|
};
|
||
|
}
|
||
|
else if (typeof(ICollection).IsAssignableFrom(fieldType))
|
||
|
{
|
||
|
gatherer += (obj, clips) =>
|
||
|
{
|
||
|
var collection = (ICollection)field.GetValue(obj);
|
||
|
if (collection != null)
|
||
|
{
|
||
|
foreach (var item in collection)
|
||
|
{
|
||
|
GatherFromObject(item, clips, depth + 1);
|
||
|
}
|
||
|
}
|
||
|
};
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
gatherer += (obj, clips) =>
|
||
|
{
|
||
|
var source = field.GetValue(obj);
|
||
|
if (source == null)
|
||
|
return;
|
||
|
|
||
|
var sourceObject = source as Object;
|
||
|
if (!ReferenceEquals(sourceObject, null) && sourceObject == null)
|
||
|
return;
|
||
|
|
||
|
GatherFromObject(source, clips, depth + 1);
|
||
|
};
|
||
|
}
|
||
|
}
|
||
|
|
||
|
type = type.BaseType;
|
||
|
}
|
||
|
|
||
|
return gatherer;
|
||
|
}
|
||
|
|
||
|
/************************************************************************************************************************/
|
||
|
}
|
||
|
}
|
||
|
|
||
|
#endif
|
||
|
|