Spiners MeetUp vol.5 UnityでのSpine実装方法 〜後編〜
「Spiners MeetUp」とは、Spineを使用しているアニメーター「Spiner」同士でノウハウを共有し、そしてSpiner同士をつなぐ場となることを目的としたイベントです。本イベントは、2020年5月から開催しており、これまで合計4回のイベントを行ってきました。
Spiners MeetUp Vol.5では、Spineアニメーター初級者の技術力向上を支援するため、第1回から共同主催をさせて頂いている株式会社ArtnerとG2 Studios株式会社で、Spineを使った研究開発を行い、プロジェクトデータと解説記事を公開いたしました。
▼配布中のプロジェクトデータ
前編ではUnity上でSpineを表示するところまでの解説をしました。
後編ではSpineのアニメーションの制御について解説します。
▼Spiners MeetUp vol.5 UnityでのSpine実装方法 〜前編〜
全体の流れ
アニメーションを確認する
早速、Unity上でSpineのアニメーションを再生してみます。
まず再生するアニメーションを指定し、Spineのデータが問題なく出力できているか確認します。
SkeletonAnimationのAnimation Nameから再生したいアニメーションを選択します。
動画ではSkeletonAnimationのAnimationNameを手動で切り替えるテストをしています。
この状態でUnityを再生することで、実際にアニメーションを再生することができます。
再生中にもAnimation Nameを切り替えることで、他のアニメーションを確認することができます。
このアニメーションは、Spineのアニメーションと連動したものになります。
Spine側でフォルダ「janken」に格納しているので、Unity側でもjankenに含まれる形でアニメーションが用意されています。
アニメーションの確認ができましたら、次はアニメーションの切り替えをスクリプトから行えるようにします。
Spineアニメーションをスクリプトから制御する
実装を確認するためのスクリプトを作成しました。このスクリプトを、Hierarchyに配置したゲームオブジェクトに設定してください。
再生に関して、このスクリプトを元に説明したいと思います。全体像をご覧いただき、各要素に関して説明したいと思います。
動画ではSampleSpineAnimationControllerのTestAnimationNameに手入力でアニメーション名を入力し、Aキーでテスト実行しています。
using Spine.Unity;
using UnityEngine;
/// <summary> Spineアニメーションの再生を確認サンプルクラス </summary>
public class SampleSpineAnimationController : MonoBehaviour
{
/// <summary> 再生するアニメーション名 </summary>
[SerializeField]
private string testAnimationName = "janken/idle";
/// <summary> ゲームオブジェクトに設定されているSkeletonAnimation </summary>
private SkeletonAnimation skeletonAnimation = default;
/// <summary> Spineアニメーションを適用するために必要なAnimationState </summary>
private Spine.AnimationState spineAnimationState = default;
private void Start()
{
// ゲームオブジェクトのSkeletonAnimationを取得
skeletonAnimation = GetComponent<SkeletonAnimation>();
// SkeletonAnimationからAnimationStateを取得
spineAnimationState = skeletonAnimation.AnimationState;
}
private void Update()
{
// Aキーの入力でアニメーションを切り替えるテスト
if (Input.GetKeyDown(KeyCode.A))
{
PlayAnimation();
}
}
/// <summary>
/// Spineアニメーションを再生
/// testAnimationNameに再生したいアニメーション名を記載してください。
/// </summary>
private void PlayAnimation()
{
// アニメーション「testAnimationName」を再生
spineAnimationState.SetAnimation(0, testAnimationName, true);
}
}
▼初期化について
using Spine.Unity;
まずはSkeletonAnimationを使うための宣言をします。
usingを忘れて悩むことも多々あるので注意が必要です。
/// <summary> ゲームオブジェクトに設定されているSkeletonAnimation </summary>
private SkeletonAnimation skeletonAnimation = default;
/// <summary> Spineアニメーションを適用するために必要なAnimationState </summary>
private Spine.AnimationState spineAnimationState = default;
private void Start()
{
// ゲームオブジェクトのSkeletonAnimationを取得
skeletonAnimation = GetComponent<SkeletonAnimation>();
// SkeletonAnimationからAnimationStateを取得
spineAnimationState = skeletonAnimation.AnimationState;
}
次にアニメーションさせるために必要な変数の宣言と、その初期化です。SkeletonAnimationはSpineデータをHierarchyに配置した際に生成されるゲームオブジェクトに最初から設定されているコンポーネントです。skeletonAnimationはspineAnimationStateの初期化に必要になります。
Inspector上ではSkeletonAnimationのAnimationNameを切り替えることでアニメーションが変更されることを確認しましたが、それと同等のことをスクリプトで行うためにはspineAnimationStateでアニメーションを切り替える関数を実行する必要があります。
spineAnimationStateの初期化が完了したら、これを使ってアニメーションを再生します。
▼再生について
/// <summary> 再生するアニメーション名 </summary>
[SerializeField]
private string testAnimationName = "janken/idle";
再生するアニメーション名を指定する変数を宣言します。
Inspectorからアニメーション名を直接入力することでアニメーションを指定できるようにしています。
テスト用なのでこのような実装になっていますが、サンプルプロジェクトでは別の実装方法で実現しています。そちらも合わせて確認してもらえればと思います。
private void Update()
{
// Aキーの入力でアニメーションを切り替えるテスト
if (Input.GetKeyDown(KeyCode.A))
{
PlayAnimation();
}
}
アニメーション再生のテスト実行処理です。
Aキーを入力することでアニメーションを切り替えることができるようになっています。
/// <summary>
/// Spineアニメーションを再生
/// testAnimationNameに再生したいアニメーション名を記載してください。
/// </summary>
private void PlayAnimation()
{
// アニメーション「testAnimationName」を再生
spineAnimationState.SetAnimation(0, testAnimationName, true);
}
spineAnimationState.SetAnimation(0, testAnimationName, true);で実際にアニメーションを再生します。引数はそれぞれ以下のような内容になっています。
-0 : アニメーションを再生するTrackIndex(他のアニメーションを重ねて再生する際に使用。後述する目ぱちの項目で追加で解説します)
-testAnimationName : 再生するアニメーション名
-true : ループさせるかどうか
ここまでの内容が実装できていると、以下の動画のような状態になります。
Inspectorでアニメーション名を指定し、Aキーを押すとアニメーションが切り替わります。
アニメーション名の指定方法と、アニメーションの切り替え条件はアプリの仕様に合わせて実装を変えていくのが良いかと思います。
アニメーションの終了を検知する
「現在再生しているアニメーションが終了したら次に何らかの処理を行いたい時」に必要なのが、終了を検知する機能です。
今回作成したアプリでは、アニメーション終了後、別のアニメーションを再生したり、関数を実行するために用いています。
「Spineアニメーションをスクリプトから制御する」に記載したスクリプトを編集して解説していきます。
今回はAキーでアニメーションを再生して、そのアニメーションが完了した時に別のアニメーションを再生される処理を作成しました。
全容は以下の通りです。
動画ではAキーを入力した際にSampleSpineAnimationControllerのTestAnimationNameBeforeに設定されたアニメーションを再生し、そのアニメーションの終了を検知してTestAnimationNameBeforeに設定されたアニメーションを再生しています。
using Spine;
using Spine.Unity;
using UnityEngine;
/// <summary> Spineアニメーションの再生を確認サンプルクラス </summary>
public class SampleSpineAnimationController : MonoBehaviour
{
/// <summary> 最初に再生するアニメーション名 </summary>
[SerializeField]
private string testAnimationNameBefore = "janken/janken_pre";
/// <summary> 次に再生するアニメーション名 </summary>
[SerializeField]
private string testAnimationNameAfter = "janken/janken_choki";
/// <summary> ゲームオブジェクトに設定されているSkeletonAnimation </summary>
private SkeletonAnimation skeletonAnimation = default;
/// <summary> Spineアニメーションを適用するために必要なAnimationState </summary>
private Spine.AnimationState spineAnimationState = default;
private void Start()
{
// ゲームオブジェクトのSkeletonAnimationを取得
skeletonAnimation = GetComponent<SkeletonAnimation>();
// SkeletonAnimationからAnimationStateを取得
spineAnimationState = skeletonAnimation.AnimationState;
}
private void Update()
{
// Aキーの入力でアニメーションを切り替えるテスト
if (Input.GetKeyDown(KeyCode.A))
{
PlayAnimation();
}
}
/// <summary>
/// Spineアニメーションを再生
/// testAnimationNameに再生したいアニメーション名を記載してください。
/// </summary>
private void PlayAnimation()
{
// アニメーション「testAnimationName」を再生
TrackEntry trackEntry = spineAnimationState.SetAnimation(0, testAnimationNameBefore, true);
// 完了通知を取得準備
trackEntry.Complete += OnSpineComplete;
}
private void OnSpineComplete(TrackEntry trackEntry)
{
// アニメーション完了時に行う処理を記載
spineAnimationState.SetAnimation(0, testAnimationNameAfter, true);
}
}
▼準備
using Spine;
完了通知を取得するためにTrackEntryを使用する必要があるので、上記を宣言します。
/// <summary>
/// Spineアニメーションを再生
/// testAnimationNameに再生したいアニメーション名を記載してください。
/// </summary>
private void PlayAnimation()
{
// アニメーション「testAnimationName」を再生
TrackEntry trackEntry = spineAnimationState.SetAnimation(0, testAnimationNameBefore, true);
// 完了通知を取得準備
trackEntry.Complete += OnSpineComplete;
}
private void OnSpineComplete(TrackEntry trackEntry)
{
// アニメーション完了時に行う処理を記載
spineAnimationState.SetAnimation(0, testAnimationNameAfter, true);
}
spineAnimationState.SetAnimationの戻り値としてTrackEntryを用意します。
trackEntry.Completeにアニメーション再生完了時に実行する関数を登録します。
サンプルではOnSpineComplete内で次のアニメーションを再生していますが、例えばUniRxを用いて完了時に通知を発行する処理を行い、アニメーションの完了を管理することもできます。
目ぱちアニメーションを重ねて再生する
待機アニメーションなど、Spineの全体を動かすようなアニメーションを再生している際に、目ぱちなど、一部分を動かしたい時の対応方法について解説します。
ここまでのスクリプトを更新します。以下をご確認ください。
動画ではBキーを入力した際に目ぱちを実行し、待機アニメーションを再生中に別のアニメーションを重ねて再生しています。
using Spine;
using Spine.Unity;
using UnityEngine;
/// <summary> Spineアニメーションの再生を確認サンプルクラス </summary>
public class SampleSpineAnimationController : MonoBehaviour
{
/// <summary> 最初に再生するアニメーション名 </summary>
[SerializeField]
private string testAnimationNameBefore = "janken/janken_pre";
/// <summary> 次に再生するアニメーション名 </summary>
[SerializeField]
private string testAnimationNameAfter = "janken/janken_choki";
/// <summary> 重ねる目ぱちアニメーション名 </summary>
[SerializeField]
private string testAnimationNameSubTrack = "janken/facial_eye_blink_B";
/// <summary> ゲームオブジェクトに設定されているSkeletonAnimation </summary>
private SkeletonAnimation skeletonAnimation = default;
/// <summary> Spineアニメーションを適用するために必要なAnimationState </summary>
private Spine.AnimationState spineAnimationState = default;
/// <summary> メインのTrackIndex、全身のアニメーションの再生に使用 </summary>
private readonly int mainTrackIndex = 100;
/// <summary> サブのTrackIndex、一部分のアニメーションの再生に使用 </summary>
private readonly int subTrackIndex = 10;
private void Start()
{
// ゲームオブジェクトのSkeletonAnimationを取得
skeletonAnimation = GetComponent<SkeletonAnimation>();
// SkeletonAnimationからAnimationStateを取得
spineAnimationState = skeletonAnimation.AnimationState;
}
private void Update()
{
// Aキーの入力でアニメーションを切り替えるテスト
if (Input.GetKeyDown(KeyCode.A))
{
PlayAnimation(mainTrackIndex, testAnimationNameBefore, true);
}
// Bキーの入力でアニメーションを重ねるテスト
if (Input.GetKeyDown(KeyCode.B))
{
PlayAnimation(subTrackIndex, testAnimationNameSubTrack, false);
}
}
/// <summary>
/// Spineアニメーションを再生
/// testAnimationNameに再生したいアニメーション名を記載してください。
/// </summary>
/// <param name="trackIndex">再生するTrackIndex</param>
/// <param name="animName">再生するアニメーション名</param>
/// <param name="isLoop">ループさせるかどうか</param>
private void PlayAnimation(int trackIndex, string animName, bool isLoop)
{
// アニメーション「testAnimationName」を再生
TrackEntry trackEntry = spineAnimationState.SetAnimation(trackIndex, animName, isLoop);
// 完了通知を取得準備
trackEntry.Complete += OnSpineComplete;
}
/// <summary>
/// Spineアニメーションが完了した時の処理
/// </summary>
/// <param name="trackEntry"></param>
private void OnSpineComplete(TrackEntry trackEntry)
{
// メイントラック以外は処理を中断
if (trackEntry.TrackIndex != mainTrackIndex) return;
spineAnimationState.SetAnimation(mainTrackIndex, testAnimationNameAfter, true);
}
}
/// <summary> 重ねる目ぱちアニメーション名 </summary>
[SerializeField]
private string testAnimationNameSubTrack = "janken/facial_eye_blink_B";
目ぱちのアニメーション名を設定します。このアニメーションを、再生中の別のアニメーションに重ねて再生させる作りにします。
/// <summary> メインのTrackIndex、全身のアニメーションの再生に使用 </summary>
private readonly int mainTrackIndex = 100;
/// <summary> サブのTrackIndex、一部分のアニメーションの再生に使用 </summary>
private readonly int subTrackIndex = 10;
Spineのアニメーションを重ねて表示させるためには、TrackIndexを別にして対応する必要があります。
「Spineアニメーションをスクリプトから制御する」で登場したspineAnimationState.SetAnimationの第一引数にTrackIndexを指定することで、アニメーションの再生を重ねることができます。
TrackIndexは数字が大きいほど優先的に再生されます。mainTrackIndexとsubTrackIndexのどちらの数字を大きくするかは、そのSpineアニメーションの設計に合わせて決めるべきかと思います。
今回のアプリの設計では目ぱちを常に再生しながら、全体のアニメーションを切り変えるようにしています。
例えば動画のように、アニメーションの中で目も動かすようにしているようなアニメーションでは、mainTrackIndexがsubTrackIndexより小さいと、意図したアニメーションになりません。
そのため、mainTrackIndexを大きくすることで、全体のアニメーションの中で目を動かすようにしていた場合はそちらが優先され、目を動かすアニメーションが設定されていない場合は目ぱちを行うようになります。
逆を言えば通常待機モーションなどの最中に目ぱちをさせたい場合、Spineではそのアニメーション内で目の動きを設定してはならないので注意してください。
private void Update()
{
// Aキーの入力でアニメーションを切り替えるテスト
if (Input.GetKeyDown(KeyCode.A))
{
PlayAnimation(mainTrackIndex, testAnimationNameBefore, true);
}
// Bキーの入力でアニメーションを重ねるテスト
if (Input.GetKeyDown(KeyCode.B))
{
PlayAnimation(subTrackIndex, testAnimationNameSubTrack, false);
}
}
/// <summary>
/// Spineアニメーションを再生
/// testAnimationNameに再生したいアニメーション名を記載してください。
/// </summary>
/// <param name="trackIndex">再生するTrackIndex</param>
/// <param name="animName">再生するアニメーション名</param>
/// <param name="isLoop">ループさせるかどうか</param>
private void PlayAnimation(int trackIndex, string animName, bool isLoop)
{
// アニメーション「testAnimationName」を再生
TrackEntry trackEntry = spineAnimationState.SetAnimation(trackIndex, animName, isLoop);
// 完了通知を取得準備
trackEntry.Complete += OnSpineComplete;
}
Bキーの入力時に目ぱちアニメーションを再生させます。
目ぱちアニメーションを重ねて再生させるためにPlayAnimationも更新します。
PlayAnimationに再生するTrackIndex、アニメーション名、ループの是非を指定できるようにしました。
さらに別のTrackIndexを指定して、例えば口パクをさせることも可能です。
以上でアニメーション切り替えに関する解説は終了です。
アニメーションの切り替え条件、再生終了通知を受け取った後の処理、TrackIndexの優先度など、実際の制作ではここまでの解説を参考にその仕様に合わせて作成していただければと思います。
Skinの切り替えをスクリプトから制御する
SpineにはSkinという機能があります。
本アプリではその機能を用いてキャラクターの表情を切り替えられるようにしました。
動画ではCキーを入力した際にSkinが切り替わる確認をしています。
using Spine;
using Spine.Unity;
using UnityEngine;
/// <summary> Spineアニメーションの再生を確認サンプルクラス </summary>
public class SampleSpineAnimationController : MonoBehaviour
{
/// <summary> 最初に再生するアニメーション名 </summary>
[SerializeField]
private string testAnimationNameBefore = "janken/janken_pre";
/// <summary> 次に再生するアニメーション名 </summary>
[SerializeField]
private string testAnimationNameAfter = "janken/janken_choki";
/// <summary> 重ねる目ぱちアニメーション名 </summary>
[SerializeField]
private string testAnimationNameSubTrack = "janken/facial_eye_blink_B";
/// <summary> 切り替えテスト用スキン名 </summary>
[SerializeField]
private string testSkinName = "skin_sad";
/// <summary> ゲームオブジェクトに設定されているSkeletonAnimation </summary>
private SkeletonAnimation skeletonAnimation = default;
/// <summary> Spineアニメーションを適用するために必要なAnimationState </summary>
private Spine.AnimationState spineAnimationState = default;
/// <summary> メインのTrackIndex、全身のアニメーションの再生に使用 </summary>
private readonly int mainTrackIndex = 100;
/// <summary> サブのTrackIndex、一部分のアニメーションの再生に使用 </summary>
private readonly int subTrackIndex = 10;
private void Start()
{
// ゲームオブジェクトのSkeletonAnimationを取得
skeletonAnimation = GetComponent<SkeletonAnimation>();
// SkeletonAnimationからAnimationStateを取得
spineAnimationState = skeletonAnimation.AnimationState;
}
private void Update()
{
// Aキーの入力でアニメーションを切り替えるテスト
if (Input.GetKeyDown(KeyCode.A))
{
PlayAnimation(mainTrackIndex, testAnimationNameBefore, true);
}
// Bキーの入力でアニメーションを重ねるテスト
if (Input.GetKeyDown(KeyCode.B))
{
PlayAnimation(subTrackIndex, testAnimationNameSubTrack, false);
}
// Cキーの入力でアニメーションを重ねるテスト
if (Input.GetKeyDown(KeyCode.C))
{
SetSkin(testSkinName);
}
}
/// <summary>
/// Spineアニメーションを再生
/// testAnimationNameに再生したいアニメーション名を記載してください。
/// </summary>
/// <param name="trackIndex">再生するTrackIndex</param>
/// <param name="animName">再生するアニメーション名</param>
/// <param name="isLoop">ループさせるかどうか</param>
private void PlayAnimation(int trackIndex, string animName, bool isLoop)
{
// アニメーション「testAnimationName」を再生
TrackEntry trackEntry = spineAnimationState.SetAnimation(trackIndex, animName, isLoop);
// 完了通知を取得準備
trackEntry.Complete += OnSpineComplete;
}
/// <summary>
/// Spineアニメーションが完了した時の処理
/// </summary>
/// <param name="trackEntry"></param>
private void OnSpineComplete(TrackEntry trackEntry)
{
// メイントラック以外は処理を中断
if (trackEntry.TrackIndex != mainTrackIndex) return;
spineAnimationState.SetAnimation(mainTrackIndex, testAnimationNameAfter, true);
}
/// <summary>
/// スキンを変更する
/// </summary>
/// <param name="skinName">スキン名</param>
private void SetSkin(string skinName)
{
skeletonAnimation.skeleton.SetSkin(skinName);
skeletonAnimation.skeleton.SetSlotsToSetupPose();
}
}
/// <summary> 切り替えテスト用スキン名 </summary>
[SerializeField]
private string testSkinName = "skin_sad";
testSkinNameで切り替えるスキン名を設定します。
スキン名はSpineで設定されたものになります。
UnityではSkeletonAnimationのInitialSkinで確認できます。
private void Update()
{
// Cキーの入力でアニメーションを重ねるテスト
if (Input.GetKeyDown(KeyCode.C))
{
SetSkin(testSkinName);
}
}
/// <summary>
/// スキンを変更する
/// </summary>
/// <param name="skinName">スキン名</param>
private void SetSkin(string skinName)
{
skeletonAnimation.skeleton.SetSkin(skinName);
skeletonAnimation.skeleton.SetSlotsToSetupPose();
}
今回はCキーの入力でスキンの切り替えができる実装にしました。
スキンの切り替えはSkeletonAnimationで行うため、SetSkinにスキンを指定することでskeletonAnimation.skeleton.SetSkin("スキン名");を実行しスキンを動的に切り替えています。
イベント(Event)の取得をスクリプトから行う
Spineにはイベント(Event)という機能があります。
本アプリではその機能を用いて口パクや、ボイスの再生を実行しています。
今回の解説ではテスト用に作成したイベントを取得し、実際の挙動が確認できるものを用意しました。
動画ではSpineでイベントを設定したアニメーションを再生し、スクリプトでイベントを実際に取得しています。
Spineでは、以下の画像のようにイベントが設定されています。
janken_test_eventアニメーションの19フレーム目にOnTestEventというイベントを実行します。
そのイベントは文字列(String)の値として「Test Event」を持っています。
イベントにOnTestEventを作成し、アニメーションの任意のフレームで鍵ボタンを押すことでそのキーにイベントを登録することができます。
/// <summary>
/// Spineアニメーションを再生
/// testAnimationNameに再生したいアニメーション名を記載してください。
/// </summary>
/// <param name="trackIndex">再生するTrackIndex</param>
/// <param name="animName">再生するアニメーション名</param>
/// <param name="isLoop">ループさせるかどうか</param>
private void PlayAnimation(int trackIndex, string animName, bool isLoop)
{
// アニメーション「testAnimationName」を再生
TrackEntry trackEntry = spineAnimationState.SetAnimation(trackIndex, animName, isLoop);
// 完了通知を取得準備
trackEntry.Complete += OnSpineComplete;
// イベントを取得準備
trackEntry.Event += OnTestEvent;
}
/// <summary>
/// Spineのキーに登録されたてテストイベントを実行
/// </summary>
/// <param name="trackEntry"></param>
/// <param name="e"></param>
private void OnTestEvent(TrackEntry trackEntry, Spine.Event e)
{
if (e.Data.Name != "OnTestEvent") return;
Debug.Log(e.String);
}
PlayAnimationでイベントを取得するためにtrackEntry.Event += OnTestEvent;を行います。
OnTestEventの引数にSpine.Eventがあり、そこからSpineで設定した様々なデータの取得が可能です。
上記のスクリプトの中ではe.Data.Nameでイベント名、e.Stringでイベントに登録した文字列データを取得できます。その他、e.Intやe.FloatもSpine上で設定した値の取得に使用することが可能です。
後編終了です。お疲れ様でした!ありがとうございました!
以上でSpine作品をUnityでアニメーションを切り替える解説は終了です。
ここまでご覧いただき、ありがとうございました。