YAMADA TAISHI’s diary

ゲームについてとか私の日記とか。このブログのあらゆるコードは好きにどうぞ。利用規約があるものは記事内のGitHubのRepositoryのリンクで貼られていると思うので、そちらを参照ください。

【アドカレ】Timelineの拡張をわかりやすく解説してみる

こんにちは、やまだたいし( やまだ たいし (@OrotiYamatano) / Twitter )です。
本記事はUnityゲーム開発者ギルドアドベントカレンダー2の22日目の記事です。

目次


UnityのTimeLineの拡張について


UnityのTimeLineの拡張をしたので分かりやすく説明をするために本記事を作成しました。

テキストを読み終えてないと TimeLineが停止するエディタ拡張(自作)を
参考にTime Line拡張の書き方の紹介をします。

TimeLineの拡張方法


タイムラインの拡張方法は主に2つあります。

1.Control Track
2.カスタムPlayable Track
です。

Control TrackはインターフェースをMonobehaviorに実装する方法なのですが、
拡張できるメソッドが再生開始、停止時、タイム設定しかありません。

今回はカスタムPlayable Trackについてのご紹介です。

そもそもTimeLineの構造


TimeLineの拡張は主に3つの要素で構成されます。
Track、Mixer、Clipです。

ClipとMixerと聞いて「あれUnityの他の機能でも同じネーミング使ってるところがあるぞ?」と気づかれた方さすがです。

実はUnityのAnimationもAudioも同じような形で保持されています。
Animation Mixer,Animation Clip
Audio Mixer, Audio Clip

実はどちらもUnityのPlayableという機能で実装されています。

Unityのさまざまな時間の関係する拡張機能はPlayableという形で作られています。
Userでも触れるように作ってあるため、ものすごく頑張ればTimeLineのような機能をユーザー自ら作り上げることも可能です。


TimeLineのアセットの中身


厳密なTimelineクラス設計までは見に行ってませんが、大体このような感じで考えれば想像がつきやすいと思います。

組み合わせとしては4つのクラスでTimeLineは構成されています。
(厳密にはGroupTrackというのがあったりするのですが割愛)

1.Trackの構造を表すPlayableAsset
2.Trackの再生方法を表すPlayableBehaviour
3.Clipの構造を表すPlayableAsset
4.Clipの再生方法を表すPlayableBehaviour
です。

今回は仮にそれぞれ
1.PlayableTrack
2.PlayableMixer
3.PlayableClipAsset
4.PlayableClipBehaviour
と呼ぶことにします。

PlayableTrack


まずPlayableTrackScriptableObjectを元に作られています。
[ScriptableObject→PlayableAsset→Track Asset→(そしてユーザーがoverrideして作る)PlayableTrack]

そこに Playable という struct を持ちます。
これが Mixer です。 MixerPlayableBehaviour を元にして作られます。

(mixerタイポってる……)

PlayableTrack 自体はTrackを表すのでSerializeFieldに変数を実装するとインスペクターに設定項目が現れます。

ちなみにBaindingsの設定もPlayableTrackアトリビュートを設定することでアタッチ出来るものを設定できます。

PlayableMixer


PlayableTrack にてCreateTrackMixer という関数をオーバーライドしてScriptPlayableCreateの処理を行いMixerを作ります。

ScriptPlayableCreateの処理にてMixer の元になるクラスを渡しています。
PlayableMixerPlayableBehaviourを継承…オーバーロードで作ります。

PlayableMixerはインスペクターから設定できる箇所がないので、
Mixerで色々参照したい時は基本的にCreateTrackMixerをする際にTrackから値を貰ってきます。

PlayableMixermixingを行うクラスです。
Clip同士のMixを行います。
また、Clipのクラスではクリップ内の再生制御ができないのに対し、Mixではクリップ外の制御ができます。

(↓こんな感じで表示される)

PlayableClipAsset・PlayableClipBehaviour


PlayableClipAssetPlayable Track同様Playable Assetにて構成されています。
PlayableClipTrack Asset(Playable Track)に配列で保持されます。

