Godot で HP の自動回復を実装しようとすると、つい PlayerEnemy のスクリプトに「時間計測 → 回復処理 → エフェクト再生」まで全部書いてしまいがちですよね。さらに、敵ごと・味方ごとに回復量や間隔を変えたいとなると、継承ツリーがどんどん増えたり、条件分岐だらけの巨大スクリプトになってしまいます。

でも本当は、「HP を管理するコンポーネント」と「自動回復するコンポーネント」は別物のはずです。Godot のノード階層にロジックをベタ書きしていくよりも、小さなコンポーネントをペタペタ貼っていく方が、後からの調整や再利用が圧倒的に楽になります。

そこで今回は、親ノードにある HealthManager を一定時間ごとに呼び出して HP を少しずつ回復させる、シンプルなコンポーネント RegenEffect を用意しました。
プレイヤーにも敵にも、動く回復床にも、同じコンポーネントを貼るだけで「自動回復」が実現できます。

【Godot 4】貼るだけ自動リジェネ!「RegenEffect」コンポーネント

以下が、コピペでそのまま使える RegenEffect.gd のフルコードです。
このコンポーネントは、親ノードにある HealthManager(例:class_name HealthManager なスクリプト)を探して、一定間隔で heal(amount) を呼び出すだけの、非常に小さい部品です。


## RegenEffect.gd
## 親ノードにアタッチされた HealthManager を一定間隔で回復させるコンポーネント
## 想定: 親に `func heal(amount: float)` を持つ HealthManager があること
extends Node
class_name RegenEffect

## --- 設定パラメータ(インスペクタから調整できます) ---

@export_category("Regen Settings")

## 何秒ごとに回復させるか(例: 1.0 なら 1 秒ごと)
@export_range(0.1, 60.0, 0.1)
var interval_seconds: float = 1.0

## 1 回の tick で何 HP 回復するか
@export_range(0.1, 9999.0, 0.1)
var heal_amount: float = 1.0

## 自動回復を開始した状態にするか
@export var start_enabled: bool = true

## HP が最大でも回復処理を呼ぶかどうか
## 通常は false 推奨。true にすると HealthManager 側で制御したいときに便利。
@export var heal_even_if_full: bool = false

@export_category("Target")

## 親以外のノードを指定したい場合に使う。
## null の場合は自動で親から HealthManager を探します。
@export var explicit_health_manager: NodePath

## --- 内部状態 ---

var _health_manager: Object = null
var _timer: Timer
var _is_active: bool = false


func _ready() -> void:
    # タイマーを生成して自分の子として追加
    _timer = Timer.new()
    _timer.one_shot = false
    _timer.wait_time = interval_seconds
    _timer.autostart = false
    add_child(_timer)

    _timer.timeout.connect(_on_timer_timeout)

    # 回復対象の HealthManager を探す
    _resolve_health_manager()

    # 自動開始設定なら有効化
    if start_enabled:
        enable()
    else:
        disable()


func _resolve_health_manager() -> void:
    ## 明示的に NodePath が指定されていればそれを使う
    if explicit_health_manager != NodePath(""):
        var node := get_node_or_null(explicit_health_manager)
        if node:
            _health_manager = node
        else:
            push_warning(
                "RegenEffect: explicit_health_manager '%s' が見つかりませんでした。" % [explicit_health_manager]
            )
            _health_manager = null
        return

    ## NodePath が指定されていなければ、親から HealthManager を探す
    var parent := get_parent()
    if parent == null:
        push_warning("RegenEffect: 親ノードが存在しません。HealthManager を解決できません。")
        _health_manager = null
        return

    # ここでは「heal メソッドを持っているか」で最低限のチェックをする
    if "heal" in parent:
        _health_manager = parent
    else:
        push_warning("RegenEffect: 親ノードに heal() を持つ HealthManager が見つかりません。")
        _health_manager = null


func enable() -> void:
    ## 自動回復を有効化する
    if _health_manager == null:
        _resolve_health_manager()

    if _health_manager == null:
        push_warning("RegenEffect: HealthManager が解決できないため enable() できません。")
        _is_active = false
        _timer.stop()
        return

    _is_active = true
    _timer.wait_time = interval_seconds
    _timer.start()


