Let's Enjoy Unreal Engine

Unreal Engineを使って遊んでみましょう

UE4 ブループリントとC++のバランスを深く考える

最近仕事柄UE4でのブループリントとC++のバランスをどうしていくべきかを考えることが多くなってきました。今回はそのことについてもう少し深く掘り下げていきたいと思います。

まず前提としてPCゲームを作る際にはそこまで深く考える必要はありません。PCゲームはほとんどの場合、高性能なCPUが搭載されており、メモリーも8GB以上の環境であることがほとんどです。そのような状況でブループリントとC++がどうとかというのは好みの問題になってくるでしょう。

おさらいとして、ヒストリアさんの以下の記事と自分が以前に書いたUE4のブループリントの記事を紹介しておきます。

[UE4] C++ or Blueprint?|株式会社ヒストリア

UE4 比較的大規模におけるブループリントの運用 - Let's Enjoy Unreal Engine


基本的な考え方はこの時から変わりません。ここから更に一歩踏み込んだ内容の記事がUE4の公式ドキュメントに追加されました。※現在英語のみ

Balancing Blueprint and C++


この辺りの内容を参考にもう少し細かく突っ込んでいきましょう。まず公式ドキュメントでも書かれていますが、ゲームの設計に「正しい答えはない」というのが結論です。ただ、よりよくするための手段はあります。


f:id:alwei:20180818182553p:plain

メリット

メリットについて公式ドキュメントからそれぞれ抜粋します。

C++クラスのメリット

・ランタイムパフォーマンスの向上
・明示的なデザインが可能
・広範なAPIへのアクセス
・多くのデータコントロール
・厳密なネットワークレプリケーション
・より良い数学計算
・差分/マージの容易性


などが挙げられています。

ブループリントクラスのメリット

・高速なクリエイション
・高速なイテレーション
・視覚化されるフロー
・フレキシブルな編集
・簡単なデータの扱い


非常に明確でわかりやすいメリットです。これらを意識した上で次に進んでみましょう。

ロジックとデータ

大規模なゲームを制作するためには、データドリブンという考え方が大事になってきます。ゲームというのは大まかに分けると"ロジック"と"データ"に分けられます。ロジックを表現する時は多くの場合、コードを使うことが多いです。ただし、UE4ではブループリントでもロジックを記述することができます。

データはC++コードでも表現可能ですが、一般的にコード上にデータをベタ書きすることをハードコードと呼び、外部から触ることが不可能になるため、推奨されません。対してブループリントはロジックとデータと同時に扱いつつ、外部からも編集しやすい作りになっています。このことからゲームデザイナーやアーティストからすればブループリントは非常に都合の良い存在となります。

しかしブループリントの変数やプロパティはC++から直接参照することは困難です。そのためにUE4ではC++上で直接データを扱わずに、C++クラスを継承したブループリント内でデータをセットする方式が推奨されています。C++からデータを読み込みたい場合、今のUE4には"Asset Manager"という仕組みがあり、エディター上で定義されたゲームデータアセットから読み込むという方式が推奨されています。この方が非同期読み込みでの対応や必要なアセットを逐一読み込むことができるからです。

Asset Managerについては以下のドキュメントを参考に。※英語

Asset Management


以下も参考になります。

UE4 アセットマネージメントフレームワークについて - Let's Enjoy Unreal Engine

【UE4】AssetManagerを使用したレベルストリームの高速化 | 株式会社アンナプルナ


C++で書く時にはロジックとデータの分離の仕方をしっかりと考えましょう。そうでなければプログラマーが全てのデータを管理するハメになり、デザイナーやアーティストが上手くゲームをコントロールできなくなってしまいます。

パフォーマンス

一般的にブループリントが問題になるのはこのパフォーマンスの部分です。ただしこれはPCゲームではなく、コンソールゲームやモバイルゲームの場合です。PCゲームは高速なCPUやGPU、メモリーが載っていることが多いため、そこまで気にする必要はありません。

「パフォーマンスが問題になるのなら始めからC++で書けばいいのでは?」と、こういう結論になりやすいですが、これは非常に危険です。全員が全員、UE4C++に詳しければ問題がないかもしれませんが、一般的にC++は非常に危険性が高いプログラム言語とされています。ネイティブにアクセスできる分、どんなことでも出来てしまいますが、アプリケーションをクラッシュさせることも容易です。また、多くのルールを理解しなくては上手く使いこなすことはできません。

パフォーマンスが問題になると言いましたが、実際のところブループリント自体はそこまで遅いわけではありません。ブループリントのノードはほとんどの場合C++で直接記述されており、その場合においてはC++コードを呼び出した時とほとんど速度の差はありません。

しかし例えばブループリントから"ForEachLoop"を使う時、これは場合によっては相当ボトルネックになる可能性があります。

f:id:alwei:20180818192035p:plain


このケースの場合、配列要素に対して直接関数を呼びだすことで早くなる可能性があります。

