前回の続きです。
・前回: UE4 プログラミング言語 Blueprint (1)
●イベントグラフと関数グラフ
Blueprint 命令の編集画面には 2種類あります。イベントグラフと関数グラフです。
↓イベントグラフの画面。イベント応答、カスタムイベントの定義などを行う。
↓関数グラフの画面。関数単位の定義を行う。
イベントグラフでは、BeginPlay や Tick といった特定のイベントを受け取るだけでなく、自分でカスタムのイベントを作ることもできます。
関数グラフでは、出力を持った関数の定義が可能です。Pure 関数もこちらの画面で作ります。
イベントグラフや関数グラフで定義した命令はどちらもサブルーチンとして呼び出すことができます。呼び出し方に違いはなく、見た目もほぼ同じです。Blueprint 上は機能差がありますが、C++ API から見ると関数もイベントも同じものです。そのため説明中ではイベントのことも含めて関数と呼ぶ場合があります。
カスタムイベントと関数どちらも親クラスの命令をオーバーロード可能で、どちらもインターフェースの実装を行うことができます。イベントは戻り値を返すことができないので、戻り値がある場合は関数になり、無い場合はイベントが優先されるようです。
Blueprint で最初に触るのはイベントグラフなので、イベントグラフに命令を置いていくだけでも十分プログラムを作ることができます。特にイベントグラフの場合は、1つのグラフに複数のイベントのコードを記述できます。関数グラフでは1関数だけなので、複数まとめて俯瞰しながら作っていく場合もイベントグラフの方が便利になります。
●イベントグラフと関数グラフの大きな違い
イベントグラフと関数グラフの一番大きな違いは、プログラムを途中から再実行できるかどうかにあります。イベントグラフは任意の場所で実行の中断と再開ができます。
その代表例が Delay 命令です。Delay は後続の命令の実行を遅らせるためにその場で一時停止しします。
↑ FuncA の 1秒後に FuncB を実行
中断した場合、再開できるように途中の状態をどこかに保存して置く必要があります。一般的な言語のクロージャーだと参照している変数の値がキャプチャされるのですが、Blueprint の場合は最初からローカル変数を排除することで仕組みを簡単にしています。
イベントグラフ内ではローカル変数を使うことができません。変数はすべてメンバ変数になるので、中断しても内容は保持されています。その代わりイベントグラフではリエントラントにしづらく再帰呼び出しにも向いていないことになります。
またイベントは関数と違い戻り値を返すことができません。途中で中断した場合はそのまま呼び出し元に返ってくるので、そのタイミングでは戻り値が確定していないからです。結果が必要な場合は、メンバ変数など別の手段を用いることになります。
関数グラフの場合は途中で止めたり再実行することができません。もちろん Delay 系の命令は使えないことになります。
その代わり一度呼び出せば最後まで実行するので戻り値を返すことができます。同様にローカル変数も利用可能です。インスタンスメモリを増やさないし名前の衝突も防げます。再帰呼び出しも容易です。
●中断の仕組みと Delay 命令
イベントグラフの Delay 命令は一見その場で停止して待っているように見えますが、内部では Event (Latent Action) を登録して即座に呼び出し元に戻ります。Action に登録された後続の命令は、あとから異なるコンテキストで実行されます。
もともと Blueprint の関数やイベントは、後続の命令がない場合に暗黙の Return とみなします。
↑後ろの実行ピンがつながっていないと Return 相当。FuncA → FuncB → FuncC の順で実行する。
Delay も全く同じです。Delay の場合は後続の命令と実行線がつながっていても Return になります。
↑実行順に注意。FuncA → FuncC → 1秒 → FuncB の順で実行する。FuncB は 1秒後に World の Tick から呼ばれる。
一部のフロー制御命令は関数呼び出しに似た働きを内包しています。下記は Sequence ノードの例です。
↑FuncB のあと、暗黙の return で次の実行ピン(Then 1)へつながる。FuncA → FuncB → FuncC の順で実行する。
Sequence は複数繋いだ命令実行線を順番に実行します。それぞれの実行線は関数呼び出しと同じ暗黙の Return で終了しています。そのため Delay も Sequence の実行を終了する命令として機能します。
下記のように Sequence を使っても順番に時間待ちをしながら実行することはありません。Delay 命令のたびにすぐ次の実行ピンに飛ぶからです。3つの実行インスタンス FuncA, FuncB, FuncC を同時に Action に登録することになります。(「Sequence の例 A」)
↑ 1秒経ってから同じタイミングで FuncA, FuncB, FuncC が呼ばれる
この Sequence の挙動をうまく活用すると、Return の代わりに即時実行線を持った新しい Delay 命令を作ることができます。↓はマクロ “Delay2” です。
この Delay2 ↑ は暗黙の Return を行いません。Delay の直後に Then 1 を続けて実行するからです。そのため Delayed を Action に登録したあと Immediate 以降を続けて実行します。実行線が 2分岐しているので、Delay は新しい実行インスタンスを作り出す命令に相当します。
Delay2 で置き換えてみると、既存の命令の挙動もより理解しやすくなります。例えば上に挙げた「Sequence の例 A」は↓次のように Delay2 で置き換えることができます。すべて即時実行線でつながっていることになります。
↓もし順番に時間待ちするような Sequence を作りたいなら、次のように実行線の接続先を変更すればよいわけです。
試しに、上と同じ働きをする Sequence を MultiGate で作ってみます。↓マクロ “Sequence2” です。
使用例
↑これで 1秒待ちながら 1秒 → FuncA → 1秒 → FuncB → 1秒 → FuncC の順番で実行するようになります。ただし元に戻る線が必要なのであまり使いやすくありません。Sequence 本来の良さが失われてしまうため、これだけだとあまり意味がないかもしれません。
● Loop と非同期処理
Sequence だけでなく、関数呼び出しに似た構造を持っているフロー命令は他にもあります。ForLoop や ForEach などのループ命令です。それもそのはずで、ループ命令はマクロで定義されており内部で Sequence が使われています。
一回のループは暗黙の return で終了します。よって Delay も「即座に次のループに進む命令」として機能することになります。時間待ちしながらループするような処理を作ることができません。
↑ループ自体は同一フレームで完了し、FuncA → FuncC を呼び出したあと、1秒後 に FuncB が「一回だけ」呼ばれます。Delay はインスタンスのノード毎に Action を多重登録できない仕組みなので、ループ回数に依存せず FuncB は一度しか呼ばれません。(Sequence の例では Delay ノードが 3個あったので 3回呼ばれます)
↓Sequence2 と同じ方法で Delay 待ちしながら loop 実行できる命令を作ってみます。マクロ “ForLoopWithBreak2”
↑これを使うと時間待ちしながら Loop できるようになります。
使用例
↑ FuncA → 1秒 → FuncB → 1秒 → FuncB → 1秒 → FuncB → FuncC
LoopBody の最後を必ず Next pin に繋げる必要があります。Sequence2 と違い、この手法はいろいろ応用できます。
中断機能付き Delay を作ってみます。
Delay のように時間待ちしますが、内部ではポーリングして条件が成立したら即時終了します。例えば何らかのボタンを押したらスキップして次に進むようなイメージです。上の例では 10秒待ってから FuncB に進みますが、その間に [Q] key が押されるとすぐに FuncC を実行します。
中身は下記の通りです。↓マクロ “Delay3″。
もう少しシンプルにして、async/await のような非同期待ち命令を作ってみます。ポーリングなので効率は良くないですが、一連の非同期処理を並べて書けるようになります。↓マクロ “Await”
Interval ごとに Exp をポーリングし、結果が True になるまで待ちます。True になった時点で Completed に進みます。途中で Break が True になった場合は Breaked の方に進みます。何かを待ちながら処理を継続するようなケースを簡単に書くことができます。
必要になることは無いと思いますが、例えば RPC でサーバーを呼び出したあと結果を待つなど。(RepNotify OnRep の代わり)
例えば Actor が目的地まで移動し、その後アニメーションを再生、最後にアニメーション流しながら音声が終わるのを待つような処理を考えます。時間のかかる処理を順番に実行しますが、下記のように直列に書けます。
動作効率は良くないですが、パフォーマンスよりも見た目でわかりやすい方を優先したい、といった場合に使えます。C++ だけだとこういうコードを書けないので、スクリプト言語らしくて面白いと思います。
●イベントと Callback
イベントグラフでは、イベントノードから赤い線を引っ張ることで関数オブジェクトを渡せるようになります。登録したイベントは Callback として呼び出されます。
下記の例では TimerEvent (FuncA) が一定時間ごとに呼ばれます。
本来想定されている非同期処理のしかたはポーリングではなくこちらです。イベントグラフで複数のイベントをまとめて定義できる理由もわかります。
Event Dispatcher を使えば、Blueprint から Callback 関数を呼び出すこともできます。下記は “Delegate” という名の Event Dispatcher に Delegate_Event を登録し、直後に呼び出しています。
↑ FuncA → FuncB → FuncC の順で実行
Event Dispatcher は C# でいう delegate のことです。UE4 でも C++ API では Delegate と呼ばれています。Blueprint の Event Dispatcher は UE4 C++ API の Dynamic Multicast Delegate に相当します。Event Dispatcher はインターフェースが一致していればよいので、呼び出し側が相手の class の詳細を知っている必要がありません。参照の依存を断ち切る働きもあります。
順番に非同期待ちを行う例に Event Dispatcher を使ってみます。各動作が最後に NextAction を呼び出しているものとします。
↑最後は 2つの動作を待っているため NextAction が2回呼ばれるのを待っています。
Dispatcher への登録が長いのでマクロで置き換えてみます。また、指定回数呼ばれてから次に進むノードも作ってみます。
↓マクロ “BindEventM”
↓マクロ “CounterMacro”
↓マクロを使ってシンプルになりました。
同じように NextEvent を待つ Await3 も作ってみます。↓マクロ “Await3”
使用例
こちらもある程度簡単にできました。さらに回数のカウントも自動化できそうです。Blueprint も色々工夫できるので面白いのではないかと思います。
次回に続きます。
関連エントリ
・UE4 プログラミング言語 Blueprint (1)
・4倍速い Ryzen 9 3950X の UE4 コンパイル速度
・UE4 UnrealBuildTool の設定 BuildConfiguration.xml
・UE4 UnrealBuildTool VisualStudio の選択を行う
・UE4 UnrealBuildTool *.Build.cs のコードを共有する