PlayableClipBehaviourPlayableMixerとほぼ同様です。
ScriptPlayableCreateの処理にてClip の元になるクラスを渡しています。
PlayableBehaviourを継承…オーバーロードで作られます。

Clipではクリップ内の再生制御しかできないので、
基本的には処理内容はPlayableMixerに記述しPlayableClipAssetPlayableClipBehaviourでは動作に最低限必要な内容を記載します。

PlayableClipAssetにはインスペクターの内容が記述可能です。


実際にスクリプトを書いてみる


実際に私が作ったスクリプトを簡単に解説しようと思います。

PlayableTrack


Trackの構造を表すPlayableAsset

Script


NobelPlayableTrack.cs

using UnityEngine;
using UnityEngine.Playables;
using UnityEngine.Timeline;

namespace SimpleNovel.TimeLine
{
    [TrackColor(0.05f,1.00f,0.71f)]
    [TrackClipType(typeof(NobelPlayableClip) )]
    [TrackBindingType(typeof(GameObject))]
    public class NobelPlayableTrack:TrackAsset
    {
        public GameObject novelPrefab;
        public override Playable CreateTrackMixer(PlayableGraph graph, GameObject go, int inputCount)
        {
            var mixer = ScriptPlayable<NobelPlayableMixer>.Create(graph,inputCount);
            mixer.GetBehaviour().NovelPrefab = novelPrefab;
            return mixer;
        }
    }
}

Sctiptについての解説


ここでは先ほども解説した通りTrackの構造定義とPlayableMixerの生成を行います。
CreateTrackMixer にて直接クリップを取得してくることも可能です。

今回はミキサーにてテキスト表示のアセット基本となるPrefabを前もってPlayable へ渡しています。
PlayableClipAssetPlayableTrack の紐付けはアトリビュートに書くことで紐付けができます。

PlayableMixer


Trackの再生方法を表すPlayableBehaviour

Script


NobelPlayableMixer.cs

using Cysharp.Threading.Tasks;
using SimpleNovel.NovelEvent;
using UnityEditor;
using UnityEngine;
using UnityEngine.Playables;

namespace SimpleNovel.TimeLine
{
    public class NobelPlayableMixer:PlayableBehaviour
    {
        public GameObject NovelPrefab;
        private GameObject _novelParent;
        private GameObject _tempObject;
        private NovelEventMediator _mediator;
        private int _activeIndex = -1;

        public override void ProcessFrame(Playable playable, FrameData info, object playerData)
        {
            _novelParent = (GameObject)playerData;
            if (_activeIndex >= 0)
            {
                var inputPlayable = playable.GetInput(_activeIndex);
                if (inputPlayable.GetPlayState() == PlayState.Paused && inputPlayable.GetTime() > 0f)
                {
                    //何もしない
                }
            }

            var presented = false;
            var inputCount = playable.GetInputCount();
            for (var i = 0; i < inputCount; i++)
            {
                var inputWeight = playable.GetInputWeight(i);
                if (!(inputWeight > 0f)) continue;
                if (_activeIndex != i)
                {
                    _activeIndex = i;
                    var inputPlayable = (ScriptPlayable<NobelDataBehaviour>)playable.GetInput(i);
                    NobelDataBehaviour input = inputPlayable.GetBehaviour ();
                    BeginClip(_activeIndex,input);
                }
                presented = true;
                break;
            }

            if (presented || _activeIndex < 0) return;
            // クリップのないフレーム
            EndClip(_activeIndex,playable);
            _activeIndex = -1;
        }


        private void BeginClip(int clipNum,NobelDataBehaviour behaviour)
        {
            
#if UNITY_EDITOR  
            if (!EditorApplication.isPlaying)
            {
                return;
            }
#endif
            if (NovelPrefab == null) {
                return;
            }
            _tempObject = Object.Instantiate(NovelPrefab, _novelParent.transform);   //ノベルを作成
        
            //ノベルの値設定スクリプトに値を入れていく
            _mediator = _tempObject.GetComponent<NovelEventMediator>();
            _mediator.luaScript = behaviour.luaScript;
        }

