Spine事件 & AnimationState回调函数

注意: 本页面为事件触发的通俗易懂版解释. 对于事件精准的定义, 请见API参考文档中的AnimationStateListener 方法一节.

Spine.AnimationStateTrackEntry 对象以C#事件的形式为动画提供了回调的功能. 可以用他们对动画播放进行基本的管理. 在Spine.AnimationState上注册事件将触发所有轨道上的动画的事件, 而在一个TrackEntry上注册事件则只会触发单个排队动画所发出的事件.

新手程序员必读: 回调的意思是,你可以告诉系统,当特定情况发生时可以通过给它一个方法来调用, 从而"告知"你事件的发生. 事件在回调中表示有意义的断点——有事发生时, 你可以为事件系统提供一个函数/方法来订阅事件或用你自己的代码来处理这些断点.

当事件发生时,调用函数/方法的过程被称为触发。很多C#文档中叫做"raising". 但是Spine的文档会写作"firing". 它们其实是一个意思.

回调的结构和语法会根据语言而有不同改变。可以在文档下方查看C#语法的示例.


Fig 1. 不包括混合(mixing)/淡入淡出(crossfading)的事件触发流程图.

Spine.AnimationStateTrackEntry 将触发以下事件:

  • Start 当动画开始播放时触发,
    • 当调用SetAnimation时立刻触发.
    • 它也可以在一个队列中的动画开始播放时触发.
  • End 当动画从轨道中被移除(或中断)时触发,
    • 当当前动画快要播放完成时你调用SetAnimation中断了它, 该事件将被触发.
    • 当使用ClearTrack或者ClearTracks清除了轨道时, 该事件也会被触发.
    • 在混合(mix)/淡入淡出(crossfade)期间,在mix完成后End事件将被触发.
    • 当注册到AnimationState时, 永远不要End事件处理中调用SetAnimation, 它会引发无限递归. 请看下面的警告. 可以注册一个TrackEntry.End来替代.
    • 注意, 默认情况下, 非循环动画的TrackEntries不再在动画的持续时间内停止. 相反, 会继续无限期地保持最后一帧, 直到你移除它或用其他动画取代它. 如果你想让你的TrackEntry达到其动画持续时长(duration)时清空轨道, 请将TrackEntry.TrackEnd设置为动画持续时长.
  • Dispose 当AnimationState释放一个(在其生命周期结束时的)TrackEntry时, 会对TrackEntry触发.
    • 像spine-libgdx和spine-csharp这样的运行时会把TrackEntry对象缓存,以减小非必要的GC压力。这在Unity中尤为重要,因为Unity的GC实现有较为老旧低效.
    • 当TrackEntries被释放后,一定要记得移除对它们的所有引用,因为它们可能稍后就会被写入多余数据或触发意外事件.
    • Dispose事件会在End事件后立即触发.
  • Interrupt 当设置了新的动画且当前有一个动画还在播放时触发.
    • 当一个动画开始mixing/crossfading到另一个动画时触发.
  • Complete 当动画完成时触发,
    • 当一个非循环的动画播放完毕时触发,无论是否存在下一个在排队的动画.
    • 在循环动画每次循环结束后,也会触发.
  • Event 任何用户自定义事件被监听到时触发.
    • 这些事件点在Spine编辑器的动画中设置的。它们显示围为紫色的关键帧。在树状视图中也可以看到一个紫色的icon.
    • 为了区分不同的事件,你需要检查Spine.Event eName参数。(或者Data引用).
    • 当你想要按照动画节点去播放声音时它会非常有用,比如播放脚步声。它也可以根据Spine动画去同步或者通知非Spine系统,比如Unity的粒子系统或者产生单独的特效,甚至是诸如对齐发射子弹的时刻这样的游戏逻辑(如果你真的想这么做的话).
    • 每个TrackEntry都有一个EventThreshold属性. 它定义了在淡入淡出的哪些时间点上不触发用户事件. 更多信息请参见混合过程中的事件.

在一个动画播放完成后,另一个队列中的动画即将开始播放的时候,事件触发的顺序为: Complete, End, Start.

警告: 永远不要在订阅AnimationState.End的方法中调用SetAnimation(除非你的方法里实现了防止无限递归的逻辑)。因为当一个动画被中断就会触发End事件,而SetAnimation会中断当前的任何动画,通过AnimationState.End处理事件会引发无限递归(End -> Handle -> SetAnimation -> End -> Handle -> SetAnimation),导致Unity无响应然后堆栈溢出。一个简单的解决方案是: 注册到单个的TrackEntry.End上即可.

对比: AnimationState和TrackEntry事件

AnimationState和一个TrackEntry对象都会触发上文列出的Spine动画事件.

订阅AnimationState本身的事件, 会返回所有在其上播放的动画的回调.

