Spine事件 & AnimationState回调函数
注意: 本页面为事件触发的通俗易懂版解释. 对于事件精准的定义, 请见API参考文档中的AnimationStateListener 方法一节.
Spine.AnimationState和TrackEntry 对象以C#事件的形式为动画提供了回调的功能. 可以用他们对动画播放进行基本的管理. 在Spine.AnimationState
上注册事件将触发所有轨道上的动画的事件, 而在一个TrackEntry
上注册事件则只会触发单个排队动画所发出的事件.
新手程序员必读: 回调的意思是,你可以告诉系统,当特定情况发生时可以通过给它一个方法来调用, 从而"告知"你事件的发生. 事件在回调中表示有意义的断点——有事发生时, 你可以为事件系统提供一个函数/方法来订阅事件或用你自己的代码来处理这些断点.
当事件发生时,调用函数/方法的过程被称为触发。很多C#文档中叫做"raising". 但是Spine的文档会写作"firing". 它们其实是一个意思.
回调的结构和语法会根据语言而有不同改变。可以在文档下方查看C#语法的示例.
Fig 1. 不包括混合(mixing)/淡入淡出(crossfading)的事件触发流程图.
Spine.AnimationState
和 TrackEntry
将触发以下事件:
- 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 e
的Name
参数。(或者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
. 代码注释详细解释了其过程.
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
.
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论坛帖子.