AnimationBindings.cs 44 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071
  1. // Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2024 Kybernetik //
  2. #if UNITY_EDITOR && UNITY_IMGUI
  3. using System;
  4. using System.Collections.Generic;
  5. using System.Text;
  6. using UnityEditor;
  7. using UnityEngine;
  8. namespace Animancer.Editor
  9. {
  10. /// <summary>[Editor-Only] The general type of object an <see cref="AnimationClip"/> can animate.</summary>
  11. /// https://kybernetik.com.au/animancer/api/Animancer.Editor/AnimationType
  12. public enum AnimationType
  13. {
  14. /// <summary>Unable to determine a type.</summary>
  15. None,
  16. /// <summary>A Humanoid rig.</summary>
  17. Humanoid,
  18. /// <summary>A Generic rig.</summary>
  19. Generic,
  20. /// <summary>A <see cref="Generic"/> rig which only animates a <see cref="SpriteRenderer.sprite"/>.</summary>
  21. Sprite,
  22. }
  23. /// <summary>[Editor-Only]
  24. /// Various utility functions relating to the properties animated by an <see cref="AnimationClip"/>.
  25. /// </summary>
  26. /// https://kybernetik.com.au/animancer/api/Animancer.Editor/AnimationBindings
  27. public class AnimationBindings : AssetPostprocessor
  28. {
  29. /************************************************************************************************************************/
  30. #region Animation Types
  31. /************************************************************************************************************************/
  32. private static Dictionary<AnimationClip, bool> _ClipToIsSprite;
  33. /// <summary>Determines the <see cref="AnimationType"/> of the specified `clip`.</summary>
  34. public static AnimationType GetAnimationType(AnimationClip clip)
  35. {
  36. if (clip == null)
  37. return AnimationType.None;
  38. if (clip.isHumanMotion)
  39. return AnimationType.Humanoid;
  40. AnimancerUtilities.InitializeCleanDictionary(ref _ClipToIsSprite);
  41. if (!_ClipToIsSprite.TryGetValue(clip, out var isSprite))
  42. {
  43. var bindings = AnimationUtility.GetObjectReferenceCurveBindings(clip);
  44. for (int i = 0; i < bindings.Length; i++)
  45. {
  46. var binding = bindings[i];
  47. if (binding.type == typeof(SpriteRenderer) &&
  48. binding.propertyName == "m_Sprite")
  49. {
  50. isSprite = true;
  51. break;
  52. }
  53. }
  54. _ClipToIsSprite.Add(clip, isSprite);
  55. }
  56. return isSprite
  57. ? AnimationType.Sprite
  58. : AnimationType.Generic;
  59. }
  60. /************************************************************************************************************************/
  61. /// <summary>Determines the <see cref="AnimationType"/> of the specified `animator`.</summary>
  62. public static AnimationType GetAnimationType(Animator animator)
  63. {
  64. if (animator == null)
  65. return AnimationType.None;
  66. if (animator.isHuman)
  67. return AnimationType.Humanoid;
  68. // If all renderers are SpriteRenderers, it's a Sprite animation.
  69. // Otherwise it's Generic.
  70. var renderers = animator.GetComponentsInChildren<Renderer>();
  71. if (renderers.Length == 0)
  72. return AnimationType.Generic;
  73. for (int i = 0; i < renderers.Length; i++)
  74. if (renderers[i] is not SpriteRenderer)
  75. return AnimationType.Generic;
  76. return AnimationType.Sprite;
  77. }
  78. /************************************************************************************************************************/
  79. /// <summary>Determines the <see cref="AnimationType"/> of the specified `gameObject`.</summary>
  80. public static AnimationType GetAnimationType(GameObject gameObject)
  81. {
  82. var type = AnimationType.None;
  83. var animators = gameObject.GetComponentsInChildren<Animator>();
  84. for (int i = 0; i < animators.Length; i++)
  85. {
  86. var animatorType = GetAnimationType(animators[i]);
  87. switch (animatorType)
  88. {
  89. case AnimationType.Humanoid: return AnimationType.Humanoid;
  90. case AnimationType.Generic: return AnimationType.Generic;
  91. case AnimationType.Sprite:
  92. if (type == AnimationType.None)
  93. type = AnimationType.Sprite;
  94. break;
  95. case AnimationType.None:
  96. default:
  97. break;
  98. }
  99. }
  100. return type;
  101. }
  102. /************************************************************************************************************************/
  103. #endregion
  104. /************************************************************************************************************************/
  105. private static bool _CanGatherBindings = true;
  106. /// <summary>No more than one set of bindings should be gathered per frame.</summary>
  107. private static bool CanGatherBindings()
  108. {
  109. if (!_CanGatherBindings)
  110. return false;
  111. _CanGatherBindings = false;
  112. EditorApplication.delayCall += () => _CanGatherBindings = true;
  113. return true;
  114. }
  115. /************************************************************************************************************************/
  116. private static Dictionary<GameObject, BindingData> _ObjectToBindings;
  117. /// <summary>Returns a cached <see cref="BindingData"/> representing the specified `gameObject`.</summary>
  118. /// <remarks>Note that the cache is cleared by <see cref="EditorApplication.hierarchyChanged"/>.</remarks>
  119. public static BindingData GetBindings(GameObject gameObject, bool forceGather = true)
  120. {
  121. AnimancerUtilities.InitializeCleanDictionary(ref _ObjectToBindings);
  122. if (!_ObjectToBindings.TryGetValue(gameObject, out var bindings))
  123. {
  124. if (!forceGather && !CanGatherBindings())
  125. return null;
  126. bindings = new(gameObject);
  127. _ObjectToBindings.Add(gameObject, bindings);
  128. }
  129. return bindings;
  130. }
  131. /************************************************************************************************************************/
  132. private static Dictionary<AnimationClip, EditorCurveBinding[]> _ClipToBindings;
  133. /// <summary>Returns a cached array of all properties animated by the specified `clip`.</summary>
  134. public static EditorCurveBinding[] GetBindings(AnimationClip clip)
  135. {
  136. AnimancerUtilities.InitializeCleanDictionary(ref _ClipToBindings);
  137. if (!_ClipToBindings.TryGetValue(clip, out var bindings))
  138. {
  139. var curveBindings = AnimationUtility.GetCurveBindings(clip);
  140. var objectBindings = AnimationUtility.GetObjectReferenceCurveBindings(clip);
  141. bindings = new EditorCurveBinding[curveBindings.Length + objectBindings.Length];
  142. Array.Copy(curveBindings, bindings, curveBindings.Length);
  143. Array.Copy(objectBindings, 0, bindings, curveBindings.Length, objectBindings.Length);
  144. _ClipToBindings.Add(clip, bindings);
  145. }
  146. return bindings;
  147. }
  148. /************************************************************************************************************************/
  149. /// <summary>Called when Unity imports an animation.</summary>
  150. protected virtual void OnPostprocessAnimation(GameObject root, AnimationClip clip)
  151. => OnAnimationChanged(clip);
  152. /// <summary>Clears any cached values relating to the `clip` since they may no longer be correct.</summary>
  153. public static void OnAnimationChanged(AnimationClip clip)
  154. {
  155. if (_ObjectToBindings != null)
  156. foreach (var binding in _ObjectToBindings.Values)
  157. binding.OnAnimationChanged(clip);
  158. _ClipToBindings?.Remove(clip);
  159. }
  160. /************************************************************************************************************************/
  161. /// <summary>Clears all cached values in this class.</summary>
  162. public static void ClearCache()
  163. {
  164. _ObjectToBindings.Clear();
  165. _ClipToBindings.Clear();
  166. }
  167. /************************************************************************************************************************/
  168. /// <summary>
  169. /// A collection of data about the properties on a <see cref="UnityEngine.GameObject"/>
  170. /// and its children which can be animated and the relationships between those properties
  171. /// and the properties that individual <see cref="AnimationClip"/>s are trying to animate.
  172. /// </summary>
  173. public class BindingData
  174. {
  175. /************************************************************************************************************************/
  176. /// <summary>The target object that this data represents.</summary>
  177. public readonly GameObject GameObject;
  178. /// <summary>Creates a new <see cref="BindingData"/> representing the specified `gameObject`.</summary>
  179. public BindingData(GameObject gameObject)
  180. => GameObject = gameObject;
  181. /************************************************************************************************************************/
  182. private AnimationType? _ObjectType;
  183. /// <summary>The cached <see cref="AnimationType"/> of the <see cref="GameObject"/>.</summary>
  184. public AnimationType ObjectType
  185. {
  186. get
  187. {
  188. _ObjectType ??= GetAnimationType(GameObject);
  189. return _ObjectType.Value;
  190. }
  191. }
  192. /************************************************************************************************************************/
  193. private HashSet<EditorCurveBinding> _ObjectBindings;
  194. /// <summary>The cached properties of the <see cref="GameObject"/> and its children which can be animated.</summary>
  195. public HashSet<EditorCurveBinding> ObjectBindings
  196. {
  197. get
  198. {
  199. if (_ObjectBindings == null)
  200. {
  201. _ObjectBindings = new();
  202. var transforms = GameObject.GetComponentsInChildren<Transform>();
  203. for (int i = 0; i < transforms.Length; i++)
  204. {
  205. var bindings = AnimationUtility.GetAnimatableBindings(transforms[i].gameObject, GameObject);
  206. _ObjectBindings.UnionWith(bindings);
  207. }
  208. }
  209. return _ObjectBindings;
  210. }
  211. }
  212. /************************************************************************************************************************/
  213. private HashSet<string> _ObjectTransformBindings;
  214. /// <summary>
  215. /// The <see cref="EditorCurveBinding.path"/> of all <see cref="Transform"/> bindings in
  216. /// <see cref="ObjectBindings"/>.
  217. /// </summary>
  218. public HashSet<string> ObjectTransformBindings
  219. {
  220. get
  221. {
  222. if (_ObjectTransformBindings == null)
  223. {
  224. _ObjectTransformBindings = new();
  225. foreach (var binding in ObjectBindings)
  226. {
  227. if (binding.type == typeof(Transform))
  228. _ObjectTransformBindings.Add(binding.path);
  229. }
  230. }
  231. return _ObjectTransformBindings;
  232. }
  233. }
  234. /************************************************************************************************************************/
  235. /// <summary>
  236. /// Determines the <see cref="MatchType"/> representing the properties animated by the `state`
  237. /// in comparison to the properties that actually exist on the target <see cref="GameObject"/>
  238. /// and its children.
  239. /// <para></para>
  240. /// Also compiles a `message` explaining the differences if that parameter is not null.
  241. /// </summary>
  242. public MatchType GetMatchType(
  243. Animator animator,
  244. AnimancerState state,
  245. StringBuilder message,
  246. bool forceGather = true)
  247. {
  248. using (SetPool<AnimationClip>.Instance.Acquire(out var clips))
  249. {
  250. state.GatherAnimationClips(clips);
  251. var bindings = message != null
  252. ? new Dictionary<EditorCurveBinding, bool>()
  253. : null;
  254. var existingBindingCount = 0;
  255. var match = default(MatchType);
  256. if (animator.avatar == null)
  257. {
  258. message?.AppendLine()
  259. .Append($"{LinePrefix}The {nameof(Animator)} has no {nameof(Avatar)}.");
  260. if (animator.isHuman)
  261. match = MatchType.Error;
  262. }
  263. foreach (var clip in clips)
  264. {
  265. var clipMatch = GetMatchType(clip, message, bindings, ref existingBindingCount, forceGather);
  266. if (match < clipMatch)
  267. match = clipMatch;
  268. }
  269. AppendBindings(message, bindings, existingBindingCount);
  270. return match;
  271. }
  272. }
  273. /************************************************************************************************************************/
  274. private const string LinePrefix = "- ";
  275. private Dictionary<AnimationClip, MatchType> _BindingMatches;
  276. /// <summary>
  277. /// Determines the <see cref="MatchType"/> representing the properties animated by the `clip`
  278. /// in comparison to the properties that actually exist on the target <see cref="GameObject"/>
  279. /// and its children.
  280. /// <para></para>
  281. /// Also compiles a `message` explaining the differences if that parameter is not null.
  282. /// </summary>
  283. public MatchType GetMatchType(
  284. AnimationClip clip,
  285. StringBuilder message,
  286. Dictionary<EditorCurveBinding, bool> bindingsInMessage,
  287. ref int existingBindingCount,
  288. bool forceGather = true)
  289. {
  290. AnimancerUtilities.InitializeCleanDictionary(ref _BindingMatches);
  291. if (_BindingMatches.TryGetValue(clip, out var match))
  292. {
  293. if (bindingsInMessage == null)
  294. return match;
  295. }
  296. else if (!forceGather && !CanGatherBindings())
  297. {
  298. return MatchType.Unknown;
  299. }
  300. var objectType = ObjectType;
  301. var clipType = GetAnimationType(clip);
  302. if (clipType != objectType)
  303. {
  304. if (message != null)
  305. {
  306. message.AppendLine()
  307. .Append($"{LinePrefix}This message does not necessarily mean anything is wrong," +
  308. $" but if something is wrong then this might help you identify the problem.");
  309. message.AppendLine()
  310. .Append($"{LinePrefix}The {nameof(AnimationType)} of the '")
  311. .Append(clip.name)
  312. .Append("' animation is ")
  313. .Append(clipType)
  314. .Append(" while the '")
  315. .Append(GameObject.name)
  316. .Append("' Rig is ")
  317. .Append(objectType)
  318. .Append(". See the documentation for more information about Animation Types:" +
  319. $" {Strings.DocsURLs.Inspector}#animation-types");
  320. }
  321. switch (clipType)
  322. {
  323. default:
  324. case AnimationType.None:
  325. case AnimationType.Humanoid:
  326. match = MatchType.Error;
  327. if (message == null)
  328. goto SetMatch;
  329. else
  330. break;
  331. case AnimationType.Generic:
  332. case AnimationType.Sprite:
  333. match = MatchType.Warning;
  334. break;
  335. }
  336. }
  337. var bindingMatch = GetMatchType(
  338. clip,
  339. message,
  340. bindingsInMessage,
  341. ref existingBindingCount);
  342. if (match < bindingMatch)
  343. match = bindingMatch;
  344. SetMatch:
  345. _BindingMatches[clip] = match;
  346. return match;
  347. }
  348. /************************************************************************************************************************/
  349. private MatchType GetMatchType(
  350. AnimationClip clip,
  351. StringBuilder message,
  352. Dictionary<EditorCurveBinding, bool> bindingsInMessage,
  353. ref int existingBindingCount)
  354. {
  355. var bindings = GetBindings(clip);
  356. if (bindings.Length == 0)
  357. return MatchType.Empty;
  358. var bindingCount = bindings.Length;
  359. var hasMissingReferences = false;
  360. var matchCount = 0;
  361. for (int i = 0; i < bindings.Length; i++)
  362. {
  363. var binding = bindings[i];
  364. if (ShouldIgnoreBinding(binding))
  365. {
  366. bindingCount--;
  367. continue;
  368. }
  369. var matches = MatchesObjectBinding(binding);
  370. if (matches)
  371. matchCount++;
  372. if (bindingsInMessage != null && !bindingsInMessage.ContainsKey(binding))
  373. {
  374. bindingsInMessage.Add(binding, matches);
  375. if (matches)
  376. existingBindingCount++;
  377. }
  378. if (HasMissingReferences(clip, message, binding))
  379. hasMissingReferences = true;
  380. }
  381. if (matchCount == bindingCount && !hasMissingReferences)
  382. return MatchType.Correct;
  383. else if (matchCount != 0)
  384. return MatchType.Warning;
  385. else
  386. return MatchType.Error;
  387. }
  388. /************************************************************************************************************************/
  389. private static bool ShouldIgnoreBinding(EditorCurveBinding binding)
  390. {
  391. if (binding.type == typeof(Animator) && string.IsNullOrEmpty(binding.path))
  392. {
  393. switch (binding.propertyName)
  394. {
  395. case "MotionQ.w":
  396. case "MotionQ.x":
  397. case "MotionQ.y":
  398. case "MotionQ.z":
  399. case "MotionT.x":
  400. case "MotionT.y":
  401. case "MotionT.z":
  402. case "RootQ.w":
  403. case "RootQ.x":
  404. case "RootQ.y":
  405. case "RootQ.z":
  406. case "RootT.x":
  407. case "RootT.y":
  408. case "RootT.z":
  409. return true;
  410. }
  411. }
  412. return false;
  413. }
  414. /************************************************************************************************************************/
  415. private bool MatchesObjectBinding(EditorCurveBinding binding)
  416. {
  417. if (binding.type == typeof(Transform))
  418. {
  419. switch (binding.propertyName)
  420. {
  421. case "m_LocalEulerAngles.x":
  422. case "m_LocalEulerAngles.y":
  423. case "m_LocalEulerAngles.z":
  424. case "localEulerAnglesRaw.x":
  425. case "localEulerAnglesRaw.y":
  426. case "localEulerAnglesRaw.z":
  427. return ObjectTransformBindings.Contains(binding.path);
  428. }
  429. }
  430. return ObjectBindings.Contains(binding);
  431. }
  432. /************************************************************************************************************************/
  433. private bool HasMissingReferences(
  434. AnimationClip clip,
  435. StringBuilder message,
  436. EditorCurveBinding binding)
  437. {
  438. var references = AnimationUtility.GetObjectReferenceCurve(clip, binding);
  439. if (references == null)
  440. return false;
  441. for (int i = 0; i < references.Length; i++)
  442. {
  443. if (references[i].value == null)
  444. {
  445. if (message != null)
  446. {
  447. var path = binding.path;
  448. message.AppendLine()
  449. .Append($"{LinePrefix}Null reference found in keyframe ")
  450. .Append(i)
  451. .Append(" of ")
  452. .Append(binding.type.Name)
  453. .Append(" binding: ");
  454. if (!string.IsNullOrEmpty(path))
  455. message.Append(path)
  456. .Append('.');
  457. message.Append(binding.propertyName);
  458. }
  459. return true;
  460. }
  461. }
  462. return false;
  463. }
  464. /************************************************************************************************************************/
  465. private static void AppendBindings(
  466. StringBuilder message,
  467. Dictionary<EditorCurveBinding, bool> bindings,
  468. int existingBindingCount)
  469. {
  470. if (bindings == null ||
  471. bindings.Count <= existingBindingCount)
  472. return;
  473. message.AppendLine()
  474. .Append(LinePrefix + "This message has been copied to the clipboard" +
  475. " (in case it is too long for Unity to display in the Console).");
  476. message.AppendLine()
  477. .Append(LinePrefix)
  478. .Append(bindings.Count - existingBindingCount)
  479. .Append(" of ")
  480. .Append(bindings.Count)
  481. .Append(" bindings do not exist in the Rig: [x] = Missing, [o] = Exists");
  482. using (ListPool<EditorCurveBinding>.Instance.Acquire(out var sortedBindings))
  483. {
  484. sortedBindings.AddRange(bindings.Keys);
  485. sortedBindings.Sort((a, b) =>
  486. {
  487. var result = a.path.CompareTo(b.path);
  488. if (result != 0)
  489. return result;
  490. if (a.type != b.type)
  491. {
  492. if (a.type == typeof(Transform))
  493. return -1;
  494. else if (b.type == typeof(Transform))
  495. return 1;
  496. result = a.type.Name.CompareTo(b.type.Name);
  497. if (result != 0)
  498. return result;
  499. }
  500. return a.propertyName.CompareTo(b.propertyName);
  501. });
  502. var previousBinding = default(EditorCurveBinding);
  503. var pathSplit = Array.Empty<string>();
  504. for (int iBinding = 0; iBinding < sortedBindings.Count; iBinding++)
  505. {
  506. var binding = sortedBindings[iBinding];
  507. if (binding.path != previousBinding.path)
  508. {
  509. var newPathSplit = binding.path.Split('/');
  510. var iSegment = Math.Min(newPathSplit.Length - 1, pathSplit.Length - 1);
  511. for (; iSegment >= 0; iSegment--)
  512. {
  513. if (pathSplit[iSegment] == newPathSplit[iSegment])
  514. break;
  515. }
  516. iSegment++;
  517. if (!string.IsNullOrEmpty(binding.path))
  518. {
  519. for (; iSegment < newPathSplit.Length; iSegment++)
  520. {
  521. message.AppendLine();
  522. for (int iIndent = 0; iIndent < iSegment; iIndent++)
  523. message.Append(Strings.Indent);
  524. message.Append("> ").Append(newPathSplit[iSegment]);
  525. }
  526. }
  527. pathSplit = newPathSplit;
  528. }
  529. if (TransformBindings.Append(bindings, sortedBindings, ref iBinding, message))
  530. continue;
  531. message.AppendLine();
  532. if (binding.path.Length > 0)
  533. for (int iIndent = 0; iIndent < pathSplit.Length; iIndent++)
  534. message.Append(Strings.Indent);
  535. message
  536. .Append(bindings[binding] ? "[o] " : "[x] ")
  537. .Append(binding.type.GetNameCS(false))
  538. .Append('.')
  539. .Append(binding.propertyName);
  540. previousBinding = binding;
  541. }
  542. }
  543. }
  544. /************************************************************************************************************************/
  545. private static class TransformBindings
  546. {
  547. [Flags]
  548. private enum Flags
  549. {
  550. None = 0,
  551. PositionX = 1 << 0,
  552. PositionY = 1 << 1,
  553. PositionZ = 1 << 2,
  554. RotationX = 1 << 3,
  555. RotationY = 1 << 4,
  556. RotationZ = 1 << 5,
  557. RotationW = 1 << 6,
  558. EulerX = 1 << 7,
  559. EulerY = 1 << 8,
  560. EulerZ = 1 << 9,
  561. ScaleX = 1 << 10,
  562. ScaleY = 1 << 11,
  563. ScaleZ = 1 << 12,
  564. }
  565. private static bool HasAll(Flags flag, Flags has) => (flag & has) == has;
  566. private static bool HasAny(Flags flag, Flags has) => (flag & has) != Flags.None;
  567. /************************************************************************************************************************/
  568. private static readonly Flags[]
  569. PositionFlags = { Flags.PositionX, Flags.PositionY, Flags.PositionZ },
  570. RotationFlags = { Flags.RotationX, Flags.RotationY, Flags.RotationZ, Flags.RotationW },
  571. EulerFlags = { Flags.EulerX, Flags.EulerY, Flags.EulerZ },
  572. ScaleFlags = { Flags.ScaleX, Flags.ScaleY, Flags.ScaleZ };
  573. /************************************************************************************************************************/
  574. public static bool Append(
  575. Dictionary<EditorCurveBinding, bool> bindings,
  576. List<EditorCurveBinding> sortedBindings,
  577. ref int index,
  578. StringBuilder message)
  579. {
  580. var binding = sortedBindings[index];
  581. if (binding.type != typeof(Transform))
  582. return false;
  583. if (string.IsNullOrEmpty(binding.path))
  584. message.AppendLine().Append('>');
  585. else
  586. message.Append(':');
  587. using (ListPool<EditorCurveBinding>.Instance.Acquire(out var otherBindings))
  588. {
  589. var flags = GetFlags(bindings, sortedBindings, ref index, otherBindings, out var anyExists);
  590. message.Append(anyExists ? " [o]" : " [x]");
  591. var first = true;
  592. AppendProperty(message, ref first, flags, PositionFlags, "position", "xyz");
  593. AppendProperty(message, ref first, flags, RotationFlags, "rotation", "wxyz");
  594. AppendProperty(message, ref first, flags, EulerFlags, "euler", "xyz");
  595. AppendProperty(message, ref first, flags, ScaleFlags, "scale", "xyz");
  596. for (int i = 0; i < otherBindings.Count; i++)
  597. {
  598. if (anyExists)
  599. message.Append(',');
  600. binding = otherBindings[i];
  601. message
  602. .Append(" [")
  603. .Append(bindings[binding] ? 'o' : 'x')
  604. .Append("] ")
  605. .Append(binding.propertyName);
  606. }
  607. }
  608. return true;
  609. }
  610. /************************************************************************************************************************/
  611. private static Flags GetFlags(
  612. Dictionary<EditorCurveBinding, bool> bindings,
  613. List<EditorCurveBinding> sortedBindings,
  614. ref int index,
  615. List<EditorCurveBinding> otherBindings,
  616. out bool anyExists)
  617. {
  618. var flags = Flags.None;
  619. anyExists = false;
  620. var binding = sortedBindings[index];
  621. CheckFlags:
  622. switch (binding.propertyName)
  623. {
  624. case "m_LocalPosition.x": flags |= Flags.PositionX; break;
  625. case "m_LocalPosition.y": flags |= Flags.PositionY; break;
  626. case "m_LocalPosition.z": flags |= Flags.PositionZ; break;
  627. case "m_LocalRotation.x": flags |= Flags.RotationX; break;
  628. case "m_LocalRotation.y": flags |= Flags.RotationY; break;
  629. case "m_LocalRotation.z": flags |= Flags.RotationZ; break;
  630. case "m_LocalRotation.w": flags |= Flags.RotationW; break;
  631. case "m_LocalEulerAngles.x": flags |= Flags.EulerX; break;
  632. case "m_LocalEulerAngles.y": flags |= Flags.EulerY; break;
  633. case "m_LocalEulerAngles.z": flags |= Flags.EulerZ; break;
  634. case "localEulerAnglesRaw.x": flags |= Flags.EulerX; break;
  635. case "localEulerAnglesRaw.y": flags |= Flags.EulerY; break;
  636. case "localEulerAnglesRaw.z": flags |= Flags.EulerZ; break;
  637. case "m_LocalScale.x": flags |= Flags.ScaleX; break;
  638. case "m_LocalScale.y": flags |= Flags.ScaleY; break;
  639. case "m_LocalScale.z": flags |= Flags.ScaleZ; break;
  640. default: otherBindings.Add(binding); goto SkipFlagExistence;
  641. }
  642. if (bindings != null &&
  643. bindings.TryGetValue(binding, out var exists))
  644. {
  645. bindings = null;
  646. anyExists = exists;
  647. }
  648. SkipFlagExistence:
  649. if (index + 1 < sortedBindings.Count)
  650. {
  651. var nextBinding = sortedBindings[index + 1];
  652. if (nextBinding.type == typeof(Transform) &&
  653. nextBinding.path == binding.path)
  654. {
  655. index++;
  656. binding = nextBinding;
  657. goto CheckFlags;
  658. }
  659. }
  660. return flags;
  661. }
  662. /************************************************************************************************************************/
  663. private static void AppendProperty(
  664. StringBuilder message,
  665. ref bool first,
  666. Flags flags,
  667. Flags[] propertyFlags,
  668. string propertyName,
  669. string flagNames)
  670. {
  671. var all = Flags.None;
  672. for (int i = 0; i < propertyFlags.Length; i++)
  673. all |= propertyFlags[i];
  674. if (!HasAny(flags, all))
  675. return;
  676. AppendSeparator(message, ref first, " ", ", ").Append(propertyName);
  677. if (!HasAll(flags, all))
  678. {
  679. var firstSub = true;
  680. for (int i = 0; i < propertyFlags.Length; i++)
  681. {
  682. if (HasAll(flags, propertyFlags[i]))
  683. {
  684. AppendSeparator(message, ref firstSub, "(", ", ").Append(flagNames[i]);
  685. }
  686. }
  687. message.Append(')');
  688. }
  689. }
  690. /************************************************************************************************************************/
  691. private static StringBuilder AppendSeparator(
  692. StringBuilder message,
  693. ref bool first,
  694. string prefix,
  695. string separator)
  696. {
  697. if (first)
  698. {
  699. first = false;
  700. return message.Append(prefix);
  701. }
  702. else return message.Append(separator);
  703. }
  704. /************************************************************************************************************************/
  705. }
  706. /************************************************************************************************************************/
  707. /// <summary>
  708. /// Logs a description of the issues found when comparing the properties animated by the `state` to the
  709. /// properties that actually exist on the target <see cref="GameObject"/> and its children.
  710. /// </summary>
  711. public void LogIssues(AnimancerState state, MatchType match)
  712. {
  713. var animator = state.Graph?.Component?.Animator;
  714. var newMatch = match;
  715. var message = StringBuilderPool.Instance.Acquire();
  716. switch (match)
  717. {
  718. default:
  719. case MatchType.Unknown:
  720. message.Append("The animation bindings are still being checked.");
  721. Debug.Log(EditorGUIUtility.systemCopyBuffer = message.ReleaseToString(), animator);
  722. break;
  723. case MatchType.Correct:
  724. message.Append("No issues were found when comparing the properties animated by '")
  725. .Append(state)
  726. .Append("' to the Rig of '")
  727. .Append(animator.name)
  728. .Append("'.");
  729. Debug.Log(EditorGUIUtility.systemCopyBuffer = message.ReleaseToString(), animator);
  730. break;
  731. case MatchType.Empty:
  732. message.Append("'")
  733. .Append(state)
  734. .Append("' does not animate any properties so it will not do anything.");
  735. Debug.Log(EditorGUIUtility.systemCopyBuffer = message.ReleaseToString(), animator);
  736. break;
  737. case MatchType.Warning:
  738. message.Append("Possible Bug Detected: some of the details of '")
  739. .Append(state)
  740. .Append("' do not match the Rig of '")
  741. .Append(animator.name)
  742. .Append("' so the animation might not work correctly.");
  743. newMatch = GetMatchType(animator, state, message);
  744. Debug.LogWarning(EditorGUIUtility.systemCopyBuffer = message.ReleaseToString(), animator);
  745. break;
  746. case MatchType.Error:
  747. message.Append("Possible Bug Detected: the details of '")
  748. .Append(state)
  749. .Append("' do not match the Rig of '")
  750. .Append(animator.name)
  751. .Append("' so the animation might not work correctly.");
  752. newMatch = GetMatchType(animator, state, message);
  753. Debug.LogError(EditorGUIUtility.systemCopyBuffer = message.ReleaseToString(), animator);
  754. break;
  755. }
  756. if (newMatch != match)
  757. Debug.LogWarning($"{nameof(MatchType)} changed from {match} to {newMatch}" +
  758. " between the initial check and the button press.");
  759. }
  760. /************************************************************************************************************************/
  761. /// <summary>[Internal] Removes any cached values relating to the `clip`.</summary>
  762. internal void OnAnimationChanged(AnimationClip clip)
  763. {
  764. _BindingMatches?.Remove(clip);
  765. }
  766. /************************************************************************************************************************/
  767. }
  768. /************************************************************************************************************************/
  769. #region GUI
  770. /************************************************************************************************************************/
  771. /// <summary>
  772. /// A summary of the compatability between the properties animated by an <see cref="AnimationClip"/>
  773. /// and the properties that actually exist on a particular <see cref="GameObject"/> (and its children).
  774. /// </summary>
  775. public enum MatchType
  776. {
  777. /// <summary>All properties exist.</summary>
  778. Correct,
  779. /// <summary>Not yet checked.</summary>
  780. Unknown,
  781. /// <summary>The <see cref="AnimationClip"/> does not animate anything.</summary>
  782. Empty,
  783. /// <summary>Some of the animated properties do not exist on the object.</summary>
  784. Warning,
  785. /// <summary>None of the animated properties exist on the object.</summary>
  786. Error,
  787. }
  788. /************************************************************************************************************************/
  789. /// <summary>
  790. /// Draws an icon indicating the <see cref="MatchType"/> of the `state`
  791. /// compared to the object it is being played on.
  792. /// <para></para>
  793. /// Clicking the icon calls <see cref="BindingData.LogIssues"/>.
  794. /// </summary>
  795. public static void DoBindingMatchGUI(ref Rect area, AnimancerState state)
  796. {
  797. if (AnimancerEditorUtilities.IsChangingPlayMode ||
  798. !AnimancerGraphDrawer.VerifyAnimationBindings ||
  799. state.Graph == null ||
  800. state.Graph.Component == null ||
  801. state.Graph.Component.Animator == null)
  802. return;
  803. var animator = state.Graph.Component.Animator;
  804. var bindings = GetBindings(animator.gameObject, false);
  805. if (bindings == null)
  806. return;
  807. var match = bindings.GetMatchType(animator, state, null, false);
  808. var icon = GetIcon(match);
  809. if (icon == null)
  810. return;
  811. var buttonArea = AnimancerGUI.StealFromRight(ref area, area.height + 1, AnimancerGUI.StandardSpacing);
  812. var iconArea = buttonArea.Expand(-1);
  813. iconArea.y++;
  814. GUI.DrawTexture(iconArea, icon);
  815. if (buttonArea.Contains(Event.current.mousePosition))
  816. EditorGUI.DrawRect(buttonArea, AnimancerGUI.Grey(1, 0.2f));
  817. if (AnimancerGUI.TryUseClickEvent(buttonArea, 0))
  818. bindings.LogIssues(state, match);
  819. }
  820. /************************************************************************************************************************/
  821. #endregion
  822. /************************************************************************************************************************/
  823. #region Icons
  824. /************************************************************************************************************************/
  825. /// <summary>Get an icon = corresponding to the specified <see cref="MatchType"/>.</summary>
  826. public static Texture GetIcon(MatchType match)
  827. {
  828. return match switch
  829. {
  830. MatchType.Unknown => null,
  831. MatchType.Empty => AnimancerIcons.Info,
  832. MatchType.Warning => AnimancerIcons.Warning,
  833. MatchType.Error => AnimancerIcons.Error,
  834. _ => null,
  835. };
  836. }
  837. /************************************************************************************************************************/
  838. /// <summary>A unit test to make sure that the icons are properly loaded.</summary>
  839. public static void AssertIcons()
  840. {
  841. var matchTypes = (MatchType[])Enum.GetValues(typeof(MatchType));
  842. for (int i = 0; i < matchTypes.Length; i++)
  843. {
  844. var match = matchTypes[i];
  845. var icon = GetIcon(match);
  846. switch (matchTypes[i])
  847. {
  848. case MatchType.Correct:
  849. case MatchType.Unknown:
  850. Debug.Assert(icon == null, $"The icon for {nameof(MatchType)}.{match} should be null.");
  851. break;
  852. case MatchType.Empty:
  853. case MatchType.Warning:
  854. case MatchType.Error:
  855. default:
  856. Debug.Assert(icon != null, $"The icon for {nameof(MatchType)}.{match} was not loaded.");
  857. break;
  858. }
  859. }
  860. }
  861. /************************************************************************************************************************/
  862. #endregion
  863. /************************************************************************************************************************/
  864. }
  865. }
  866. #endif