【Unreal Engine】UE5の体力とダメージ入門:Apply DamageとEvent AnyDamageで攻撃を当てる

作成: 2026-07-20

UEの標準ダメージ経路を図解。Apply Damage / Point / Radialの3種と受け側のEvent AnyDamage、Damage CauserとInstigatorの違い、Damage Typeによる属性分け、そして連続ヒットで一瞬で死ぬバグを止める無敵時間の作り方まで。

敵に攻撃を当てたい。とりあえず相手をCastしてHP変数を直接減らす方法で動きました。ところが敵の種類が3つに増え、壊せる樽とトゲのトラップを足したあたりで、攻撃側のBlueprintがCastの分岐だらけになります。

UEには Apply Damage という標準のダメージ経路があります。攻撃する側は相手が何者かを知らないまま「このActorに20ダメージ」とだけ伝え、受け取る側が Event AnyDamage で自分の都合に合わせて処理します。この記事では、3種類のApplyノードの使い分け、受け側のイベント、Damage CauserとInstigatorの違い、そして連続ヒットで一瞬で死ぬバグを止める無敵時間の作り方を解説します。

攻撃側から矢印が伸び、敵・樽・トラップの3つに同じ形で届くダメージの流れ

この記事でわかること

  • HP変数を直接減らす方法が、あとで破綻する理由
  • Apply Damage / Apply Point Damage / Apply Radial Damage の使い分け
  • 受け側の Event AnyDamage と、二重に減るよくある事故
  • Damage CauserInstigator は誰を指すのか
  • Damage Type クラスで炎・毒などの属性を分ける
  • 実践: 攻撃を当ててHPを減らし、0で倒し、被弾直後は無敵にする

Sponsored

HP変数を直接減らすと、あとで詰まる

最初に思いつくのは、攻撃が当たった相手を Cast To BP_Enemy して、その Health 変数を直接書き換える方法です。敵が1種類のうちは問題なく動きます。

問題は、ダメージを受けるものが増えたときに起きます。

左は攻撃側から敵・樽・トラップそれぞれへCastの分岐が伸びる図、右はApply Damageの1本だけが伸びる図の対比

BP_Enemy BP_Boss BP_Barrel BP_Spike と対象が増えるたびに、攻撃側へCastの分岐を足すことになります。しかも攻撃手段が剣・弓・爆弾と増えれば、その全部に同じ分岐が要ります。 対象の数 × 攻撃手段の数 だけ書き直しが発生する形です。

Apply Damageを使うと、攻撃側が知る必要があるのは次の3つだけになります。

  • 誰に当たったか(Actor)
  • いくつ減らすか(float)
  • どういう種類の攻撃か(Damage Type クラス)

受け側が何のクラスかは問いません。ダメージを受けたい物を増やすときは、そのBlueprintに Event AnyDamage を1つ足すだけで済みます。攻撃側は一切触りません。

これはBlueprint Interfaceを使った疎結合と同じ考え方で、ダメージについてはエンジン側があらかじめ用意してくれている、と捉えると分かりやすいです。

Sponsored

ダメージを与える3つのノード

ダメージを送る側のノードは3種類あります。どれも GameplayStatics のノードで、どこのBlueprintからでも呼べます。

ノード対象主な用途
Apply Damage指定した1体接触ダメージ、剣が当たった、毒の継続ダメージ
Apply Point Damage指定した1体(当たった位置つき)銃撃、部位ごとのダメージ倍率
Apply Radial Damage範囲内のすべて爆発、範囲魔法、落石
Apply Damage・Point Damage・Radial Damageの3つを、1体・1体��+着弾点・球の範囲で対比した図

Apply Damage

最も基本の形です。入力ピンは次のとおりです。

ピン内容
Damaged ActorActorダメージを受ける相手
Base Damagefloat減らす量
Event InstigatorControllerこの攻撃の責任者(後述)
Damage CauserActor実際にぶつかった物(後述)
Damage Type Classclass攻撃の種類。未指定なら標準の DamageType

戻り値は TakeDamage を通った量(float) です。ここで注意したいのは、 Event AnyDamage の中でHPを減らさなかったり処理を止めたりしても、戻り値は変わらない という点です。通常は送った値がそのまま返ります。

0が返るのは、対象の Can Be Damaged が false のときなど、ダメージ経路そのものが通らなかった場合です。「受け側で弾いたから0になる」わけではありません。

また、 Apply Radial Damage の戻り値だけは bool で、「1体以上に適用できたか」を返します。3種類とも数値が返ると思っていると、ここで混乱します。

