【UE5】BlueprintとC++の使い分けポイント

プログラマーの尾関です。

Unreal Engineは BlueprintとC++のどちらでもゲームシステムやロジックを実装できます。ですが実装を進めていくに従って、Blueprintで実装した処理が複雑化してメンテナンスが難しくなり、「C++で実装しておけば良かった…」と後悔することも多いと思います。

そこで今回は、Unrealでゲーム開発を考えている方向けに Unreal Engineにおける BlueprintとC++それぞれの苦手とするところをまとめ、どういった方針でそれぞれを使い分けていくとよいのかをまとめてみます。

目次

Blueprintが苦手とするところ

Blueprintはプログラムが得意でない方でも扱いやすいように「処理の流れがグラフィカルで見やすい」「高度な処理もグラフを呼び出すだけで実現できる」といったメリットがあります。ですがその反面以下のデメリットもあります。

  1. パフォーマンスを求められる場面。速度面でのペナルティがある
  2. 複雑なロジックの構築には向かない。分岐が複雑になるほどグラフが大きくなってしまい視認性が悪く、管理が難しくなってしまう
  3. バイナリデータのアセットとして扱われるため、複数人での同時編集ができない。変更点の管理が難しい
  4. C++から呼び出したい定義(例えば構造体やクラス、関数)をBlueprintで定義すると C++からは基本的に参照できない(C++で定義したものを Blueprintから呼び出すことは可能)

これらについて詳しく説明をしていきます。

①パフォーマンスを必要とする部分

Blueprintは非プログラマーでも扱いやすいように作られていますが、それゆえパフォーマンスのペナルティが存在します。そして、Blueprintのパフォーマンスのネックになりやすい代表的なものとして「Tickが重たい」ということがあります。

これは「BlueprintのTickを使ってはいけない」という極端なことではありません。例えば1つのActorのTickを回すだけであればオーバーヘッドはわずかです。

ですが、大量のActorのBlueprintのTickを回すとなると話は変わってきます。そのときにはCPU負荷が急増し、ゲーム全体のパフォーマンス低下に繋がります。

まとめ

大量のActorのTickを回す場合には、C++でTickを実装するのが良いです。
例えば公式のコミュニティにも以下の情報があります。

 Blueprint tick has quite a considerable overhead compared to C++ tick. 
I have measured that around 100-150 of them, connected to nothing,
will spend 1 millisecond of cpu time on consoles. If you need ticking, have the ticking in C++.
Use timers and/or timelines if you want time based events on blueprints.
----
BlueprintのTickは、C++のTickと比べてかなり大きなオーバーヘッドがあります。
計測したところ、何も接続されていないTickを100~150個ほど実行すると、コンソール上で1ミリ秒のCPU時間を消費します。
Tickが必要な場合は、C++でTick処理を行ってください。
Blueprintで時間ベースのイベントを実行したい場合は、TimerやTimelineを使用してください。

どうしてもBlueprintを使いたい場合は毎フレームTickを回すのではなく、例えばTimerイベントを使う (例えば1秒に1回だけ実行する) ことで呼び出し回数を減らすアプローチも有効です。

②複雑なロジックの管理が難しい

Blueprintは直感的なビジュアルスクリプティングツールとして多くの利点がありますが、複雑なロジックの管理にはいくつかの根本的な弱点があります。

■1. 分岐が多くなると処理を追うのが大変

Blueprintはノードベースで処理を組み立てるため、分岐やループが増えるとノード同士の接続が複雑化し、処理の流れを目で追うのが難しくなります
特に条件分岐やループが多用されるロジックでは、ノードの線が交錯し「スパゲッティ化」しやすく、全体像の把握やバグの特定が困難です。

やや極端な例ですが、以下はゲーム開始時のデバッグ設定を反映させるBlueprintです。

似たような処理が多くそれらを関数化するともう少し見やすくなりますが、コピペを多用するとこのような煩雑なグラフができあがってしまいます。このあたりC++でもメンテナンスされていないコードでは同じ問題が発生しますが、テキストベースのC++コードと比べて整理が難しいという問題は残ります。

Information

ただし、例えばデバッグ用の処理で使い捨てでメンテナンスもあまり行わない…ということであれば、こういったBlueprintでも許容されることがあります

