LoginSignup
10
2

More than 3 years have passed since last update.

UnityTimelineを使ってSpineを動かす

Last updated at Posted at 2019-12-16

本記事は、サムザップ Advent Calendar 2019 #1の12/17の記事です。

今回は、同じ CyberAgentグループ である 株式会社ジークレスト の新作アプリ「星鳴エコーズ」のバトル部分に導入したUnityTimeline×Spineの実装事例を紹介します。

星鳴エコーズについて

メインターゲットは女性向きですが、世界観やストーリーが作り込まれており、男性でも楽しめるRPGです。
ラノベとか好きな方には刺さるかも(?)

公式サイト

ゲーム画面

ss1.png ss2.png
ss3.png ss4.png

実装要件

以下の要件のもと、新しく仕組みやツールを作る工数ももったいないということでUnity標準機能のTimelineを使いました。

  • 2Dアニメーション(spine)
  • エフェクト・SEを含む一連のアニメーションカットをつくりたい
  • クリエーターだけで実装したい

Timelineの詳細な使い方や基本操作は 公式リファレンス や他の方の記事を見てください。

TimelineでSpineを動かしてみる

Spineのバインド

Spineには、Timeline用のトラックとして「Spine Animation State Track」「Spine Skeleton Frip Track」の二つが用意されています。
「Spine Animation State Track」はアニメーション用、「Spine Skeleton Frip Track」はモデルの反転描画用に使用します。
timelineEditor.png
使い方は、簡単です。
1. TimelineのエディタGUIメニュー「Spine.Unity.Playables」から「Spine Animation State Track」を選択。
2. タイムラインインスタンスにSpineオブジェクト(SkeletonAnimationコンポーネントがアタッチされているもの)を設定。
3. トラック上で右クリック。メニューから「Add Spine Animation State Clip」を選択。
timelineInspector.png
4. 追加したクリップのインスペクタから、「Animation Reference」プロパティにアニメーションを指定。
5. 同様に3~4を複数設定して、一連のアニメーションを完成させます。

以上です。
(さらに、このあたりの設定を自動生成する自作ツールも作りましたが、長くなるので今回は割愛で。)

躓きポイント

デフォルト設定で動かすと、クリップとクリップのつなぎのタイミングで謎なアニメーションフレームがありました。
これはSpineがアニメーション補完してくれているためで、もしその補完をやりたくない場合は、インスペクタの「Custom Duration」プロパティをチェックオフに、「Mix Duration」をゼロに設定すると良いです。

自作トラックを追加

Timelineの各種設定を自作するには以下4つの継承クラスが必要となります。
これらを使って、タイムラインインスタンス(下記例ではInstanceObjectクラス)に、指定したパラメータを渡しイベント発火を行えます。
複雑な処理やデータキャッシュを考えると、Timeline側では処理ロジックは書かず、発火のみ行った方が良いです。

MyPlayableBehaviour.cs
/// <summary> クリップの振る舞い </summary>
[Serializable]
public class MyPlayableBehaviour : PlayableBehaviour
{
    [Header("再生ID")]
    public int PlayId;

    public override void OnGraphStart(Playable playable)
    {
    }
}
MyClip.cs
/// <summary> クリップの定義 </summary>
[Serializable]
public class MyClip : PlayableAsset, ITimelineClipAsset
{
    public MyPlayableBehaviour template = new MyPlayableBehaviour(); // パラメータ

    public ClipCaps clipCaps => ClipCaps.None;

    public override Playable CreatePlayable(PlayableGraph graph, GameObject owner)
    {
        return ScriptPlayable<MyPlayableBehaviour>.Create(graph, template);
    }
}
MyMixerBehaviour.cs
/// <summary> トラックの振る舞い </summary>
public class MyMixerBehaviour : PlayableBehaviour
{
    public List<TimelineClip> Clips;

    public override void ProcessFrame(Playable playable, FrameData info, object playerData)
    {
        var trackBinding = playerData as InstanceObject; // タイムラインインスタンス
        if (trackBinding == null)
            return;

        double time = trackBinding.Director.time;
        for (int i = 0; i < Clips.Count; i++)
        {
            var clip = Clips[i];
            var playableInput = (ScriptPlayable<MyPlayableBehaviour>)playable.GetInput(i);
            var input = playableInput.GetBehaviour();
            if (clip.start <= time && time <= clip.end) // クリップ時間のみ再生
                trackBinding.OnEvent(input.PlayId);
        }
    }
}
MyTrack.cs
/// <summary> トラックの定義 </summary>
[Serializable]
[TrackColor(0.2f, 0.63f, 1.0f)] // このあたりはGUIの見た目や制約条件
[TrackClipType(typeof(MyClip))]
[TrackBindingType(typeof(InstanceObject))]
public class MyTrack : TrackAsset
{
    public override Playable CreateTrackMixer(PlayableGraph graph, GameObject go, int inputCount)
    {
        var mixer = ScriptPlayable<MyMixerBehaviour>.Create(graph, inputCount);
        mixer.GetBehaviour().Clips = new List<TimelineClip>(GetClips());
        return mixer;
    }
}}

タイムラインインスタンスの動的な設定

実際のゲーム開発となると、Spineアバターモデルをコード上から設定することもあると思います。
そのような時は、SetGenericBindingを使ってバインドできます。

Sample.cs
void Start()
{
    PlayableDirector timeline = GetComponent<PlayableDirector>();
    SkeletonAnimation spineObject = ResourceLoad(spineName) as SkeletonAnimation;

    // タイムラインインスタンスの設定
    foreach (var playableAssetOutput in timeline.playableAsset.outputs)
    {
        if (playableAssetOutput.streamName == "Spine Animation Track" ||
            playableAssetOutput.streamName == "Spine Animation State Track")
        {
            timeline.SetGenericBinding(playableAssetOutput.sourceObject, spineObject);
        }
    }

    // アニメーションクリップの設定
    TimelineAsset timelineAsset = (TimelineAsset)timeline.playableAsset;
    foreach (var track in timelineAsset.GetOutputTracks())
    {
        if (!(track is SpineAnimationStateTrack))
            continue;
        foreach (var clip in track.GetClips())
        {
            var spineClip = clip.asset as SpineAnimationStateClip;
            var clipName = spineClip.template.animationReference.Animation.Name;
            var animationClip = ResourceLoad(clipName);
            spineClip.template.animationReference = animationClip;
        }
    }
}

まとめ

UnityTimelineの記事はそこそこあるのですが、Spineとの組み合わせ情報が少なく、試行錯誤でした。
どちらも使いやすい機能ですので、今後のゲーム開発にお役に立てたらと思います。
明日は@phasmatodeanです。

10
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
10
2