なお Base Damage に0を入れると、受け側のイベントはそもそも呼ばれません。「ダメージ0でヒット演出だけ出す」といった使い方はできないので注意してください。

Apply Radial Damage

範囲ダメージだけは考え方が違います。相手を指定するのではなく、 原点と半径を指定して、そこに居るActorをエンジンが集めてくれます

ピン内容
Origin爆心地の座標
Damage Radius影響半径
Ignore Actors巻き込みたくないActorの配列(自分自身など)
Do Full Damageオンなら半径内は距離に関係なくBase Damage。オフ(既定)だと爆心のBase Damageから半径外端の0へ線形に減衰する
Damage Prevention Channel遮蔽の判定に使うチャンネル。既定は Visibility

Damage Prevention Channelは、爆心地から対象へ線を飛ばして 壁の向こう側に居るなら当てない ための判定です。既定のままなら、壁を挟んだ相手は爆風を受けません。

爆発の当たりが妙に広い、あるいは全く当たらないというときは、対象のコリジョンプリセットがこのチャンネルをどう扱っているかを確認してください。

これが標準の仕組みを使う最大の実利です。爆発の処理を自分で書くなら、球の重なりを取ってループを回し、遮蔽チェックをして、対象ごとにCastして、と数十ノードになります。Apply Radial Damage 1つで済み、しかも 後から追加した壊せるオブジェクトにも自動で届きます

Sponsored

受け取る側:Event AnyDamage

ダメージを受けたいActorのイベントグラフで右クリックし、対応するイベントを追加します。

受け側イベント呼ばれるタイミング
Event AnyDamageすべてのダメージ(3種類すべて)
Event PointDamageApply Point Damage のときだけ
Event RadialDamageApply Radial Damage のときだけ

Event AnyDamage のピンは4つです。

ピン内容
Damagefloat受けた量
Damage TypeDamageType攻撃の種類(オブジェクト)
Instigated ByController責任者
Damage CauserActorぶつかった物

注意: Event AnyDamage は、PointでもRadialでも呼ばれます。 そのため Event AnyDamageEvent PointDamage の両方に「HPを減らす」処理を書くと、銃撃のときだけ2回減ります。原因が分かりにくい定番の事故です。 HPの減算は Event AnyDamage に1本化し、PointDamage は着弾位置や当たった部位の取得だけに使ってください。

1発のApply Point DamageがEvent AnyDamageとEvent PointDamageの両方に届き、両方でHPを減らすと-40になる誤りと、AnyDamageにだけ減算を置いた正しい形を対比した図

Event PointDamage には Hit Location Hit Normal Bone Name Shot From Direction といったピンがあります。ヘッドショット判定は Bone Namehead かどうかで分けるのが最も簡単です。

Damage CauserとInstigatorの違い

この2つは混同しやすいのですが、指しているものが違います。

プレイヤーのコントローラーからキャラ、キャラから弾、弾から敵へ矢印が伸び、Instigatorが最上流、Damage Causerが直前であることを示した図
指すもの
Instigator (Instigated By)Controller誰の攻撃か。責任の所在プレイヤーのPlayerController
Damage CauserActor何が当たったか。物理的な発生源発射された弾のActor

プレイヤーが撃った弾が敵に当たる場面では、次のようになります。

  • Instigator = プレイヤーのPlayerController
  • Damage Causer = BP_Projectile(弾)

型が違うのがポイントです。Instigatorが Controller なのは、「攻撃の責任者を、操作している主体の単位で渡す」ためです。キャラクターが倒れても、そのControllerを見れば誰の攻撃かを辿れます。

ただし Controllerが必ず残り続けるわけではありません 。AIControllerは設定によって破棄されます。集計を確実にしたいなら、倒した時点でスコアを記録しておくのが安全です。

実務では次のように使い分けます。

  • スコアやキルログ → Instigatorを見る(誰の手柄か)
  • ノックバックの方向 → Damage Causerを見る(どこから飛んできたか)
  • 自分の攻撃で自分が死ぬのを防ぐ → Damage Causerが自分自身か、あるいはInstigatorが自分のControllerかを確認する

弾を発射するとき、Spawn Actor from ClassInstigator ピンに 自分(Self) を渡しておきます。こちらのピンの型は Pawn で、ダメージノードのControllerとは別物です。弾側では Get Instigator Controller で責任者のControllerを取り出せます。ここを空のままにしていると、爆発でスコアが誰にも入らない、という不具合になります。

Damage Typeで属性を分ける