func disable() -> void:
    ## 自動回復を無効化する
    _is_active = false
    if _timer:
        _timer.stop()


func is_enabled() -> bool:
    return _is_active


func set_interval(seconds: float) -> void:
    ## 実行中に回復間隔を変更したいとき用
    interval_seconds = max(0.1, seconds)
    if _timer:
        _timer.wait_time = interval_seconds


func set_heal_amount(amount: float) -> void:
    ## 実行中に回復量を変更したいとき用
    heal_amount = max(0.0, amount)


func _on_timer_timeout() -> void:
    if not _is_active:
        return
    if _health_manager == null:
        # 何らかの理由で対象が消えた場合など
        disable()
        return

    # HealthManager 側に is_full_hp() があれば、それを利用して過剰な呼び出しを避ける
    if not heal_even_if_full and "is_full_hp" in _health_manager:
        if _health_manager.is_full_hp():
            return

    # 実際の回復処理を呼び出す
    # HealthManager 側に float / int どちらでもいいので heal(amount) が必要
    _health_manager.heal(heal_amount)

使い方の手順

ここでは、プレイヤー、そして 回復床(回復ゾーン) の 3 つの例で使い方を見ていきます。

① HealthManager を用意する

まずは、HP を管理するコンポーネント HealthManager を用意します。
最低限、heal(amount) メソッドさえあれば RegenEffect は動きますが、ついでに is_full_hp() も実装しておくと無駄な回復呼び出しをスキップできて効率的です。


## HealthManager.gd
extends Node
class_name HealthManager

@export_range(1, 9999, 1)
var max_hp: int = 100

var current_hp: int

func _ready() -> void:
    current_hp = max_hp

func heal(amount: float) -> void:
    current_hp = min(max_hp, current_hp + int(amount))
    # デバッグ用にログを出す
    # print("Healed: ", amount, " current_hp: ", current_hp)

func damage(amount: float) -> void:
    current_hp = max(0, current_hp - int(amount))
    # print("Damaged: ", amount, " current_hp: ", current_hp)

func is_full_hp() -> bool:
    return current_hp >= max_hp

② プレイヤーに自動回復を付ける

プレイヤーのシーン構成例はこんな感じです。

Player (CharacterBody2D)
 ├── Sprite2D
 ├── CollisionShape2D
 ├── HealthManager (Node)          # HP を管理するコンポーネント
 └── RegenEffect (Node)            # 自動回復コンポーネント

手順:

  1. HealthManager.gdHealthManager という Node にアタッチして、Player の子に置きます。
  2. RegenEffect.gd を別の Node にアタッチし、同じく Player の子に置きます。
  3. RegenEffect のインスペクタで

    • interval_seconds: 1.0(1 秒ごと)

    • heal_amount: 2.0(2 HP 回復)

    • start_enabled: ON

    • explicit_health_manager: 空のまま(親=Player から自動解決)


    に設定します。


この構成では、RegenEffect の親である PlayerHealthManager を直接持っていないので、明示的に NodePath を指定してあげるのが安全です。
例えば、Player から見て HealthManager のパスが "HealthManager" なら、explicit_health_manager にそのパスをセットしましょう。

シーン構成をこう変えるとより分かりやすいです:

Player (CharacterBody2D)
 ├── Sprite2D
 ├── CollisionShape2D
 └── HealthManager (Node)
      └── RegenEffect (Node)

この場合、RegenEffect の親が HealthManager になるので、explicit_health_manager を空にしておけば、親から自動で解決されます。
この「機能ごとに小さな島を作って、その中にコンポーネントをまとめる」構成の方が、あとで見返したときにかなり楽ですね。

③ 敵キャラクターにだけ遅い自動回復を付ける

敵キャラには「戦闘が終わってしばらくするとゆっくり回復する」みたいな仕様を付けたいとします。
でも、そのためだけに EnemyWithRegen.gd みたいな派生クラスを増やすのは避けたいですよね。

Enemy (CharacterBody2D)
 ├── Sprite2D
 ├── CollisionShape2D
 ├── HealthManager (Node)
 └── RegenEffect (Node)

