【Unreal Engine】BlueprintとC++の役割分担:HPの計算を共通にして、敵ごとの倒れ方を変える

作成: 2025-12-12最終更新: 2026-09-07

BlueprintとC++をどこで分けるか、敵のHPと演出で考えます。C++で共通のHP計算、子Blueprintで最大HPと倒れ方を設定し、Apply Damageで確認。変更頻度だけに頼らない判断、参照と差分の注意も図解します。

スライムのHPを50から70へ変えたい。倒れた後、すぐ消えるのではなく少し残したい。一方で、HPがマイナスになる不具合は、すべての敵でまとめて直したい。

同じ敵の機能でも、変えたいものは別々です。全員に共通するHPの計算と、種類ごとに調整する数値・演出を分ける と、どこを直せばよいかが見えやすくなります。

C++の共通処理とBlueprintの調整を組み合わせて、一体の敵を作るイメージ

この記事では、C++の親クラスにHP管理を置き、子Blueprintで最大HPと倒れ方を変えます。C++クラスを追加してビルドできる人を想定しています。初回の環境構築とマクロの読み方は、C++への第一歩から始めてください。

この記事でわかること

  • 共通の処理と、種類ごとの設定を分けて考える
  • C++から子Blueprintの演出イベントを呼ぶ
  • 20ダメージを送り、HPが50→30→10→0と減ることを確かめる
  • C++を変えずに、敵の体力と消えるまでの時間を調整する

Sponsored

基本形:同じ親から、違う敵を作る

親クラス は共通の処理や変数を持つ設計図です。子クラス は、その作りを引き継いで使います。今回はC++のEnemyBaseを親にし、BP_SlimeとBP_Golemという子Blueprintを作ります。

EnemyBaseのHP計算を引き継ぎ、BP_SlimeはHP50、BP_GolemはHP120と見た目を設定する
置く場所今回の担当
C++の親ダメージでHPを減らす。0未満にしない。死亡済みなら二度目の処理をしない
子Blueprint最大HPと見た目を選ぶ。被弾を表示し、倒れた後の動きを組む

HPの計算式を直すときは、親のC++を一か所変えます。スライムだけを強くするときは、BP_Slimeの最大HPを変えます。共通の修正は親へ、種類ごとの調整は子へ という届き方になります。

これはC++にしかできない共通化ではありません。Blueprintの親やActor Componentでも、同じ処理を使い回せます。ここでは、C++で用意した土台をBlueprintから調整する形を試します。

判断するのは「何を変えたいか」

「頻繁に変わるものはBlueprintへ」は、出発点として役立ちます。ただし、変更頻度だけで全部は決まりません。まず、値を変えたいのか、処理のルールを変えたいのか を分けてみます。

最大HPを50から70へ調整する変更と、全敵に共通するHP計算を直す変更の比較

「スライムの最大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計算などの共通ルール共通の親やComponentBPで十分か、C++で管理する利点があるか
C++とBPの両方から使うStruct・EnumC++で型を定義同じ型を両方で扱う必要があるか
試作中の敵AIやレベルの仕掛けBlueprintまず挙動を試し、共通部分が見えてから分ける
計測して重いと分かったループC++への移行を検討そもそも毎回その処理をする必要があるか

StructやEnumは、Blueprintだけで使うならBlueprintで作って構いません。「型」は、アイテムがどんな項目を持つか、敵がどんな状態を取れるか、といったデータの形です。C++でも同じ形を使う予定なら、C++で定義してBlueprintへ公開すると扱いやすくなります。

数値の置き場所も、敵が何種類になったら必ず移す、という決まりはありません。見た目や数値の組をまとめて差し替えたくなったら、Data Assetを検討します。

見ながら調整したいものはBlueprint、両方から使う型や重いループはC++を検討する
Sponsored

C++へ移すだけでは解決しないこと

Blueprintも検索と差分確認ができる

Blueprintには、使われている場所を探すFind Referencesや、変更前後を比べるUE Diff Toolがあります。「昨日の処理とどこが違うか」を確認できます。

ただし、Blueprintの.uassetはバイナリ形式なので、通常のコードと同じようにGitで行単位の差分を読み、自動でマージする運用には向きません。差分を見ることと、二人の変更を一つにまとめることは別 です。同じBPを同時に触るなら、編集担当を分けるなどの共同作業の決め方が必要になります。

アセットを読み込む量は、参照の持ち方でも変わる

ハード参照 は、あるアセットが別のアセットを直接必要とするつながりです。元を読み込むと、参照先も一緒に読み込まれます。

たとえばUIがHPを知るためだけに、大きな見た目のアセットを持つBP_Bossの型へ依存していたら、UIを使うだけでもその依存が影響します。HPを読む入口を軽い親に置き、UIはその親の型を使う形を考えられます。

UIから重い子BPへの型依存と、共通の親の型だけを使う場合の違い

図の矢印は、アセットや型の依存です。右側でも、実際に登場させたボスの見た目は読み込みます。「UIが必要とする型のために、ボスの見た目まで要求する」つながりを減らす考え方です。

