ブループリントと C++ のバランスを調整する

ブループリントと C++ を組み合わせてゲームを作成する方法と、それに伴う決定事項について説明します。

Choose your operating system:

Windows

macOS

Linux

前提トピック

このページは以下のトピックへの知識があることを前提にしています。まず以下のトピックの内容についてご確認をお願いします。

ゲームの技術的な設計を作成する際は、ブループリントで実装するものと C++ で実装するものを決定することが重要な考慮事項の一つとして挙げられます。本ドキュメントでは、このような考慮事項についてどのように決定していくかを考え、堅牢なデータドリブン ゲームプレイ システムを構築するにあたって推奨される事項を紹介します。また、本ドキュメントは、基本的なプログラミングに関するドキュメントを確認したのち、さらに詳細について学習することを希望するプログラマーまたはテクニカル デザイナー向けに作成されています。ゲームの構築においては常に一つの正しい答えがあるわけではありません。本ドキュメントでは、考慮すべき事項を読者が導き出すことができるようお手伝いします。

ゲームプレイ ロジックおよびデータ

ゲームに含まれるものは、大まかに ロジック データ に分けることができます。ゲームプレイ ロジックはゲーム内のパーツに対する 指示 および 構造 であり、ロジックによって使用されるゲームプレイ データはゲームの動作を表します。画面上にキャラクターを描画する C++ コードがロジック ベースであり、そのキャラクターの物理的な外見はデータをベースとしているなど、これらが明確に分かれている場合もありますが、実際はこれらのカテゴリはブレンドされておりプロジェクトが複雑になり得ることがあるため、これらの分け方と利用可能なオプションについて理解することが重要です。

Unreal Engine 4 (UE4) には、ゲームプレイ ロジックの実装に関するいくつかのオプションが用意されています。

  • C++ クラス: 変数および関数は C++ で定義されており、ベースのゲームプレイ ロジックを実装します。

  • ブループリント クラス: ロジックは、 ブループリント イベント グラフ またはこれらのグラフで呼び出される 関数 で実装されます。追加の変数を加えることもできます。

  • カスタム システム: 多くのシステムおよびゲームには、ゲームプレイ ロジックの一部分を記述する小規模の「マイクロ言語」が含まれています。UE4 マテリアル エディタ シーケンサー エディタ および AI ビヘイビアツリー は、すべてゲームプレイ ロジックの保存に関するカスタム システムの例として見ることができます。

データについてはより多くのオプションを利用できます。

  • C++ クラス: ネイティブ クラス コンストラクタにより、デフォルト値とサポート データの継承が設定されます。データを関数のローカル変数にハードコード化することもできますが、こうすると追跡が困難になります。

  • コンフィギュレーション ファイル: Ini ファイルおよび コンソール変数 では、C++ コンストラクタで宣言されたデータのオーバーライドをサポートします。もしくは、クエリを直接受けることができます。

  • ブループリント クラス: ブループリント クラス デフォルトは C++ クラス コンストラクタと同様に機能し、データの継承をサポートします。データは、関数のローカル変数またはピン リテラル値で安全に設定することもできます。

  • データ アセット: インスタンス化できない、データの継承を必要としないオブジェクトについては、ブループリント デフォルトよりもスタンドアロンのデータ アセットを使用するほうがよりわかりやすくシンプルです。

  • テーブル: データは、 データ テーブル またはカーブ テーブルとしてインポートすることができます。また、ランタイムとして読み取ることも可能です。

  • 配置済みのインスタンス: データは、レベルまたは他のアセット内で設定されているブループリントまたは C++ クラスのインスタンス内に保存することができ、クラスのデフォルト値をオーバーライドします。

  • カスタム システム: ロジックと同様に、データの保存については多くのカスタム方法があります。 ゲーム保存: ラインタイムのゲーム保存ファイルは、上記のデータ タイプのオーバーライドまたは変更に使用できます。

一般的に、これらのリストの下の派生オプションでは、その上にあるベース レベル オプションをオーバーライドして拡張します。このため、ベース レベル システムでは、拡張システムによって定義されるものにアクセスして使用することが困難になります。例えば、ブループリント クラスで追加された変数に C++ クラスからアクセスすることは非常に困難であるため、お勧めできません。このような問題を避けるために、共通してアクセス可能な最もベースなレベルで関数および変数を定義することを推奨します。ロジックについては、ベース レベルで全体的に実装するか、ベース レベルにスタブ関数を置いて派生レベルでロジックをオーバーライドすることが理にかなっています。

より多くの可能性があり、より深い継承が一般的であるため、データに関連するルールはより複雑でシステムに特有なものとなります。変数のデフォルト値は、変数が定義されたレベルで提供する必要があります。こうすることで、派生レベルでこれらの値をオーバーライドすることができます。また、カスタム ルールに応じて、データをオブジェクトから別のオブジェクトにコピーするロジックを作成することも一般的です。データ アーキテクチャに関する他の一般的な問題の一部について、以下で説明します。

