![](/img/spine-runtimes-guide/spine-unity/events.png)
# Spine事件 & AnimationState回调函数

> **注意:** 本页面为事件触发的通俗易懂版解释. 对于事件精准的定义, 请见API参考文档中的[AnimationStateListener 方法](/spine-api-reference#AnimationStateListener-Methods)一节.


[**Spine.AnimationState**](/spine-api-reference#AnimationState)和[**TrackEntry**](/spine-api-reference#TrackEntry) 对象以[C#事件](https://msdn.microsoft.com/en-us/library/awbftdfh.aspx)的形式为动画提供了回调的功能. 可以用他们对动画播放进行基本的管理. 在[`Spine.AnimationState`](/spine-api-reference#AnimationState)上注册事件将触发所有轨道上的动画的事件, 而在一个[`TrackEntry`](/spine-api-reference#TrackEntry)上注册事件则只会触发单个排队动画所发出的事件.

> **新手程序员必读**: [回调](https://en.wikipedia.org/wiki/Callback_%28computer_programming%29)的意思是，你可以告诉系统，当*特定情况*发生时可以通过给它一个方法来调用, 从而"告知"你事件的发生. [事件](https://en.wikipedia.org/wiki/Event_%28computing%29)在回调中表示有意义的断点——有事发生时, 你可以为事件系统提供一个函数/方法来订阅事件或用你自己的代码来处理这些断点.
>
> 当事件发生时，调用函数/方法的过程被称为触发。很多C#文档中叫做"raising". 但是Spine的文档会写作"firing". 它们其实是一个意思. 
>
> 回调的结构和语法会根据语言而有不同改变。可以在文档下方查看C#语法的示例.

![](/img/spine-runtimes-guide/spine-unity/callbackchart.png)  
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`](/spine-api-reference#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`](/spine-api-reference#AnimationState-setAnimation2)(除非你的方法里实现了防止无限递归的逻辑)。因为当一个动画被中断就会触发`End`事件，而`SetAnimation`会中断当前的任何动画，通过`AnimationState.End`处理事件会引发无限递归(`End -> Handle -> SetAnimation -> End -> Handle -> SetAnimation`)，导致Unity无响应然后堆栈溢出。一个简单的解决方案是: 注册到单个的`TrackEntry.End`上即可.

### 对比: AnimationState和TrackEntry事件

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

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

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

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

### 混合过程中的事件

![](/img/spine-runtimes-guide/spine-unity/callbackchart-mix.png)  

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

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

#### 混合(Mix)过程中的用户事件
一个TrackEntry的**EventThreshold**控制在混合期间如何处理用户事件. 

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

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

## 代码示例

该示例为一个订阅了`AnimationState`的事件的`MonoBehaviour`. 代码注释详细解释了其过程.
```csharp
// 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`.

```csharp
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-unity#SkeletonMecanim-Events)章节.

# 进阶内容

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