AnimationGatherer.cs 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367
  1. // Animancer // https://kybernetik.com.au/animancer // Copyright 2022 Kybernetik //
  2. #if UNITY_EDITOR
  3. using System;
  4. using System.Collections;
  5. using System.Collections.Generic;
  6. using UnityEngine;
  7. using Object = UnityEngine.Object;
  8. namespace Animancer.Editor
  9. {
  10. /// <summary>[Editor-Only]
  11. /// A system that procedurally gathers animations throughout the hierarchy without needing explicit references.
  12. /// </summary>
  13. /// https://kybernetik.com.au/animancer/api/Animancer.Editor/AnimationGatherer
  14. ///
  15. public class AnimationGatherer : IAnimationClipCollection
  16. {
  17. /************************************************************************************************************************/
  18. #region Recursion Guard
  19. /************************************************************************************************************************/
  20. private const int MaxFieldDepth = 7;
  21. /************************************************************************************************************************/
  22. private static readonly HashSet<object>
  23. RecursionGuard = new HashSet<object>();
  24. private static int _CallCount;
  25. private static bool BeginRecursionGuard(object obj)
  26. {
  27. if (RecursionGuard.Contains(obj))
  28. return false;
  29. RecursionGuard.Add(obj);
  30. return true;
  31. }
  32. private static void EndCall()
  33. {
  34. if (_CallCount == 0)
  35. RecursionGuard.Clear();
  36. }
  37. /************************************************************************************************************************/
  38. #endregion
  39. /************************************************************************************************************************/
  40. #region Fields and Accessors
  41. /************************************************************************************************************************/
  42. /// <summary>All the <see cref="AnimationClip"/>s that have been gathered.</summary>
  43. public readonly HashSet<AnimationClip> Clips = new HashSet<AnimationClip>();
  44. /// <summary>All the <see cref="ITransition"/>s that have been gathered.</summary>
  45. public readonly HashSet<ITransition> Transitions = new HashSet<ITransition>();
  46. /************************************************************************************************************************/
  47. /// <inheritdoc/>
  48. public void GatherAnimationClips(ICollection<AnimationClip> clips)
  49. {
  50. try
  51. {
  52. foreach (var clip in Clips)
  53. clips.Add(clip);
  54. foreach (var transition in Transitions)
  55. clips.GatherFromSource(transition);
  56. }
  57. catch (Exception exception)
  58. {
  59. HandleException(exception);
  60. }
  61. }
  62. /************************************************************************************************************************/
  63. #endregion
  64. /************************************************************************************************************************/
  65. #region Cache
  66. /************************************************************************************************************************/
  67. private static readonly Dictionary<GameObject, AnimationGatherer>
  68. ObjectToGatherer = new Dictionary<GameObject, AnimationGatherer>();
  69. /************************************************************************************************************************/
  70. static AnimationGatherer()
  71. {
  72. UnityEditor.EditorApplication.hierarchyChanged += ClearCache;
  73. UnityEditor.Selection.selectionChanged += ClearCache;
  74. }
  75. /************************************************************************************************************************/
  76. /// <summary>Clears all cached gatherers.</summary>
  77. public static void ClearCache() => ObjectToGatherer.Clear();
  78. /************************************************************************************************************************/
  79. #endregion
  80. /************************************************************************************************************************/
  81. /// <summary>Should exceptions thrown while gathering animations be logged? Default is false to ignore them.</summary>
  82. public static bool logExceptions;
  83. /// <summary>Logs the `exception` if <see cref="logExceptions"/> is true. Otherwise does nothing.</summary>
  84. private static void HandleException(Exception exception)
  85. {
  86. if (logExceptions)
  87. Debug.LogException(exception);
  88. }
  89. /************************************************************************************************************************/
  90. /// <summary>
  91. /// Returns a cached <see cref="AnimationGatherer"/> containing any <see cref="AnimationClip"/>s referenced by
  92. /// components in the same hierarchy as the `gameObject`. See <see cref="ICharacterRoot"/> for details.
  93. /// </summary>
  94. public static AnimationGatherer GatherFromGameObject(GameObject gameObject)
  95. {
  96. if (!BeginRecursionGuard(gameObject))
  97. return null;
  98. try
  99. {
  100. _CallCount++;
  101. if (!ObjectToGatherer.TryGetValue(gameObject, out var gatherer))
  102. {
  103. gatherer = new AnimationGatherer();
  104. ObjectToGatherer.Add(gameObject, gatherer);
  105. gatherer.GatherFromComponents(gameObject);
  106. }
  107. return gatherer;
  108. }
  109. catch (Exception exception)
  110. {
  111. HandleException(exception);
  112. return null;
  113. }
  114. finally
  115. {
  116. _CallCount--;
  117. EndCall();
  118. }
  119. }
  120. /// <summary>
  121. /// Fills the `clips` with any <see cref="AnimationClip"/>s referenced by components in the same hierarchy as
  122. /// the `gameObject`. See <see cref="ICharacterRoot"/> for details.
  123. /// </summary>
  124. public static void GatherFromGameObject(GameObject gameObject, ICollection<AnimationClip> clips)
  125. {
  126. var gatherer = GatherFromGameObject(gameObject);
  127. gatherer?.GatherAnimationClips(clips);
  128. }
  129. /// <summary>
  130. /// Fills the `clips` with any <see cref="AnimationClip"/>s referenced by components in the same hierarchy as
  131. /// the `gameObject`. See <see cref="ICharacterRoot"/> for details.
  132. /// </summary>
  133. public static void GatherFromGameObject(GameObject gameObject, ref AnimationClip[] clips, bool sort)
  134. {
  135. var gatherer = GatherFromGameObject(gameObject);
  136. if (gatherer == null)
  137. return;
  138. using (ObjectPool.Disposable.AcquireSet<AnimationClip>(out var clipSet))
  139. {
  140. gatherer.GatherAnimationClips(clipSet);
  141. AnimancerUtilities.SetLength(ref clips, clipSet.Count);
  142. clipSet.CopyTo(clips);
  143. }
  144. if (sort)
  145. Array.Sort(clips, (a, b) => a.name.CompareTo(b.name));
  146. }
  147. /************************************************************************************************************************/
  148. private void GatherFromComponents(GameObject gameObject)
  149. {
  150. var root = AnimancerEditorUtilities.FindRoot(gameObject);
  151. using (ObjectPool.Disposable.AcquireList<MonoBehaviour>(out var components))
  152. {
  153. root.GetComponentsInChildren(true, components);
  154. GatherFromComponents(components);
  155. }
  156. }
  157. /************************************************************************************************************************/
  158. private void GatherFromComponents(List<MonoBehaviour> components)
  159. {
  160. var i = components.Count;
  161. GatherClips:
  162. try
  163. {
  164. while (--i >= 0)
  165. {
  166. GatherFromObject(components[i], 0);
  167. }
  168. }
  169. catch (Exception exception)
  170. {
  171. HandleException(exception);
  172. goto GatherClips;
  173. }
  174. }
  175. /************************************************************************************************************************/
  176. /// <summary>Gathers all animations from the `source`s fields.</summary>
  177. private void GatherFromObject(object source, int depth)
  178. {
  179. if (source == null)
  180. return;
  181. if (source is AnimationClip clip)
  182. {
  183. Clips.Add(clip);
  184. return;
  185. }
  186. if (!MightContainAnimations(source.GetType()))
  187. return;
  188. if (!BeginRecursionGuard(source))
  189. return;
  190. try
  191. {
  192. if (Clips.GatherFromSource(source))
  193. return;
  194. }
  195. catch (Exception exception)
  196. {
  197. HandleException(exception);
  198. }
  199. finally
  200. {
  201. RecursionGuard.Remove(source);
  202. }
  203. GatherFromFields(source, depth);
  204. }
  205. /************************************************************************************************************************/
  206. /// <summary>Types mapped to a delegate that can quickly gather their clips.</summary>
  207. private static readonly Dictionary<Type, Action<object, AnimationGatherer>>
  208. TypeToGathererDelegate = new Dictionary<Type, Action<object, AnimationGatherer>>();
  209. /// <summary>
  210. /// Uses reflection to gather <see cref="AnimationClip"/>s from fields on the `source` object.
  211. /// </summary>
  212. private void GatherFromFields(object source, int depth)
  213. {
  214. if (depth >= MaxFieldDepth ||
  215. source == null ||
  216. !BeginRecursionGuard(source))
  217. return;
  218. var type = source.GetType();
  219. if (!TypeToGathererDelegate.TryGetValue(type, out var gatherClips))
  220. {
  221. gatherClips = BuildClipGathererDelegate(type, depth);
  222. TypeToGathererDelegate.Add(type, gatherClips);
  223. }
  224. gatherClips?.Invoke(source, this);
  225. }
  226. /************************************************************************************************************************/
  227. /// <summary>
  228. /// Creates a delegate to gather <see cref="AnimationClip"/>s from all relevant fields in a given `type`.
  229. /// </summary>
  230. private static Action<object, AnimationGatherer> BuildClipGathererDelegate(Type type, int depth)
  231. {
  232. if (!MightContainAnimations(type))
  233. return null;
  234. Action<object, AnimationGatherer> gathererDelegate = null;
  235. while (type != null)
  236. {
  237. var fields = type.GetFields(AnimancerEditorUtilities.InstanceBindings);
  238. for (int i = 0; i < fields.Length; i++)
  239. {
  240. var field = fields[i];
  241. var fieldType = field.FieldType;
  242. if (!MightContainAnimations(fieldType))
  243. continue;
  244. if (fieldType == typeof(AnimationClip))
  245. {
  246. gathererDelegate += (obj, gatherer) =>
  247. {
  248. var clip = (AnimationClip)field.GetValue(obj);
  249. gatherer.Clips.Gather(clip);
  250. };
  251. }
  252. else if (typeof(IAnimationClipSource).IsAssignableFrom(fieldType) ||
  253. typeof(IAnimationClipCollection).IsAssignableFrom(fieldType))
  254. {
  255. gathererDelegate += (obj, gatherer) =>
  256. {
  257. var source = field.GetValue(obj);
  258. gatherer.Clips.GatherFromSource(source);
  259. };
  260. }
  261. else if (typeof(ICollection).IsAssignableFrom(fieldType))
  262. {
  263. gathererDelegate += (obj, gatherer) =>
  264. {
  265. var collection = (ICollection)field.GetValue(obj);
  266. if (collection != null)
  267. {
  268. foreach (var item in collection)
  269. {
  270. gatherer.GatherFromObject(item, depth + 1);
  271. }
  272. }
  273. };
  274. }
  275. else
  276. {
  277. gathererDelegate += (obj, gatherer) =>
  278. {
  279. var source = field.GetValue(obj);
  280. if (source == null ||
  281. (source is Object sourceObject && sourceObject == null))
  282. return;
  283. gatherer.GatherFromObject(source, depth + 1);
  284. };
  285. }
  286. }
  287. type = type.BaseType;
  288. }
  289. return gathererDelegate;
  290. }
  291. /************************************************************************************************************************/
  292. private static bool MightContainAnimations(Type type)
  293. {
  294. return
  295. !type.IsPrimitive &&
  296. !type.IsEnum &&
  297. !type.IsAutoClass &&
  298. !type.IsPointer;
  299. }
  300. /************************************************************************************************************************/
  301. }
  302. }
  303. #endif