■2. グラフが増えるほど画面占有率が高くなり一覧性が悪くなる

上記の悪い例を見ても分かる通り、Blueprintはノードを画面上に広げて配置するため、処理量が増えるとグラフが広がり、スクロールやズームが頻繁に必要になります。
C++のように数十行で済む処理でも、BPでは画面全体を占有する巨大なグラフになることが多く、全体構造を一望できなくなります。

■3. サブルーチン化しても画面の切り替わりなど可読性が悪い

大きくなりすぎたBlueprintのグラフはサブルーチンやマクロ、折りたたみなどでコンパクトにまとめることが可能です。

ただこれも一長一短で、サブルーチンごとに別画面に遷移する必要があり、複数の関数やイベントをまたいで処理を追う場合、何度も画面を切り替えることになります

これにより、ロジック全体の流れや依存関係を把握しづらくなります。

■4. その他(検索・差分・マージ・コメント・可読性)
  1. 検索しにくい/マージできない:BPはバイナリ形式のため、テキスト検索やバージョン管理システムでの差分を比較したりマージが困難です。当然ながら複数人での同時編集もできません
  2. コメントが書きにくい:BPにはコメント機能がありますが、テキストコードほど柔軟ではなく、運用によっては情報共有や意図の伝達が難しくなります
  3. SequenceやDelay、Do Onceなどの見た目で流れがつかみにくい:一部ノードは視覚的な流れが直感的でない場合があり、グラフの可読性を損なう要因となります

以下は Sequenceノードの簡単な説明です。複数の順次実行される処理を縦方向にまとめるのは便利ですが、使いすぎると横と縦に処理が伸びてしまう問題があります。

以下は Do Onceノードの説明です。フラグ変数を省略できるのは便利ですが、これも複数使われたり頻繁にResetが行われるといった処理になるとデバッグが困難になることがあります。

まとめ

Blueprintは直感的で強力なツールですが、複雑なロジックや大規模な処理の管理には明確な限界があります。

  • 分岐や処理が増えるほど視認性・一覧性・可読性が低下し、再利用や差分管理、チーム開発時の運用にも不向きです

このため、複雑なロジックや長期的な保守が必要な処理はC++で実装し、Blueprintは主に検証や演出、UI制御などに使い分けるのが良いです。

個人的には「開発初期段階では Blueprintでお手軽に高速なプロトタイピングを行い、実装の方向性が見えてきたタイミングでC++に置き換える」といった運用方法も有効と考えています。

③バイナリデータのアセットとして扱われるため、複数人での同時編集ができない

Blueprintはテキストデータではなくバイナリのアセットです。複数人が同時に編集してしまうと、基本的にマージできません。そのため編集を行う際にはファイルロックをして、他の人からの編集を防ぐ必要があります。

例えばプレイヤーキャラクターのゲームロジックを 1つの Pawn の Blueprintに書いてしまうと、修正が多く行われたときに作業ができない…などということがありますね。そのため重要なゲームロジックはBlueprintではなく、C++で書いたほうが気軽に編集できてマージもしやすくなって良いと思います。

④Blueprintで定義した構造体や関数はC++から呼び出せない

UnrealのC++に慣れていない方の落とし穴として、Blueprintで定義した構造体(Blueprint Struct)や関数(Blueprint Function)は、C++から直接呼び出すことができないという制約があります。

よくあるのがデータテーブル用の「行構造体」ですね。

これをBlueprintとして作ってしまうとC++からは基本的にアクセスできません。

Blueprintで定義した構造体や関数は「エディタ上でのみ有効なリソース」であり、C++の型システムやコンパイル時チェックの対象外であり、C++とBlueprintの間には明確な「方向性の壁」を持っているといます。「C++→BP」は可能(C++で公開したものをBPで使う)が、「BP→C++」は不可(BPで作ったものをC++で使うことはできない)という設計思想です。

