GodotでHP制御を書くとき、ついこういう「なんちゃってベースクラス」を作りがちですよね。
# ありがちな書き方(あまり良くない例)
class_name Damageable
extends CharacterBody2D
var hp: int = 100
func apply_damage(amount: int) -> void:
hp -= amount
if hp <= 0:
die()
ここに「シールド」を足したくなった瞬間、
- プレイヤーも敵も、全部このクラスを継承しないといけない
- ボスだけ特殊なシールド仕様にしたいのに、継承ツリーがどんどん複雑になる
- UI側(シールドバー)やエフェクトとロジックがベッタリ結合しがち
…と、だんだん「継承の沼」にはまっていきます。
そこで今回は、「シールドはシールドとして独立したコンポーネントにする」アプローチを取ります。
HPロジックとは疎結合にして、必要なノードにポン付けできる ShieldGenerator コンポーネントを用意しておくと、
- プレイヤーにも敵にも、動く床にも、好きなノードに後付けできる
- シールドの挙動(自動回復、クールダウンなど)を1か所で管理・改造できる
- ノード階層を深くせずに、コンポーネントをぶら下げるだけで機能追加できる
という「継承より合成」な世界線になります。
【Godot 4】HPの前にシールドで受け止めろ!「ShieldGenerator」コンポーネント
このコンポーネントは、
- シールド値の最大値・現在値
- ダメージを受けたときの「シールド優先消費」
- 一定時間ダメージを受けていないときの自動回復
- シールドが割れた・全回復したときのシグナル通知
をまとめて面倒見ます。
ダメージ処理側は「とりあえず ShieldGenerator があれば先にそっちを削る」というシンプルな呼び出しだけでOKにしましょう。
フルコード:ShieldGenerator.gd
## シールド値を管理するコンポーネント
## - ダメージはまずシールドから消費される
## - シールドが残った / 割れた / フル回復したタイミングでシグナル通知
## - 一定時間ダメージを受けていないときに自動回復も可能
class_name ShieldGenerator
extends Node
## 最大シールド値
@export_range(0, 9999, 1)
var max_shield: int = 50:
set(value):
max_shield = max(value, 0)
_current_shield = clampi(_current_shield, 0, max_shield)
## 開始時のシールド値
@export_range(0, 9999, 1)
var start_shield: int = 50
## 自動回復を行うかどうか
@export var enable_regeneration: bool = true
## ダメージを受けてから何秒間ダメージがなければ回復を開始するか
@export_range(0.0, 60.0, 0.1)
var regen_delay: float = 3.0
## 1秒あたり何ポイント回復するか
@export_range(0.0, 999.0, 0.1)
var regen_rate_per_sec: float = 10.0
## シールドが0になったときに自動回復を止めるか
## (例:シールドが割れたらしばらく復活しないゲームデザイン向け)
@export var stop_regen_when_broken: bool = false
## デバッグ用: シールド変化をprintするか
@export var debug_log: bool = false
## 現在のシールド値(読み取り専用にしたいので setter は公開しない)
var _current_shield: int = 0
## 外部から読めるようにgetterを用意
var current_shield: int:
get:
return _current_shield
## 最後にダメージを受けたゲーム内時間(秒)
var _last_damage_time: float = -INF
## シールドが現在「割れている」状態かどうか
var is_broken: bool:
get:
return _current_shield <= 0
## --- シグナル定義 ---
## シールド値が変化したとき
signal shield_changed(current: int, max: int)
## シールドが0になった瞬間
signal shield_broken
## シールドがフルまで回復した瞬間
signal shield_fully_recharged
func _ready() -> void:
# 初期値を設定
max_shield = max_shield # setterを通して補正
_current_shield = clampi(start_shield, 0, max_shield)
if debug_log:
print("[ShieldGenerator] Ready. shield = %d / %d" % [_current_shield, max_shield])
emit_signal("shield_changed", _current_shield, max_shield)
func _process(delta: float) -> void:
if not enable_regeneration:
return
if max_shield <= 0:
return
if stop_regen_when_broken and is_broken:
return
# ダメージを受けてから regen_delay 秒以上経っていなければ回復しない
var now := Time.get_ticks_msec() / 1000.0
if now - _last_damage_time < regen_delay:
return
if _current_shield < max_shield and regen_rate_per_sec > 0.0:
var before := _current_shield
# deltaを使って秒間レートで回復
var amount := int(regen_rate_per_sec * delta)
if amount <= 0:
# レートが小さすぎる場合、蓄積しても良いが
# シンプルに「最低1回復」にしてしまう手もある
amount = 1
_current_shield = clampi(_current_shield + amount, 0, max_shield)
if _current_shield != before:
if debug_log:
print("[ShieldGenerator] Regen: %d -> %d" % [before, _current_shield])
emit_signal("shield_changed", _current_shield, max_shield)
if _current_shield == max_shield:
emit_signal("shield_fully_recharged")
## ダメージを適用し、「どれだけHPに通したか」を返す
## - return: シールドで吸収しきれず、HPに伝わったダメージ量
func apply_damage(amount: int) -> int:
if amount <= 0:
return 0
var before := _current_shield
_last_damage_time = Time.get_ticks_msec() / 1000.0
if _current_shield >= amount:
# シールドで全部吸収できる
_current_shield -= amount
if debug_log:
print("[ShieldGenerator] Damage %d absorbed. shield: %d -> %d" %
[amount, before, _current_shield])
emit_signal("shield_changed", _current_shield, max_shield)
# HPにはダメージ0
return 0
# シールドでは吸収しきれないぶん
var leftover := amount - _current_shield
_current_shield = 0
if debug_log:
print("[ShieldGenerator] Damage %d broke shield. leftover = %d" %
[amount, leftover])
emit_signal("shield_changed", _current_shield, max_shield)
emit_signal("shield_broken")
# シールドで吸収しきれなかったぶんをHP側に返す
return leftover
## シールドを回復する(ポーション・スキルなどから呼ぶ想定)
func heal_shield(amount: int) -> void:
if amount <= 0:
return
var before := _current_shield
_current_shield = clampi(_current_shield + amount, 0, max_shield)
if _current_shield != before:
if debug_log:
print("[ShieldGenerator] Heal %d. shield: %d -> %d" %
[amount, before, _current_shield])
emit_signal("shield_changed", _current_shield, max_shield)
if _current_shield == max_shield:
emit_signal("shield_fully_recharged")
## シールドを強制的に設定する(デバッグ・チート用)
func set_shield(value: int) -> void:
var before := _current_shield
_current_shield = clampi(value, 0, max_shield)
if _current_shield != before:
if debug_log:
print("[ShieldGenerator] Set shield: %d -> %d" % [before, _current_shield])
emit_signal("shield_changed", _current_shield, max_shield)
## シールドを完全にリセットする
## - 例: プレイヤーがリスポーンしたとき
func reset_shield() -> void:
_current_shield = clampi(start_shield, 0, max_shield)
_last_damage_time = -INF
if debug_log:
print("[ShieldGenerator] Reset. shield = %d / %d" % [_current_shield, max_shield])
emit_signal("shield_changed", _current_shield, max_shield)
使い方の手順
ここでは典型的な「プレイヤー」と「敵」にシールドを付ける例で進めます。
手順①:スクリプトを用意してコンポーネント化
- 上のコードを
res://components/shield_generator.gdなどに保存します。 - Godotエディタで開くと、ノード追加ダイアログの検索バーに
ShieldGeneratorと打てば出てくるようになります(class_nameのおかげですね)。
手順②:プレイヤーシーンにアタッチ
例として、こんなプレイヤーシーンを想定します。
Player (CharacterBody2D) ├── Sprite2D ├── CollisionShape2D ├── ShieldGenerator (Node) ← これを追加 └── Damageable (Node) ← HP管理コンポーネント(例)
プレイヤー本体のスクリプト例:
# Player.gd
extends CharacterBody2D
@onready var shield: ShieldGenerator = $ShieldGenerator
@onready var damageable: Node = $Damageable # ここでは仮のHP管理コンポーネントとする
func take_damage(amount: int) -> void:
# まずシールドにダメージを渡す
var leftover := shield.apply_damage(amount)
# シールドで吸収しきれなかったぶんだけHPに通す
if leftover > 0:
damageable.call("apply_damage", leftover)
シールドの数値をUIに反映したい場合は、shield_changed シグナルをUI側で拾えばOKです。
# 例: PlayerHUD.gd
extends CanvasLayer
@onready var player: Node2D = $"../Player"
@onready var shield_bar: TextureProgressBar = %ShieldBar
func _ready() -> void:
var shield: ShieldGenerator = player.get_node("ShieldGenerator")
shield.shield_changed.connect(_on_shield_changed)
# 初期値を反映
_on_shield_changed(shield.current_shield, shield.max_shield)
func _on_shield_changed(current: int, max_value: int) -> void:
shield_bar.max_value = max_value
shield_bar.value = current
手順③:敵にもそのままポン付け
敵シーンも同じコンポーネントを再利用できます。
Enemy (CharacterBody2D) ├── Sprite2D ├── CollisionShape2D ├── ShieldGenerator (Node) ← プレイヤーと同じコンポーネント └── EnemyHealth (Node) ← 敵専用のHPロジックでもOK
# Enemy.gd
extends CharacterBody2D
@onready var shield: ShieldGenerator = $ShieldGenerator
@onready var health: Node = $EnemyHealth
func apply_hit(damage: int) -> void:
var leftover := shield.apply_damage(damage)
if leftover > 0:
health.call("apply_damage", leftover)
敵だけ「シールドは自動回復しない」ようにしたければ、
インスペクタで enable_regeneration のチェックを外すだけで完了です。
プレイヤーと敵でロジックを分ける必要はありません。
手順④:動く床やオブジェクトにもシールドを付ける
例えば「壊れるけどシールドで守られている動く床」なんてギミックも、コンポーネントを付けるだけで実現できます。
MovingPlatform (Node2D) ├── Sprite2D ├── CollisionShape2D ├── PlatformMover (Node) ← 移動ロジック ├── ShieldGenerator (Node) ← シールド └── PlatformHealth (Node) ← 耐久値
# MovingPlatform.gd
extends Node2D
@onready var shield: ShieldGenerator = $ShieldGenerator
@onready var health: Node = $PlatformHealth
func hit_by_player(damage: int) -> void:
var leftover := shield.apply_damage(damage)
if leftover > 0:
health.call("apply_damage", leftover)
こうしておくと、「ダメージを持つオブジェクトは全部同じプロトコル(apply_damage → ShieldGenerator → HP)」で扱えるので、ゲーム全体の設計がかなりスッキリします。
メリットと応用
この ShieldGenerator コンポーネントを使うことで、
- シーン構造がスッキリ
HPやシールドのロジックを各キャラのスクリプトにベタ書きしなくて済むので、Player.gdやEnemy.gdは「入力・AI・移動」など本来の責務に集中できます。 - 使い回しが簡単
プレイヤー、敵、ギミック、ボス…どこにでも同じシールドロジックをポン付けできます。
継承ツリーを増やす必要がないので、あとから「やっぱり雑魚にもシールド付けたい」みたいな仕様変更にも強いです。 - ゲームデザインの調整が楽
max_shieldやregen_delay,regen_rate_per_secをインスペクタからいじるだけで、
「プレイヤーはガンガン回復するけど敵はしない」「ボスは一度割れたら復活しない」など、
数値いじりで多彩なバリエーションを作れます。 - UI・エフェクトとの連携がシンプル
シグナル(shield_changed,shield_broken,shield_fully_recharged)を拾うだけで、
シールドバーの更新や、シールドブレイク演出を簡単にトリガーできます。
改造案:シールドブレイク時に一時的に回復を禁止する
例えば「シールドが割れたら、数秒間は自動回復しない」みたいな仕様を入れたい場合、
以下のような簡単な拡張ができます。
# ShieldGenerator.gd の一部に追加
@export_range(0.0, 60.0, 0.1)
var broken_regen_lock_time: float = 5.0 # 割れた後、この秒数だけ回復禁止
var _regen_locked_until: float = -INF
func _process(delta: float) -> void:
if not enable_regeneration:
return
var now := Time.get_ticks_msec() / 1000.0
if now < _regen_locked_until:
return # ロック中は回復しない
# 既存の処理に続く...
# (_last_damage_time や regen_delay のチェックなど)
func apply_damage(amount: int) -> int:
var leftover := _apply_damage_internal(amount)
if is_broken and broken_regen_lock_time > 0.0:
_regen_locked_until = Time.get_ticks_msec() / 1000.0 + broken_regen_lock_time
return leftover
こうした改造も、ShieldGenerator という1コンポーネントだけをいじれば全キャラに反映されるのが、合成ベース設計の気持ちいいところですね。
「とりあえず何でも Node にしてぶら下げていく」スタイルで、シールド周りもコンポーネント化していきましょう。