敵用の設定例:

  • interval_seconds: 3.0(3 秒ごと)
  • heal_amount: 5.0(5 HP 回復)
  • start_enabled: ON

これだけで、プレイヤーと同じ HealthManager を使い回しつつ、「回復間隔」と「回復量」だけを変えた別の挙動にできます。
スクリプトを継承で分岐させる必要がないので、シーンを複製してパラメータを変えるだけでバリエーションが作れます。

④ 回復床(回復ゾーン)として使う

RegenEffect は「親にある HealthManager を回復する」だけのコンポーネントなので、プレイヤー自身ではなく、床やゾーンに貼ることもできます。

例えば、プレイヤーが乗っている間だけ回復する「回復床」を作りたい場合:

HealPad (Area2D)
 ├── CollisionShape2D
 ├── HealthManager (Node)     # この床専用の HP(不要ならダミーでもOK)
 └── RegenEffect (Node)       # 床自身を自動回復させる or 他の対象に応用

このままだと「床自身の HP を回復する」だけですが、少し応用して「エリア内のプレイヤーの HP を回復する」ようなコンポーネントと組み合わせると、プレイヤーの HealthManager を NodePath で指定して回復させることもできます。

例えば、プレイヤーが常に "/root/World/Player/HealthManager" にいるなら、explicit_health_manager にそのパスを入れておけば、床に乗っていなくても常にプレイヤーを回復する「マップ全体リジェネ」みたいなギミックも作れます。

メリットと応用

RegenEffect をコンポーネントとして分離することで、次のようなメリットがあります。

  • プレイヤーや敵のスクリプトがスリムになる
    「時間計測」と「HP 操作」をそれぞれ別コンポーネントに任せることで、Player.gdEnemy.gd は「入力処理」「行動 AI」に集中できます。
  • 継承ツリーを増やさずにバリエーションが作れる
    回復の有無・速度・量を「シーン+パラメータ」で決められるので、「HP 自動回復する敵」「しない敵」を別クラスにする必要がありません。
  • レベルデザインの試行錯誤が楽になる
    ステージごとに「この敵だけ回復量を半分に」「この部屋だけ全体リジェネ」などを、スクリプトを一切触らずにインスペクタだけで調整できます。
  • ノード階層が浅くても機能を盛れる
    Godot の「ノード継承ツリー」を深くするのではなく、「小さい Node を横に並べる」構成にできるので、後から見ても迷子になりにくいです。

さらに、ちょっとした改造で「戦闘中は回復しない」「毒状態のときは逆に減る」なども簡単に実装できます。

例えば、「ダメージを受けてから数秒間は自動回復を停止する」改造案:


## RegenEffect.gd に追記する例

@export_category("Combat Lockout")
@export_range(0.0, 30.0, 0.1)
var lockout_after_damage: float = 3.0  # ダメージ後何秒は回復しないか

var _lockout_timer: Timer

func _ready() -> void:
    # 既存の _ready の末尾あたりに追加
    _lockout_timer = Timer.new()
    _lockout_timer.one_shot = true
    _lockout_timer.autostart = false
    add_child(_lockout_timer)

    # 既存処理はそのまま...
    # (省略)

func notify_damaged() -> void:
    ## HealthManager から呼んでもらう想定
    if lockout_after_damage > 0.0:
        _lockout_timer.start(lockout_after_damage)

func _on_timer_timeout() -> void:
    if not _is_active:
        return
    if _lockout_timer and _lockout_timer.time_left > 0.0:
        # ロックアウト中は回復しない
        return

    # 既存の回復処理...
    if _health_manager == null:
        disable()
        return

    if not heal_even_if_full and "is_full_hp" in _health_manager:
        if _health_manager.is_full_hp():
            return

    _health_manager.heal(heal_amount)

このように、RegenEffect 自体は「時間ベースの回復」という最小限の責務に絞りつつ、必要になったときにだけ機能を足していくスタイルにすると、プロジェクト全体がかなり見通しよくなります。
「継承より合成」の力を借りて、HP 周りのロジックもどんどんコンポーネント化していきましょう。