相反,当订阅TrackEntry事件时,你将只订阅播放动画的那个具体实例。在这一TrackEntry结束后就被释放掉,不会产生任何新的事件.

TrackEntry事件会在对应的AnimationState事件之前触发.

混合过程中的事件

当你有设置了mix time(或者在你Skeleton Data Asset中设置了Default Mix),下一段动画开始播放且透明度逐渐增加,同时前一段动画依然在skeleton上生效, 这需要一小段时间.

我们把这段时间称为"淡入淡出"或"混合(mix)".

混合(Mix)过程中的用户事件

一个TrackEntry的EventThreshold控制在混合期间如何处理用户事件.

  • 默认值为0,当下一段动画开始播放时,立即停止触发用户事件.
  • 若置为0.5,就会在淡入淡出/混合过程到一半再停止触发用户事件.
  • 若置为1,将继续触发事件,直到淡入淡出/混合过程的最后一帧.

给动画设置合适的EventThreshold值是很重要的,因为你可能有交叠(overlapping)的相同动画,而它们不应该触发同一个事件,或者你希望即使动画已被中断但仍然触发其事件.

代码示例

该示例为一个订阅了AnimationState的事件的MonoBehaviour. 代码注释详细解释了其过程.

C#
// Sample written for for Spine 3.7
using UnityEngine;
using Spine;
using Spine.Unity;

// Add this to the same GameObject as your SkeletonAnimation
public class MySpineEventHandler : MonoBehaviour {

   // The [SpineEvent] attribute makes the inspector for this MonoBehaviour
   // draw the field as a dropdown list of existing event names in your SkeletonData.
   [SpineEvent] public string footstepEventName = "footstep";

   void Start () {
      var skeletonAnimation = GetComponent<SkeletonAnimation>();
      if (skeletonAnimation == null) return;

      // This is how you subscribe via a declared method.
      // The method needs the correct signature.
      skeletonAnimation.AnimationState.Event += HandleEvent;

      skeletonAnimation.AnimationState.Start += delegate (TrackEntry trackEntry) {
         // You can also use an anonymous delegate.
         Debug.Log(string.Format("track {0} started a new animation.", trackEntry.TrackIndex));
      };

      skeletonAnimation.AnimationState.End += delegate {
         // ... or choose to ignore its parameters.
         Debug.Log("An animation ended!");
      };
   }

   void HandleEvent (TrackEntry trackEntry, Spine.Event e) {
      // Play some sound if the event named "footstep" fired.
      if (e.Data.Name == footstepEventName) {
         Debug.Log("Play a footstep sound!");
      }
   }
}

HandleEventWithAudioExample

该示例为一个附带场景的声音事件处理程序的MonoBehaviour.

C#
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

namespace Spine.Unity.Examples {
   public class HandleEventWithAudioExample : MonoBehaviour {

      public SkeletonAnimation skeletonAnimation;
      [SpineEvent(dataField: "skeletonAnimation", fallbackToTextField: true)]
      public string eventName;

      [Space]
      public AudioSource audioSource;
      public AudioClip audioClip;
      public float basePitch = 1f;
      public float randomPitchOffset = 0.1f;

      [Space]
      public bool logDebugMessage = false;

      Spine.EventData eventData;

      void OnValidate () {
         if (skeletonAnimation == null) GetComponent<SkeletonAnimation>();
         if (audioSource == null) GetComponent<AudioSource>();
      }

      void Start () {
         if (audioSource == null) return;
         if (skeletonAnimation == null) return;
         skeletonAnimation.Initialize(false);
         if (!skeletonAnimation.valid) return;

         eventData = skeletonAnimation.Skeleton.Data.FindEvent(eventName);
         skeletonAnimation.AnimationState.Event += HandleAnimationStateEvent;
      }

      private void HandleAnimationStateEvent (TrackEntry trackEntry, Event e) {
         if (logDebugMessage) Debug.Log("Event fired! " + e.Data.Name);
         //bool eventMatch = string.Equals(e.Data.Name, eventName, System.StringComparison.Ordinal); // Testing recommendation: String compare.
         bool eventMatch = (eventData == e.Data); // Performance recommendation: Match cached reference instead of string.
         if (eventMatch) {
            Play();
         }
      }

      public void Play () {
         audioSource.pitch = basePitch + Random.Range(-randomPitchOffset, randomPitchOffset);
         audioSource.clip = audioClip;
         audioSource.Play();
      }
   }

}

SkeletonMecanim事件

请参阅spine-unity文档页面上的SkeletonMecanim-Events章节.

进阶内容

由于Spine的运行时是开源的,并且你可以在你的项目中随意修改,你当然也可以在AnimationState或你修改过的运行时中定义和触发你自己的事件。更多信息请参见官方的spine-unity论坛帖子.