unrealengine.hatenablog.com


またPure関数の場合には更に顕著となり、これはかなりのボトルネックになります。以下の記事のように一度変数にキャッシュするか、専用にキャッシュするマクロを作ってしまうのもありです。

unwitherer.blogspot.com


このように積もり積もって、ブループリントが重いという印象を持たれるケースがあるようです。

またブループリントはTickが多くなれば多くなるほど、遅くなってくるという傾向があります。可能な限りTickをオフにします。

f:id:alwei:20180818193038p:plain


"Start With Tick Enabled"をオフにしていれば、Tick関数が呼び出されなくなります。これはコンポーネントにおいても同様です。デフォルトではオフにしておく方が良いでしょう。処理が重たくなってきたら"Tick Interval(secs)"の値をゼロ以外に設定するのも非常に効果が高いです。

Tickなしにどうやってゲームを作るの?と思われるかもしれませんが、UE4はイベントドリブン(イベントが発生してから処理を開始する考え方)による仕組みが非常に充実しており、Tickがなくても汎用的な仕組みは十分に作ることができます。特にブループリントはイベントドリブンで作りやすいように多くの機能が用意されています。イベントドリブンでしっかりと制作すれば、C++を使っている時と比較してもそこまで大きな速度差は発生しないはずです。

ここまでやってみて。まだブループリントが重い!!ということがあるかもしれません。そういう時はプロファイラーを利用します。

プロファイラ ツール リファレンス


重い場合、まずはどこが重いのかをしっかりと検証しましょう。プロファイラーを使えばどこが重たいのかがわかるはずです。そして重点的に重いと思われる部分を探し、ボトルネックを見つけた時にC++へとコードを変換するようにしましょう。可能であればNativizationツールを利用します。

ブループリントのネイティブ化


ほとんどの場合はこれでC++コードへ変換可能ですが、問題が発生することもあります。手動で修正可能なものであればいいですが、問題範囲が大きければそのままロジックを一部C++側で書いてしまうという方法もあるでしょう。これでパフォーマンスのボトルネックを解消します。

設計的な注意点

以下も公式から抜粋した内容です。そのまま翻訳したものではありません。

高価なブループリントキャストを避ける

BP_BというブループリントからBP_Aという様々アセットを参照するブループリントがあった場合、BP_BからBP_Aへのキャストは避けましょう。複雑な参照を持つことになり、BP_Bを読み込むだけで、BP_Aの参照する情報も全て読み込む必要性が出てきます。これはC++で定義したネイティブベースクラスか最小限のブループリントベースクラスを作成しておくことで、キャスト時のコストを避けることが可能です。

ブループリントの循環参照を避ける

循環参照とは名前の通りブループリントが別のブループリントを参照する際に、お互いがお互いを参照し合ってループしている状態を言います。上記のキャストを繰り返していると発生しやすいです。これは最悪、ブループリント内の情報を破壊する可能性もあり、非常に危険です。この問題も上記の"高価なブループリントキャストを避ける"と同様の対策が可能です。

C++クラスからアセット参照を避ける

FObjectFinderとFClassFinderを使用してC++コンストラクターからアセットを参照することは可能ですが、可能な場合は避ける必要があります。この方法で参照されるアセットはプロジェクトの起動時にロードされるため、参照が実際には必要ない場合は読み込み時間とメモリの問題が発生します。対策として、既に紹介している、Asset Managerの仕組みを使うか、設定ファイルを使って読み込むことで対策が可能です。

文字列によるアセット参照を避ける

C++上で文字列を使ってLoadObjectのような関数を使って手動でロードが可能ですが、これらの参照はクッカーによってトラッキングできないので、パッケージゲームで問題が発生する可能性があります。代わりにFSoftObjectPathもしくはTSoftObjectPtrな弱参照を使用し、iniまたはブループリントからそれらを設定して、オンデマンドに、または非同期にロードさせる必要があります。

ユーザー定義の構造体(Struct)と列挙型(Enum)に注意

C++で定義されている構造体と列挙型はC++とブループリント両方で利用できますが、ユーザー定義(ブループリント上で)の構造体と列挙型はC++では使用できません。もしC++で参照が必要な構造体、列挙型であれば、早いうちにC++で実装することをお勧めします。

ネットワークアーキテクチャを考える

ネットワークアーキテクチャはクラス設計に大きく影響する可能性があるので、早いうちから念頭において設計する必要があります。レプリケートされたデータの適切なフローを作成するためにイテレーションが難しくなるような意思決定が必要なるかもしれません。

非同期読み込みを考える

ゲームが大きくなると、ゲームに表示されるものを全てロードするのはでなく、必要に応じてアセットをロードする必要があります。このためにはハードリファレンスの代わりにソフトリファレンス、またはプライマリーアセットIDを使用するようにします。そしてAssetManagerを使い、非同期にロードを行うようにします。C++で低レベルな制御を行うSteamableManagerも利用することができます。