C++ およびブループリント

上記のリストから、C++ またはブループリントのクラスのいずれかを使用してロジックおよびデータを保存できることに留意してください。このため、ほとんどのゲームプレイ システムではいずれか (または一部を組み合わせて) で実装されます。それぞれのゲームおよび開発チームは異なるため、どれを使用するかについて決定する際の「正しい選択」はありませんが、ここでは C++ またはブループリントの使用を決定する際に役に立つ、一般的なガイドラインを紹介します。

C++ クラスの利点

  • より高速なランタイム パフォーマンス :一般的に、C++ ロジックはブループリント ロジックに比べて非常に高速に機能します。理由については以下で説明します。

  • 明確なデザイン :C++ の変数または関数を公開する場合に、対象の正確な公開をより詳細に制御できるため、特定の関数および変数を保護し、クラスの正式な API を構築することができます。これにより、必要以上に大きい、理解が困難なブループリントの作成を避けることができます。

  • より幅広いアクセス :C++ で定義された (そして適切に公開された) 関数および変数には他のすべてのシステムからアクセスすることが可能なため、異なるシステム間での情報のやり取りについては理想的と言えます。また、C++ に対しては、ブループリントよりもより多くのエンジン機能が公開されています。

  • より詳細なデータ制御 :データのロードおよび保存については、C++ はより特定の機能にアクセスすることができます。これにより、バージョンの変更やシリアル化を高度にカスタマイズされた方法で処理することができます。

  • ネットワークのレプリケーション :ブループリントでのレプリケーションに対するサポートはわかりやすくシンプルなものになっており、より小規模なゲームまたは一度きりの特有のアクタ向けにデザインされています。レプリケーション帯域幅またはタイミングについてより厳格な制御が必要な場合は、C++ を使用する必要があります。

  • 優れた演算処理 :ブループリントでは複雑な演算が困難で処理が遅くなる場合があるため、多くの演算を要する操作については C++ を検討してください。

  • より簡単な Diff/Merge 処理 :C++ コードおよびデータ (さらにコンフィギュレーション、可能性としてカスタム ソリューションも) はテキストとして保存されます。これによって複数のブランチで同時に作業することが容易になります。

ブループリント クラスの利点

  • より迅速な作成 :ほとんどのユーザーにとって、新しいブループリント クラスを作成して変数および関数を追加する作業は、C++ で同様の作業を行う場合よりも迅速に行うことができます。そのため、ブループリントを使用することで新しいシステムのプロトタイプを素早く作成できます。

  • より迅速なイテレーション :ホット リロードを活用した場合でも、ゲームを再コンパイルするよりも、ブループリントのロジックを変更してエディタ内でプレビューするほうが格段に素早く作業を完了させることができます。これは既存および新規のシステムの両方に当てはまるため、すべての「調整可能」な値は可能であればアセット内に保存してください。

  • 優れたフロー :C++ での「ゲーム フロー」の視覚化は複雑になることがあるため、「ゲーム フロー」はブループリント (または視覚化専用の Behavior Trees などのカスタム システム) で実装したほうがよい場合が多くあります。遅延および非同期ノードにより、C++ デリゲートを使用する場合よりもフローに沿うことが容易になります。

  • 柔軟な編集 :特定の技術的なトレーニング受けていないデザイナーやアーティストであってもブループリントの作成および編集が可能なため、エンジニア以外のメンバーによる変更が必要なアセットについては、ブループリントの使用が理想的であると言えます。

  • より簡単なデータ使用方法 :ブループリント クラス内でのデータ保存は C++ クラス内での保存と比べて非常にシンプルで安全であるため、データとロジックが緊密に混在するクラスについてはブループリントが適切です。

ブループリントから C++ に変換する

ブループリントでは作成およびイテレーションをより簡単に行えるため、ブループリントでプロトタイプを構築してから、機能の一部またはすべてを C++ に移行することが一般的な方法です。一般的には、移行は、満足のいくシステムのベース機能を確立し、他のユーザーがそれを破損させることなく使用できるように、さらに強固に仕上げる段階である「リファクタリング ポイント」に達した際に行うことを推奨します。この時点で、どのクラス、関数および変数を C++ に移行するか、そして何をブループリントに残すかを決定する必要があります。これを決定する前に、C++ へのリファクタリングを行うためのプロセスについて理解する必要があります。