炎・毒・落下・爆発のように攻撃の性質を分けたいときは、 DamageType クラスを使います。

  1. コンテンツブラウザで Blueprint Class を作り、親クラスに DamageType を選ぶ
  2. DT_Fire DT_Poison のような名前を付ける
  3. Apply Damageの Damage Type Class ピンで指定する

受け側では、Event AnyDamageDamage Type ピンから判定します。

Event AnyDamage
  └─ Damage Type ─→ Cast To DT_Fire
        ├─ 成功 → 炎エフェクト再生 / 木の敵はダメージ2倍
        └─ 失敗 → 通常処理

DamageTypeのBlueprintには変数を追加できます。 ElementIcon (表示用アイコン)や bIgnoreArmor (防御無視かどうか)を持たせておくと、受け側は種類を1つずつ判定する代わりに、その値を読むだけで済みます。

補足: 属性が10種類を超え、しかも「炎かつ範囲」のように組み合わせたくなったら、DamageTypeクラスを増やし続けるよりGameplay Tagsで分類する方が管理しやすくなります。

無敵時間(i-frame)

被弾処理を作ると、ほぼ確実に「触れた瞬間にHPが全部消える」現象にぶつかります。

原因は単純で、 攻撃判定が重なっている間、毎フレームダメージが入っているから です。60FPSなら1秒で60回です。10ダメージの敵に触れただけでHP600が消えます。

上段は毎フレーム矢印が刺さってHPバーが一瞬で空になる図、下段は最初の1回だけ通り残りが弾かれる図の対比

これを止めるのが 無敵時間(i-frame) です。格闘ゲームやアクションゲームで、被弾後にキャラが点滅する、あの時間です。

作り方は、フラグとタイマーの2つだけです。

  1. bool 型の変数 Is Invincible を用意する
  2. Event AnyDamage の先頭で Branch を置き、Is Invincibletrue なら何もせず終える
  3. ダメージを通したら Is Invincibletrue にする
  4. Set Timer by Event で一定時間後に Is Invinciblefalse に戻す

Set Timer by Event は、指定した秒数の後にカスタムイベントを1回だけ呼ぶノードです。Delay ノードでも同じことはできますが、 待機中の Delay に再び到達すると、後から来た呼び出しは無視されます 。被弾のように何度も走る処理では、意図した回数だけ動くTimerの方が扱いやすくなります(時間を毎回リセットしたい場合は Retriggerable Delay を使います)。

補足: Actorには Can Be Damaged というフラグがあり、Set Can Be Damaged で切り替えられます。 Apply系3ノードはこれを参照するので、これをfalseにするだけでも無敵になります 。単純に全部を無効化するならこちらが手軽です。

一方、「炎属性だけ無効」「無敵中もノックバックは受ける」「ヒット演出だけは出す」といった細かい制御をしたい場合は、上のように 受け側で自分のフラグを見て弾く 方が作りやすくなります。

無敵時間の長さは、演出の都合で決めます。目安として、アクションゲームの被弾なら 0.51.5 秒あたりが扱いやすい範囲です。

Sponsored

実践:攻撃を当てて敵を倒す

メトロイドヴァニアの敵、ベルトスクロールの雑魚、見下ろしARPGの近接攻撃。 攻撃を当てる → HPが減る → 0で倒れる → 連打では入らない という流れは、ジャンルを問わず必ず要ります。ここではその一連を最後まで組みます。

完成形

E キーで正面を殴ると、目の前のCubeのHPが100から20ずつ減り、 5回目で0になって消えます 。連打しても1秒に1回しか通りません。

プレイヤーがCubeを殴るたびにHP表示が80→60→40→20→0と減り、連打しても弾かれる様子を示した図

再現条件

Third Person テンプレート で新規プロジェクトを作り、次の2つを用意します。

受け側: Actorを親クラスにBlueprintを作り、BP_Damageable と名付けます。StaticMesh コンポーネントを追加し、Cube を割り当てます。

このとき、詳細パネルで次の3点を確認してください。ここが合っていないとLine Traceが当たらず、ダメージも通りません。

確認する場所
StaticMesh の Collision PresetsBlockAllVisibilityBlock する設定)
Class Defaults の Can Be Damagedtrue(既定でオン)
レベルへの配置プレイヤーの 正面500cm以内 、かつ キャラクターの腰の高さ(Line Traceは Get Actor Location から水平に飛ぶため、床置きのCubeだと下を通り抜けます)
変数名既定値用途
MaxHealthFloat100.0最大体力
CurrentHealthFloat100.0現在の体力
IsInvincibleBooleanfalse無敵中かどうか
InvincibleTimeFloat1.0無敵の持続秒数

