【UE5】BlueprintとC++の使い分けポイント
プログラマーの尾関です。
Unreal Engineは BlueprintとC++のどちらでもゲームシステムやロジックを実装できます。ですが実装を進めていくに従って、Blueprintで実装した処理が複雑化してメンテナンスが難しくなり、「C++で実装しておけば良かった…」と後悔することも多いと思います。
そこで今回は、Unrealでゲーム開発を考えている方向けに Unreal Engineにおける BlueprintとC++それぞれの苦手とするところをまとめ、どういった方針でそれぞれを使い分けていくとよいのかをまとめてみます。
目次
Blueprintが苦手とするところ
Blueprintはプログラムが得意でない方でも扱いやすいように「処理の流れがグラフィカルで見やすい」「高度な処理もグラフを呼び出すだけで実現できる」といったメリットがあります。ですがその反面以下のデメリットもあります。
- パフォーマンスを求められる場面。速度面でのペナルティがある
- 複雑なロジックの構築には向かない。分岐が複雑になるほどグラフが大きくなってしまい視認性が悪く、管理が難しくなってしまう
- バイナリデータのアセットとして扱われるため、複数人での同時編集ができない。変更点の管理が難しい
- C++から呼び出したい定義(例えば構造体やクラス、関数)をBlueprintで定義すると C++からは基本的に参照できない(C++で定義したものを Blueprintから呼び出すことは可能)
これらについて詳しく説明をしていきます。
①パフォーマンスを必要とする部分
Blueprintは非プログラマーでも扱いやすいように作られていますが、それゆえパフォーマンスのペナルティが存在します。そして、Blueprintのパフォーマンスのネックになりやすい代表的なものとして「Tickが重たい」ということがあります。
これは「BlueprintのTickを使ってはいけない」という極端なことではありません。例えば1つのActorのTickを回すだけであればオーバーヘッドはわずかです。
ですが、大量のActorのBlueprintのTickを回すとなると話は変わってきます。そのときにはCPU負荷が急増し、ゲーム全体のパフォーマンス低下に繋がります。
②複雑なロジックの管理が難しい
Blueprintは直感的なビジュアルスクリプティングツールとして多くの利点がありますが、複雑なロジックの管理にはいくつかの根本的な弱点があります。
■1. 分岐が多くなると処理を追うのが大変
Blueprintはノードベースで処理を組み立てるため、分岐やループが増えるとノード同士の接続が複雑化し、処理の流れを目で追うのが難しくなります。
特に条件分岐やループが多用されるロジックでは、ノードの線が交錯し「スパゲッティ化」しやすく、全体像の把握やバグの特定が困難です。
やや極端な例ですが、以下はゲーム開始時のデバッグ設定を反映させるBlueprintです。

似たような処理が多くそれらを関数化するともう少し見やすくなりますが、コピペを多用するとこのような煩雑なグラフができあがってしまいます。このあたりC++でもメンテナンスされていないコードでは同じ問題が発生しますが、テキストベースのC++コードと比べて整理が難しいという問題は残ります。
■2. グラフが増えるほど画面占有率が高くなり一覧性が悪くなる
上記の悪い例を見ても分かる通り、Blueprintはノードを画面上に広げて配置するため、処理量が増えるとグラフが広がり、スクロールやズームが頻繁に必要になります。
C++のように数十行で済む処理でも、BPでは画面全体を占有する巨大なグラフになることが多く、全体構造を一望できなくなります。
■3. サブルーチン化しても画面の切り替わりなど可読性が悪い
大きくなりすぎたBlueprintのグラフはサブルーチンやマクロ、折りたたみなどでコンパクトにまとめることが可能です。

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

これにより、ロジック全体の流れや依存関係を把握しづらくなります。
■4. その他(検索・差分・マージ・コメント・可読性)
- 検索しにくい/マージできない:BPはバイナリ形式のため、テキスト検索やバージョン管理システムでの差分を比較したりマージが困難です。当然ながら複数人での同時編集もできません
- コメントが書きにくい:BPにはコメント機能がありますが、テキストコードほど柔軟ではなく、運用によっては情報共有や意図の伝達が難しくなります
- SequenceやDelay、Do Onceなどの見た目で流れがつかみにくい:一部ノードは視覚的な流れが直感的でない場合があり、グラフの可読性を損なう要因となります
以下は Sequenceノードの簡単な説明です。複数の順次実行される処理を縦方向にまとめるのは便利ですが、使いすぎると横と縦に処理が伸びてしまう問題があります。

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

③バイナリデータのアセットとして扱われるため、複数人での同時編集ができない
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++側で定義し、BlueprintType
やBlueprintCallable
などでBPに公開するのが定石です。Blueprintでしか定義できない特殊な処理(Widget Blueprint によって実行されるイベント。例えばボタンクリックイベントの処理やUIのテキスト設定など)が必要な場合は、C++から「BPで実装されるはずのイベント」呼び出し、BPでそのイベントを実装する(BlueprintImplementableEvent
やBlueprintNativeEvent
)という方法を使います。
例えば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側でイベントの中身を実装します。

C++が苦手とするところ
「Blueprintに問題があるのであればすべてC++で実装するのが良いのでは…?」というのも悪くないですが、Blueprintは Unreal Engineの強みでもあり、それゆえC++にも苦手なことがあります。
いくつか例をあげていきます。
- お手軽に動作テストや実験ができない
- インスタンスの生成やアセットの読み込みのコードが長くなりがち
- アセットパスが変更された際に手動でパスを変更する必要がある。BlueprintはUnrealエディタで設定するためアセットパスの変更をある程度自動で管理できる
- セーフティに記述しないとハングアップが発生しやすい(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++に移植」というハイブリッド運用が定着しています
②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++でアセットのパスを指定して直接読み込むと以下の記述となります。
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を精査するフェーズが必要となります。
おしまい
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を有効活用するのに役立てれば幸いです。