スライムのHPを50から70へ変えたい。倒れた後、すぐ消えるのではなく少し残したい。一方で、HPがマイナスになる不具合は、すべての敵でまとめて直したい。
同じ敵の機能でも、変えたいものは別々です。全員に共通するHPの計算と、種類ごとに調整する数値・演出を分ける と、どこを直せばよいかが見えやすくなります。
この記事では、C++の親クラスにHP管理を置き、子Blueprintで最大HPと倒れ方を変えます。C++クラスを追加してビルドできる人を想定しています。初回の環境構築とマクロの読み方は、C++への第一歩から始めてください。
この記事でわかること
- 共通の処理と、種類ごとの設定を分けて考える
- C++から子Blueprintの演出イベントを呼ぶ
- 20ダメージを送り、HPが50→30→10→0と減ることを確かめる
- C++を変えずに、敵の体力と消えるまでの時間を調整する
基本形:同じ親から、違う敵を作る
親クラス は共通の処理や変数を持つ設計図です。子クラス は、その作りを引き継いで使います。今回はC++のEnemyBaseを親にし、BP_SlimeとBP_Golemという子Blueprintを作ります。

| 置く場所 | 今回の担当 |
|---|---|
| C++の親 | ダメージでHPを減らす。0未満にしない。死亡済みなら二度目の処理をしない |
| 子Blueprint | 最大HPと見た目を選ぶ。被弾を表示し、倒れた後の動きを組む |
HPの計算式を直すときは、親のC++を一か所変えます。スライムだけを強くするときは、BP_Slimeの最大HPを変えます。共通の修正は親へ、種類ごとの調整は子へ という届き方になります。
これはC++にしかできない共通化ではありません。Blueprintの親やActor Componentでも、同じ処理を使い回せます。ここでは、C++で用意した土台をBlueprintから調整する形を試します。
判断するのは「何を変えたいか」
「頻繁に変わるものはBlueprintへ」は、出発点として役立ちます。ただし、変更頻度だけで全部は決まりません。まず、値を変えたいのか、処理のルールを変えたいのか を分けてみます。

「スライムの最大HPを70にする」は設定の変更です。「どの敵もHPが0を下回らないようにする」はルールの変更です。どちらも同じHPの話ですが、毎回触る場所を分けられます。
宣言する場所と、値を調整する場所は別
C++に変数を宣言しても、その値までコードに固定する必要はありません。
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Enemy|Status")
float MaxHealth = 100.0f;
これは「C++にMaxHealthという変数を用意し、子BlueprintのClass Defaultsで初期値を変えられる」指定です。BP_Slimeで50に設定すれば、その子から作る敵は50で始まります。数値の調整だけなら、C++のビルドを挟まずに試せます。
処理をどちらに書くか迷ったら、次も考えます。
- 誰が調整するか。エディタ上で見ながら試したい処理なら、Blueprintが扱いやすい。
- 必要な機能がノードから使えるか。公開されていないAPIや外部ライブラリを使うなら、C++が候補になる。
- 何に時間がかかっているか。性能が理由なら、処理時間を測ってから対象を選ぶ。
たとえば大量の計算が重いなら、その計算だけをC++へ移す方法があります。敵の見た目やUIまで、まとめて移す必要はありません。
置き場所に迷ったときの目安
| 作りたいもの | 最初の置き場所の例 | 判断するポイント |
|---|---|---|
| 最大HP、移動速度、使うメッシュ | 子Blueprint、Data Asset | コードを開かずに調整したいか |
| 被弾・死亡時の音や見た目 | Blueprint | 見ながら差し替えるか |
| HP計算などの共通ルール | 共通の親やComponent | BPで十分か、C++で管理する利点があるか |
| C++とBPの両方から使うStruct・Enum | C++で型を定義 | 同じ型を両方で扱う必要があるか |
| 試作中の敵AIやレベルの仕掛け | Blueprint | まず挙動を試し、共通部分が見えてから分ける |
| 計測して重いと分かったループ | C++への移行を検討 | そもそも毎回その処理をする必要があるか |
StructやEnumは、Blueprintだけで使うならBlueprintで作って構いません。「型」は、アイテムがどんな項目を持つか、 敵がどんな状態を取れるか、といったデータの形です。C++でも同じ形を使う予定なら、C++で定義してBlueprintへ公開すると扱いやすくなります。
数値の置き場所も、敵が何種類になったら必ず移す、という決まりはありません。見た目や数値の組をまとめて差し替えたくなったら、Data Assetを検討します。