そのため、C++からも利用したい構造体や関数は、必ずC++側で定義し、BlueprintTypeBlueprintCallableなどでBPに公開するのが定石です。Blueprintでしか定義できない特殊な処理(Widget Blueprint によって実行されるイベント。例えばボタンクリックイベントの処理やUIのテキスト設定など)が必要な場合は、C++から「BPで実装されるはずのイベント」呼び出し、BPでそのイベントを実装する(BlueprintImplementableEventBlueprintNativeEvent)という方法を使います。

例えばC++側は以下のように実装します。

// ■ヘッダの定義
// テキストの設定をC++側でBlueprintImplementableEventとして定義 (C++では中身は実装しない)
  // テキストの設定.
  UFUNCTION(BlueprintImplementableEvent,Category = "HUD3D")
  void OnSetText(const FName& Text);

 // テキストの設定 (外部公開用).
 UFUNCTION(BlueprintCallable)
 void SetText(const FName& Text);
-------------------------------------------

// ■cppでの実装
// テキストの設定 (外部公開用).
void UCPP_UserWidgetBase::SetText(const FName& Text)
{
  // BlueprintImplementableEventは外部から呼び出せないのでここで呼び出す.
  OnSetText(Text);
}

そしてBP側でイベントの中身を実装します。

まとめ

Blueprintで定義した構造体や関数はC++から直接呼び出せないという制約は、Unreal Engineの型システムと設計思想に起因します。
双方向で利用したい型や処理は、C++で定義しBlueprintに公開するのが原則です。
この制約を理解し、設計段階で「どちらで定義するか」を明確に切り分けることが、Unreal Engineを使った効率的な開発につながります。

C++が苦手とするところ

「Blueprintに問題があるのであればすべてC++で実装するのが良いのでは…?」というのも悪くないですが、Blueprintは Unreal Engineの強みでもあり、それゆえC++にも苦手なことがあります。

いくつか例をあげていきます。

  1. お手軽に動作テストや実験ができない
  2. インスタンスの生成やアセットの読み込みのコードが長くなりがち
  3. アセットパスが変更された際に手動でパスを変更する必要がある。BlueprintはUnrealエディタで設定するためアセットパスの変更をある程度自動で管理できる
  4. セーフティに記述しないとハングアップが発生しやすい(Blueprintの場合は、例えば null参照でも停止を回避できる可能性が高い)

①Unreal/C++独自マクロの学習コストの高さ、ビルド時間など開発サイクルの遅さなど実装速度を求められるプロトタイプ作成には向いていない

Unreal EngineのC++開発には、独自マクロの学習コストの高さビルド時間の長さといった課題があり、特にプロトタイピングなど実装速度が求められる場面では不利になることが多いです。

■1. Unreal/C++独自マクロの学習コストの高さ

Unreal EngineでC++を扱い始めたときに面食らうのがその学習コストのハードルです。Unreal EngineのC++は、標準C++に加えて独自のマクロ(UCLASS()UFUNCTION()UPROPERTY()など)を多用します。これらはUnreal独自のリフレクションシステムやガーベジコレクション、エディタ連携のために必須ですが、通常のC++とは異なる記述やルールが多く、初学者や他エンジン経験者にとって「なんでこんな書き方をしなければならないのか…?」と困惑することも多いです。

具体的には「.generated.hを最後に#includeしないとエラー」「GENERATED_BODY()を書き忘れてエラー」「デリゲートの特殊な書き方を覚えるのが大変&使い方の癖が強い」「UPROPERTYをつけないと突然クラッシュする」「UFUNCTIONをつけないとUnreal Engine特有のオブジェクトが渡せない」などなど…。

■2. ビルド時間など開発サイクルの遅さ
  • Unreal EngineのC++は、プロジェクトの規模が大きくなるほどビルド(コンパイル)時間が長くなります。特にヘッダーファイル(.h)の変更は多くの依存ファイルを再コンパイルさせるため、数分から十数分かかることも珍しくありません
  • UE5ではUE4よりもビルド時間がさらに増加する傾向があり、同じプロジェクトでもビルドアクション数の増加や1アクションあたりのコンパイル時間が大幅に伸びているという報告もあります
  • 小さな変更(例:UPROPERTY追加や関数の微修正)でも数分単位のビルド待ちが発生することがあり、「実装→テスト→修正」のサイクルが非常に遅くなるのが現場の実感です

