Godot 4 でアクションゲームを作っていると、だいたいこんな構成になりがちですよね。

  • Player が CharacterBody2D を継承していて
  • そこに「HP管理」「攻撃処理」「ダメージ処理」「ノックバック」などを全部ベタ書き
  • 敵もまた別クラスを継承して、似たようなコードをコピペ…

「攻撃が当たったらHPを回復する(吸血攻撃)」を入れようとすると、さらに面倒です。

  • Player の _on_hit_something() にロジックを追加
  • 敵にも似たような処理を追加
  • 将来「ボスだけ吸血量を変えたい」「武器ごとに吸血率を変えたい」となって地獄化

継承ベースで書き始めると、「ちょっとした仕様追加」がどんどんクラスを太らせていきます。
そこで登場するのが、今回のコンポーネント Lifesteal です。

攻撃がヒットしたときに、与えたダメージの一定割合を自動でHPに還元する 処理を、1つの独立したコンポーネントとして切り出します。
プレイヤーでも敵でも、HPを持つ存在 にポン付けするだけで「吸血攻撃」を実装できるようにしてしまいましょう。


【Godot 4】攻撃のたびにHPモリモリ回復!「Lifesteal」コンポーネント

今回のコンポーネントは:

  • 「攻撃ヒット」を通知するシグナルを購読し
  • 「与えたダメージ量」から「吸血量」を計算し
  • 自分自身の HP コンポーネントに回復を依頼する

という、かなりシンプルな構成です。
ポイントは、「HP管理」も「攻撃処理」も別コンポーネントに分け、Lifesteal はそれらの間をゆるくつなぐだけ にしていることですね。


前提:シンプルな Health コンポーネント

Lifesteal 単体でも説明できますが、「どこにHPを回復させるか?」の受け皿が必要なので、
まずは最低限の Health コンポーネントを用意しておきます(すでに自作のHPコンポーネントがあるなら、読み替えてOKです)。


# Health.gd
class_name Health
extends Node

## シンプルな HP 管理コンポーネント
## - 他のコンポーネントから HP を増減してもらう想定

@export var max_health: float = 100.0:
	set(value):
		max_health = max(value, 1.0)
		current_health = clamp(current_health, 0.0, max_health)

@export var current_health: float = 100.0

signal died
signal health_changed(current: float, max: float)

func _ready() -> void:
	# 初期化時に clamping しておく
	current_health = clamp(current_health, 0.0, max_health)
	emit_signal("health_changed", current_health, max_health)

func apply_damage(amount: float) -> void:
	## ダメージを受ける(マイナス方向)
	if amount <= 0.0:
		return
	current_health = clamp(current_health - amount, 0.0, max_health)
	emit_signal("health_changed", current_health, max_health)
	if current_health <= 0.0:
		emit_signal("died")

func heal(amount: float) -> void:
	## 回復処理(プラス方向)
	if amount <= 0.0:
		return
	current_health = clamp(current_health + amount, 0.0, max_health)
	emit_signal("health_changed", current_health, max_health)

この Health をプレイヤーや敵に付けておいて、
そこに Lifesteal コンポーネントが「回復お願いしまーす」と話しかけるイメージです。


前提:AttackHitNotifier(攻撃ヒット通知コンポーネント)の例

Lifesteal は「攻撃が当たったとき」を知る必要があります。
ここでは例として、攻撃判定がヒットしたときにシグナルを発行するコンポーネント AttackHitNotifier を用意します。


# AttackHitNotifier.gd
class_name AttackHitNotifier
extends Node

## 攻撃がヒットしたことを通知するだけのコンポーネント
## - 実際のヒット判定(Area2D / RayCast / 物理)は別ノード側で行い、
##   その結果をこのコンポーネントに伝える想定

signal attack_hit(target: Node, damage: float)

func notify_hit(target: Node, damage: float) -> void:
	## 他のスクリプトから呼び出してもらう
	emit_signal("attack_hit", target, damage)

実際のゲームでは、剣の当たり判定(Area2D)や弾丸の body_entered シグナルから
notify_hit() を呼ぶ形になると思ってください。


本体:Lifesteal コンポーネント(吸血攻撃)


# Lifesteal.gd
class_name Lifesteal
extends Node

## Lifesteal(吸血攻撃)コンポーネント
## 攻撃ヒット時に、与えたダメージの一定割合を自分の HP として回復する。
##
## 前提:
## - 同じシーン内に Health コンポーネントが存在すること(親ノードか、任意のパス)
## - 攻撃側に AttackHitNotifier コンポーネントがあり、attack_hit シグナルを発行していること

@export_range(0.0, 1.0, 0.01)
var lifesteal_ratio: float = 0.3
## 与えたダメージに対して、何割を吸血するか
## 例: 0.3 なら 30% を HP として回復

