SpineAnimationStateMixerBehaviour.cs 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356
  1. /******************************************************************************
  2. * Spine Runtimes License Agreement
  3. * Last updated September 24, 2021. Replaces all prior versions.
  4. *
  5. * Copyright (c) 2013-2022, Esoteric Software LLC
  6. *
  7. * Integration of the Spine Runtimes into software or otherwise creating
  8. * derivative works of the Spine Runtimes is permitted under the terms and
  9. * conditions of Section 2 of the Spine Editor License Agreement:
  10. * http://esotericsoftware.com/spine-editor-license
  11. *
  12. * Otherwise, it is permitted to integrate the Spine Runtimes into software
  13. * or otherwise create derivative works of the Spine Runtimes (collectively,
  14. * "Products"), provided that each user of the Products must obtain their own
  15. * Spine Editor license and redistribution of the Products in any form must
  16. * include this license and copyright notice.
  17. *
  18. * THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY
  19. * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
  20. * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
  21. * DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY
  22. * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
  23. * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES,
  24. * BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND
  25. * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
  26. * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
  27. * THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
  28. *****************************************************************************/
  29. #define SPINE_EDITMODEPOSE
  30. using System;
  31. using UnityEngine;
  32. using UnityEngine.Playables;
  33. using UnityEngine.Timeline;
  34. namespace Spine.Unity.Playables {
  35. public class SpineAnimationStateMixerBehaviour : PlayableBehaviour {
  36. float[] lastInputWeights;
  37. bool lastAnyClipPlaying = false;
  38. public int trackIndex;
  39. public bool unscaledTime;
  40. ScriptPlayable<SpineAnimationStateBehaviour>[] startingClips
  41. = new ScriptPlayable<SpineAnimationStateBehaviour>[2];
  42. IAnimationStateComponent animationStateComponent;
  43. bool pauseWithDirector = true;
  44. bool endAtClipEnd = true;
  45. float endMixOutDuration = 0.1f;
  46. bool isPaused = false;
  47. TrackEntry pausedTrackEntry;
  48. float previousTimeScale = 1;
  49. TrackEntry timelineStartedTrackEntry;
  50. public override void OnBehaviourPause (Playable playable, FrameData info) {
  51. if (pauseWithDirector) {
  52. if (!isPaused)
  53. HandlePause(playable);
  54. isPaused = true;
  55. }
  56. }
  57. public override void OnGraphStop (Playable playable) {
  58. if (!isPaused && endAtClipEnd)
  59. HandleClipEnd();
  60. }
  61. public override void OnBehaviourPlay (Playable playable, FrameData info) {
  62. if (isPaused)
  63. HandleResume(playable);
  64. isPaused = false;
  65. }
  66. protected void HandlePause (Playable playable) {
  67. if (animationStateComponent.IsNullOrDestroyed()) return;
  68. TrackEntry current = animationStateComponent.AnimationState.GetCurrent(trackIndex);
  69. if (current != null && current == timelineStartedTrackEntry) {
  70. previousTimeScale = current.TimeScale;
  71. current.TimeScale = 0;
  72. pausedTrackEntry = current;
  73. }
  74. }
  75. protected void HandleResume (Playable playable) {
  76. if (animationStateComponent.IsNullOrDestroyed()) return;
  77. TrackEntry current = animationStateComponent.AnimationState.GetCurrent(trackIndex);
  78. if (current != null && current == pausedTrackEntry) {
  79. current.TimeScale = previousTimeScale;
  80. }
  81. }
  82. protected void HandleClipEnd () {
  83. if (animationStateComponent.IsNullOrDestroyed()) return;
  84. var state = animationStateComponent.AnimationState;
  85. if (endAtClipEnd &&
  86. timelineStartedTrackEntry != null &&
  87. timelineStartedTrackEntry == state.GetCurrent(trackIndex)) {
  88. if (endMixOutDuration >= 0)
  89. state.SetEmptyAnimation(trackIndex, endMixOutDuration);
  90. else // pause if endMixOutDuration < 0
  91. timelineStartedTrackEntry.TimeScale = 0;
  92. timelineStartedTrackEntry = null;
  93. }
  94. }
  95. // NOTE: This function is called at runtime and edit time. Keep that in mind when setting the values of properties.
  96. public override void ProcessFrame (Playable playable, FrameData info, object playerData) {
  97. var skeletonAnimation = playerData as SkeletonAnimation;
  98. var skeletonGraphic = playerData as SkeletonGraphic;
  99. animationStateComponent = playerData as IAnimationStateComponent;
  100. var skeletonComponent = playerData as ISkeletonComponent;
  101. if (animationStateComponent.IsNullOrDestroyed() || skeletonComponent == null) return;
  102. var skeleton = skeletonComponent.Skeleton;
  103. var state = animationStateComponent.AnimationState;
  104. if (!Application.isPlaying) {
  105. #if SPINE_EDITMODEPOSE
  106. PreviewEditModePose(playable, skeletonComponent, animationStateComponent,
  107. skeletonAnimation, skeletonGraphic);
  108. #endif
  109. return;
  110. }
  111. int inputCount = playable.GetInputCount();
  112. float rootSpeed = GetRootPlayableSpeed(playable);
  113. // Ensure correct buffer size.
  114. if (this.lastInputWeights == null || this.lastInputWeights.Length < inputCount) {
  115. this.lastInputWeights = new float[inputCount];
  116. for (int i = 0; i < inputCount; i++)
  117. this.lastInputWeights[i] = default(float);
  118. }
  119. var lastInputWeights = this.lastInputWeights;
  120. int numStartingClips = 0;
  121. bool anyClipPlaying = false;
  122. // Check all clips. If a clip that was weight 0 turned into weight 1, call SetAnimation.
  123. for (int i = 0; i < inputCount; i++) {
  124. float lastInputWeight = lastInputWeights[i];
  125. float inputWeight = playable.GetInputWeight(i);
  126. bool clipStarted = lastInputWeight == 0 && inputWeight > 0;
  127. if (inputWeight > 0)
  128. anyClipPlaying = true;
  129. lastInputWeights[i] = inputWeight;
  130. if (clipStarted && numStartingClips < 2) {
  131. ScriptPlayable<SpineAnimationStateBehaviour> clipPlayable = (ScriptPlayable<SpineAnimationStateBehaviour>)playable.GetInput(i);
  132. startingClips[numStartingClips++] = clipPlayable;
  133. }
  134. }
  135. // unfortunately order of clips can be wrong when two start at the same time, we have to sort clips
  136. if (numStartingClips == 2) {
  137. ScriptPlayable<SpineAnimationStateBehaviour> clipPlayable0 = startingClips[0];
  138. ScriptPlayable<SpineAnimationStateBehaviour> clipPlayable1 = startingClips[1];
  139. if (clipPlayable0.GetDuration() > clipPlayable1.GetDuration()) { // swap, clip 0 ends after clip 1
  140. startingClips[0] = clipPlayable1;
  141. startingClips[1] = clipPlayable0;
  142. }
  143. }
  144. for (int j = 0; j < numStartingClips; ++j) {
  145. ScriptPlayable<SpineAnimationStateBehaviour> clipPlayable = startingClips[j];
  146. SpineAnimationStateBehaviour clipData = clipPlayable.GetBehaviour();
  147. pauseWithDirector = !clipData.dontPauseWithDirector;
  148. endAtClipEnd = !clipData.dontEndWithClip;
  149. endMixOutDuration = clipData.endMixOutDuration;
  150. if (clipData.animationReference == null) {
  151. float mixDuration = clipData.customDuration ? GetCustomMixDuration(clipData) : state.Data.DefaultMix;
  152. state.SetEmptyAnimation(trackIndex, mixDuration);
  153. } else {
  154. if (clipData.animationReference.Animation != null) {
  155. animationStateComponent.UnscaledTime = this.unscaledTime;
  156. TrackEntry currentEntry = state.GetCurrent(trackIndex);
  157. Spine.TrackEntry trackEntry;
  158. float customMixDuration = clipData.customDuration ? GetCustomMixDuration(clipData) : 0.0f;
  159. if (currentEntry == null && customMixDuration > 0) {
  160. state.SetEmptyAnimation(trackIndex, 0); // ease in requires empty animation
  161. trackEntry = state.AddAnimation(trackIndex, clipData.animationReference.Animation, clipData.loop, 0);
  162. } else
  163. trackEntry = state.SetAnimation(trackIndex, clipData.animationReference.Animation, clipData.loop);
  164. float clipSpeed = (float)clipPlayable.GetSpeed();
  165. trackEntry.EventThreshold = clipData.eventThreshold;
  166. trackEntry.DrawOrderThreshold = clipData.drawOrderThreshold;
  167. trackEntry.TrackTime = (float)clipPlayable.GetTime() * clipSpeed * rootSpeed;
  168. trackEntry.TimeScale = clipSpeed * rootSpeed;
  169. trackEntry.AttachmentThreshold = clipData.attachmentThreshold;
  170. trackEntry.HoldPrevious = clipData.holdPrevious;
  171. trackEntry.Alpha = clipData.alpha;
  172. if (clipData.customDuration)
  173. trackEntry.MixDuration = customMixDuration / rootSpeed;
  174. timelineStartedTrackEntry = trackEntry;
  175. }
  176. //else Debug.LogWarningFormat("Animation named '{0}' not found", clipData.animationName);
  177. }
  178. // Ensure that the first frame ends with an updated mesh.
  179. if (skeletonAnimation) {
  180. skeletonAnimation.Update(0);
  181. skeletonAnimation.LateUpdate();
  182. } else if (skeletonGraphic) {
  183. skeletonGraphic.Update(0);
  184. skeletonGraphic.LateUpdate();
  185. }
  186. }
  187. startingClips[0] = startingClips[1] = ScriptPlayable<SpineAnimationStateBehaviour>.Null;
  188. if (lastAnyClipPlaying && !anyClipPlaying)
  189. HandleClipEnd();
  190. this.lastAnyClipPlaying = anyClipPlaying;
  191. }
  192. #if SPINE_EDITMODEPOSE
  193. AnimationState dummyAnimationState;
  194. public void PreviewEditModePose (Playable playable,
  195. ISkeletonComponent skeletonComponent, IAnimationStateComponent animationStateComponent,
  196. SkeletonAnimation skeletonAnimation, SkeletonGraphic skeletonGraphic) {
  197. if (Application.isPlaying) return;
  198. if (animationStateComponent.IsNullOrDestroyed() || skeletonComponent == null) return;
  199. int inputCount = playable.GetInputCount();
  200. float rootSpeed = GetRootPlayableSpeed(playable);
  201. int lastNonZeroWeightTrack = -1;
  202. for (int i = 0; i < inputCount; i++) {
  203. float inputWeight = playable.GetInputWeight(i);
  204. if (inputWeight > 0) lastNonZeroWeightTrack = i;
  205. }
  206. if (lastNonZeroWeightTrack != -1) {
  207. ScriptPlayable<SpineAnimationStateBehaviour> inputPlayableClip =
  208. (ScriptPlayable<SpineAnimationStateBehaviour>)playable.GetInput(lastNonZeroWeightTrack);
  209. SpineAnimationStateBehaviour clipData = inputPlayableClip.GetBehaviour();
  210. var skeleton = skeletonComponent.Skeleton;
  211. bool skeletonDataMismatch = clipData.animationReference != null && clipData.animationReference.SkeletonDataAsset &&
  212. skeletonComponent.SkeletonDataAsset.GetSkeletonData(true) != clipData.animationReference.SkeletonDataAsset.GetSkeletonData(true);
  213. if (skeletonDataMismatch) {
  214. Debug.LogWarningFormat("SpineAnimationStateMixerBehaviour tried to apply an animation for the wrong skeleton. Expected {0}. Was {1}",
  215. skeletonComponent.SkeletonDataAsset, clipData.animationReference.SkeletonDataAsset);
  216. }
  217. // Getting the from-animation here because it's required to get the mix information from AnimationStateData.
  218. Animation fromAnimation = null;
  219. float fromClipTime = 0;
  220. bool fromClipLoop = false;
  221. if (lastNonZeroWeightTrack != 0 && inputCount > 1) {
  222. var fromClip = (ScriptPlayable<SpineAnimationStateBehaviour>)playable.GetInput(lastNonZeroWeightTrack - 1);
  223. var fromClipData = fromClip.GetBehaviour();
  224. fromAnimation = fromClipData.animationReference != null ? fromClipData.animationReference.Animation : null;
  225. fromClipTime = (float)fromClip.GetTime() * (float)fromClip.GetSpeed() * rootSpeed;
  226. fromClipLoop = fromClipData.loop;
  227. }
  228. Animation toAnimation = clipData.animationReference != null ? clipData.animationReference.Animation : null;
  229. float toClipTime = (float)inputPlayableClip.GetTime() * (float)inputPlayableClip.GetSpeed() * rootSpeed;
  230. float mixDuration = clipData.mixDuration;
  231. if (!clipData.customDuration && fromAnimation != null && toAnimation != null) {
  232. mixDuration = animationStateComponent.AnimationState.Data.GetMix(fromAnimation, toAnimation);
  233. }
  234. if (trackIndex == 0)
  235. skeleton.SetToSetupPose();
  236. // Approximate what AnimationState might do at runtime.
  237. if (fromAnimation != null && mixDuration > 0 && toClipTime < mixDuration) {
  238. dummyAnimationState = dummyAnimationState ?? new AnimationState(skeletonComponent.SkeletonDataAsset.GetAnimationStateData());
  239. var toEntry = dummyAnimationState.GetCurrent(0);
  240. var fromEntry = toEntry != null ? toEntry.MixingFrom : null;
  241. bool isAnimationTransitionMatch = (toEntry != null && toEntry.Animation == toAnimation && fromEntry != null && fromEntry.Animation == fromAnimation);
  242. if (!isAnimationTransitionMatch) {
  243. dummyAnimationState.ClearTracks();
  244. fromEntry = dummyAnimationState.SetAnimation(0, fromAnimation, fromClipLoop);
  245. fromEntry.AllowImmediateQueue();
  246. if (toAnimation != null) {
  247. toEntry = dummyAnimationState.SetAnimation(0, toAnimation, clipData.loop);
  248. toEntry.HoldPrevious = clipData.holdPrevious;
  249. toEntry.Alpha = clipData.alpha;
  250. }
  251. }
  252. // Update track times.
  253. fromEntry.TrackTime = fromClipTime;
  254. if (toEntry != null) {
  255. toEntry.TrackTime = toClipTime;
  256. toEntry.MixTime = toClipTime;
  257. }
  258. // Apply Pose
  259. dummyAnimationState.Update(0);
  260. dummyAnimationState.Apply(skeleton);
  261. } else {
  262. if (toAnimation != null)
  263. toAnimation.Apply(skeleton, 0, toClipTime, clipData.loop, null, clipData.alpha, MixBlend.Setup, MixDirection.In);
  264. }
  265. if (skeletonAnimation) {
  266. skeletonAnimation.Update(0);
  267. skeletonAnimation.LateUpdate();
  268. } else if (skeletonGraphic) {
  269. skeletonGraphic.Update(0);
  270. skeletonGraphic.LateUpdate();
  271. }
  272. }
  273. // Do nothing outside of the first clip and the last clip.
  274. }
  275. #endif
  276. float GetRootPlayableSpeed (Playable playable) {
  277. PlayableGraph graph = playable.GetGraph();
  278. int rootPlayableCount = graph.GetRootPlayableCount();
  279. if (rootPlayableCount == 1)
  280. return (float)graph.GetRootPlayable(0).GetSpeed();
  281. else {
  282. for (int rootIndex = 0; rootIndex < rootPlayableCount; ++rootIndex) {
  283. var rootPlayable = graph.GetRootPlayable(rootIndex);
  284. for (int i = 0, n = rootPlayable.GetInputCount(); i < n; ++i) {
  285. var playableChild = rootPlayable.GetInput(i);
  286. if (playableChild.Equals(playable)) {
  287. return (float)rootPlayable.GetSpeed();
  288. }
  289. }
  290. }
  291. }
  292. return 1.0f;
  293. }
  294. float GetCustomMixDuration (SpineAnimationStateBehaviour clipData) {
  295. if (clipData.useBlendDuration) {
  296. TimelineClip clip = clipData.timelineClip;
  297. return (float)Math.Max(clip.blendInDuration, clip.easeInDuration);
  298. } else {
  299. return clipData.mixDuration;
  300. }
  301. }
  302. }
  303. }