Godot 4でアクションゲームやRPGを作っていると、ダメージ処理まわりがどんどん複雑になってきますよね。典型的なのは、
- プレイヤーや敵それぞれに「ダメージ計算ロジック」を継承でベタ書きしてしまう
- 「ガード」「シールド」「反射」などの特殊効果を入れるたびに、親クラスのダメージ処理をいじる羽目になる
- 結果として、共通のBaseCharacter.gdが肥大化し、怖くて触れない巨大クラスが爆誕する
Godotのシーン/ノードシステムは便利ですが、「プレイヤー → キャラクター基底クラス → さらに共通基底…」という継承ツリーに頼りすぎると、あとから「ダメージ反射したい」「一部の敵だけ反射したい」といった要望に対応しづらくなります。
そこで今回は、「ダメージ反射」だけに責務を絞ったコンポーネントとして、DamageReflection を用意します。プレイヤーでも敵でも、ノードにペタっとアタッチするだけで、受けたダメージの一部を攻撃者に返すことができます。
深い継承や条件分岐まみれの巨大スクリプトから卒業して、「継承より合成(Composition)」でダメージ処理をスッキリさせていきましょう。
【Godot 4】攻撃してきた相手にお返しだ!「DamageReflection」コンポーネント
コンポーネントの考え方
この DamageReflection コンポーネントは、
- 「ダメージを受けた」というイベントをフックして
- 攻撃してきた相手(attacker)に、一定割合のダメージを返す
という一点に専念します。
そのために、ゲーム側(プレイヤーや敵側)には、
take_damage(amount: float, attacker: Node)のような「ダメージを受ける関数」からDamageReflectionのon_damage_received(amount, attacker)を呼んでもらう
というシンプルな連携を行います。
「ダメージ反射できるかどうか」は、DamageReflection をアタッチしているかどうかで決まるので、プレイヤーでもボスでも、同じロジックを再利用できます。
フルコード: DamageReflection.gd
extends Node
class_name DamageReflection
## DamageReflection
## 受けたダメージの一部を、攻撃してきた相手(attacker)に反射するコンポーネント。
##
## 想定する連携:
## - 所有者(owner / get_parent() など)がダメージを受けるとき、
## 自分の take_damage() などから on_damage_received() を呼び出す。
##
## - attacker には「ダメージを与えられるノード」を渡す。
## 例: attacker.apply_damage(reflected_amount, owner)
##
## このコンポーネントは「どうやってダメージを与えるか」の実装詳細には踏み込まず、
## 攻撃側に対して「指定したメソッドを呼び出す」という形で疎結合にしています。
## 反射倍率。1.0 なら受けたダメージと同じだけ反射、0.5 なら半分だけ反射。
@export_range(0.0, 10.0, 0.05)
var reflection_ratio: float = 0.5
## 最低でもこの値以上は反射する(0 なら無効)。
@export_range(0.0, 9999.0, 0.1)
var min_reflection_damage: float = 0.0
## この値を超えては反射しない(0 以下なら無制限)。
@export_range(0.0, 9999.0, 0.1)
var max_reflection_damage: float = 0.0
## 反射ダメージを丸める方法
enum RoundingMode {
ROUND_NONE, ## 丸めない(float のまま)
ROUND_FLOOR, ## 小数点以下切り捨て
ROUND_CEIL, ## 小数点以下切り上げ
ROUND_ROUND ## 四捨五入
}
@export var rounding_mode: RoundingMode = RoundingMode.ROUND_NONE
## 反射ダメージを与えるときに呼ぶメソッド名。
## attacker.<method_name>(reflected_damage: float, source: Node) の形で呼び出される想定。
## 例: "apply_damage" にしておき、attacker 側では
## func apply_damage(amount: float, source: Node) -> void:
## health -= amount
@export var attacker_damage_method: StringName = &"apply_damage"
## 反射時に、所有者(self.get_owner_or_parent()) を attacker 側に「source」として渡すかどうか。
@export var pass_source_to_attacker: bool = true
## 反射を一時的に無効化したいときに使うフラグ。
@export var enabled: bool = true
## 反射が発生したときに通知するシグナル。
signal damage_reflected(reflected_amount: float, attacker: Node)
func _ready() -> void:
# owner を自動的に親ノードにしておく(必要に応じて書き換え可能)
if owner == self:
# シーンに直接置かれた場合など、owner が自分自身のケースでは
# 親ノードを owner とみなす
if get_parent() != null:
owner = get_parent()
## 外部から呼び出してもらうエントリポイント。
## - amount: 受けたダメージ量
## - attacker: 攻撃してきたノード(null の場合は何もしない)
func on_damage_received(amount: float, attacker: Node) -> void:
if not enabled:
return
if attacker == null:
return
if amount <= 0.0:
return
var reflected_amount := _calculate_reflection(amount)
if reflected_amount <= 0.0:
return
_apply_reflection(attacker, reflected_amount)
emit_signal("damage_reflected", reflected_amount, attacker)
## 反射ダメージ量を計算する内部関数。
func _calculate_reflection(amount: float) -> float:
var reflected := amount * reflection_ratio
# 下限適用
if min_reflection_damage > 0.0 and reflected < min_reflection_damage:
reflected = min_reflection_damage
# 上限適用
if max_reflection_damage > 0.0 and reflected > max_reflection_damage:
reflected = max_reflection_damage
# 丸め処理
match rounding_mode:
RoundingMode.ROUND_FLOOR:
reflected = floor(reflected)
RoundingMode.ROUND_CEIL:
reflected = ceil(reflected)
RoundingMode.ROUND_ROUND:
reflected = round(reflected)
_:
# ROUND_NONE の場合は何もしない
pass
return reflected
## 実際に attacker にダメージを返す処理。
func _apply_reflection(attacker: Node, reflected_amount: float) -> void:
if attacker == null:
return
if not attacker.has_method(attacker_damage_method):
# 反射対象が想定メソッドを持っていない場合は何もしない。
# 必要ならここで warning を出してもよい。
push_warning("DamageReflection: Attacker '%s' has no method '%s'" % [attacker.name, attacker_damage_method])
return
if pass_source_to_attacker:
# attacker.apply_damage(reflected_amount, source) のような形で呼び出す
attacker.call(attacker_damage_method, reflected_amount, _get_source_node())
else:
# ダメージ量だけ渡すシンプルな呼び出し
attacker.call(attacker_damage_method, reflected_amount)
## 攻撃側に「誰からのダメージか」を渡したいときに使う。
## デフォルトでは owner を返すが、owner が設定されていない場合は親ノードを返す。
func _get_source_node() -> Node:
if owner != null and owner != self:
return owner
if get_parent() != null:
return get_parent()
return self
使い方の手順
前提: ダメージ処理のインターフェース
このコンポーネントは「攻撃する側」と「攻撃される側」の間に立つので、最低限以下のようなインターフェースを用意しておくとスムーズです。
- 攻撃される側(プレイヤーや敵):
take_damage(amount: float, attacker: Node) - 攻撃する側(敵やプレイヤー):
apply_damage(amount: float, source: Node)(名前は任意)
この2つをつないであげることで、DamageReflection が「受けたダメージの一部を attacker に返す」ことができます。
手順①: DamageReflection.gd をプロジェクトに追加
- 上記の
DamageReflection.gdをプロジェクト内(例:res://components/DamageReflection.gd)に保存します。 - Godot エディタでスクリプトを開き、エラーがないことを確認します。
手順②: プレイヤー側にコンポーネントをアタッチ
例として、プレイヤーが敵の攻撃を受けたときにダメージの一部を反射する構成を考えます。
Player (CharacterBody2D) ├── Sprite2D ├── CollisionShape2D └── DamageReflection (Node)
プレイヤー本体(Player.gd)は、ざっくりこんなイメージです:
extends CharacterBody2D
@export var max_health: float = 100.0
var health: float
# DamageReflection コンポーネントへの参照
@onready var damage_reflection: DamageReflection = $DamageReflection
func _ready() -> void:
health = max_health
## 外部(敵など)から呼ばれる「ダメージを受ける関数」
func take_damage(amount: float, attacker: Node) -> void:
if amount <= 0.0:
return
health -= amount
print("Player took ", amount, " damage. HP => ", health)
# ここで DamageReflection コンポーネントに通知
if damage_reflection:
damage_reflection.on_damage_received(amount, attacker)
if health <= 0.0:
die()
func die() -> void:
print("Player died")
queue_free()
ポイントは、プレイヤーのダメージ処理自体はシンプルなままにしておき、
「反射したいときだけ DamageReflection.on_damage_received() を呼ぶ」という構造にしていることです。
手順③: 敵側に「ダメージを受けるメソッド」を実装
敵は、プレイヤーに攻撃したときに「attacker」として渡される側です。DamageReflection のデフォルト設定では attacker.apply_damage(reflected_amount, source) を呼ぶので、apply_damage() を実装しておきます。
Enemy (CharacterBody2D) ├── Sprite2D └── CollisionShape2D
extends CharacterBody2D
@export var max_health: float = 50.0
var health: float
func _ready() -> void:
health = max_health
## DamageReflection から呼ばれる想定のメソッド
## amount: 反射ダメージ量
## source: 誰からのダメージか(プレイヤーなど)
func apply_damage(amount: float, source: Node) -> void:
if amount <= 0.0:
return
health -= amount
print("Enemy took reflected damage ", amount, " from ", source.name, ". HP => ", health)
if health <= 0.0:
die()
func die() -> void:
print("Enemy died")
queue_free()
これで、
- 敵がプレイヤーを攻撃 → プレイヤーの
take_damage()が呼ばれる - プレイヤーの
take_damage()内でDamageReflection.on_damage_received()を呼ぶ DamageReflectionがEnemy.apply_damage()を呼び、ダメージをお返し
という流れが完成します。
手順④: 実際の攻撃処理からつなぎ込む
最後に、敵がプレイヤーを殴る処理の一例を示します。
ここではシンプルに「プレイヤーに直接 take_damage() を呼ぶ」形にしています。
# Enemy.gd の一部例
@export var contact_damage: float = 10.0
func _physics_process(delta: float) -> void:
# 例: プレイヤーに接触していたらダメージを与える
var player := _find_player_in_range()
if player:
_attack_player(player)
func _find_player_in_range() -> Node:
# 実際には Area2D や距離判定などでプレイヤーを探す処理を書く
# ここではダミーとして null を返す
return null
func _attack_player(player: Node) -> void:
if not player.has_method("take_damage"):
return
# attacker として self (この敵) を渡す
player.call("take_damage", contact_damage, self)
これで、プレイヤーが DamageReflection を持っていればダメージを反射し、持っていなければ普通にダメージを受けるだけ、というコンポーネント指向な振る舞いになります。
メリットと応用
メリット①: 「反射できる/できない」をアタッチで切り替えられる
継承ベースで「ReflectableCharacter」みたいなクラスを作ると、
- プレイヤーと敵で別々に実装することになりがち
- 途中で「この敵だけ反射をオフにしたい」となったときに面倒
ですが、DamageReflection コンポーネントなら、
- 反射したいノードに
DamageReflectionを追加するだけ - 不要になったらノードを削除 or
enabled = falseにするだけ
と、シーン構成レベルで ON/OFF を切り替えられます。
メリット②: シーン構造がフラットで見通しが良い
「反射を持つ敵」「反射を持たない敵」をクラス階層で分けてしまうと、
EnemyBaseEnemyWithReflectionEnemyWithShieldAndReflection
みたいにどんどんクラスが増えていきます。
コンポーネント方式なら、
Enemy (CharacterBody2D) ├── Sprite2D ├── CollisionShape2D ├── DamageReflection └── ShieldComponent
といった感じで、「この敵は何を持っているのか」がノードツリーを見れば一目で分かるようになります。
メリット③: レベルデザイン時の調整が楽
DamageReflection には、
reflection_ratio(反射倍率)min_reflection_damage/max_reflection_damage(下限/上限)rounding_mode(ダメージの丸め方)
といったパラメータが用意されているので、
- 序盤の敵には「ちょっとだけ反射」
- 中盤のボスには「受けたダメージと同じだけ反射」
- 終盤のトラップには「固定値で大ダメージを反射」
といった調整を、エディタ上でスライダーを動かすだけで実現できます。
応用例: トゲ床やカウンター技にも使える
このコンポーネントは「受けたダメージを元に反射する」という仕組みなので、
- トゲ床がプレイヤーからの攻撃を「跳ね返す」ギミック
- ボスの「カウンター状態」のときだけ
enabled = trueにして反射する
といった使い方も簡単です。
改造案: 一定確率でしか反射しない「運ゲー反射」
例えば、「20% の確率でしか反射しない」仕様にしたい場合は、
以下のような関数を追加して on_damage_received() の中から呼ぶ形に変えると良いです。
@export_range(0.0, 1.0, 0.01)
var reflection_chance: float = 1.0 # 1.0 で 100%、0.2 で 20%
func on_damage_received(amount: float, attacker: Node) -> void:
if not enabled:
return
if attacker == null:
return
if amount <= 0.0:
return
if not _should_reflect():
return
var reflected_amount := _calculate_reflection(amount)
if reflected_amount <= 0.0:
return
_apply_reflection(attacker, reflected_amount)
emit_signal("damage_reflected", reflected_amount, attacker)
func _should_reflect() -> bool:
return randf() <= reflection_chance
このように、DamageReflection 自体をサクッと改造していけるのも、責務が「反射」に限定されたコンポーネントだからこその強みですね。
ダメージ処理まわりはゲームのコアになりがちなので、最初から巨大な基底クラスに押し込めず、DamageReflection のようなコンポーネントで小さく分割していくと、後々の拡張がかなり楽になります。ぜひ、自分のプロジェクト流の「ダメージ系コンポーネント群」を育ててみてください。
