SpineイベントとAnimationStateのコールバック

注意: このページは各イベントがいつ発生するかについてより簡単な説明を提供することを目的としています。 正確な定義については、APIリファレンスページのAnimationStateListener Methodsを参照してください。

Spine.AnimationStateと個々のTrackEntryオブジェクトは、C#イベントという形でアニメーションのコールバックのための機能を提供します。これらを使って、アニメーション再生の基本的なポイントを処理することができます。Spine.AnimationStateのイベントに登録すると、すべてのトラックのアニメーションのイベントが発生し、単一のTrackEntryのイベントに登録すると、エンキューされた単一のアニメーションが発行するイベントのみが発生します。

初心者プログラマー向けの補足: コールバックとは、何か特定のことが起こったときに呼び出すメソッドをシステムに与えることで、そのことを「通知」するように指示することです。この場合、イベントシステムが呼び出す関数やメソッドを提供することで、サブスクライブしたり、独自のコードで処理したりすることができます。

イベントが発生したとき、提供した関数/メソッドを呼び出す処理を、イベントを「発生させる(raising)」または「発射する(firing)」といいます。ほとんどのC#のドキュメントでは、これを「発生させる(raising)」と呼んでいますが、Spineの一部ドキュメントでは、「発射する(firing)」と呼んでいる場合もあります。これらは同じ意味です。

コールバック機能の構造や構文は、言語によって異なります。C#の構文の例については、以下のサンプルコードを参照してください。


図1. ミキシング/クロスフェーディングを行わずに上げたイベントのチャート。

Spine.AnimationState および TrackEntry は以下のイベントを発生させます:

  • Startは、アニメーションの再生開始時に発生します。
    • これは、SetAnimationを呼び出したときに適用されます。
    • または、キューに入れられたアニメーションの再生が開始されたときにも発生します。
  • Endは、アニメーションがトラックから削除されるときに発生します。
    • これは、現在のアニメーションが終了する前にSetAnimationを呼び出した場合に適用されます。
    • ClearTrackClearTracksでトラックをクリアしたときにも、これが発生します。
    • ミックス/クロスフェードを行う際は、ミックス終了後にEndが発生します。
    • AnimationStateに登録する場合、EndイベントをSetAnimationを呼び出すメソッドで処理すると、無限ループになってしまう可能性があるため処理しないでください。詳しくは後述の警告を参照してください。代わりに単一のTrackEntry.Endに登録します。
    • デフォルトでは、非ループアニメーションのTrackEntryは、アニメーションのデュレーションで停止しないことに注意してください。代わりに、最後のフレームは、クリアするか、他のアニメーションに置き換わるまで、無限に適用され続けます。もし、TrackEntryがアニメーションのデュレーションに達した時にトラックを削除したい場合は、TrackEntry.TrackEndをアニメーションのデュレーションと同じに揃えてください。
  • Disposeは、AnimationStateがTrackEntryを破棄するとき(ライフサイクルの終了時)に各TrackEntryに対して発生します。
    • spine-libgdxやspine-csharpなどのランタイムはTrackEntryオブジェクトをプールして、不必要なGC(garbage collector)プレッシャーを回避しています。これは、古くて効率の悪いガベージコレクションの実装を持つUnityでは特に重要です。
    • TrackEntryが破棄されたときには、TrackEntryへの参照をすべて消去することが重要です。なぜなら、TrackEntryには、後で読むつもりも監視するつもりもないデータが含まれていたり、イベントが発生したりする可能性があるからです。
    • DisposeはEndの後、ただちに発生します。
  • Interruptは、新しいアニメーションが設定された際に、現在のアニメーションがまだ再生されている場合に発生します。
    • これは、アニメーションが他のアニメーションとミキシング/クロスフェードを開始するときに発生します。
  • Completeは、アニメーションがその全てのデュレーションまで完了したときに発生します。
    • これは、次のアニメーションがキューに入っているかどうかにかかわらず、ループしていないアニメーションの再生が終了したときに発生します。
    • また、ループアニメーションの場合はループが1周するたびに発生します。
  • Eventは、いずれかのユーザー定義のイベントが検出されるたびに発生します。
    • これはSpineエディター上でアニメーション内にキーを記録したイベントです。紫色のキーで、紫色のアイコンはツリービューでも確認できます。
    • 異なるイベントを区別するためには、Spine.Event eパラメーターでそのName(またはData reference)を確認する必要があります。
    • これは足音など、アニメーションのポイントに合わせて音を鳴らす必要がある場合に便利です。また、Spineのアニメーションに合わせてSpine以外のシステムを同期させたり、シグナルを送ることもできます。例えば、Unityのパーティクルシステムや別エフェクトのスポーン、あるいは弾丸を発射するタイミングなどのゲームロジックにも使えます(本当に必要であればですが)。
    • 各TrackEntryにはEventThresholdプロパティがあります。これは、クロスフェード内のどの時点でユーザーイベントが発生しなくなるかを定義します。詳細は、後述のミキシング中のイベントを参照してください。

