Validate.cs 8.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188
  1. // Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2024 Kybernetik //
  2. using System;
  3. using System.Collections.Generic;
  4. using UnityEngine;
  5. using UnityEngine.Playables;
  6. namespace Animancer
  7. {
  8. /// <summary>
  9. /// Enforces various rules throughout the system, most of which are compiled out if UNITY_ASSERTIONS is not defined
  10. /// (by default, it is only defined in the Unity Editor and in Development Builds).
  11. /// </summary>
  12. /// https://kybernetik.com.au/animancer/api/Animancer/Validate
  13. ///
  14. public static partial class Validate
  15. {
  16. /************************************************************************************************************************/
  17. /// <summary>[Assert-Conditional]
  18. /// Throws if the `clip` is <c>null</c>, not an asset, or marked as <see cref="AnimationClip.legacy"/>.
  19. /// </summary>
  20. /// <exception cref="NullReferenceException"/>
  21. /// <exception cref="ArgumentException"/>
  22. [System.Diagnostics.Conditional(Strings.Assertions)]
  23. public static void AssertAnimationClip(AnimationClip clip, bool throwIfNull, string operation)
  24. {
  25. #if UNITY_ASSERTIONS
  26. if (clip == null)
  27. {
  28. if (!throwIfNull)
  29. return;
  30. #pragma warning disable IDE0041 // Use 'is null' check (that would suggest changing to == which is wrong).
  31. var error = ReferenceEquals(clip, null)
  32. ? $"Unable to {operation} because the {nameof(AnimationClip)} is null."
  33. : $"Unable to {operation} because the {nameof(AnimationClip)} has been destroyed.";
  34. throw new NullReferenceException(error);
  35. #pragma warning restore IDE0041 // Use 'is null' check.
  36. }
  37. #if UNITY_EDITOR
  38. if (OptionalWarning.DynamicAnimation.IsEnabled() &&
  39. !UnityEditor.EditorUtility.IsPersistent(clip))
  40. OptionalWarning.DynamicAnimation.Log(
  41. $"Attempted to {operation} using an {nameof(AnimationClip)} '{clip.name}' which is not an asset." +
  42. " Unity doesn't suppport dynamically creating animations for Animancer in runtime builds." +
  43. " This warning should be disabled if you only intend to use the animation in the" +
  44. " Unity Editor and not create it in a runtime build.",
  45. clip);
  46. #endif
  47. if (clip.legacy)
  48. throw new ArgumentException(
  49. $"Unable to {operation} because the {nameof(AnimationClip)} '{clip.name}' is a lagacy animation" +
  50. " and therefore cannot be used by Animancer" +
  51. " If it was imported as part of a model then the model's Rig type must be Humanoid or Generic." +
  52. " Otherwise you can use the 'Toggle Legacy' function in the clip's context menu" +
  53. " (via the cog icon in the top right of its Inspector).");
  54. #endif
  55. }
  56. /************************************************************************************************************************/
  57. /// <summary>[Assert-Conditional] Throws if the <see cref="AnimancerNodeBase.Graph"/> is not the `graph`.</summary>
  58. /// <exception cref="ArgumentException"/>
  59. [System.Diagnostics.Conditional(Strings.Assertions)]
  60. public static void AssertGraph(AnimancerNode node, AnimancerGraph graph)
  61. {
  62. #if UNITY_ASSERTIONS
  63. if (node.Graph != graph)
  64. {
  65. AnimancerNodeBase.MarkAsUsed(node);
  66. throw new ArgumentException(
  67. $"{nameof(AnimancerNode)}.{nameof(AnimancerNode.Graph)} mismatch:" +
  68. $" cannot use a node in an {nameof(AnimancerGraph)} that is not its {nameof(AnimancerNode.Graph)}: " +
  69. node.GetDescription());
  70. }
  71. #endif
  72. }
  73. /************************************************************************************************************************/
  74. /// <summary>[Assert-Conditional] Throws if the `node`'s <see cref="Playable"/> is invalid.</summary>
  75. /// <exception cref="InvalidOperationException"/>
  76. [System.Diagnostics.Conditional(Strings.Assertions)]
  77. public static void AssertPlayable(AnimancerNode node)
  78. {
  79. #if UNITY_ASSERTIONS
  80. if (node._Playable.IsValid() &&
  81. node.Graph._PlayableGraph.IsValid())
  82. return;
  83. var description = node.ToString();
  84. var stackTrace = AnimancerNode.GetConstructorStackTrace(node);
  85. if (stackTrace != null)
  86. description += "\n\n" + stackTrace;
  87. AnimancerNodeBase.MarkAsUsed(node);
  88. if (node is AnimancerState state)
  89. state.Destroy();
  90. if (node.Graph == null)
  91. throw new InvalidOperationException(
  92. $"{nameof(AnimancerNode)}.{nameof(AnimancerNode.Graph)} hasn't been set so its" +
  93. $" {nameof(Playable)} hasn't been created. It can be set by playing the state" +
  94. $" or calling {nameof(AnimancerState.SetGraph)} on it directly." +
  95. $" {nameof(AnimancerState.SetParent)} would also work if the parent has a" +
  96. $" {nameof(AnimancerNode.Graph)}." +
  97. $"\n• Node: {description}");
  98. else if (!node.Graph._PlayableGraph.IsValid())
  99. throw new InvalidOperationException(
  100. $"{nameof(AnimancerGraph)}.{nameof(AnimancerGraph.PlayableGraph)} has already been destroyed." +
  101. $" This is often caused by a character attempting to access a state on a different character," +
  102. $" such as if they share a Transition and are both accessing its State without realising it" +
  103. $" only holds the most recently played state." +
  104. $"\n• Graph: {node.Graph}" +
  105. $"\n• Node: {description}");
  106. else
  107. throw new InvalidOperationException(
  108. $"{nameof(AnimancerNode)}.{nameof(AnimancerNodeBase.Playable)}" +
  109. $" has either been destroyed or was never created." +
  110. $"\n• Graph: {node.Graph}" +
  111. $"\n• Node: {description}");
  112. #endif
  113. }
  114. /************************************************************************************************************************/
  115. /// <summary>[Assert-Conditional]
  116. /// Throws if the `state` was not actually assigned to its specified <see cref="AnimancerNode.Index"/> in
  117. /// the `states`.
  118. /// </summary>
  119. /// <exception cref="InvalidOperationException"/>
  120. /// <exception cref="IndexOutOfRangeException">
  121. /// The <see cref="AnimancerNode.Index"/> is larger than the number of `states`.
  122. /// </exception>
  123. [System.Diagnostics.Conditional(Strings.Assertions)]
  124. public static void AssertCanRemoveChild(AnimancerState state, IList<AnimancerState> childStates, int childCount)
  125. {
  126. #if UNITY_ASSERTIONS
  127. var index = state.Index;
  128. if (index < 0)
  129. throw new InvalidOperationException(
  130. $"Cannot remove a child state that did not have an {nameof(state.Index)} assigned");
  131. if ((uint)index >= (uint)childCount)
  132. throw new IndexOutOfRangeException(
  133. $"{nameof(AnimancerState)}.{nameof(state.Index)} ({index})" +
  134. $" is outside the collection of states (Count {childCount})");
  135. if (childStates[index] != state)
  136. throw new InvalidOperationException(
  137. $"Cannot remove a child state that was not actually connected to its port on {state.Parent}:" +
  138. $"\n• Port: {index}" +
  139. $"\n• Connected Child: {AnimancerUtilities.ToStringOrNull(childStates[index])}" +
  140. $"\n• Disconnecting Child: {AnimancerUtilities.ToStringOrNull(state)}");
  141. #endif
  142. }
  143. /************************************************************************************************************************/
  144. /// <summary>[Assert-Conditional] Throws if the `weight` is negative, infinity, or NaN.</summary>
  145. /// <exception cref="ArgumentOutOfRangeException"/>
  146. [System.Diagnostics.Conditional(Strings.Assertions)]
  147. public static void AssertSetWeight(AnimancerNode node, float weight)
  148. {
  149. #if UNITY_ASSERTIONS
  150. if (!(weight >= 0) || weight == float.PositiveInfinity)// Reversed comparison includes NaN.
  151. {
  152. AnimancerNodeBase.MarkAsUsed(node);
  153. throw new ArgumentOutOfRangeException(
  154. nameof(weight),
  155. weight,
  156. $"{nameof(AnimancerNode.Weight)} must be a finite positive value");
  157. }
  158. #endif
  159. }
  160. /************************************************************************************************************************/
  161. }
  162. }