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.gd と Lifesteal.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_path:AttackHitNotifierへのパス。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 を皮切りにコンポーネント志向の設計にシフトしていきましょう。
