AnimancerEvent.cs 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355
  1. // Animancer // https://kybernetik.com.au/animancer // Copyright 2018-2024 Kybernetik //
  2. using System;
  3. using System.Runtime.CompilerServices;
  4. using System.Text;
  5. using UnityEngine;
  6. namespace Animancer
  7. {
  8. /// <summary>
  9. /// A <see cref="callback"/> delegate paired with a <see cref="normalizedTime"/> to determine when to invoke it.
  10. /// </summary>
  11. /// <remarks>
  12. /// <strong>Documentation:</strong>
  13. /// <see href="https://kybernetik.com.au/animancer/docs/manual/events/animancer">
  14. /// Animancer Events</see>
  15. /// </remarks>
  16. /// https://kybernetik.com.au/animancer/api/Animancer/AnimancerEvent
  17. ///
  18. public partial struct AnimancerEvent : IEquatable<AnimancerEvent>
  19. {
  20. /************************************************************************************************************************/
  21. #region Event
  22. /************************************************************************************************************************/
  23. /// <summary>The <see cref="AnimancerState.NormalizedTime"/> at which to invoke the <see cref="callback"/>.</summary>
  24. public float normalizedTime;
  25. /// <summary>The delegate to invoke when the <see cref="normalizedTime"/> passes.</summary>
  26. public Action callback;
  27. /************************************************************************************************************************/
  28. /// <summary>The largest possible float value less than 1.</summary>
  29. /// <remarks>
  30. /// This value is useful for placing events at the end of a looping animation since they do not allow the
  31. /// <see cref="normalizedTime"/> to be greater than or equal to 1.
  32. /// </remarks>
  33. public const float
  34. AlmostOne = 0.99999994f;
  35. /************************************************************************************************************************/
  36. /// <summary>The event name used for <see cref="Sequence.EndEvent"/>s.</summary>
  37. /// <remarks>
  38. /// This is a <see cref="StringReference.Unique"/> so that even if the same name happens
  39. /// to be used elsewhere, it would be treated as a different name.
  40. /// The reason for this is explained in <see cref="NamedEventDictionary.AssertNotEndEvent"/>.
  41. /// </remarks>
  42. public static readonly StringReference
  43. EndEventName = StringReference.Unique("EndEvent");
  44. /************************************************************************************************************************/
  45. /// <summary>Does nothing.</summary>
  46. /// <remarks>This delegate can be used for events which would otherwise have a <c>null</c> <see cref="callback"/>.</remarks>
  47. public static readonly Action
  48. DummyCallback = Dummy;
  49. /// <summary>Does nothing.</summary>
  50. /// <remarks>Used by <see cref="DummyCallback"/>.</remarks>
  51. private static void Dummy() { }
  52. /// <summary>Is the `callback` <c>null</c> or the <see cref="DummyCallback"/>?</summary>
  53. public static bool IsNullOrDummy(Action callback)
  54. => callback == null
  55. || callback == DummyCallback;
  56. /************************************************************************************************************************/
  57. /// <summary>Creates a new <see cref="AnimancerEvent"/>.</summary>
  58. public AnimancerEvent(float normalizedTime, Action callback)
  59. {
  60. this.normalizedTime = normalizedTime;
  61. this.callback = callback;
  62. }
  63. /************************************************************************************************************************/
  64. /// <summary>Returns a string describing the details of this event.</summary>
  65. public readonly override string ToString()
  66. {
  67. var text = StringBuilderPool.Instance.Acquire();
  68. text.Append($"{nameof(AnimancerEvent)}(");
  69. AppendDetails(text);
  70. text.Append(')');
  71. return text.ReleaseToString();
  72. }
  73. /************************************************************************************************************************/
  74. /// <summary>Appends the details of this event to the `text`.</summary>
  75. public readonly void AppendDetails(StringBuilder text)
  76. {
  77. text.Append("NormalizedTime: ")
  78. .Append(normalizedTime)
  79. .Append(", Callback: ")
  80. .AppendDelegate(callback);
  81. }
  82. /************************************************************************************************************************/
  83. #endregion
  84. /************************************************************************************************************************/
  85. #region Invocation
  86. /************************************************************************************************************************/
  87. /// <summary>The details of the event currently being triggered.</summary>
  88. /// <remarks>Cleared after the event is invoked.</remarks>
  89. // Having the underlying field here can cause type initialization errors due to circular dependencies.
  90. public static Invocation Current
  91. {
  92. [MethodImpl(MethodImplOptions.AggressiveInlining)]
  93. get => Invocation.Current;
  94. }
  95. /************************************************************************************************************************/
  96. /// <summary>
  97. /// A cached delegate which calls <see cref="Invocation.InvokeBoundCallback"/>
  98. /// on the <see cref="Current"/>.
  99. /// </summary>
  100. public static readonly Action
  101. InvokeBoundCallback = InvokeCurrentBoundCallback;
  102. /// <summary>
  103. /// Calls <see cref="Invocation.InvokeBoundCallback"/> on the <see cref="Current"/>.
  104. /// </summary>
  105. private static void InvokeCurrentBoundCallback()
  106. => Current.InvokeBoundCallback();
  107. /************************************************************************************************************************/
  108. /// <summary>The custom parameter of the event currently being triggered.</summary>
  109. /// <remarks>Cleared after the event is finished.</remarks>
  110. public static object CurrentParameter { get; private set; }
  111. /// <summary>Calls <see cref="ConvertableUtilities.ConvertOrThrow"/> on the <see cref="CurrentParameter"/>.</summary>
  112. public static T GetCurrentParameter<T>()
  113. => ConvertableUtilities.ConvertOrThrow<T>(CurrentParameter);
  114. /// <summary>Returns a new delegate which invokes the `callback` using <see cref="GetCurrentParameter{T}"/>.</summary>
  115. /// <remarks>
  116. /// If <typeparamref name="T"/> is <see cref="string"/>,
  117. /// consider using <see cref="Parametize(Action{string})"/> instead of this.
  118. /// </remarks>
  119. /// <exception cref="ArgumentNullException">The `callback` is <c>null</c>.</exception>
  120. public static Action Parametize<T>(Action<T> callback)
  121. {
  122. #if UNITY_ASSERTIONS
  123. if (callback == null)
  124. throw new ArgumentNullException(
  125. nameof(callback),
  126. $"Can't {nameof(Parametize)} a null callback.");
  127. #endif
  128. return () => callback(GetCurrentParameter<T>());
  129. }
  130. /// <summary>Returns a new delegate which invokes the `callback` using the <see cref="CurrentParameter"/>.</summary>
  131. /// <exception cref="ArgumentNullException">The `callback` is <c>null</c>.</exception>
  132. public static Action Parametize(Action<string> callback)
  133. {
  134. #if UNITY_ASSERTIONS
  135. if (callback == null)
  136. throw new ArgumentNullException(
  137. nameof(callback),
  138. $"Can't {nameof(Parametize)} a null callback.");
  139. #endif
  140. return () => callback(CurrentParameter?.ToString());
  141. }
  142. /************************************************************************************************************************/
  143. /// <summary>[Assert-Only]
  144. /// Logs an error if the `callback` doesn't contain a <see cref="Parameter{T}.Invoke"/>
  145. /// so that adding to it with <see cref="Parametize{T}(Action{T})"/> can use that parameter.
  146. /// </summary>
  147. [System.Diagnostics.Conditional(Strings.Assertions)]
  148. public static void AssertContainsParameter<T>(Action callback)
  149. {
  150. if (!ContainsParameterInvoke<T>(callback))
  151. Debug.LogWarning(
  152. $"Adding parametized callback will do nothing because the existing callback" +
  153. $" doesn't contain a {typeof(T).GetNameCS()} parameter." +
  154. $"\n• Existing Callback: {callback.ToStringDetailed()}");
  155. }
  156. /// <summary>Does the `callback` contain a <see cref="Parameter{T}.Invoke"/>?</summary>
  157. private static bool ContainsParameterInvoke<T>(Action callback)
  158. {
  159. if (callback == null)
  160. return false;
  161. if (IsParameterInvoke<T>(callback))
  162. return true;
  163. var invocations = AnimancerReflection.GetInvocationList(callback);
  164. if (invocations.Length == 1 && ReferenceEquals(invocations[0], callback))
  165. return false;
  166. for (int i = 0; i < invocations.Length; i++)
  167. {
  168. var invocation = invocations[i];
  169. if (IsParameterInvoke<T>(invocation))
  170. return true;
  171. }
  172. return false;
  173. }
  174. /// <summary>Is the `callback` a call to <see cref="Parameter{T}.Invoke"/>?</summary>
  175. private static bool IsParameterInvoke<T>(Delegate callback)
  176. => callback.Target is IParameter parameter
  177. && callback.Method.Name == nameof(IInvokable.Invoke)
  178. && typeof(T).IsAssignableFrom(parameter.Value.GetType());
  179. /************************************************************************************************************************/
  180. /// <summary>
  181. /// Adds this event to the <see cref="Invoker"/>
  182. /// which will call <see cref="Invocation.Invoke"/> later in the current frame.
  183. /// </summary>
  184. [MethodImpl(MethodImplOptions.AggressiveInlining)]
  185. public readonly void DelayInvoke(
  186. StringReference eventName,
  187. AnimancerState state)
  188. => Invoker.Add(new(this, eventName, state));
  189. /************************************************************************************************************************/
  190. /// <summary>[Assert-Conditional]
  191. /// This method should be called when an animation is played.
  192. /// It asserts that either no event is currently being triggered
  193. /// or that the event is being triggered inside `playing`.
  194. /// Otherwise, it logs <see cref="OptionalWarning.EventPlayMismatch"/>.
  195. /// </summary>
  196. [System.Diagnostics.Conditional(Strings.Assertions)]
  197. public static void AssertEventPlayMismatch(AnimancerGraph playing)
  198. {
  199. #if UNITY_ASSERTIONS
  200. if (Current.State == null ||
  201. Current.State.Graph == playing ||
  202. OptionalWarning.EventPlayMismatch.IsDisabled())
  203. return;
  204. OptionalWarning.EventPlayMismatch.Log(
  205. $"An Animancer Event triggered by '{Current.State}' on '{Current.State.Graph}'" +
  206. $" was used to play an animation on a different character ('{playing}')." +
  207. $"\n\nThis most commonly happens when a Transition is shared by multiple characters" +
  208. $" and they all register their own callbacks to its events which leads to" +
  209. $" those events being triggered by the wrong character." +
  210. $" See the Shared Events page for more information: " +
  211. Strings.DocsURLs.SharedEventSequences +
  212. $"\n\n{Current}",
  213. playing.Component);
  214. #endif
  215. }
  216. /************************************************************************************************************************/
  217. /// <summary>
  218. /// Returns either the <see cref="AnimancerGraph.DefaultFadeDuration"/>
  219. /// or the <see cref="AnimancerState.RemainingDuration"/>
  220. /// of the <see cref="Current"/> state (whichever is higher).
  221. /// </summary>
  222. public static float GetFadeOutDuration()
  223. => GetFadeOutDuration(Current.State, AnimancerGraph.DefaultFadeDuration);
  224. /// <summary>
  225. /// Returns either the `minDuration` or the <see cref="AnimancerState.RemainingDuration"/>
  226. /// of the <see cref="Current"/> state (whichever is higher).
  227. /// </summary>
  228. public static float GetFadeOutDuration(float minDuration)
  229. => GetFadeOutDuration(Current.State, minDuration);
  230. /// <summary>
  231. /// Returns either the `minDuration` or the <see cref="AnimancerState.RemainingDuration"/>
  232. /// of the `state` (whichever is higher).
  233. /// </summary>
  234. public static float GetFadeOutDuration(AnimancerState state, float minDuration)
  235. {
  236. if (state == null)
  237. return minDuration;
  238. var time = state.Time;
  239. var speed = state.EffectiveSpeed;
  240. if (speed == 0)
  241. return minDuration;
  242. float remainingDuration;
  243. if (state.IsLooping)
  244. {
  245. var previousTime = time - speed * Time.deltaTime;
  246. var inverseLength = 1f / state.Length;
  247. // If we just passed the end of the animation, the remaining duration would technically be the full
  248. // duration of the animation, so we most likely want to use the minimum duration instead.
  249. if (Math.Floor(time * inverseLength) != Math.Floor(previousTime * inverseLength))
  250. return minDuration;
  251. }
  252. if (speed > 0)
  253. {
  254. remainingDuration = (state.Length - time) / speed;
  255. }
  256. else
  257. {
  258. remainingDuration = time / -speed;
  259. }
  260. return Math.Max(minDuration, remainingDuration);
  261. }
  262. /************************************************************************************************************************/
  263. #endregion
  264. /************************************************************************************************************************/
  265. #region Operators
  266. /************************************************************************************************************************/
  267. /// <summary>Are the <see cref="normalizedTime"/> and <see cref="callback"/> equal?</summary>
  268. public static bool operator ==(AnimancerEvent a, AnimancerEvent b)
  269. => a.Equals(b);
  270. /// <summary>Are the <see cref="normalizedTime"/> and <see cref="callback"/> not equal?</summary>
  271. public static bool operator !=(AnimancerEvent a, AnimancerEvent b)
  272. => !a.Equals(b);
  273. /************************************************************************************************************************/
  274. /// <summary>[<see cref="IEquatable{AnimancerEvent}"/>]
  275. /// Are the <see cref="normalizedTime"/> and <see cref="callback"/> of this event equal to `other`?
  276. /// </summary>
  277. public readonly bool Equals(AnimancerEvent other)
  278. => callback == other.callback
  279. && normalizedTime.IsEqualOrBothNaN(other.normalizedTime);
  280. /// <inheritdoc/>
  281. public readonly override bool Equals(object obj)
  282. => obj is AnimancerEvent animancerEvent
  283. && Equals(animancerEvent);
  284. /// <inheritdoc/>
  285. public readonly override int GetHashCode()
  286. => AnimancerUtilities.Hash(-78069441,
  287. normalizedTime.GetHashCode(),
  288. callback.SafeGetHashCode());
  289. /************************************************************************************************************************/
  290. #endregion
  291. /************************************************************************************************************************/
  292. }
  293. }