さらに Event Dispatcher を1つ追加し、OnHealthChanged と名付けます。入力として NewHealth (Float) と MaxHealth (Float) の2つを持たせます。

攻撃側: BP_ThirdPersonCharacter を開きます。攻撃の判定にはLine Traceを使い、正面500cmに線を飛ばします。

入力は、動作確認を優先して E キーの直接入力(イベントグラフで右クリック →「Keyboard Events > E」)で組みます。Enhanced Inputで作る場合は、IA_Attack を作って IMC_DefaultE に割り当ててください(→ Enhanced Inputの記事)。

攻撃側のグラフ

BP_ThirdPersonCharacter のイベントグラフに、次を組みます。

E (Pressed)
  → Line Trace By Channel
        Start   = Get Actor Location
        End     = Get Actor Location + (Get Actor Forward Vector × 500)
        Channel = Visibility
        Ignore Self = true
  → Branch (Return Value)
      True → Break Hit Result → Hit Actor
           → Apply Damage
                 Damaged Actor    = Hit Actor
                 Base Damage      = 20.0
                 Event Instigator = Get Controller
                 Damage Causer    = Self

Event InstigatorGet Controller を渡しておくのを忘れないでください。ここが空だと、後からキルログやスコアを足すときに誰の攻撃か分からなくなります。

受け側のグラフ

BP_Damageable のイベントグラフに、被弾から死亡までを1本で組みます。

Event AnyDamageからBranch・HP減算・Dispatcher・タイマー・死亡判定へつながるBlueprintノードグラフ
Event AnyDamage (Damage / Damage Type / Instigated By / Damage Causer)
  → Branch (Condition = IsInvincible)
      True  → (何もしない。ここで終わり)
      False ↓
  → Set CurrentHealth = Clamp(CurrentHealth - Damage, 0.0, MaxHealth)
  → Call OnHealthChanged (NewHealth = CurrentHealth, MaxHealth = MaxHealth)
  → Set IsInvincible = true
  → Set Timer by Event
        Event   = カスタムイベント「EndInvincible」
        Time    = InvincibleTime
        Looping = false
  → Branch (Condition = CurrentHealth <= 0.0)
      True → Print String "Destroyed" → Destroy Actor

カスタムイベント EndInvincible
  → Set IsInvincible = false

Clamp を挟んでいるのは、HPがマイナスに沈むのを防ぐためです。UIのHPバーは CurrentHealth / MaxHealth で割合を出すので、負の値が入ると表示が崩れます。

確認する

Playして、Cubeの正面に立って E を押してください。

  • 1回押す80 と表示される(初期値の100は、別途Printしない限り出ません)
  • 5回押す(1秒以上あけて)80 → 60 → 40 → 20 → 0 と並び、5回目の直後に Destroyed が出てCubeが消える
  • 素早く3連打する → 1回しか通らず、80 が1度出るだけ

HPが見えないと確かめにくいので、Set CurrentHealth の直後に Print String をつないで CurrentHealth を出しておいてください。