        private async void EndClip(int clipNum, Playable playable)
        {
#if UNITY_EDITOR  
            if (!EditorApplication.isPlaying)
            {
                return;
            }
#endif
            playable.GetGraph().GetRootPlayable(0).SetSpeed(0); //TimeLineの再生を止める
            await UniTask.WaitUntil(() => _mediator != null && _mediator.GetPlayed());      //テキスト読んでる途中?
            
            if (_tempObject != null){
                Object.Destroy(_tempObject);    //ヒエラルキーに残りっぱなしは嫌なので消しとく
            }
            playable.GetGraph().GetRootPlayable(0).SetSpeed(1); //TimeLineの再生を再開
        }
        
        
        public override void OnGraphStop(Playable playable)
        {
            if (_tempObject != null){
                Object.Destroy(_tempObject);    //ヒエラルキーに残りっぱなしは嫌なので消しとく
            }
        }
    }
}

Sctiptについての解説


ここではTrackの再生制御、Mixを行います。
各Clipの初回フレームにてInput情報を取得しClipに渡しています。
ちなみにluaSctiptはテキストのスクリプトです。

そして、そのままInstantiateを行いテキスト再生を開始します。
Clipの最終フレームにてテキスト送りが終わっていなかった場合にはTimeLineのスピードを0にしUniTaskを使って処理待ちしています。
テキスト送りが終わり次第TimeLineのスピードを元に戻します。

注意点としてはTimeLineの普通の再生でもInstantiate が行われてしまうのでクローンobjectばかりになることです。
そうならないように通常再生でなければ処理を中止させるように条件付きコンパイルを追加しています。
また、再生途中でエディタ停止しても残り続けるのでClipから出る時とOnGraphStop にてゲームオブジェクトをDestroyしています。

PlayableClipAsset・PlayableClipBehaviour


Clipの構造を表すPlayableAsset(PlayableClipAsset)
Clipの再生方法を表すPlayableBehaviour(PlayableClipBehaviour)

Script


NobelPlayableClip.cs

using System;
using UnityEngine;
using UnityEngine.Playables;
using UnityEngine.Timeline;

namespace SimpleNovel.TimeLine
{
    [Serializable]
    public class NobelDataBehaviour : PlayableBehaviour
    {
        [SerializeField]
        public TextAsset luaScript;
        
        public override void OnBehaviourPlay(Playable playable, FrameData info)
        {
            //うまく動かないことがあるのでミキサーで処理を行う
        }
    }
    public class NobelPlayableClip:PlayableAsset,ITimelineClipAsset
    {
        [SerializeField]
        private NobelDataBehaviour template;
        public override Playable CreatePlayable(PlayableGraph graph, GameObject owner)
        {
            return ScriptPlayable<NobelDataBehaviour>.Create(graph, template);
        }

        public ClipCaps clipCaps => ClipCaps.None;
    }
}

Sctiptについての解説


ここではClip内の再生制御(NobelDataBehaviour)とClipのデータ構造定義(NobelPlayableClip)です。

テキスト制御のluaSctiptを持っているだけで特に何もしていません。
強いていうなら、ClipのMixタイプの設定としてClipCaps.Noneを設定しているくらいです。

スクリプトについて総括


細かいことを言えばMarkerなどもあり他にも色々なことが出来るのですが、基本としてはこれで十分なはずです。

一見難しそうに思えるかもしれませんが、ルールに紐づいて
1.Trackの構造を表すPlayableAsset
2.Trackの再生方法を表すPlayableBehaviour
3.Clipの構造を表すPlayableAsset
4.Clipの再生方法を表すPlayableBehaviour
を作ればいいだけです。

気を付けるポイントとしてはクラス名が一部スクリプト名と合っていないとうまく挙動しないことでしょうか。
まぁ難しいことではないので、ぜひ皆さんもいろんな拡張を作ったら共有していただきたいです!
皆さんもレッツ拡張!!

まとめ


という感じでTimeLineの制御について書かせていただきました。
アドカレとしては大遅刻ですが、悪くない記事になったのではないかと思います。
最近はUnity公式からanime toolboxも公開されますますTime Lineの拡張を求められることが多くなってくると思います。
この記事が皆さんのお力になればと思います。

参考記事


www.youtube.com