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: 1
  • start_health: 1
  • death_animation_name: “break”
  • auto_free_parent_on_death: false
  • free_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ライフを送りましょう。