Godot 4でアクションゲームやRPGを作っていると、ダメージ処理まわりがどんどん複雑になってきますよね。典型的なのは、

  • プレイヤーや敵それぞれに「ダメージ計算ロジック」を継承でベタ書きしてしまう
  • 「ガード」「シールド」「反射」などの特殊効果を入れるたびに、親クラスのダメージ処理をいじる羽目になる
  • 結果として、共通のBaseCharacter.gdが肥大化し、怖くて触れない巨大クラスが爆誕する

Godotのシーン/ノードシステムは便利ですが、「プレイヤー → キャラクター基底クラス → さらに共通基底…」という継承ツリーに頼りすぎると、あとから「ダメージ反射したい」「一部の敵だけ反射したい」といった要望に対応しづらくなります。

そこで今回は、「ダメージ反射」だけに責務を絞ったコンポーネントとして、DamageReflection を用意します。プレイヤーでも敵でも、ノードにペタっとアタッチするだけで、受けたダメージの一部を攻撃者に返すことができます。

深い継承や条件分岐まみれの巨大スクリプトから卒業して、「継承より合成(Composition)」でダメージ処理をスッキリさせていきましょう。

【Godot 4】攻撃してきた相手にお返しだ!「DamageReflection」コンポーネント

コンポーネントの考え方

この DamageReflection コンポーネントは、

  • 「ダメージを受けた」というイベントをフックして
  • 攻撃してきた相手(attacker)に、一定割合のダメージを返す

という一点に専念します。

そのために、ゲーム側(プレイヤーや敵側)には、

  • take_damage(amount: float, attacker: Node) のような「ダメージを受ける関数」から
  • DamageReflectionon_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 をプロジェクトに追加

  1. 上記の DamageReflection.gd をプロジェクト内(例: res://components/DamageReflection.gd)に保存します。
  2. 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() を呼ぶ
  • DamageReflectionEnemy.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 を切り替えられます。

メリット②: シーン構造がフラットで見通しが良い

「反射を持つ敵」「反射を持たない敵」をクラス階層で分けてしまうと、

  • EnemyBase
  • EnemyWithReflection
  • EnemyWithShieldAndReflection

みたいにどんどんクラスが増えていきます。

コンポーネント方式なら、

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 のようなコンポーネントで小さく分割していくと、後々の拡張がかなり楽になります。ぜひ、自分のプロジェクト流の「ダメージ系コンポーネント群」を育ててみてください。