C++へ移すだけでは解決しないこと
Blueprintも検索と差分確認ができる
Blueprintには、使われている場所を探すFind Referencesや、変更前後を比べるUE Diff Toolがあります。「昨日の処理とどこが違うか」を確認できます。
ただし、Blueprintの.uassetはバイナリ形式なので、通常のコードと同じようにGitで行単位の差分を読み、自動でマージする運用には向きません。差分を見ることと、二人の変更を一つにまとめることは別 です。同じBPを同時に触るなら、編集担当を分けるなどの共同作業の決め方が必要になります。
アセットを読み込 む量は、参照の持ち方でも変わる
ハード参照 は、あるアセットが別のアセットを直接必要とするつながりです。元を読み込むと、参照先も一緒に読み込まれます。
たとえばUIがHPを知るためだけに、大きな見た目のアセットを持つBP_Bossの型へ依存していたら、UIを使うだけでもその依存が影響します。HPを読む入口を軽い親に置き、UIはその親の型を使う形を考えられます。

図の矢印は、アセットや型の依存です。右側でも、実際に登場させたボスの見た目は読み込みます。「UIが必要とする型のために、ボスの見た目まで要求する」つながりを減らす考え方です。
C++でも、親クラスがメッシュなどをハード参照すれば読み込みは増えます。Interfaceを入れただけで、他に残った参照が消えるわけでもありません。Reference Viewerで実際のつながりを確かめ、必要なときだけ読む設計はソフト参照と非同期ロードで扱います。
C++のStructも、変更後のデータ確認は必要
既存の型や項目名を変えると、その型を使うアセットやセーブの引き継ぎが必要になることがあります。C++なら変更をテキストで追いやすくなりますが、古い値を自動で守ってくれるわけではありません。大きな変更はコピーで試し、以前のデータを読み戻せるかも確かめます。
実践:HP管理と倒れる演出を分ける
まず作るもの
練習用に、移動しない敵を一体作ります。見た目はEngine標準のSphereでスライムに見立てます。Hキーで20ダメージずつ送り、3発で平たくなり、2秒後に消える形です。HPの数値はOutput Logで確認します。

今回は、C++とBlueprintの間を次の順で行き来します。
- テスト用のBlueprintがApply Damageを呼ぶ。
- 敵のC++がダメージを受け取り、HPを更新する。
- C++が子Blueprintへ「被弾した」「倒れた」と知らせる。
- 子Blueprintが、その知らせに合わせて見た目を変える。

C++はじめの一歩のBlueprintCallableは、「BPからC++の関数を呼べる」指定でした。今回使う BlueprintImplementableEventは、C++から呼ぶ処理の中身をBPで作る 指定です。C++は呼ぶ時点を決め、子はそこで何を見せるかを決めます。
C++の親を用意する
C++はじめの一歩で作ったCppFirstStepプロジェクトへ、親をActor、名前を EnemyBase としてC++クラスを追加します。今回は歩行やアニメーションを使わないので、CharacterではなくActorを選びます。
自動ビルドが終わったらUEを保存して閉じ、生成された2ファイルを次の内容へ置き換えます。既存プロジェクトで試す場合は、CPPFIRSTSTEP_API をそのプロジェクトで生成されたAPIマクロ名にします。
EnemyBase.h
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "EnemyBase.generated.h"
UCLASS(Blueprintable)
class CPPFIRSTSTEP_API AEnemyBase : public AActor
{
GENERATED_BODY()
public:
AEnemyBase();
virtual float TakeDamage(float DamageAmount, const FDamageEvent& DamageEvent,
AController* EventInstigator, AActor* DamageCauser) override;
UFUNCTION(BlueprintPure, Category = "Enemy|Status")
float GetHealthPercent() const;
protected:
virtual void BeginPlay() override;
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Enemy|Status",
meta = (ClampMin = "1.0"))
float MaxHealth = 100.0f;
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Enemy|Status")
float CurrentHealth = 0.0f;
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Enemy|Status")
bool bIsDead = false;
UFUNCTION(BlueprintImplementableEvent, Category = "Enemy|Effects")
void OnEnemyHitVisual();
UFUNCTION(BlueprintImplementableEvent, Category = "Enemy|Effects")
void OnEnemyDefeatedVisual();
};
EnemyBase.cpp
#include "EnemyBase.h"
#include "Components/SceneComponent.h"
AEnemyBase::AEnemyBase()
{
PrimaryActorTick.bCanEverTick = false;
SetRootComponent(CreateDefaultSubobject<USceneComponent>(TEXT("SceneRoot")));
}
void AEnemyBase::BeginPlay()
{
MaxHealth = FMath::Max(1.0f, MaxHealth);
CurrentHealth = MaxHealth;
bIsDead = false;
Super::BeginPlay();
UE_LOG(LogTemp, Log, TEXT("%s HP: %.0f / %.0f"),
*GetName(), CurrentHealth, MaxHealth);
}
float AEnemyBase::TakeDamage(float DamageAmount, const FDamageEvent& DamageEvent,
AController* EventInstigator, AActor* DamageCauser)
{
if (bIsDead || !CanBeDamaged() || DamageAmount <= 0.0f)
{
return 0.0f;
}
const float AcceptedDamage =
Super::TakeDamage(DamageAmount, DamageEvent, EventInstigator, DamageCauser);
if (AcceptedDamage <= 0.0f)
{
return 0.0f;
}
const float PreviousHealth = CurrentHealth;
CurrentHealth = FMath::Clamp(CurrentHealth - AcceptedDamage, 0.0f, MaxHealth);
// 演出を呼ぶ前に、死亡済みの状態を確定する
bIsDead = (CurrentHealth <= 0.0f);
if (bIsDead)
{
SetCanBeDamaged(false);
SetActorEnableCollision(false);
}
UE_LOG(LogTemp, Log, TEXT("%s HP: %.0f / %.0f"),
*GetName(), CurrentHealth, MaxHealth);
OnEnemyHitVisual();
if (bIsDead)
{
OnEnemyDefeatedVisual();
}
return PreviousHealth - CurrentHealth;
}
float AEnemyBase::GetHealthPercent() const
{
return (MaxHealth > 0.0f) ? (CurrentHealth / MaxHealth) : 0.0f;
}
保存後、Visual StudioでDevelopment Editor / Win64を選び、ゲームのCppFirstStepプロジェクトをビルドします。成功したら.uprojectを開き直します。
コードの中で守っていること
BeginPlayで、子BPに設定したMaxHealthをCurrentHealthへ入れます。ヘッダーにあるCurrentHealthの0は開始前の値で、Play開始時には50などの最大HPになります。最大HPは1以上にそろえ、この例ではHP0の敵を初期配置しません。
TakeDamage は、Actorがダメージを受け取る既存の関数です。オーバーライド は、親にある関数を、自分のクラス用の処理へ作り替えること。ここでは Super::TakeDamage で親の処理も呼び、その後に自分のHPを減らしています。
引数が長いのは、ダメージ量だけでなく、攻撃の情報・攻撃を指示したController・直接当てたActorも受け取るためです。この実践では、まずダメージ量だけを使います。