一般的な最初のステップとして、ブループリント クラスの継承元となる一連の「ベース」 C++ クラスを作成します。ゲームのベース ネイティブ クラスを作成したら、すべてのプロトタイプ ブループリントの親子付けを新しいネイティブ クラスに変更する必要があります。これを完了したら、変数および関数をブループリント クラスからネイティブ C++ に移行し始めることができます。ネイティブ クラスにある変数または関数が同じタイプであり、ブループリント変数として名前が付いている場合は、ブループリントのロード時にそれへの参照が自動的に変換されます。ただし、ブループリントへの外部参照については、代わりにネイティブ ベース クラスをポイントするよう変更することをお勧めします。この一例として、 ActionRPG サンプルの作業時に、次のブロックを 「 DefaultEngine.ini 」 ファイルに追加しました。

[CoreRedirects]
+ClassRedirects=(OldName="BP_Item_C", NewName="/Script/ActionRPG.RPGItem", OverrideClassName="/Script/CoreUObject.Class")
+ClassRedirects=(OldName="BP_Item_Potion_C", NewName="/Script/ActionRPG.RPGPotionItem", OverrideClassName="/Script/CoreUObject.Class")
+ClassRedirects=(OldName="BP_Item_Skill_C", NewName="/Script/ActionRPG.RPGSkillItem", OverrideClassName="/Script/CoreUObject.Class")
+ClassRedirects=(OldName="BP_Item_Weapon_C", NewName="/Script/ActionRPG.RPGWeaponItem", OverrideClassName="/Script/CoreUObject.Class")

上記のコード ブロックでは Core Redirects システムを使用して、 Blueprint BP Item C へのすべての参照を、新しいネイティブ クラス RPGItem を参照するように変換します。今は UBlueprintGeneratedClass ではなく UClass であることを示す必要があるため、 OverrideClassName オプションは必須となります。最初の再親子付けおよび修正が完了したら、ブループリントのコンパイルに関する未解決の問題を修正し、ゲーム内のすべてのブループリントを再保存する必要があります。ブループリントの警告が ゼロ の状態になるようリファクタリングを完了することを目指し、新しい問題が加わった際に容易に追跡できるようにします。すべてが完全に機能するようになったら、修正プロセス中に追加した CoreRedirects を削除して、 ini ファイルをクリーンアップすることができます。

パフォーマンスに関する懸念事項

ブループリントではなく C++ を使用する主な理由の一つとしてパフォーマンスが挙げられます。ただし、ほとんどの場合、ブループリントのパフォーマンスに実際に問題があるわけではありません。大まかに主な違いは、ブループリントでの個別のノードの実行が一連の C++ コードの実行に比べて遅いことですが、ノード内で実行された場合は、 C++ から呼び出された場合と同じ速さで実行されます。例えば、負荷の低いトップレベルのノードをいくつか待ち、負荷の高い Physics Trace 関数を呼び出すブループリント クラスの場合、そのクラスを C++ に変換してもパフォーマンスに大幅な向上は見られません。ただし、ループに対して多くのタイトを持つブループリント クラスや、数百にもおよぶノードに展開するネスト構造のマクロを持つブループリント クラスについては、そのコードを C++ に移行することを考慮してください。最も深刻なパフォーマンスの問題の一つは Tick 関数です。ブループリント ティックの実行はネイティブ ティックと比べて非常に遅く、多くのインスタンスを持つクラスに対してはティックの使用は避けてください。代わりに、タイマーまたはデリゲートを使用して、ブループリント クラスを必要時にのみ機能させるようにすることをお勧めします。

ブループリント クラスを原因とするパフォーマンスの問題が発生しているかどうかを確認する最適な方法は、 プロファイラ ツール を使用することです。プロジェクトでのパフォーマンスの状況を確認するには、最初にブループリント クラスがパフォーマンスに極度の負担をかける状況 (多くの敵をスポーンするなど) を作り出し、プロファイラ ツールを使用してプロファイルをキャプチャします。プロファイル ツールを使用して Game Thread Tick にアクセスし、個別のブループリント クラスに到達するまでツリーを展開します。同じクラスのすべてのインスタンスがグループ化されている場合があるので注意してください。ブループリント クラス内で、時間がかかっているブループリント関数を見つけ、それを展開します。ほとんどの時間が Self にある場合は、ブループリントのオーバーヘッドによってパフォーマンスが低下していることを示しています。ただし、ほとんどの時間がその関数内にネストされている他のネイティブ イベントにある場合、ブループリントのオーバーヘッドは問題にはなりません。

ブループリントのネイティブ化 を使用すると多くの懸念事項を軽減できますが、障害となることもあります。まず、クック ワークフローが変更されて、クックされたゲームでのイテレーションが遅くなる可能性があることです。また、ネイティブ化されたブループリントのランタイム ロジックは通常のブループリントのものとは異なるため、ゲームの詳細に応じて異なるバグまたは動作が発生することがあります。ネイティブ化ではブループリントのほとんどの機能がサポートされますが、一部のあいまいなものについてはサポートされない可能性があります。最後に、自身で C++ に変換した場合ほどパフォーマンスは必ずしも向上しないことが挙げられます。ネイティブ化してもパフォーマンスに関するすべての問題を解決できるわけではありませんが、これらに対する潜在的なソリューションとしてネイティブ化を考慮してください。