@export var min_lifesteal: float = 0.0
## 吸血量の下限。0 のままなら制限なし。
## 例: 1.0 にすると、1 未満の回復は切り捨て(小数ダメージが多いゲームで有用)

@export var max_lifesteal: float = 9999.0
## 吸血量の上限。大ダメージ時の回復量を抑えたい場合に使用

@export var health_node_path: NodePath = NodePath("..")
## 回復先の Health コンポーネントへのパス。
## デフォルトでは「親ノード」を指すので、
## 親ノードに Health を付けているケースに対応。

@export var attack_notifier_path: NodePath = NodePath("../AttackHitNotifier")
## 攻撃ヒットを通知してくれる AttackHitNotifier へのパス。
## デフォルトでは「親ノードの子にある AttackHitNotifier」を想定。

@export var enabled: bool = true
## コンポーネントの有効 / 無効フラグ。
## 一時的に吸血効果をオフにしたいときに使用(デバフなど)。

var _health: Health
var _attack_notifier: AttackHitNotifier

func _ready() -> void:
	# Health 参照を取得
	if health_node_path != NodePath():
		var node = get_node_or_null(health_node_path)
		if node is Health:
			_health = node
		else:
			push_warning("Lifesteal: health_node_path が Health ではありません: %s" % health_node_path)
	else:
		push_warning("Lifesteal: health_node_path が未設定です")

	# AttackHitNotifier 参照を取得
	if attack_notifier_path != NodePath():
		var notifier_node = get_node_or_null(attack_notifier_path)
		if notifier_node is AttackHitNotifier:
			_attack_notifier = notifier_node
			_attack_notifier.attack_hit.connect(_on_attack_hit)
		else:
			push_warning("Lifesteal: attack_notifier_path が AttackHitNotifier ではありません: %s" % attack_notifier_path)
	else:
		push_warning("Lifesteal: attack_notifier_path が未設定です")

func _on_attack_hit(target: Node, damage: float) -> void:
	## 攻撃ヒット時に呼ばれるコールバック。
	## - target: ダメージを受けた相手
	## - damage: 与えたダメージ量
	if not enabled:
		return
	if _health == null:
		push_warning("Lifesteal: Health が見つからないため、吸血できません")
		return
	if damage <= 0.0:
		return

	var raw_heal := damage * lifesteal_ratio
	# 下限・上限でクランプ
	var heal_amount := clamp(raw_heal, min_lifesteal, max_lifesteal)

	if heal_amount <= 0.0:
		return

	_health.heal(heal_amount)

func set_enabled(value: bool) -> void:
	## スクリプトから有効/無効を切り替えるためのヘルパー
	enabled = value

このコンポーネントは「攻撃ヒット時に attack_hit シグナルを受け取り、
計算した回復量を Health.heal() に渡す」だけ、という非常に限定的な責務にしています。

  • HP管理は Health
  • 攻撃判定・ヒット検出は AttackHitNotifier + 他のノード
  • 吸血ロジックは Lifesteal

と完全に分離されているので、どれか1つだけ差し替えたり、別のゲームでもそのまま再利用しやすい構造になっています。


使い方の手順

手順①:プレイヤー(または敵)に Health コンポーネントを付ける

例として、2D のプレイヤーキャラを想定します。

Player (CharacterBody2D)
 ├── Sprite2D
 ├── CollisionShape2D
 ├── Health (Node)              # HP 管理
 ├── AttackHitNotifier (Node)   # 攻撃ヒット通知
 └── Lifesteal (Node)           # 吸血攻撃コンポーネント

1. Player シーンを開く
2. 子ノードとして Node を追加し、スクリプトに Health.gd をアタッチ
3. 同様に AttackHitNotifier.gdLifesteal.gd もそれぞれ子ノードにアタッチ

Health のパラメータ(max_health など)はインスペクタからお好みで設定しておきましょう。

手順②:攻撃ヒット時に AttackHitNotifier を呼ぶ

次に、「攻撃が当たったときに AttackHitNotifier.notify_hit() を呼ぶ」処理を書きます。
ここでは例として、プレイヤーの剣の当たり判定に Area2D を使うケースを示します。

Player (CharacterBody2D)
 ├── Sprite2D
 ├── CollisionShape2D
 ├── SwordHitbox (Area2D)
 │    └── CollisionShape2D
 ├── Health (Node)
 ├── AttackHitNotifier (Node)
 └── Lifesteal (Node)

SwordHitbox に以下のようなスクリプトを付けます:


# SwordHitbox.gd
extends Area2D

@export var damage: float = 20.0

var _notifier: AttackHitNotifier

func _ready() -> void:
	_notifier = get_parent().get_node("AttackHitNotifier") as AttackHitNotifier
	body_entered.connect(_on_body_entered)

