Spine Events and AnimationState callbacks
Note: This page aims to provide an easier explanation when each event is raised. For a precise definition see the API Reference page on AnimationStateListener Methods.
Spine.AnimationState and individual TrackEntry objects provide functionality for animation callbacks in the form of C# events. You can use these to handle some basic points of animation playback. Registering to events at Spine.AnimationState
will raise events for animations on all tracks, while registering to events at a single TrackEntry
raises only events issued by the single enqueued animation.
For the novice programmer: Callbacks mean you can tell a system to "inform" you when something specific happens by giving it a method to call when that something happens. Events are the meaningful points in program execution— in this case, ones that you can subscribe to or handle with your own code by providing a function/method for the event system to call.
When the event happens, the process of calling the function/method you provided is called "raising" or "firing" the event. Most C# documentation will call it "raising". Some Spine documentation will call it "firing". Those mean the same thing.
The structure and syntax for callback functionality varies from language to language. See the sample code at the bottom for examples of C# syntax.
Fig 1. Chart of Events raised without mixing/crossfading.
Spine.AnimationState
and TrackEntry
raise the following events:
- Start is raised when an animation starts playing,
- This applies to right when you call
SetAnimation
. - It can also be raised when a queued animation starts playing.
- This applies to right when you call
- End is raised when an animation will be removed from the track,
- This applies to when you call
SetAnimation
before the current animation has a chance to finish. - This is also raised when you clear the track using
ClearTrack
orClearTracks
. - During a mix/crossfade, end is raised after a mix is completed.
- When registering at
AnimationState
, don't handle theEnd
event with a method that callsSetAnimation
if this can lead to infinite recursion. See the warning below. Register to a singleTrackEntry.End
instead. - Note that by default, non-looping animation TrackEntries do not stop at the duration of the animation. Instead, the last frame continues to be applied indefinitely until you clear it or some other animation replaces it. If you want the track to be cleared when your
TrackEntry
reaches its animation duration, setTrackEntry.TrackEnd
to the animation duration.
- This applies to when you call
- Dispose is raised for TrackEntries when AnimationState disposes of a TrackEntry (at the end of its life cycle).
- Runtimes like spine-libgdx and spine-csharp pool TrackEntry objects to avoid unnecessary GC pressure. This is particularly important in Unity which has an old and less efficient garbage collection implementation.
- It is important to clear all your references to TrackEntries when they are disposed since they could later contain data or raise events that you did not intend to read or observe.
- Dispose in raised immediately after End.
- Interrupt is raised when a new animation is set and a current animation is still playing.
- This is raised when an animation starts mixing/crossfading into another animation.
- Complete is raised an animation completes its full duration,
- This is raised when a non-looping animation finishes playing, whether or not a next animation is queued.
- This is also raised every time a looping animation finishes an loop.
- Event is raised whenever any user-defined event is detected.
- These are events you keyed in animations in Spine editor. They are purple keys. A purple icon can also be found in the Tree view.
- To distinguish between different events, you need to check the
Spine.Event e
parameter for itsName
. (orData
reference). - This is useful for when you have to play sounds according to points the animation like footsteps. It can also be used to synchronize or signal non-Spine systems according to Spine animations, such as Unity particle systems or spawning separate effects, or even game logic such as timing when to fire bullets (if you really want to).
- Each TrackEntry has an
EventThreshold
property. This defines at what point within a crossfade user events stop being raised. See Events During Mixing below for more information.
At the junction where an animation completes playback, and a queued animation will start, the events are raised in this order: Complete
, End
, Start
.
WARNING: Don't subscribe to
AnimationState.End
with a method that callsSetAnimation
(unless your handler method logic prevents infinite recursion). SinceEnd
is raised when an animation is interrupted, andSetAnimation
interrupts any existing animation, handling the event viaAnimationState.End
can cause an infinite recursion ofEnd -> Handle -> SetAnimation -> End -> Handle -> SetAnimation
, causing Unity to freeze until a stack overflow happens. An easy solution is to register to a singleTrackEntry.End
instead.
AnimationState vs TrackEntry events
Both AnimationState, and individual TrackEntry objects, raise Spine animation events listed above.
Subscribing to events on AnimationState itself will give you a callback from all animations that are played on it.
In contrast, when subscribing to TrackEntry events, you will only be subscribed to that particular instance of animation playback. After that TrackEntry ends, it will be disposed and no further events will come from it.
TrackEntry events are raised before the corresponding AnimationState events.
Events During Mixing
When you have a mix time set (or Default Mix
on your Skeleton Data Asset), there is a span of time where the next animation starts being mixed with an increasing alpha, and the previous animation is still being applied to the skeleton.
We can call this span of time, the "crossfade" or "mix".
User events during mixing
A TrackEntry's EventThreshold controls how user events are treated during the mix duration.
- With the default value
0
, user events stop being raised immediately when the next animation starts playing. - If you set it to
0.5
, user events stop being raised halfway through the crossfade/mix. - If you set it to
1
, events will continue to be raised up until the very last frame of the crossfade/mix.
Setting EventThreshold
to the appropriate value for your animations is important, as you may have overlapping animations that have the same animations and shouldn't raise the same ones, or want events to still be raised even if the animation has been interrupted.
Sample Code
Here is a sample MonoBehaviour
that subscribes to AnimationState
's events. Read the comments to see what's going on.
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
Here is a sample sound event handler MonoBehaviour that comes with the sample scenes.
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 Events
Please see section SkeletonMecanim-Events on the spine-unity documentation page.
Advanced
Since the Spine runtimes are source-available and fully modifiable in your project, you can of-course define and raise your own events in AnimationState or in whatever version of it you make. See the official spine-unity forums for more information.