はじめてのScriptPlayable
おひさしです。ちょびえです。今回もUnityのお話でPlayableGraphのScriptPlayableについて書いてみようと思います。
本当はUnity2017で入ったTimelineの話を書きたいんですが、Timelineを細かく調整するにあたってPlayableGraph, ScriptPlayableは避けても通れない機能です。
ScriptPlayableは機能単体でみるととても地味なのですが、わからないまま使うと結構危ういところもあり、マニュアルやサンプルも少なめなのでScriptPlayableを使ったPlayableGraphを単体で実行できる所までを書いてみようと思います。めっちゃ地味なんですがTimeline関連の自前で作る人にはいろいろ参考になるかと。
ScriptPlayableとは
Unity2017.1のScript Referenceを見てみると…
はい。よくわからないですね。dllの中身を確認するとScriptPlayableは
1 |
public struct ScriptPlayable<T> : IPlayable, IEquatable<ScriptPlayable<T>> where T : class, IPlayableBehaviour, new() |
ということなので、ScriptPlayableはPlayableBehaviourのガワ、と思ってもらえればOKかと。
PlayableAPIがNativeで作成されている関係上こうなっているのだと思います。
ScriptPlayableを使うことでユーザー定義の振る舞いを定義できる機能でTimelineなどを扱う場合はきちんと把握しておくべき機能となります。
PlayableBehaviourはIPlayableBehaviour, ICloneableを実装するクラスで下記メソッドが期待されます。
◆開始/終了
OnGraphStart グラフの再生時に呼ばれる
OnGraphStop グラフの停止時に呼ばれる
◆生成/破棄
OnPlayableCreate 生成時のコールバック
OnPlayableDestroy 破棄時のコールバック
◆実行時
OnBehaviourPause Playableの状態がポーズになったら呼ばれる
OnBehaviourPlay Playableの状態がPlayになったら呼ばれる
PrepareFrame ProcessFrameより前にPlay状態のNode全体で再帰的に呼ばれる
ProcessFrame PrepareFrameより後にPlay状態のNode全体で再帰的に呼ばれる
という感じです。
ScriptPlayableのPlayableGraphを作る(1Playableのみ)
細かい話はおいといて、さくっとScriptPlayableだけのPlayableGraphを作ってみます。
まずはSampleBehaviour.csを作成します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 |
using UnityEngine; using UnityEngine.Playables; public class SampleBehaviour : PlayableBehaviour { public string Name; public override void OnGraphStart(Playable playable) { Debug.LogFormat("{0} start: {1}",Time.frameCount, Name); } public override void OnBehaviourPause(Playable playable, FrameData info) { Debug.LogFormat("{0} pause: {1}", Time.frameCount, Name); var count = playable.GetInputCount(); // 子の状態まで停止してくれないので好みによって止めてもよいと思う for (var i = 0; i < count; i++) { var input = playable.GetInput(i); input.SetPlayState(PlayState.Paused); } } public override void OnBehaviourPlay(Playable playable, FrameData info) { Debug.LogFormat("{0} play: {1}", Time.frameCount, Name); } public override void OnGraphStop(Playable playable) { Debug.LogFormat("{0} stop: {1}", Time.frameCount, Name); } public override void OnPlayableCreate(Playable playable) { Debug.LogFormat("{0} create: {1}", Time.frameCount, Name); } public override void OnPlayableDestroy(Playable playable) { Debug.LogFormat("{0} destroy: {1}", Time.frameCount, Name); } public override void PrepareFrame(Playable playable, FrameData info) { Debug.LogFormat("{0} prepare: {1}, {2}", Time.frameCount, n, info.frameId); } public override void ProcessFrame(Playable playable, FrameData info, object playerData) { Debug.LogFormat("{0} process: {1}", Time.frameCount, Name); } } |
つぎにテスト用のコードを作ります。まずは一つのPlayableだけを持つPlayableGraphを作ります
SamplePlayable.cs
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 |
using UnityEngine; using UnityEngine.Playables; public class SamplePlayable : MonoBehaviour { private PlayableGraph graph; // Use this for initialization void Start() { graph = PlayableGraph.Create(); // 第二引数はInputの数。後でも設定できます // PlayableBehaviourはScriptPlayable<T>.Createを使って作ることを期待しているので // 直接作らないように注意しましょう。 var root = ScriptPlayable<SampleBehaviour>.Create(graph, 0); // SampleBehaviourにアクセスする場合はGetBehaviour()を使います root.GetBehaviour().Name = "root"; // PlayableGraphのOutputに追加するためにScriptPlayableOutputを作成し // SetSourcePlayableで作成したPlayableをセットします。所謂Rootノードみたいなもんです。 var output = ScriptPlayableOutput.Create(graph, "output"); output.SetSourcePlayable(root); output.SetReferenceObject(gameObject); // 準備ができたのでPlayをコールします。 // Manual以外の場合はUnity側で実行されるのでUpdate中に呼ばなくてもOKです。 graph.Play(); } private void OnDestroy() { if (graph.IsValid()) { // graphは必ずDestroyして開放してあげる必要がある graph.Destroy(); } } } |
ScriptPlayableOutputは2017の現状だとScriptPlayableを持つOutputという目印です。
適当なGameObjectにSamplePlayableをAdd Componentして再生してみるとこんなログがでます。
1Playableだけだとあんまりおもしろくないですが、こんなもんです。
ScriptPlayableのPlayableGraphを作る(親子)
続いて親子関係を持ったPlayableを作ってみます。
親
子 子
というかんじのグラフです。
SamplePlayable2.cs
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 |
using UnityEngine; using UnityEngine.Playables; public class SamplePlayable2 : MonoBehaviour { private PlayableGraph graph; // Use this for initialization void Start() { graph = PlayableGraph.Create(); // 第二引数はInputの数。後でも設定できます // PlayableBehaviourはScriptPlayable<T>.Createを使って作ることを期待しているので // 直接作らないように注意しましょう。 var root = ScriptPlayable<SampleBehaviour>.Create(graph, 2); // Inputは2という指定をしてることに注目 // SampleBehaviourにアクセスする場合はGetBehaviour()を使います root.GetBehaviour().Name = "root"; // PlayableGraphのOutputに追加するためにScriptPlayableOutputを作成し // SetSourcePlayableで作成したPlayableをセットします。所謂Rootノードみたいなもんです。 var output = ScriptPlayableOutput.Create(graph, "output"); output.SetSourcePlayable(root); output.SetReferenceObject(gameObject); // 子Playableを作成します。今回の子はLeafノードにあたるのでInputCountは0を指定します。 var child1 = ScriptPlayable<SampleBehaviour>.Create(graph, 0); child1.GetBehaviour().Name = "child1"; var child2 = ScriptPlayable<SampleBehaviour>.Create(graph, 0); child2.GetBehaviour().Name = "child2"; // Playableを接続します。 // 子, 子Output番号 親, 親Input番号となるので注意しましょう。 // OutputやInputは先にどれくらい設定できるか指定しないとエラーとなってしまうので注意 graph.Connect(child1, 0, root, 0); graph.Connect(child2, 0, root, 1); // 準備ができたのでPlayをコールします。 // Manual以外の場合はUnity側で実行されるのでUpdate中に呼ばなくてもOKです。 graph.Play(); } private void OnDestroy() { if (graph.IsValid()) { // graphは必ずDestroyして開放してあげる必要がある graph.Destroy(); } } } |
こんな感じ。すべてのPlayableが実行されています。
SetTime, SetDurationをつかってみる
IPlayableを実装した項目はPlayableExtensionsの拡張メソッドが使えます。
SamplePlayable3.cs
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 |
using UnityEngine; using UnityEngine.Playables; public class SamplePlayable3 : MonoBehaviour { private PlayableGraph graph; // Use this for initialization void Start() { graph = PlayableGraph.Create(); // 第二引数はInputの数。後でも設定できます // PlayableBehaviourはScriptPlayable<T>.Createを使って作ることを期待しているので // 直接作らないように注意しましょう。 var root = ScriptPlayable<SampleBehaviour>.Create(graph, 2); // Inputは2という指定をしてることに注目 // SampleBehaviourにアクセスする場合はGetBehaviour()を使います root.GetBehaviour().Name = "root"; // PlayableExtensionsを使ってPlayableの時間と長さをセットします。 root.SetTime(0); root.SetDuration(1.0); // PlayableGraphのOutputに追加するためにScriptPlayableOutputを作成し // SetSourcePlayableで作成したPlayableをセットします。所謂Rootノードみたいなもんです。 var output = ScriptPlayableOutput.Create(graph, "output"); output.SetSourcePlayable(root); output.SetReferenceObject(gameObject); // 子Playableを作成します。今回の子はLeafノードにあたるのでInputCountは0を指定します。 var child1 = ScriptPlayable<SampleBehaviour>.Create(graph, 0); child1.GetBehaviour().Name = "child1"; var child2 = ScriptPlayable<SampleBehaviour>.Create(graph, 0); child2.GetBehaviour().Name = "child2"; // PlayableExtensionsを使ってPlayableの時間と長さをセットします。 // 子は試しにdurationを変えておきましょぅ。 child1.SetTime(0); child1.SetDuration(0.5); child2.SetTime(0); child2.SetDuration(1.0); // Playableを接続します。 // 子, 子Output番号 親, 親Input番号となるので注意しましょう。 // OutputやInputは先にどれくらい設定できるか指定しないとエラーとなってしまうので注意 graph.Connect(child1, 0, root, 0); graph.Connect(child2, 0, root, 1); // 準備ができたのでPlayをコールします。 // Manual以外の場合はUnity側で実行されるのでUpdate中に呼ばなくてもOKです。 graph.Play(); } private void OnDestroy() { if (graph.IsValid()) { // graphは必ずDestroyして開放してあげる必要がある graph.Destroy(); } } } |
きっとこれで実行するといい感じに指定時間、長さでやってくれるんだろうな、と思いますがそんなことはなく。
Time,Durationはあくまで参考情報なので自前で制御する必要があります。
SampleBehaviourのPrepareFrameを改変します。とりあえず動けばいいので適当です。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
public override void PrepareFrame(Playable playable, FrameData info) { var time = playable.GetTime(); var count = playable.GetInputCount(); var stop_count = 0; // とりあえず動けばいいので雑に for (var i = 0; i < count; i++) { var input = playable.GetInput(i); var duration = input.GetDuration(); var begin = input.GetTime(); if (time >= begin && time < begin + duration) { Debug.LogFormat(" {0} playing", input); input.SetPlayState(PlayState.Playing); } else { Debug.LogFormat(" {0} paused", input); stop_count++; input.SetPlayState(PlayState.Paused); } } // count 0 (leaf node)だとそのままとまっちゃうので制限を入れとく if (count > 0 && stop_count >= count) { playable.SetPlayState(PlayState.Paused); } } } |
これで親子でTimeとDurationを参照したものが期待した通り動きます。
まとめると
PlayableBehaviourはマイクロな振る舞いを定義してPlayableGraphで親子関係などが作れます。
子PlayableのBlendingやTimelineのような管理などを行う場合はMixerPlayableなどの子をラップするPlayableを作成して自前でブレンドのウェイトの計算や、Play/Pauseの管理を行っていくことで任意の挙動を作ることができます。
PlayableGraphの利点としては、きちんとPlayableAssetのScriptableObjectを作ってさえいればPlayableDirectorに渡してあげるだけでどんな振る舞いもさせられるというところです。逆に言うと細かい制御とかは難しくなると思いますが、ケースバイケースということで。
また後日Timeline関連についても時間があれば書こうと思いますが、ちょっとだけ書いておくとTimelineの中身としては大体PlayableでTrack、個別Mixer、個別Playableがそれぞれ自前のやるべきことやっていい感じに動かしている、と認識しておくとよいです。
(Mixer関係のBlendingはわりと重要なので先に抑えておかないと増えた後に直すのはとても面倒です)
簡易的にPlayableBehaviourを追加する場合はPlayableTrackに突っ込むケースが多いかと思いますが、これだと書くコード事態は少ないけど個別バインドをしないといけなくなってしまいます。
個人的なお勧めはTrackAsset,MixerBehaviour, PlayableBeahviour, PlayableAsset4種をきちんと作りきってやっていくのが良いかと思います。
ここらへんはDefault Playables を読むのが一番確実なので参考にするとよいです。
補足
PlayableBehaviourでInputを取得した際にどうやってPlayableBehaviourにアクセスするかというと
こんな感じで、キャストしてGetBeavhiour()をコールすればアクセスできます。
1 2 |
var v = (ScriptPlayable<SimpleMotionBehaviour>)playable.GetInput(0); Debug.Log(v.GetBehaviour().Name); |
ダイナミックにバインディングを変える場合や細かい操作をする場合は必須になるので覚えておくとよいです。