■3. プロトタイプ作成や高速な実装には不向き

  • 上記の理由から、Unreal C++は実装速度が重視されるプロトタイピングやアイデア検証には不向きです。ビルド待ちやマクロの理解に時間を取られるため、試行錯誤や素早い反復が求められる場面では効率が落ちます
  • 実際、多くの開発現場では「まずBlueprintでプロトタイピング→安定・最適化が必要な部分だけC++に移植」というハイブリッド運用が定着しています
まとめ

これらの理由から、実装速度が求められるプロトタイプ作成や頻繁な仕様変更にはBlueprintが圧倒的に有利であり、C++は「最適化」「再利用性」「長期運用」が求められる部分に限定して使い分けるのが現実的な選択肢です

Unreal/C++は高いパフォーマンスや柔軟性、エンジンAPIへのフルアクセスが魅力ですが、独自マクロの習得コストとビルド時間の長さが大きな障壁となります

②C++でのインスタンス生成やアセットの読み込みはコードが長くなりがち

Unreal EngineのC++開発において、インスタンス生成やアセットの読み込み処理がコード量・記述量ともに長くなりがちという指摘は、実際の現場でもよく挙げられる課題です。

■1. インスタンス生成(オブジェクト/アクターの生成)

Unreal Engineでは、C++でのオブジェクト生成は通常のC++のnew演算子ではなく、エンジン独自の関数(NewObject<T>()SpawnActor<T>()など)を使う必要があります。

例えば、AActorの生成にはUWorld::SpawnActor()を使い、必要な記述が多くなりがちです。

// ヘッダファイルの定義。EditAnywhereでクラス参照を登録できるようにしておく.
	// 生成するHUD3Dのリスト.
	UPROPERTY(EditAnywhere, DisplayName="生成するHUD3D")
	TMap<EHUD3DObj, TSoftClassPtr<ACPP_HUD3DBase>> HUDClassMap;

// アクターの生成例。クラス参照からLoadSynchronous()でクラス定義を読み込むことでSpawnできる.
	// HUD3Dを確保.
	auto* pClass = LWidgetData->HUDClassMap.Find(type);
	// 生成.
	if(pClass == nullptr) {
		UE_LOG(LogTemp,Error,TEXT("HUD3D is not found."));
		return nullptr;
	}

	auto cls = pClass->LoadSynchronous();
	UWorld* World = GetWorld();
	if (World == nullptr) {
		return nullptr;
	}

	// Spawnします.
	auto* pHUD3D = World->SpawnActor<ACPP_HUD3DBase>(cls);
	pHUD3D->SetBaseScale(FVector(0.1f, 0.1f, 0.1f)); // サイズを小さくする.

	m_HUD3DMap.Add(type, pHUD3D);

同じ処理ではありませんが、BlueprintでSpawnActorを呼び出すときは、マウス操作で SpawnActorノードを探して数クリックでフィニッシュです。

このあたりゲームエンジンとしての設計思想に依存するところがありそうです。例えば "GameMaker Studio" というインディーゲーム向けゲームエンジンはできるだけ簡素な記述のインターフェースを用意していて、シンプルな記述でゲームオブジェクトを生成できます。

それに対して Unreal Engineは Blueprintを使うと簡単に作れますが、C++で書く場合は煩雑になりがちですが何でもできるように、エンジン全体の安全性・拡張性・エディタ連携を重視した設計思想にしている…と思われます。

■2. アセットの読み込み

C++でアセット(マテリアル、テクスチャ、データアセットなど)を動的に読み込む場合、Blueprintに比べて手順が多くなります。先程のアクターを生成するコードで言えばクラス参照から LoadSynchronous() でクラス定義をロードしないといけない…といったものが一例です。

まとめ

C++でのインスタンス生成やアセット読み込みは、Unreal独自のAPIや管理方式により、記述が長く冗長になりやすいです。
この冗長さが、Blueprintに比べてC++はプロトタイピングや高速な実装に不向きな理由の一つとなっています。

③アセットパスの管理が手動となる

C++でアセットのパスを指定して直接読み込むと以下の記述となります。

