プロジェクトが少し育つと、機能を足すたびに手が止まる瞬間が来ます。この敵のHP管理はC++に書くべきか、Blueprintのままでいいのか。判断の基準がないと、その場のノリで置き場所が決まり、あとから探せなくなります。
UEには、この分担についての定番の形があります。 C++で親クラスを作り、Blueprintでその子を作る という構造です。この記事では、その基本形と、何をどちらに置くかの判断基準、そして敵キャラのクラス設計を実際に切り分ける手順を解説します。
この記事でわかること
- 基本形は C++で親、Blueprintで子
- 置き場所の判断軸は 「頻繁に変わるか」「変わらないか」
- 全部C++にしない理由= 調整のテンポが落ちる
- 全部Blueprintにしない理由= 差分が追えず、参照が芋づるで膨らむ
- 実践: 敵キャラのクラス設計をC++とBPに切り分けて組む
基本形:C++で親、Blueprintで子
UEのクラスは継承でつながります。C++で作ったクラスは、そのままBlueprintの親クラスにできます。ここに分担の全体像があります。

| 層 | 担当 | 具体例 |
|---|---|---|
| C++(親) | 変わらない骨組み | HPの減算処理、死亡判定、状態の型定義、通信の仕組み |
| Blueprint(子) | 変わり続けるもの | 最大HPの値、使うメッシュ、被弾エフェクト、死亡時のSE |
この形の利点は、 修正の届き方 に出ます。HPの計算式を直したいとき、触るのはC++の親1つだけで、20体の子BPすべてに反映されます。逆にスライムのHPを50から60にしたいとき、触るのは BP_Slime の数値1つで、ビルドは発生しません。
C++クラスから子Blueprintを作る手順はBlueprintからC++への第一歩で扱っています。この記事は、その次の「何を親に置くか」の話です。
判断軸は「頻繁に変わるか」
「速いからC++」という基準で分けると、たいてい失敗します。実際に効く軸は1つで、 そのコードが今後どれくらいの頻度で変わるか です。

- めったに変わらないもの はC++へ。変更が少ないなら、ビルド待ちのコストがほとんど発生しません
- 毎日変わるもの はBlueprintへ。数値・アセット・演出は、企画の都合で何度も差し替わります
もう1つ、副次的な軸として 「同じものが何個も必要か」 があります。20体の敵が同じ処理を持つなら、その処理はC++の親に置いた方が配りやすくなります。1つのレベルにしか出てこない仕掛けなら、Blueprintに置いたままで構いません。
速度については、ボトルネックとして計測できたときだけ考えれば十分です。測り方はStatコマンドの記事を参照してください。
置き場所の早見表
迷いやすいものを並べます。プロジェクトの規模で最適解は変わりますが、個人開発から小規模チームなら、この配置で大きく外しません。
| 対象 | 置き場所 | 理由 |
|---|---|---|
| Struct / Enum の型定義 | C++ | Blueprint製は項目変更で既存データが壊れやすい |
| 基底クラス(Character・GameMode・Component) | C++ | 子に配る土台。変更が全体に効く |
| Subsystem(セーブ・設定・進行管理) | C++ | 常駐する仕組み。仕様が固まりやすい |
| 毎フレーム回るループ、多数のアクターの一括処理 | C++ | 実行速度の差が出る数少ない場所 |
| ステータス値(HP・速度・クールダウン) | Blueprint / Data Asset | 調整のたびにビルドを挟みたくない |
| 使うメッシュ・アニメーション・マテリアル | Blueprint | アセット参照はエディタで割り当てる方が速い |
| VFX・SE・カメラシェイクなどの演出 | Blueprint | 差し替え頻度が最も高い |
| UMGウィジェットのレイアウトとアニメーション | Blueprint | 見た目はデザイナが直接触る |
| レベル固有の仕掛け(このステージだけの扉) | Blueprint | 使い回さないものにC++のコストは合わない |
補足: ステータス値は、子BPのデフォルト値に置く方法と、Data Assetに切り出す方法があります。敵が3〜5種類なら子BPで十分で、20種類を超えてきたらData Assetへ移すと管理しやすくなります。
なぜ全部C++にしないのか
C++に寄せすぎると、コードの品質とは別のところで開発が遅くなります。

