UE5「Blueprint→C++変換」の本当の壁は”置き換え手順”じゃなく”依存関係の芋づる”だった
「BlueprintをC++に置き換える手順」を検索すると、クラスの継承構造を再現して、変数を移植して、関数を段階的に置き換えて……という記事はいくつも見つかります。どれも間違ってはいません。ただ、実際にUE5のプロジェクトでBlueprintをC++へ移植する作業をやってみると、本当に時間を溶かすのは「1つのクラスをどう置き換えるか」ではありませんでした。
本当の壁は、「1つC++化すると、参照している別のBlueprintも芋づる式にC++化が必要になる」という依存関係の連鎖でした。この記事では、実際の変換作業で得た経験をもとに、この”芋づる問題”の正体と、変換中に踏んだ2つの落とし穴(デリゲートの移植・分岐ロジックの読み方)を共有します。なお、登場するクラス名はすべて説明用の仮名です。
おさらい:単発クラスの置き換え手順
1クラスだけを対象にした置き換え手順は、すでに良質な解説記事がいくつもあります。ざっくり要約すると流れはこうです。
- 親クラス(例:
APawn)をC++側でも同じ継承構造で再現する - メンバ変数をBlueprintからC++へ移植する(デフォルト値の消失に注意)
- Get/Setのような単純な関数から着手し、徐々に複雑なロジックへ広げる
- いきなり全部C++化せず、モジュール単位で分割しながら動作確認する
ここまでは、対象のBlueprintが「他から参照されない・閉じたクラス」であれば、この手順通りでほぼ困りません。実際、後述する「メニュー管理クラス」の移植はこのパターンにきれいに当てはまり、依存の連鎖も発生せずクリーンに完了しました。
問題は、対象のBlueprintが他の複数のBlueprintから参照されている「ハブ」的な存在だった場合です。
本当の壁:「1つ変換すると芋づる式に増える」問題
あるUI制御用のBlueprint(ここでは「画面遷移コントローラー」と呼びます)をC++化しようとしたときのことです。対象クラス自体の変換は難しくありませんでした。問題は、そのクラスが呼んでいた次のような複数の関数でした。
GetSearchViewModel() GetPopupController() GetSubMenuController()
これらはすべて、別のBlueprint(「画面構成ファクトリー」)側で定義された関数でした。C++からBlueprint関数を直接呼ぶことはできません。つまり、1つのUIコントローラーをC++化しようとしただけなのに、参照先のファクトリークラスまで手を入れる必要が出てきたのです。図にするとこうなります。
画面遷移コントローラー(C++化したい)
└─ 呼び出し先: 画面構成ファクトリー(BP関数のまま)
└─ さらに参照: サブメニューコントローラー(C++クラスが存在せずBPのみ)
└─ ...この先も連鎖する可能性
解決策は主に2つです。
- 参照元(ファクトリー)側にもC++のゲッターを実装する
BlueprintImplementableEventとして宣言し、C++側から呼べるようにする
今回は設計を見直し、ファクトリーへの個別依存をやめて「Provider経由でViewModelやコントローラーを解決する」構成に変更することで対応しました。重要なのは、「対象クラス単体の変換難易度」と「その変換にかかる実際の工数」は別物だということです。事前に依存関係を洗い出さずに着手すると、想定外の巻き込み範囲に途中で気づいて手戻りが発生します。
落とし穴1:イベントディスパッチャの正しいC++移植先
BlueprintのEvent Dispatcher(イベントディスパッチャ)をC++に移植する際、直感的にBlueprintReadWriteを付けたくなりますが、これは誤りです。正しくはBlueprintAssignableを使います。
NG例(Blueprintの変数感覚でそのまま書いてしまう)
UPROPERTY(BlueprintReadWrite, Category="MenuInfo")
FOnMenuInfoChanged OnMenuInfoChanged;
OK例(実際に動作確認済みの移植)
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnMenuInfoChanged, FMenuInfo, MenuInfo);
UPROPERTY(BlueprintAssignable, Category="MenuInfo")
FOnMenuInfoChanged OnMenuInfoChanged;
この差は地味に見えますが、Blueprint側の「Bind Event」ノードから正しくバインドできるかどうかに直結します。あるケースでは、このディスパッチャは十数個ものアセットからBindされており、間違えたまま気づかず進めていたら影響範囲の特定にかなり時間を取られていたはずです。
落とし穴2:Switch分岐は「ピンのつながり」だけ見ても分からない
もう1つのハマりどころは、Blueprintのグラフを解析ツールでJSON化した際の分岐ロジックの読み方です。UEのBlueprintグラフをJSONとして構造化出力すると、各ピンのlinkedTo(接続先)が空になっているケースがあります。接続情報は、ノード単位ではなくグラフ全体のflows.execution/flows.dataにまとめて格納されているためです。
たとえば「ウィンドウのサイズを1段階ずつ切り替える」関数のSwitch分岐は、次の手順で初めて正確に追えました。
flows.executionで、SwitchノードのsourcePinName(=enum値の名前)から、分岐先ノードのtargetNodeGuidを辿る- 分岐先ノードの
pins[].defaultValueを見て、実際に何が代入されるかを確認する nodeProperties.meta.switchCasesやenum定義と突き合わせて、値の並びが合っているか照合する
この手順を踏んで初めて、サイズの切り替えサイクルの正しい順序と、「特定のケースではSwitchの出力ピンが未接続=何もしない」という仕様が正確に判明しました。ノードの見た目だけを目で追っていると、この「未接続=無処理」のケースは見落としがちです。
対照的にうまくいったケース:依存が閉じているクラス
同じプロジェクトで「メニュー管理クラス」を移植したときは、事情が全く違いました。このBlueprintは元々C++の親クラスを継承したサブクラスで、BP側で追加していたのはデリゲート1個・関数4個のみ。しかも、それらはすべて「C++の内部関数を呼んで、結果をブロードキャストするだけ」の薄いラッパーでした。
外部への依存の連鎖が一切なかったため、C++への移植・ビルド確認・Blueprint側の重複削除(一本化)まで、詰まることなく完了しました。この対比からも、「変換の難易度」を決めているのはBlueprintの複雑さそのものよりも、そのクラスが持つ外部依存の数だとわかります。
着手前にやるべきこと
ここまでの経験から、Blueprint→C++変換に着手する前にチェックしておきたいポイントをまとめます。
- 対象クラスが何から参照され、何を参照しているかを先に洗い出す(依存が「ハブ」か「閉じている」かで工数が大きく変わる)
- イベントディスパッチャは
BlueprintAssignableで移植する(BlueprintReadWriteではない) - Switch分岐など「見た目のノード配線」だけで判断せず、実データ(flows.execution等)で分岐先と代入値を確認する
- 「未接続=何もしない」という仕様が隠れていないか確認する
依存関係の洗い出しやJSON解析は、慣れないと地味に骨が折れる作業です。現在、UE5エディタ統合のC++プラグイン「BlueprintSerializer」でBlueprintを構造化JSONとして抽出し、そのJSONをもとにClaudeでC++下書きを生成する半自動化フローも試しています。この取り組みについては、また別の記事で詳しく紹介する予定です。
まとめ
- Blueprint→C++変換で本当に大変なのは「1クラスの置き換え手順」ではなく「依存関係の芋づる」
- 着手前に、対象クラスが他から参照される「ハブ」かどうかを見極める
- イベントディスパッチャは
BlueprintAssignableで移植する - 分岐ロジックは見た目のノードだけでなく実データで確認する
単発の置き換え手順を知りたい方は他の解説記事も参考になりますが、複数のBlueprintが絡み合った実プロジェクトで作業する際は、ぜひ「依存関係の連鎖」を最初にチェックしてみてください。
関連記事:この移植作業の前提になっているUE6でのBlueprint廃止方針は「UE6でBlueprintは廃止される?」で、変換の「検証」段階で実際に踏んだ別の落とし穴は「Blueprint→C++変換で実際に踏んだ誤検知」で紹介しています。