アーキテクチャに関する注意事項

ブループリントおよび C++ を組み合わせたゲームを構築する際は、ゲームがより広範で複雑になるため困難な問題が生じます。プロジェクトを進めていく上で、以下のことに留意してください。

  • 負荷の高いブループリントへのキャストは避ける BP_B からブループリント クラス BP_A にキャストした場合 (または関数または他のブループリントで変数タイプとして宣言した場合)、そのブループリントへのロード依存が生じます。この場合、 BP_A が 4 つの大きなスタティック メッシュおよび 20 個のサウンドを参照していると、 BP_B をロードするたびに、キャストが失敗した場合であってもそれらのスタティック メッシュとサウンドをすべてロードしなければなりません。これが、重要な関数と変数を定義するネイティブ ベース クラスまたは最小限のブループリント ベース クラスのいずれかを含めることが大切である主な理由の一つです。さらに、負荷の高いブループリントは子クラスにすることをお勧めします。

  • 循環ブループリント参照は避ける :循環参照 (クラスが別のクラスを参照し、その別のクラスが最初のクラスを参照する) は、ヘッダ ファイルのため C++ では問題ではありません。ただし、過剰な循環ブループリント参照が存在すると、エディタでロードおよびコンパイルにかかる時間が長くなる場合があります。この問題は、上記と同様に、負荷の高い子ブループリントにキャストする (または変数参照を含める) のではなく、 C++ クラスまたはより負荷の低いベース ブループリントクラスにキャストすることで緩和される場合があります。

  • C++ クラスからのアセットの参照は避ける FObjectFinder および FClassFinder を使用して C++ コンストラクタからのアセットを参照することは可能ですが、可能な限り避けてください。このように参照されたアセットはプロジェクトのスタートアップ時にロードされるため、これらの参照が実際には必要ない場合、ロード時間およびメモリに関する問題が発生します。また、コンストラクタから参照されるアセットは、簡単に削除したり名前を変更したりできなくなります。一般的には、C++ から特定のスタティック メッシュを参照するのではなく、「 Game Data 」アセットまたはブループリント タイプを作成し、Asset Manager または config ファイルを使用してこれらをロードすることが推奨されます。

  • 文字列によるアセットの参照は避ける :C++ クラスからアセットをロードする際に発生し得る問題を回避するために、C++ 関数の LoadObject などの関数を使用して、ディスク上の特定のアセットを手動でロードすることができます。ただし、これらの参照はクッカによりまったく追跡されないため、パッケージ化されたゲームで問題が発生する可能性があります。代わりに、C++ クラスの FSoftObjectPath または TSoftObjectPtr タイプを使用し、それらを ini またはブループリント クラスから設定して、オンデマンドまたは非同期ロードを介してロードします。

  • ユーザー構造体および列挙型には注意が必要 :C++ で定義されている列挙型および構造体は C++ およびブループリントの両方で使用できますが、ゲーム保存に関するセクションに記載されているとおり、ユーザー構造体および列挙型については C++ では使用できず、手動で修正することもできません。ゲームプレイ ロジックを長期的に C++ に移行しようと考えている場合は、重要な列挙型および構造体を C++ で実装することをお勧めします。基本的に、1 つ以上のブループリントで使用されるものについてはネイティブ C++ で実装してください。

  • ネットワーク アーキテクチャについて考慮する :ゲームの特定のネットワーク アーキテクチャは、クラスを構築する方法に大きな影響を及ぼします。一般的に、プロトタイプを構築する際にネットワークについては考慮されないため、アイテムのリファクタリングを開始する際は、どのアクタがどのデータをレプリケートするのかを考える必要があります。レプリケートされたデータに適したフローを作成するためには、イテレーションがより困難になるような決定を下さなければならないこともあります。

  • 非同期ロードについて考慮する: ゲームの規模が大きくなるにつれて、ゲームのロード時にすべてのアセットをロードするのではなく、オンデマンドでアセットをロードするようにしたいと考えることがあります。この時点では、 ハード参照**ではなく、 ソフト参照 または PrimaryAssetIds を使用するようアセットを変換する必要があります。 AssetManager ではアセットの非同期ロードを容易にするいくつかの関数が提供され、さらに下位レベルの関数を提供する StreamableManager** も公開されます。

Unreal Engine のドキュメントを改善するために協力をお願いします!どのような改善を望んでいるかご意見をお聞かせください。
調査に参加する
閉じる