TSoftClassPtr<AActor> BPClassPtr(TEXT("/Game/Blueprints/BP_TestActor.BP_TestActor_C"));
UClass* BPClass = BPClassPtr.LoadSynchronous(); // クラス定義の読み込み.
if (BPClass) {
    // BPClassを使ってSpawnなどが可能
}

動的にパスを作りたい場合はこの方法で問題ありませんが、アセットの配置場所の移動に注意する必要があります。

例えば BP_TestActorが「Blueprints/Test」フォルダに移動したら「/Game/Blueprints/Test/BP_TestActor.BP_TestActor_C」というパスに変更する必要があります。

そのため動的なパスでない限り、通常は UPROPERTY(EditAnywhere)でメンバ変数にクラス定義を保持しておき、Unreal Editorでそのアセットのパスを指定するという実装にします。

例えばC++の定義例です。

public: // ------------------------------------------------------- EditAnywhere var(s)
	// MRUKAnchorActor.
	UPROPERTY(EditAnywhere,Category = "GameMode")
	TSubclassOf<ACPP_MRUKAnchorActorMenuSpawner> MRAnchorActorMenuSpawnerClass;
	// タイトルシーケンス.
	UPROPERTY(EditAnywhere,Category = "GameMode")
	TSubclassOf<ACPP_TitleSequence> TitleSequenceClass;
	// 壁管理.
	UPROPERTY(EditAnywhere,Category="GameMode")
	TSubclassOf<ACPP_WallManager> WallManagerClass;
	// 床管理.
	UPROPERTY(EditAnywhere,Category="GameMode")
	TSubclassOf<ACPP_FloorManager> FloorManagerClass;

この定義を元にUnrealエディタ側でアセットを指定します。

このようにすることで、コンテンツブラウザでアセットを移動する限り、アセットのパスを変更しても自動でその変更が反映されるようになります。

④セーフティに記述しないとハングアップが発生しやすい

C++を使うと、安全性(セーフティ)を意識して記述しないと、ハングアップやクラッシュが発生しやすいです。

具体的には以下のものがあります。

  • nullptrチェック:Unreal での開発は様々な状況でインスタンスがされるため、インスタンスが存在しない前提のチェックコードが数多く必要となります
  • メモリリークとメモリ破壊:UObjectであればUnreal Engineのガベージコレクションの対象となるため基本的にメモリリークは発生しにくいですが、循環参照には注意が必要です。またC++を使っている以上、不正なポインタへのアクセスや領域外参照などの問題は発生します

C++の場合、こういった問題を意識して記述しないとハングアップがすぐに発生します。それに対して、Blueprintであればnullptrへのアクセスや不正なポインタアクセスは基本的にエラーログとして出力されるのみで、ハングアップするケースは少なめです。

そのため非エンジニアには、ハングアップが起きにくいBlueprintでロジックを書いてもらうのが有効です。ただ、非エンジニアがBlueprintを組むと、処理負荷を考えないものになりがちなので、最終的にはエンジニアがBlueprintを精査するフェーズが必要となります。

まとめ

C++で記述する際は、nullチェックやUPROPERTY()の適切な利用や IsValid()などによる不正なポインタへのアクセスを防いだり、安全性を担保するためのコーディングと運用が必須です

おしまい

BlueprintとC++の使い分けについての解説でした。

ざっくりとしたまとめとしては以下のものですね。

  • アセットパスの設定や、アニメーションBlueprintやWidget Blueprintなど、BPやUnrealエディタで設定するのが向いていることはBPでやる
  • 使い捨てでよく、単純な処理であればBlueprintで書いても良い
  • ただし複雑でBPだとメンテナンスが難しい処理や、再利用が必要な処理はC++で書いた方が良い
  • C++で参照したい構造体 (特にデータテーブル) や処理は C++ で定義する。基本的に構造体はC++で定義するのが良い
  • UnrealのネイティブクラスをBPクラスで直接継承せず、C++で定義したクラスを継承すると、後々の拡張時に余計な作業が発生することを回避できる
    • 例えば APawnをBPクラスで直接継承せず、C++で CPP_PawnBase を定義し、BPクラスでそれを継承する

以上、Unreal EngineでのC++の扱いとBlueprintを有効活用するのに役立てれば幸いです。

\ 最新情報をチェック /