アニメーションの再生が終了してキューに登録されたアニメーションが開始されるタイミングで、CompleteEndStartの順に発生します。

警告: SetAnimationを呼び出すメソッドでAnimationState.Endをサブスクライブしないでください(ハンドラーメソッドのロジックで無限再帰を防いでいる場合を除く)。Endはアニメーションが中断されたときに発生し、SetAnimationは既存のアニメーションを中断するので、AnimationState.Endを介してイベントを処理すると、End -> Handle -> SetAnimation -> End -> Handle -> SetAnimationの無限再帰を引き起こし、スタックのオーバーフローが発生するまでUnityがフリーズする可能性があります。簡単な解決策は、代わりに単一のTrackEntry.Endに登録するようにすることです。

AnimationStateとTrackEntryイベントの違い

AnimationStateと個々のTrackEntryオブジェクトの両方が、前述のSpineアニメーションイベントを発生させます。

AnimationStateのイベントをサブスクライブすると、再生されるすべてのアニメーションからのコールバックが得られます。

一方、TrackEntryのイベントをサブスクライブすると、特定のアニメーション再生のインスタンスにのみサブスクライブすることになります。TrackEntryが終了すると、それは破棄され、それ以降のイベントは発生しません。

なお、TrackEntryイベントは、対応するAnimationStateイベントの前に発生します。

ミキシング中のイベント

ミックスタイム(またはSkeleton Data AssetのDefault Mix)を設定すると、次のアニメーションがアルファ値を増加させながらミックスされ始め、前のアニメーションはまだスケルトンに適用されている、という時間が生まれます。

この時間のことを、「クロスフェード」または「ミックス」と呼びます。

ミキシング中のユーザーイベント

TrackEntryのEventThreshold は、ミックスデュレーション中のユーザー定義イベントの扱いを制御します。

  • デフォルト値の0では、次のアニメーションの再生が始まると、ユーザー定義イベントの発生が即座に停止します。
  • 0.5に設定すると、クロスフェード/ミックスの途中でユーザー定義イベントの発生が停止します。
  • 1に設定すると、クロスフェード/ミックスの最後のフレームまでイベントが発生し続けます。

EventThresholdをアニメーションに適した値に設定することは重要です。同じアニメーションが重なっていて同じものを発生させるべきではない場合や、アニメーションが中断された場合でもイベントを発生させたい場合などがあるからです。

サンプルコード

AnimationStateのイベントをサブスクライブするMonoBehaviourのサンプルはこちらです。コメントを読むと、何が起こっているのかがわかります。

C#
// Spine 3.7用に書かれたサンプルです
using UnityEngine;
using Spine;
using Spine.Unity;

// これをSkeletonAnimationと同じGameObjectに追加してください
public class MySpineEventHandler : MonoBehaviour {

   // [SpineEvent]属性(Attribute)は、このMonoBehaviourのインスペクターに、
   // SkeletonDataにある既存のイベント名のドロップダウンリストのフィールドを描画させます。
   [SpineEvent] public string footstepEventName = "footstep";

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

      // このように、宣言されたメソッドを介してサブスクライブします。
      // このメソッドには正しいシグネチャが必要です。
      skeletonAnimation.AnimationState.Event += HandleEvent;

      skeletonAnimation.AnimationState.Start += delegate (TrackEntry trackEntry) {
         // また、匿名デリゲートを使用することもできます。
         Debug.Log(string.Format("track {0} started a new animation.", trackEntry.TrackIndex));
      };

      skeletonAnimation.AnimationState.End += delegate {
         // ... あるいは、そのパラメーターを無視することを選択します。
         Debug.Log("An animation ended!");
      };
   }

   void HandleEvent (TrackEntry trackEntry, Spine.Event e) {
      // "footstep "という名前のイベントが発生したら、サウンドを再生する。
      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イベントセクションをご覧ください。

高度な利用

SpineランタイムはSource-availableソフトウェア(ソースが利用可能なソフトウェア)で、プロジェクト内で自由に変更することができますので、もちろんAnimationStateやそのバージョンで独自のイベントを定義し、発生させることができます。詳しくは公式のspine-unityフォーラムをご覧ください。