うまくいかないときの切り分けです。

  • 何も起きない → Line Traceが当たっていない。Draw Debug TypeFor Duration にして線を目で確認する(Line Traceの記事
  • 線は当たるのにHPが減らないEvent AnyDamageBP_Damageable ではなくレベルブループリント側に置かれている
  • 1回で消えるBase Damage が大きすぎるか、CurrentHealth の既定値が0のまま
  • 連打が素通りする → 無敵の BranchApply Damage の受け口ではなく、HP減算の後ろに置かれている
  • 自分自身に当たるLine Trace By ChannelIgnore Self がオフ
  • HPが負の値になるClamp を挟んでいない

ここまで組んだら、Cubeを複製して2つ目を置いてみてください。 攻撃側を一切変えずに、増やした分だけダメージが通ります。 これが最初の節で述べた利点そのものです。

ポイントは2つです。

  • 無敵の判定は必ず受け側に置く: 攻撃側で「さっき当てたから今回は飛ばす」と判断すると、攻撃手段が増えるたびに同じ判定が要ります。受け側に1つ置けば、剣でも爆発でもトゲでも同じように効きます
  • HPが変わったことは Dispatcher で知らせる: OnHealthChanged を発火しておけば、HPバーもダメージ数字もヒット演出も、受け側を触らずに後から足せます(→ Event Dispatcherの記事

HPバーを画面に出すところはUMGの記事へ続きます。OnHealthChanged にWidget側からBindするだけで繋がります。

Sponsored

体力をComponentにまとめる

敵・プレイヤー・壊せる樽で同じ処理を書いていると気付きます。 どれも中身が同じ です。

そこで、体力の処理を Actor Component に切り出します。 BP_HealthComponent を作り、変数と処理をそちらへ移します。

ここで1つ注意があります。 Actor Componentには Event AnyDamage を置けません。 ダメージイベントはActorが受け取るものだからです。代わりに、Componentの Event BeginPlayOwner のイベントに購読を登録 します。

HealthComponentがBeginPlayでOwnerのOnTakeAnyDamageにBindし、以後ダメージがComponent側の処理へ流れる図
Event BeginPlay (BP_HealthComponent)
  → Get Owner
  → Bind Event to On Take Any Damage
        Event = カスタムイベント「HandleDamage」

カスタムイベント HandleDamage (Damaged Actor / Damage / Damage Type / Instigated By / Damage Causer)
  → (前節と同じ処理)

On Take Any Damage は、Actorが持つダメージ用のDispatcherです。Event AnyDamage と同じタイミングで発火します。

これで、体力を持たせたいActorに BP_HealthComponent追加するだけ で済みます。敵を新しく作るときも、樽を作るときも、コピーは不要です。Componentへの切り出し方はComponentで再利用する記事で詳しく扱っています。

おまけ:先に知っておくと良いこと

  • 回復も同じ経路に乗せない: Apply Damage に負の値を渡して回復にするのは避けてください。Base Damage が0のときイベントが呼ばれない仕様があり、防御力の計算や無敵判定も全部そのまま適用されてしまいます。回復は Heal という別の関数を作る方が素直です
  • 防御力は受け側で引く: 攻撃側が「防御力を考慮したダメージ」を計算すると、装備を作った瞬間に攻撃側を全部直すことになります。 Event AnyDamage で受けた Damage から自分の防御力を引くのが正しい置き場所です
  • 死亡処理は「消す」より「止める」から: Destroy Actor を即座に呼ぶと、死亡アニメーションもエフェクトも再生されません。まず入力とコリジョンを止め、演出を再生し、その後にDestroyまたはプールへ返す形にします。人型の敵なら、その「演出」を物理に任せるのが Physics Assetとラグドール です
  • 落下ダメージは自分で送る: UEのCharacterは、高所から落ちても自動ではダメージを受けません。Event On Landed で落下前後の高さや VelocityZ を見て、しきい値を超えていたら自分に Apply Damage を送る形で作ります(→ Character Movementの記事
  • Apply Point Damage は命中判定をしない: 名前から「点で当たり判定を取ってくれる」と思いがちですが、当たったかどうかは自分で調べます。Line Traceで得た Hit Result と方向を、そのまま引数として渡す使い方になります
  • Bone Name での部位判定は下準備が要る: Skeletal Meshに対して正しくトレースが通り、Physics Assetが設定されている必要があります。文字列 head を比較するだけでは成立しません
  • Apply系はサーバー側でのみ動く: マルチプレイに広げるときは、サーバーでダメージを適用し、HPや演出の同期は別途組むことになります。1人用で作っている間は意識しなくて構いません
  • Damage Typeは共用の設定表: 攻撃のたびに生成されるわけではなく、クラスの既定値を読むだけです。アイコンや倍率のような 変わらない値 を置く場所で、残り時間や攻撃者などの状態は持たせられません
  • 数値が合わないときは戻り値を見る: Apply Damage の戻り値をPrint Stringに出すと、そもそもダメージ経路が通ったかどうかが分かります。0が返るなら Can Be Damaged を疑ってください(→ デバッグの記事

まとめ

  • 相手のHP変数を直接減らすと、 対象 × 攻撃手段 の数だけ書き直しが発生する
  • 送る側は Apply Damage / Point / Radial の3種。範囲攻撃はエンジンが対象を集めてくれる
  • 受ける側は Event AnyDamage に1本化 する。PointDamageにも減算を書くと二重に減る
  • Instigator は Controller(誰の攻撃か)、Damage Causer は Actor(何が当たったか) 。型が違う
  • 連続ヒットは 受け側のフラグとTimer で弾く。無敵の判定は攻撃側に置かない
  • 同じ処理を何度も書くようになったら、 Actor Component へ切り出して Owner の On Take Any Damage にBindする

いま作っているゲームで、ダメージを受けるものは何種類あるでしょうか。1つでも増える予定があるなら、この経路に乗せておく価値があります。