Clamp は、値を指定した範囲に収める計算です。残りHP10へ20ダメージが来ても、0〜最大HPに収めるので、結果は-10ではなく0になります。戻り値は今回減ったHPで、この最後の一発では10です。
bIsDead は死亡済みの印です。倒れたと決める処理を、BPの演出より先に済ませます。 2秒待って消える間にもう一度Hを押しても、HPの更新や死亡演出を繰り 返しません。
最大HPは子BPで変えますが、現在HPを減らす場所はこのC++にそろえます。BPのEvent AnyDamageにも減算を追加すると、同じ攻撃で二重に減らす原因になります。今回は、後述する専用の演出イベントを使ってください。
子Blueprintで見た目と演出を作る
1. BP_Slimeを作る
コンテンツブラウザのC++ ClassesでEnemyBaseを右クリックし、このクラスを元にBlueprintを作ります。名前は BP_Slime とします。
| 操作する場所 | 設定 |
|---|---|
| Components | Static Meshを追加し、名前をBodyにする。SceneRootの子にする |
| Body → Static Mesh | EngineのBasicShapesにあるSphere |
| Body → Transform | Location=(0, 0, 0)、Scale=(0.6, 0.6, 0.4) |
| Body → Mobility | Movable |
| Body → Collision Presets | NoCollision |
| Class Defaults → Enemy → Status | Max Health=50 |
| Class Defaults | Can Be Damagedが有効であることを確認 |
Sphereが見つからなければ、アセット選択欄のShow Engine Contentを有効にします。ここでは当たり判定で攻撃せず、指定した相手へ直接ダメージを送るため、NoCollisionで構いません。

コンパイル・保存し、Third Personのレベルで、Player Startから見える位置にBP_Slimeを一体配置します。Actor全体のScaleは(1, 1, 1)にして、床から少し浮かせます。Playして球が見え、Output Logに HP: 50 / 50 が出れば、C++の初期化と子BPの設定がつながっています。
MaxHealthのEditDefaultsOnlyは、子BPの初期設定を編集でき、レベルに置いた個体ごとの値は編集できない 指定です。この例では、同じBP_Slimeから作った敵の最大HPをそろえるために使っています。
2. 被弾したら文字を出す
BP_SlimeのEvent Graphで「Event On Enemy Hit Visual」を追加します。C++から受け継いだイベントなので、右クリック検索から選びます。同名のCustom Eventを新しく作る操作ではありません。
白い実行線をPrint Stringへつなぎ、In Stringを「命中」、Durationを0.5にします。

まずは文字で、BPまで呼び出しが届くことを確かめます。後でここを音や被弾フラッシュへ変えても、HPの計算は変わりません。
3. 倒れたら平たくし、2秒後に消す
同じEvent Graphへ「Event On Enemy Defeated Visual」を追加し、次の順につなぎます。
- Set Actor Scale 3D:Target=Self、New Scale 3D=(1, 1, 0.2)。
- Delay:Duration=2.0。
- CompletedからDestroy Actor:Target=Self。