- 調整のたびにビルドを待つ: 敵のHPを50から60にする作業が、コードの書き換えとビルドとエディタ再起動になり ます。1回あたり数十秒から数分でも、1日に何十回もやると効きます
- ヘッダーを触るとエディタを閉じる必要がある: 変数を1つ足すだけでもLive Codingでは反映できません。試行錯誤の途中でこれが挟まると、思考が途切れます
- エディタで触った方が速いものがある: Timeline、カーブ、UMGのレイアウト、アニメーションBPのステート。これらは見ながら調整するものなので、コードで書いても得がありません
- プログラマー以外が触れなくなる: チームで作るなら、数値と演出はBP側に出しておかないと、調整の依頼がすべてプログラマーに集まります
なぜ全部Blueprintにしないのか
逆にBlueprintだけで押し切ると、プロジェクトが育ったところで別の問題が出ます。
- Gitで差分が読めない: Blueprintはバイナリのアセットです。「昨日と何が変わったか」をレビューできず、2人が同じBPを編集すると、どちらかの作業を捨てるしかありません
- 参照が芋づるで膨らむ: BPが別のBPを直接参照すると、片方を読み込むだけで参照先も一緒にメモリへ乗ります。積み重なると、起動時のロード時間として跳ね返ります
- 一括修正ができない: 関数名を変えたいとき、テキスト検索も一括置換も使えません。参照先を1つずつ開いて直すことになります
- Struct変更でデータが壊れる: Blueprintで作ったStructは、項目の追加や型変更をすると、それを使っていた既存の値がリセットされることがあります。型定義をC++に置いておくと、この事故を避けられます

補足: 参照の膨らみはBP側の書き方でも減らせます。直接参照ではなくBlueprint InterfaceやEvent Dispatcherを挟むと、依存の線が細くなります。C++に移す前に、まずここを見直す価値があります。
実践:敵キャラのクラス設計を切り分ける
1体の敵を、C++とBlueprintに切り分けて組みます。見下ろしARPGのザコ敵、横スクロールアクションの雑魚、タワーディフェンスの侵攻ユニット。「HPがあって、ダメージを受けて、0になったら死ぬ」という構図は、どのジャンルでも共通です。
完成形

