UE4 ではスクリプト言語として Blueprint を使った開発ができます。Visual 言語となっており、画面上に命令を並べて線でつなぐだけで動作フローを作成することができます。
三角マークでつながった白い線が実行順序を表しています。それ以外の丸い終端でつながっている線は、引数や返り値といったデータの流れです。データの型によって線の色が変わります。Blueprint のプログラムはコードではなくグラフと呼ばれます。
高レベルな命令をバッチのように並べるだけでも良いし、Native 言語のように低レベルな命令を駆使して計算やフローを直接記述することもできます。非常に自由度が高くなっており、修正してすぐ Editor 上で実行できるので便利です。
すべて Blueprint だけで記述する必要はなく、C++ と密な連携ができる仕組みになっています。C++ で定義した class を Blueprint で継承したり interface を実装することも可能。命令も C++ で増やせるので、基礎部分やパフォーマンスが必要な処理を C++ で実装し、それらを Blueprint でまとめあげるような役割分担も可能です。
●関数ノードと Pure 関数ノード
白い線で繋いでいくのが通常の関数ノードです。一般的な命令の実行に相当します。線で繋いだ方向に順次呼び出されていき、順番が勝手に入れ替わることはありません。呼び出し回数も白い実行線の接続によって決まります。手続き型言語と同じです。
白い実行線を使わずに、引数と返り値だけを持っているのが Pure 関数ノードです。定義時にプロパティの「純粋」にチェックを入れると Pure 関数になります。C++ で作る場合は UFUNCTION() の中に BlueprintPure を入れます。引数と返り値だけを繋いでいくので、Pure 関数ノードの挙動は関数型言語によく似ています。
C++ 風の書き方に展開すると違いがわかります。通常の関数ノードの例を展開すると下記のとおり。返り値は必ず一時変数を経由して次の命令に渡されます。
int tempA= FuncA();
int tempB= FuncB( tempA, 3 );
PrintInt( tempB );
Pure 関数ノードの場合は、変数を経由しないで引数にそのまま渡しているケースに相当します。B の例を展開すると下記のとおりです。
PrintInt( FuncB( FuncA(), 3 );
Blueprint では返り値を返す関数の場合、通常の関数か Pure 関数か選ぶことができます。プロパティの参照のなど実行順序を気にしなくてもよい場合は、いちいち実行線を繋がなくてもすむ Pure 関数は便利です。
その代わり Pure 関数ノードは実行順番が明確ではなく、呼び出し回数も直感的な見た目と一致しません。そのため副作用を持つ Pure 関数ノードを作る場合は注意が必要です。
例えば下記の例は、深さ優先で考えると A->D->C->B の順序で呼ばれるような気がしますが、実際に走らせてみると FuncA->FuncC->FuncD->FuncB で呼ばれていました。
LogBlueprintUserMessages: [Prog03_2] Pure Func A LogBlueprintUserMessages: [Prog03_2] Pure Func C LogBlueprintUserMessages: [Prog03_2] Pure Func D LogBlueprintUserMessages: [Prog03_2] Pure Func B
Native 変換しても A,C,D,B の順序で呼ばれていることがわかります。
void AProg03_C__pf3730294777::bpf__PureFunc__pf(int32 bpp__A__pf, int32 bpp__B__pf, /*out*/ int32& bpp__R__pf)
{
int32 bpfv__CallFunc_PureFuncA_R__pf{};
int32 bpfv__CallFunc_PureFuncC_R__pf{};
int32 bpfv__CallFunc_PureFuncD_R__pf{};
int32 bpfv__CallFunc_PureFuncB_R__pf{};
bpf__PureFuncA__pf(/*out*/ bpfv__CallFunc_PureFuncA_R__pf);
bpf__PureFuncC__pf(bpp__B__pf, /*out*/ bpfv__CallFunc_PureFuncC_R__pf);
bpf__PureFuncD__pf(bpp__A__pf, bpfv__CallFunc_PureFuncA_R__pf, /*out*/ bpfv__CallFunc_PureFuncD_R__pf);
bpf__PureFuncB__pf(bpfv__CallFunc_PureFuncD_R__pf, bpfv__CallFunc_PureFuncC_R__pf, /*out*/ bpfv__CallFunc_PureFuncB_R__pf);
bpp__R__pf = bpfv__CallFunc_PureFuncB_R__pf;
}
●Pure 関数ノードと呼び出し回数
乱数の値を表示するプログラムを書いてみます。組み込み関数の Random Integer を使います。これは Pure 関数です。同じ Random Integer から値を取り出して 2回表示してみます。
実行結果
LogBlueprintUserMessages: [None] 62 LogBlueprintUserMessages: [None] 23
同じノードから線を引っ張っているにもかかわらず、2回とも異なる値が表示されています。画面上には 1個しか置いていないのに「Random Integer」は2回呼ばれているわけです。
「Random Integer」と同じ機能を持つ、「Pure ではない」通常の関数を作ってみます。
実行結果
LogBlueprintUserMessages: [None] 20 LogBlueprintUserMessages: [None] 20
今度は 2回とも同じ値が表示されており、見た目通り一回しか呼ばれていないことがわかります。
C++ 風の書き方で違いを表現すると下記のとおりです。
Pure 関数の場合
PrintInt( RandomInteger( 100 ) );
PrintInt( RandomInteger( 100 ) );
通常の関数ノードの場合
int tempA= RandomInt( 100 );
PrintInt( tempA );
PrintInt( tempA );
通常の関数は白い線でつながった順番で一度しか実行せず、また返り値は必ず一時変数を経由します。そのため同じ値を何度も参照することが可能です。
Pure 関数ノードの場合は、同じノードでも「通常の関数が結果を参照」した回数だけ毎回呼び出しが行われます。
もし Pure 関数で乱数の結果を再利用したい場合は、自分で変数に保存しておく必要があります。
また、通常の関数ノードの返り値が暗黙の一時変数に束縛されるという性質を利用することもできます。まず、何もしないすダミー関数 TempInt を作っておきます。
引数をそのまま返しているだけでですが、通常関数は返り値が一時変数に束縛されるので何度も参照できるようになります。
この場合も同じ乱数値を表示します。
このように Pure 関数は実行回数が見た目と異なるので、意図しないところで何度も呼び出してしまう可能性があります。負荷の高い命令や副作用のある命令は Pure 関数にしない方が良いでしょう。
●返り値を複数もつ Pure 関数
全く同じように、複数の値を返す Pure 関数も問題になりがちです。下記のように、一度に Position と Rotation を返す Pure 関数 GetPosition を作ります。
返り値を 1度しか参照していないように見えるのですが、この場合も GetPosition 関数は 2回呼ばれます。一度に複数の値をまとめて返した方が一見効率が良さそうな気がしますが、必ずしもそうではありません。
例外もあります。2つの返り値を、同じ命令の引数で同じタイミングで受け取る場合は 1回しか呼ばれません。下記の場合 GetPosition が呼ばれるのは一回だけになります。
構造体の展開も同様です。Blueprint では、構造体を展開して要素ごとに個別に参照することができます。(右クリックメニューから展開できます)
展開した構造体の値を、下記のように別の命令から個別に参照した場合も複数回呼ばれます。
例えば現在時刻を表示する時計のプログラムを作ってみます。現在時刻の取得は組み込み命令の Now です。これは Pure 関数です。
上の例では時、分、秒毎に区切って数字の描画を行っています。Now の結果である 時、分、秒 をそれぞれ一回ずつしか参照していないのに、Now はその都度毎回呼ばれてしまいます。
つまり、時、分、秒の描画時にそれぞれ異なる時刻を見ていることになります。タイミングによっては分を表示した直後に秒が桁上りして、秒だけが先に 00 になる場合があり得えます。表示だけならまだしも、アラームのように時刻を厳密に判定したい場合は問題になるでしょう。
10:05:59 ↓ 10:05:00 ↓ 10:06:00
上のようなケースでは、Now の呼び出しを最初の一回だけにして、予め変数に保存しておく必要があります。
・負荷の高い命令、副作用のある命令は Pure 関数にしない。
・複数の値を返す場合も、Pure 関数を避けた方がよい。
・動的に変化する関数の場合、同じ値を複数参照するには通常の関数にするか事前に保存する。
ちなみにゲームで管理している時間 (tick や GetGameTime) はフレーム単位で固定なので、同一のフレームであれば同じ値です。何度呼び出しても問題ありません。
●同じ Pure 関数ノードが再利用される期間を詳しく調べる
複数値を返す Pure 関数でも、同じ命令の引数から同時に参照される場合は一度しか呼ばれませんでした。この挙動をもう少し詳しく調べてみます。
下記の例は、同一式内の同一引数の複数回参照です。このケースでは Random Integer が一回だけ呼ばれて、必ず同じ値を加算した結果を返します。つまり表示される値は常に偶数です。
次のように Random Integer ノードを 2個置くとそれぞれ 1回ずつ呼ばれます。つまり乱数は 2回生成され、異なる乱数値の和を返します。もちろん表示される値が奇数になる場合もあります。
同じ命令の引数でなくても構いません。下記のように、同じ式であれば異なる場所から参照しても GetPositon2 は一度しか呼ばれません。
↓もちろん 2回書けばそれぞれの GetPosition2 が個別に呼ばれます。
↓1つの加算から GetPosition2 が 2回参照されていますが、同じ式なので一度しか呼ばれません。ただし異なる SetActorLocation から 2回参照されているので、結局 GetPosition2 は合計 2回呼ばれることになります。
↓複数の引数から同じノードを参照しています。式グラフとしては異なりますが、白い実行線を通過する前なので GetPosition2 は一度しか呼ばれません。
まとめると
・同じ式の中では、同じノードの Pure 関数を複数参照しても一度しか呼ばれない。
・白い実行線を通過することでリセットされる。同じノードでも再び Pure 関数が呼ばれる。
また同一の式グラフの中で用いる場合は、Pure 関数のノードは共有してできるだけ同じ場所から線を引いた方が効率が良いこともわかります。メモリも呼び出し回数も節約できます。
もちろんプロパティ参照のような単純な命令では全く気にする必要は全く無いと思います。それでも Now 命令のようにバグの原因となる場合もあるので、挙動はある程度理解しておいた方が良いでしょう。
●通常の関数ノードの実行回数
Pure ではない通常の関数ノードは、白い線の接続で呼び出し回数が明示的に決まると書きましたが例外があります。class object のメソッド呼び出しの場合、Target に複数のオブジェクトを与えると複数回実行を繰り返します。
下記の例では ActorA と ActorB それぞれの PrintActorPosition メソッドを呼び出します。つまりグラフ上のノードは 1つでも 2回実行します。
Target は関数を呼び出す対象のオブジェクトで、C++ の this pointer に相当します。戻り値がある場合、一時格納変数が共有されるので最後に呼ばれた値が有効となるようです。呼び出し順番は不定です。
次回に続きます。
関連エントリ
・4倍速い Ryzen 9 3950X の UE4 コンパイル速度
・UE4 UnrealBuildTool の設定 BuildConfiguration.xml
・UE4 UnrealBuildTool VisualStudio の選択を行う
・UE4 UnrealBuildTool *.Build.cs のコードを共有する