C++でも、親クラスがメッシュなどをハード参照すれば読み込みは増えます。Interfaceを入れただけで、他に残った参照が消えるわけでもありません。Reference Viewerで実際のつながりを確かめ、必要なときだけ読む設計はソフト参照と非同期ロードで扱います。

C++のStructも、変更後のデータ確認は必要

既存の型や項目名を変えると、その型を使うアセットやセーブの引き継ぎが必要になることがあります。C++なら変更をテキストで追いやすくなりますが、古い値を自動で守ってくれるわけではありません。大きな変更はコピーで試し、以前のデータを読み戻せるかも確かめます。

実践:HP管理と倒れる演出を分ける

まず作るもの

練習用に、移動しない敵を一体作ります。見た目はEngine標準のSphereでスライムに見立てます。Hキーで20ダメージずつ送り、3発で平たくなり、2秒後に消える形です。HPの数値はOutput Logで確認します。

20ずつダメージを送り、HP50から30、10、0へ減り、平たくなった敵が2秒後に消える

今回は、C++とBlueprintの間を次の順で行き来します。

  1. テスト用のBlueprintがApply Damageを呼ぶ。
  2. 敵のC++がダメージを受け取り、HPを更新する。
  3. C++が子Blueprintへ「被弾した」「倒れた」と知らせる。
  4. 子Blueprintが、その知らせに合わせて見た目を変える。
Blueprintからダメージを送り、C++のHP更新後に子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も受け取るためです。この実践では、まずダメージ量だけを使います。

死亡済みなら終了し、HPを更新して死亡状態を確定した後で、子Blueprintの演出を呼ぶ順序

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 とします。

操作する場所設定
ComponentsStatic Meshを追加し、名前をBodyにする。SceneRootの子にする
Body → Static MeshEngineのBasicShapesにあるSphere
Body → TransformLocation=(0, 0, 0)、Scale=(0.6, 0.6, 0.4)
Body → MobilityMovable
Body → Collision PresetsNoCollision
Class Defaults → Enemy → StatusMax Health=50
Class DefaultsCan Be Damagedが有効であることを確認

Sphereが見つからなければ、アセット選択欄のShow Engine Contentを有効にします。ここでは当たり判定で攻撃せず、指定した相手へ直接ダメージを送るため、NoCollisionで構いません。

SceneRootの子にSphereのBodyを置き、Class Defaultsで最大HPを50にする設定

コンパイル・保存し、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にします。

C++から呼ばれるOn Enemy Hit Visualを、命中と表示するPrint Stringへつなぐ

まずは文字で、BPまで呼び出しが届くことを確かめます。後でここを音や被弾フラッシュへ変えても、HPの計算は変わりません。

3. 倒れたら平たくし、2秒後に消す

同じEvent Graphへ「Event On Enemy Defeated Visual」を追加し、次の順につなぎます。

  1. Set Actor Scale 3D:Target=Self、New Scale 3D=(1, 1, 0.2)。
  2. Delay:Duration=2.0。
  3. CompletedからDestroy Actor:Target=Self。
死亡�イベントから自分のActorの高さを0.2倍にし、次の処理へつなぐ

続きは、その後の片付けです。

平たくした後に2秒待ち、同じActorをDestroy Actorで消す

2枚の図のAは、白い線の続きです。Aというノードは作りません。Selfは、この処理を実行しているBP_Slime自身 を指します。

BodyのScaleとは別に、Actor全体の高さを0.2倍へ変えるので、平たくつぶれた見た目になります。Delayを5.0にすると、残る時間も5秒になります。演出に合わせて消す時刻を決める部分は、子BPに置きました。

Sponsored

20ダメージずつ送り、分担を確かめる

1. 配置した敵への参照を作る

レベルでBP_Slimeを選択し、Level Blueprintを開きます。グラフを右クリックし「Create a Reference to BP_Slime」を選ぶか、Outlinerからその個体をドラッグします。

参照は、レベルに置いたどの一体へ送るかを指定するもの です。コンテンツブラウザの設計図を指定する操作とは違います。

HキーのPressedから、白い実行ピンを持つ「Is Valid」へつなぎます。Input Objectには配置したBP_Slimeの参照を入れ、Is Valid側の実行線を次へ進めます。Is Not Valid側はつながず終了します。

Hキーを押したとき、配置したBP_Slimeがまだ有効なら次へ進む

Is Validは、その相手をまだ使えるかの確認です。消えた後の敵にダメージを送らないために置きます。死亡して平たくなっている2秒間はまだ有効ですが、その間のダメージはC++の死亡済み判定が止めます。

2. Apply Damageへ同じ敵を渡す

続く白い線をApply Damageへつなぎます。Damaged Actorには、先ほどと同じBP_Slimeの参照を接続します。

入力今回の設定
Damaged Actor配置したBP_Slimeの参照
Base Damage20.0
Event Instigator / Damage Causer未接続
Damage Type Class未指定
有効と確認した続きで、同じBP_Slimeへ20ダメージを送る