func _on_body_entered(body: Node) -> void:
	# ここでは「Enemy 側が Health を持っている」前提
	if _notifier == null:
		return
	# 実際には、Enemy 側のスクリプトで apply_damage するなど、
	# もう少し丁寧なダメージ処理を挟むことが多いです。
	_notifier.notify_hit(body, damage)

これで、剣が何かに当たるたびに AttackHitNotifier.attack_hit シグナルが発火し、
それを Lifesteal コンポーネントが受け取って HP を回復します。

手順③:Lifesteal のパラメータを調整する

エディタで Lifesteal ノードを選択すると、インスペクタに以下のような項目が出ます:

  • lifesteal_ratio:吸血割合(0.0〜1.0)。0.3 なら 30% 回復。
  • min_lifesteal:回復量の下限。小さすぎる回復を無視したいときに。
  • max_lifesteal:回復量の上限。大ダメージ一発で全快させたくないときに。
  • health_node_path:回復先の Health へのパス(通常は親ノード)。
  • attack_notifier_pathAttackHitNotifier へのパス。
  • enabled:一時的に吸血効果をオフにするフラグ。

例えば:

  • 通常のプレイヤー:lifesteal_ratio = 0.2
  • 「吸血の指輪」装備中:スクリプトから lifesteal_ratio = 0.5 に変更
  • デバフ中:enabled = false にして一時的に無効化

といった調整が簡単にできます。

手順④:敵キャラにもそのまま再利用する

敵キャラにも同じようにコンポーネントを付けるだけで、「吸血する敵」を作れます。

Enemy (CharacterBody2D)
 ├── Sprite2D
 ├── CollisionShape2D
 ├── ClawHitbox (Area2D)
 │    └── CollisionShape2D
 ├── Health (Node)
 ├── AttackHitNotifier (Node)
 └── Lifesteal (Node)

敵専用のクラスを継承してゴリゴリ書く必要はなく、「Health + AttackHitNotifier + Lifesteal」 をペタペタ貼っていくだけでOKです。
「ボスだけ吸血率を高くする」みたいな調整も、インスペクタで数値を変えるだけで済みます。


メリットと応用

この Lifesteal コンポーネントを導入することで、こんなメリットがあります。

  • Player / Enemy のスクリプトがスリムになる
    攻撃ロジックやHP管理から「吸血」の分岐が消え、読みやすく保守しやすいコードになります。
  • シーン構造がフラットで見通しが良くなる
    「吸血するかどうか」はノードツリーを見れば一目瞭然。深い継承ツリーを追いかける必要がありません。
  • 再利用性が高い
    2D/3D を問わず、「攻撃ヒットをシグナルで飛ばせる環境」さえあれば、そのまま別プロジェクトに持っていけます。
  • ゲームデザインの試行錯誤が楽になる
    吸血率や上限値をインスペクタでいじるだけなので、「ちょっと強すぎるかな?」のチューニングが即座にできます。

コンポーネント志向で組んでおくと、
「この敵だけ吸血をオフにしたい」「特定のフェーズだけ吸血可能にしたい」といった仕様変更にも強くなります。

改造案:クリティカルヒット時だけ吸血量を倍にする

例えば「クリティカルヒット時だけ吸血量を増やす」ように拡張したいとします。
attack_hit シグナルに「クリティカルかどうか」のフラグを追加したバージョンを想定して、
Lifesteal 側に少しだけ手を入れると、こんな感じになります:


# 追加シグナル仕様(例):
# signal attack_hit(target: Node, damage: float, is_critical: bool)

@export var critical_lifesteal_multiplier: float = 2.0
## クリティカル時に吸血量にかける倍率。
## 例: 2.0 ならクリティカル時は 2 倍吸血。

func _on_attack_hit(target: Node, damage: float, is_critical: bool) -> void:
	if not enabled or _health == null or damage <= 0.0:
		return

	var ratio := lifesteal_ratio
	if is_critical:
		ratio *= critical_lifesteal_multiplier

	var raw_heal := damage * ratio
	var heal_amount := clamp(raw_heal, min_lifesteal, max_lifesteal)

	if heal_amount <= 0.0:
		return

	_health.heal(heal_amount)

このように、「クリティカル」「属性相性」「状態異常」など、
ゲーム特有のロジックは Lifesteal コンポーネントに少しずつ追加していけばOKです。
それでも HP管理と攻撃処理からは独立している ため、他の部分への影響は最小限で済みます。

継承で大きなクラスを1つ作るよりも、小さなコンポーネントを組み合わせてキャラを構成する ほうが、
長期的には圧倒的に楽なので、ぜひ Lifesteal を皮切りにコンポーネント志向の設計にシフトしていきましょう。