C++の AEnemyBase が「ダメージを受けてHPを減らし、0になったら死ぬ」ところまでを担当します。子BPは、最大HPの値と、被弾・死亡の演出だけを持ちます。
再現条件
AEnemyBase(親クラス:Character)に、次のメンバを用意します。
| メンバ名 | 型 | 初期値 | 指定子 | 置き場所 |
|---|---|---|---|---|
MaxHealth | float | 100.0f | EditDefaultsOnly, BlueprintReadOnly | C++で宣言・BPで値を設定 |
CurrentHealth | float | 0.0f | VisibleAnywhere, BlueprintReadOnly | C++のみ |
bIsDead | bool | false | VisibleAnywhere, BlueprintReadOnly | C++のみ |
TakeDamage(...) | virtual float オーバーライド | — | (AActor の既存関数) | C++のみ |
GetHealthPercent() | float を返す const 関数 | — | BlueprintPure | C++で実装・BPで使用 |
OnDamagedEffect(float) | 関数(float 引数1つ) | — | BlueprintImplementableEvent | C++で宣言・BPで実装 |
OnDeathEffect() | 関数(引数なし) | — | BlueprintImplementableEvent | C++で宣言・BPで実装 |
MaxHealth を EditDefaultsOnly にしているのが要点です。 子BPのクラスデフォルトでは編集できるが、レベルに置いた個体ごとには変えられない という状態になります。同じスライムなのに置いた場所でHPが違う、という事故を防げます。指定子の詳しい読み方は第一歩の記事にまとめています。
C++側:変わらない骨組み
// EnemyBase.h
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/Character.h"
#include "EnemyBase.generated.h"
UCLASS()
class MYPROJECT_API AEnemyBase : public ACharacter
{
GENERATED_BODY()
public:
AEnemyBase();
// AActor の既存関数をオーバーライドする。Apply Damage を受け取る入口
virtual float TakeDamage(float DamageAmount, FDamageEvent const& DamageEvent,
AController* EventInstigator, AActor* DamageCauser) override;
// HPバーに繋ぐ用。実行ピンのない緑ノードになる
UFUNCTION(BlueprintPure, Category = "Enemy|Status")
float GetHealthPercent() const;
protected:
virtual void BeginPlay() override;
// 子BPのクラスデフォルトで設定する。個体ごとには変えさせない
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Enemy|Status")
float MaxHealth = 100.0f;
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Enemy|Status")
float CurrentHealth = 0.0f;
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Enemy|Status")
bool bIsDead = false;
// 演出はBlueprint側で実装する
UFUNCTION(BlueprintImplementableEvent, Category = "Enemy|Effects")
void OnDamagedEffect(float DamageAmount);
UFUNCTION(BlueprintImplementableEvent, Category = "Enemy|Effects")
void OnDeathEffect();
void Die();
};
// EnemyBase.cpp
#include "EnemyBase.h"
#include "Components/CapsuleComponent.h"
#include "GameFramework/CharacterMovementComponent.h"
AEnemyBase::AEnemyBase()
{
PrimaryActorTick.bCanEverTick = false;
}
void AEnemyBase::BeginPlay()
{
Super::BeginPlay();
// 子BPで設定された MaxHealth を初期HPにする
CurrentHealth = MaxHealth;
}
float AEnemyBase::TakeDamage(float DamageAmount, FDamageEvent const& DamageEvent,
AController* EventInstigator, AActor* DamageCauser)
{
const float ActualDamage =
Super::TakeDamage(DamageAmount, DamageEvent, EventInstigator, DamageCauser);
// 死亡済み、またはダメージ0以下なら何もしない
if (bIsDead || ActualDamage <= 0.0f)
{
return 0.0f;
}
CurrentHealth = FMath::Clamp(CurrentHealth - ActualDamage, 0.0f, MaxHealth);
UE_LOG(LogTemp, Log, TEXT("%s took %.1f damage. HP: %.1f / %.1f"),
*GetName(), ActualDamage, CurrentHealth, MaxHealth);
// 被弾の演出はBP側に任せる
OnDamagedEffect(ActualDamage);
if (CurrentHealth <= 0.0f)
{
Die();
}
return ActualDamage;
}
void AEnemyBase::Die()
{
bIsDead = true;
// 倒れたあと当たり判定と移動を止める
GetCapsuleComponent()->SetCollisionEnabled(ECollisionEnabled::NoCollision);
GetCharacterMovement()->DisableMovement();
// 死亡演出はBP側に任せる
OnDeathEffect();
}
float AEnemyBase::GetHealthPercent() const
{
return (MaxHealth > 0.0f) ? (CurrentHealth / MaxHealth) : 0.0f;
}
Blueprint側:値と演出だけを持つ
EnemyBase を右クリックして BP_Slime を作り、次を設定します。
| 設定項目 | 値 |
|---|---|
| Max Health(クラスデフォルト) | 50.0 |
| Mesh | スライムのスケルタルメッシュ |
| Anim Class | ABP_Slime |
| Event On Damaged Effect | ヒットSE + マテリアルを白点滅 |
| Event On Death Effect | 消滅VFX + 2秒後に Destroy Actor |
BP_Slime(子クラス)
Event On Damaged Effect(Damage Amount を受け取る)
→ Play Sound at Location(Sound: SFX_SlimeHit)
→ Set Scalar Parameter Value on Materials(Parameter: "HitFlash", Value: 1.0)
→ Delay(0.1)→ Set Scalar Parameter Value on Materials(Value: 0.0)
Event On Death Effect
→ Spawn System at Location(System: NS_SlimeBurst)
→ Play Anim Montage(Montage: AM_SlimeDeath)
→ Delay(2.0)→ Destroy Actor
同じ要領で BP_Skeleton(Max Health = 120.0)を作れば、C++には一行も足さずに2体目が完成します。
確認する
プレイヤーの攻撃から Apply Damage(Damage = 20.0, Damaged Actor = 敵)を呼び、BP_Slime に3回当てます。
- 1発目でHPが 50から30 に減り、Output Logに
BP_Slime_C_0 took 20.0 damage. HP: 30.0 / 50.0が出ます - 2発目で 30から10 、被弾SEと白点滅が2回とも再生されます
- 3発目でHPは マイナス10ではなく0 で止まり(
Clampが効いています)、死亡VFXが出て2秒後に消えます - HPバーを
GetHealthPercentに繋いでいれば、 1.0 → 0.6 → 0.2 → 0.0 と変化します
HPが減らない場合は、攻撃側が Apply Damage を呼んでいるかを確認してください。TakeDamage はUEのダメージ処理の入口なので、直接呼ぶのではなく Apply Damage 経由で届きます(Apply Damageの投げ方と受け方は 体力とダメージ入門 で扱っています)。演出が出ない場合は、子BPのイベントグラフに Event On Damaged Effect ノードが置かれているかを見ます(→ デバッグの記事)。
ポイントは2つです。
- C++には「必ずこの順で起きること」を書く: HPを減らす、0で止める、死亡フラグを立てる、判定を切る。この順番はどの敵でも同じなので、子BPに書かせると順番違いのバグが種類ぶん生まれます
- BPに渡すのは「呼ぶタイミング」だけ:
OnDamagedEffectは何をするか知りません。演出を差し替えてもC++のビルドは発生せず、逆にHP計算を直しても演出は無傷です。この切り分けはEvent Dispatcherと同じ考え方です
おまけ:先に知っておく と良いこと
- 最初からC++に寄せなくてよい: Blueprintでプロトタイプを作り、形が固まったところで親クラスをC++に起こす順番で問題ありません。既存BPは File > Reparent Blueprint でC++の親に付け替えられます
BlueprintNativeEventという中間もある:BlueprintImplementableEventはBPでしか実装できませんが、BlueprintNativeEventはC++に既定の処理を書きつつ、必要な子BPだけがオーバーライドできます。「たいていは共通、たまに違う」処理に向きます- カテゴリは
親|子で切る:Category = "Enemy|Status"のように書くと、詳細パネルで入れ子の見出しになります。C++の変数が10個を超えたあたりから効いてきます - Blueprint Nativizationは当てにしない: BPをC++に自動変換する機能はUE5で廃止されています。速度が要るところは、最初から人の手でC++に書きます
- 迷ったらBlueprintに置く: あとからC++へ移すのは可能ですが、C++で書いたものをBPに戻すのは手間です。判断がつかないものは、まずBPで動かして様子を見る方が損が小さくなります
まとめ
- 基本形は C++で親、Blueprintで子 。修正はC++から全体へ、調整はBPで個別に
- 判断軸は速度ではなく 「頻繁に変わるか」 。変わらないものをC++へ、毎日変わるものをBPへ
- 型定義(Struct・Enum)と基底クラスとSubsystemは C++ 、数値・アセット・演出は Blueprint
- 全部C++にすると 調整のテンポ が落ち、全部BPにすると 差分と参照 で詰まる
- 実践の要は C++が順番を、BPが見た目を持つ こと。互いのビルドと差し替えを邪魔しない
いま作っている機能のうち、この1週間で3回以上値を変えたものはどれでしょうか。それはBlueprint側に置いておくべきものです。