図のBも白い線の続きです。2枚に出てくるBP_Slimeは同じ個体で、一つの参照出力を2か所へ分岐して使って構いません。

今回はテスト入力なので、攻撃したControllerや武器は渡していません。実際の攻撃へ組み込むときは、Apply Damageの基本のように、命中した相手と攻撃元を渡します。

3. Playして、一発ずつ確認する

すべてコンパイル・保存し、Play画面をクリックしてからHを押します。Output Logを HP: で絞ると、数値を追いやすくなります。文字が読める程度の間隔で押してください。

操作HPのログ見える結果
Play開始50 / 50球が表示される
Hを1回30 / 50「命中」が出る
Hを2回10 / 50「命中」が出る
Hを3回0 / 50「命中」が出て球が平たくなり、2秒後に消える
平たくなった後、もう一度H新しいHPログは出ない消えるまでの待ち時間をやり直さない
消えた後にH新しいHPログは出ない何も起きない

次にPlayを止め、BP_SlimeのMax Healthを70へ変えます。再びPlayすると 70→50→30→10→0で4発必要 になります。初期値だけで体力を調整でき、HPを減らすC++はそのままです。

4. 同じ親で、別の敵を作る

BP_Slimeを複製して BP_Golem を作り、Max Health=120、BodyのStatic MeshをCubeへ変えます。被弾文字を「ゴーレムに命中」へ変え、消えるまでのDelayを5.0にします。

レベルの別の位置に配置し、Level Blueprintで使っている敵の参照を、そのBP_Golemへ差し替えます。Is ValidとApply Damageの両方を同じ個体へそろえます。20ダメージなら6発で0になり、5秒後に消えます。

敵を増やすために、HPの減算をコピーする必要はありません。 共通のルールを直すときは親、体力や演出を変えるときは子、という分担を操作で確かめられます。

うまくいかないとき

症状確認するところ
C++のビルドが失敗するAPIマクロ、Actorを親にしたか、2ファイルとinclude順、UEを閉じたか
演出イベントが検索に出ない子BPの親がEnemyBaseか、C++のビルド成功、BlueprintImplementableEventの宣言
HPが減らないPlay画面のフォーカス、HのPressed、Is Valid側の線、Damaged Actor、Can Be Damaged
HPが二重に減るEvent AnyDamageや子BPに、もう一つ減算を作っていないか
球が平たくならない死亡イベントからの白い線、Set Actor Scale 3DのTarget=Self、Z=0.2、Mobility
消えないDelayのCompletedからDestroy Actorへつながっているか
別の敵へ送ってしまうレベルに置いた個体の参照を、確認と送信の両方でそろえたか

おまけ:今のゲームへ広げる前に

HPバーや本物の演出を足す

GetHealthPercent() は、現在HP÷最大HPを返します。HP50の敵なら、1.0→0.6→0.2→0.0になります。HPバーを追加する際に、Progress BarのPercentへ渡すための入口です。今回の練習では、バーを作る前にログで数値を確認しました。

音やNiagaraを足す場合も、子BPの演出イベントへ追加します。ここは「見せる処理」の場所なので、HPの再計算や報酬の重複判定は混ぜないようにします。マルチプレイでは、HPを決める側と演出を表示する側の通信設計も別に必要です。

共通の演出をC++に用意する場合

BlueprintImplementableEventは、今回のようにBPへ中身を任せる指定です。一方、BlueprintNativeEvent は、C++に既定の処理を置き、必要な子BPで変更できる指定です。「普段は共通の音、特別な敵だけ別の演出」のように広げたいときの候補になります。

既存のBPを一度に移さない

今のBPで十分に作れている部分は、そのままでも構いません。分けるなら、HP計算など一つの機能で試し、移す前と同じ結果になるかを確かめます。

既存BPの親を変えるときは、コピーを用意し、同名の変数・イベント・初期値を整理してからReparent Blueprintを使います。親を変えただけで、元のノード処理が自動的にC++へ移るわけではありません。

Blueprint Header ViewでC++風の宣言を確認する方法もありますが、関数の中身は自分で移す必要があります。変更後のビルドとLive Codingの使い分けは、第一歩の記事を参照してください。

次に敵を調整するとき、「数値を変える」「共通の計算を直す」「見せ方を変える」のどれかを考えてみてください。開く場所がそれぞれ決まっていれば、分担が制作を助けています。

まとめ

  • 共通の処理はC++、種類ごとの見た目と演出は子Blueprintに置く
  • C++からはイベントを呼ぶだけにして、演出の中身は知らないままにする
  • 体力や消えるまでの時間は、C++を変えずに詳細パネルで調整する

分けるか迷ったときの問いかけは、「これは全部の敵で同じか、種類ごとに変わるか」 です。同じならC++、変わるならBlueprintへ置きます。

C++側の書き方から確かめたいなら BlueprintからC++への最初の一歩、通信のつなぎ方を広げるなら Blueprintの通信方法 へ進んでください。

参考:BlueprintとC++の使い分けUFunctionsTakeDamageアセット参照UE Diff Tool

Unreal Engine このセクションのノート98