GodotでHP管理をしようとすると、つい「Player用のベースクラス」「Enemy用のベースクラス」を作って、そこにHPロジックを全部書きたくなりますよね。
でもそれをやり始めると、
- プレイヤーと敵で共通化したいけど、継承ツリーがどんどん複雑になる
- 「動く床」や「壊れるオブジェクト」にもHPを付けたいけど、ベースクラスが合わない
- HP0になった時の処理(削除・アニメーション・エフェクト)がバラバラに散らばる
といった「継承のつらみ」が一気に出てきます。
そこで今回は、どんなノードにもポン付けできる「HPコンポーネント」を作って、HPロジックを全部コンポーネントに寄せるアプローチを紹介します。
コンポーネント名は HealthManager。
HPを持ち、0になったら died シグナルを発火し、設定に応じて「親ノードを消す」か「アニメーションを再生する」挙動をしてくれます。
【Godot 4】どのノードにもHPを後付け!「HealthManager」コンポーネント
フルコード(GDScript / Godot 4)
extends Node
class_name HealthManager
## 任意のノードに「HP」を付与するコンポーネント。
## HPが0以下になったら died シグナルを発火し、
## 設定に応じて親ノードを削除したり、アニメーションを再生したりします。
signal died ## HPが0以下になった瞬間に1度だけ発火
signal health_changed(current_health, max_health) ## HPが変化したときに発火
@export_category("Health Settings")
@export var max_health: int = 100:
set(value):
max_health = max(value, 1) # 1未満にならないように
_current_health = clamp(_current_health, 0, max_health)
@export var start_health: int = 100:
set(value):
start_health = value
# _ready 前にインスペクタから変えられた場合も考慮
if not _initialized:
_current_health = clamp(start_health, 0, max_health)
@export var invincible: bool = false
## true の間はダメージを受けない(デバッグや無敵時間に便利)
@export_category("Death Behavior")
## HP0になったとき親ノードを消すかどうか
@export var auto_free_parent_on_death: bool = true
## HP0になったときに再生するアニメーション名(空なら再生しない)
@export var death_animation_name: String = ""
## アニメーション再生に使うターゲットノード(未設定なら親から AnimationPlayer を探す)
@export var animation_player_path: NodePath
## アニメーション完了後に親を削除するかどうか
@export var free_parent_after_animation: bool = true
@export_category("Debug")
## HP変化をログに出したい場合にON
@export var print_debug_log: bool = false
var _current_health: int
var _is_dead: bool = false
var _initialized: bool = false
func _ready() -> void:
# 開始時のHPをセット
_current_health = clamp(start_health, 0, max_health)
_initialized = true
emit_signal(&"health_changed", _current_health, max_health)
if print_debug_log:
print("[HealthManager] Ready. HP: %d / %d" % [_current_health, max_health])
# --- 公開API -------------------------------------------------------------
## 現在のHPを取得
func get_health() -> int:
return _current_health
## 最大HPを取得
func get_max_health() -> int:
return max_health
## HPが0かどうか
func is_dead() -> bool:
return _is_dead
## ダメージを与える(負の値を渡すと回復としても使えるが、基本は正の値推奨)
func apply_damage(amount: int) -> void:
if amount == 0:
return
if invincible and amount > 0:
# 無敵中はダメージだけ無効化(回復は通す)
return
_set_health(_current_health - amount)
## HPを回復する(オーバーヒールはしない)
func heal(amount: int) -> void:
if amount <= 0:
return
_set_health(_current_health + amount)
## HPを直接セットする(0~max_healthにクランプ)
func set_health(value: int) -> void:
_set_health(value)
# --- 内部処理 -----------------------------------------------------------
func _set_health(value: int) -> void:
if _is_dead:
# すでに死亡扱いなら、HP操作は無視する(必要ならここを変えてもOK)
return
var old_health := _current_health
_current_health = clamp(value, 0, max_health)
if _current_health == old_health:
return
if print_debug_log:
print("[HealthManager] HP changed: %d -> %d (max: %d)" % [old_health, _current_health, max_health])
emit_signal(&"health_changed", _current_health, max_health)
if _current_health <= 0:
_handle_death()
func _handle_death() -> void:
if _is_dead:
return
_is_dead = true
if print_debug_log:
print("[HealthManager] Died.")
emit_signal(&"died")
# 死亡時の挙動を決定
var parent := get_parent()
if not is_instance_valid(parent):
return
# 1. アニメーションを優先して再生
if death_animation_name != "":
var anim_player := _get_animation_player()
if anim_player and anim_player.has_animation(death_animation_name):
# アニメーション再生後に削除、または削除しない
anim_player.play(death_animation_name)
if free_parent_after_animation:
# アニメーション完了シグナルを待ってから削除
_wait_and_free_parent(anim_player, death_animation_name)
return
elif print_debug_log:
print("[HealthManager] Animation '%s' not found. Fallback to auto_free_parent_on_death." % death_animation_name)
# 2. アニメーションが無い場合 or 再生できなかった場合は、即削除オプションを見る
if auto_free_parent_on_death:
parent.queue_free()
func _get_animation_player() -> AnimationPlayer:
# 明示的に指定されていればそれを使う
if animation_player_path != NodePath():
var node := get_node_or_null(animation_player_path)
if node is AnimationPlayer:
return node
# 未指定なら、親から最初に見つかった AnimationPlayer を使う
var parent := get_parent()
if not is_instance_valid(parent):
return null
return parent.get_node_or_null("AnimationPlayer") as AnimationPlayer
func _wait_and_free_parent(anim_player: AnimationPlayer, anim_name: String) -> void:
# コルーチンでアニメーション終了を待つ
# Godot 4 の await 構文を使用
if not is_instance_valid(anim_player):
return
# すでに再生中と仮定
await anim_player.animation_finished
var parent := get_parent()
if is_instance_valid(parent):
parent.queue_free()
使い方の手順
この HealthManager は「どのノードにもアタッチできる汎用コンポーネント」として設計しています。
プレイヤー、敵、壊れるオブジェクト、動く床…何にでも同じコンポーネントを付けてOKです。
コンポーネントスクリプトを用意する
上記コードを res://components/health_manager.gd などに保存します。class_name HealthManager を使っているので、他のスクリプトから HealthManager として直接参照できます。
対象ノードに HealthManager をアタッチ
例として、プレイヤー(CharacterBody2D)にHPを付けるシーン構成は以下のようになります。
max_health: 100
start_health: 100
invincible: false
auto_free_parent_on_death: false(アニメーション後に消したい場合)
death_animation_name: “death”
animation_player_path: ../AnimationPlayer
free_parent_after_animation: true
ダメージを与える側から呼び出す
例えば、敵の攻撃がプレイヤーに当たったときにダメージを与える場合、プレイヤースクリプト側はこんな感じでOKです。
# Player.gd (例)
extends CharacterBody2D
@onready var health: HealthManager = $HealthManager
func take_hit(damage: int) -> void:
health.apply_damage(damage)
died シグナルを使ってゲーム側の処理をつなぐ
「死んだときにスコアを加算したい」「UIを更新したい」といったゲームロジックは、died シグナルに接続して書きましょう。
別の使用例:壊れる床
同じコンポーネントを「壊れる床」にもそのまま使えます。
BreakableFloor (StaticBody2D) ├── Sprite2D ├── CollisionShape2D ├── AnimationPlayer └── HealthManager (Node)
max_health: 1start_health: 1death_animation_name: “break”auto_free_parent_on_death: falsefree_parent_after_animation: true
プレイヤーが床を攻撃したときに apply_damage(1) を呼ぶだけで、「壊れる床」が完成します。
専用の BreakableFloorBase みたいな継承クラスは不要です。
メリットと応用
この HealthManager コンポーネントを使うことで、次のようなメリットがあります。
- 継承ツリーからHPロジックを追い出せる
PlayerBase, EnemyBase などの「なんでも入りクラス」からHPの責務を切り出せます。
「HPがあるかどうか」はクラス継承ではなく、「HealthManager が付いているかどうか」で判定できるようになります。 - シーン構造がフラットで分かりやすい
各シーンは「見た目」「当たり判定」「HP」「AI」などのコンポーネントを横に並べるだけ。
深いノード階層や複雑なベースシーンを作らずに済みます。 - 再利用性が高い
プレイヤー、敵、ギミック、オブジェクト…何にでも同じコンポーネントをポン付けできます。
「HP0になったときの挙動」も、アニメーション再生・即削除・シグナルで外部処理、という3段構えで柔軟に制御できます。 - テストがしやすい
HealthManager 単体でテストシーンを作り、「ダメージを与えたときに died が1回だけ出るか」などを検証しやすくなります。
さらに、応用として「一時的な無敵時間」「HPバーUIとの連携」「属性ダメージ」などを追加していくこともできます。
改造案:一時的な無敵時間(ダメージ後クールタイム)を追加する
例えば、「ダメージを受けた後 0.5 秒間だけ無敵にする」機能を足したい場合、こんな関数を追加できます。
@export var hit_invincible_time: float = 0.0 # ダメージ後の無敵時間(秒)
var _hit_invincible_timer: float = 0.0
func _process(delta: float) -> void:
if _hit_invincible_timer > 0.0:
_hit_invincible_timer -= delta
if _hit_invincible_timer <= 0.0:
invincible = false
func apply_damage(amount: int) -> void:
if amount == 0:
return
if invincible and amount > 0:
return
_set_health(_current_health - amount)
# ダメージを受けたら一時的に無敵にする
if amount > 0 and hit_invincible_time > 0.0 and not _is_dead:
invincible = true
_hit_invincible_timer = hit_invincible_time
このように、HPロジックを1つのコンポーネントに閉じ込めておけば、
「無敵時間をどうするか」「ノックバックをどうするか」といった拡張も、継承ツリーをいじらずにコンポーネント単体の改造だけで済みます。
継承より合成で、気持ちよくGodot 4ライフを送りましょう。
