Godot でアクションゲームやRPGを作っていると、つい「プレイヤー」「敵」ごとに専用スクリプトを生やして、その中に HP 計算・ダメージ処理・回避判定…と全部入りで書いてしまいがちですよね。さらにプレイヤーは CharacterBody2D を継承、敵は Node2D を継承、ボスはさらにそこから継承…とやっていると、「回避ロジックだけ共通化したいのに、継承ツリーが邪魔で切り出しづらい」という状況になりがちです。
Godot のノード階層にロジックをべったり書いてしまうと、
- 「この敵だけ回避率を上げたい」→ スクリプトをコピペ・分岐だらけ
- 「プレイヤーと敵で同じ回避判定を使いたい」→ 親クラスを共通化しないといけない
- 「一部のオブジェクトは回避を使わない」→ 空実装や条件分岐が増える
といった「継承ベタベタ構造」のつらみが出てきます。
そこで今回は、「回避判定だけを独立したコンポーネント」としてアタッチできるようにした EvasionCalculator を紹介します。攻撃を受けたときに、Agility ステータスに基づいて確率でダメージを無効化(Miss)する処理を、このコンポーネント一つに閉じ込めましょう。
【Godot 4】回避ロジックを丸ごとコンポーネント化!「EvasionCalculator」コンポーネント
EvasionCalculator は、ダメージ計算の前段で「この攻撃は当たるか?ミスか?」を判定するための小さなコンポーネントです。
- Agility(敏捷)ステータスを元に回避率を算出
- 乱数により「命中 or 回避(Miss)」を判定
- シグナルで「回避成功」「命中」を通知
- 敵・プレイヤー・オブジェクトなど、どのノードにもペタッと貼れる
という思想で作っています。ダメージロジック本体は別コンポーネントに任せて、「当たったかどうか」だけを責務として切り出しているのがポイントですね。
フルコード:EvasionCalculator.gd
extends Node
class_name EvasionCalculator
## 攻撃を受けたときに「回避判定」を行うコンポーネント。
## Agility(敏捷)ステータスに基づいて、確率でダメージを無効化(Miss)します。
##
## 想定用途:
## - プレイヤーや敵のノードにアタッチし、被ダメージ前に should_evade() を呼び出す
## - ダメージ計算コンポーネントと組み合わせて使う
## --- 設定パラメータ(インスペクタで編集可能) ---
@export_category("Evasion Settings")
@export var base_evasion_rate: float = 0.05:
## 基本回避率(0.0〜1.0)。
## Agility が 0 のときの回避率。
## 例: 0.05 なら 5% の確率で回避。
set(value):
base_evasion_rate = clamp(value, 0.0, 1.0)
@export var evasion_per_agility: float = 0.01:
## Agility 1ポイントあたりの追加回避率。
## 例: 0.01 なら Agility 10 で +10% 回避。
set(value):
evasion_per_agility = max(value, 0.0)
@export var max_total_evasion: float = 0.8:
## 回避率の上限(0.0〜1.0)。
## どれだけ Agility が高くても、この値を超えて回避しないようにする。
set(value):
max_total_evasion = clamp(value, 0.0, 1.0)
@export var use_random_seed: bool = false:
## デバッグ用:true にすると _ready() で乱数シードを固定。
## リプレイ再現やテストで結果を安定させたい場合に使います。
@export_range(-1, 999999, 1)
var debug_seed: int = 12345
@export_category("Agility Source")
@export var agility: int = 0:
## このコンポーネント自身が持つ Agility 値。
## 外部から毎フレーム更新する or 別のステータスコンポーネントから同期して使う想定。
set(value):
agility = max(value, 0)
@export var auto_fetch_agility_from_owner: bool = false:
## true の場合、owner に定義された "agility" プロパティを自動取得して使う。
## 例: owner.agility が 20 なら、それを回避計算に使う。
##
## 使い方:
## - Player.gd や Enemy.gd に "var agility: int" を定義しておく
## - このフラグを ON にすると、should_evade() 実行時に owner.agility を読む
##
## 注意:
## - owner に agility プロパティが無い場合は、ローカルの agility を使用します。
## --- シグナル ---
signal evasion_success(context)
## 回避が成功したときに発火。
## context: 任意の情報(攻撃者、攻撃ID、ダメージ値など)を呼び出し側から渡せます。
signal evasion_failed(context)
## 回避に失敗(=攻撃が命中)したときに発火。
## context: 任意の情報(攻撃者、攻撃ID、ダメージ値など)を呼び出し側から渡せます。
## --- ライフサイクル ---
func _ready() -> void:
if use_random_seed:
# デバッグ用に乱数シードを固定
randomize()
seed(debug_seed)
## --- パブリックAPI ---
func get_current_agility() -> int:
## 現在の Agility を取得する。
## auto_fetch_agility_from_owner が true なら owner.agility を優先。
if auto_fetch_agility_from_owner and owner != null:
if "agility" in owner:
var value = owner.agility
if typeof(value) in [TYPE_INT, TYPE_FLOAT]:
return int(value)
return agility
func get_evasion_rate() -> float:
## 現在の Agility に基づいて、最終的な回避率(0.0〜1.0)を返す。
var current_agility := get_current_agility()
var rate := base_evasion_rate + evasion_per_agility * float(current_agility)
rate = clamp(rate, 0.0, max_total_evasion)
return rate
func should_evade(context := null) -> bool:
## 回避判定を行い、「回避成功なら true」「命中なら false」を返す。
## あわせて、対応するシグナルも発火します。
##
## context: 任意の追加情報(攻撃者、攻撃ID、予定ダメージなど)を渡せます。
## シグナル受信側で参照できるので、ログ出力やエフェクト制御に便利です。
var rate := get_evasion_rate()
var roll := randf()
var is_evaded := roll < rate
if is_evaded:
emit_signal("evasion_success", context)
else:
emit_signal("evasion_failed", context)
return is_evaded
## --- デバッグ補助 ---
func debug_print_evasion() -> void:
## 現在の Agility と回避率を print するデバッグ用ヘルパー。
var current_agility := get_current_agility()
var rate := get_evasion_rate()
print("[EvasionCalculator] agility=%d, evasion_rate=%.2f%%" % [current_agility, rate * 100.0])
使い方の手順
ここでは 2D アクションゲームを想定して、プレイヤーと敵に EvasionCalculator を付ける例を見ていきます。
手順①:コンポーネントスクリプトを用意する
- 上記の
EvasionCalculator.gdをそのままファイルとして保存します。
例:res://components/EvasionCalculator.gd - Godot エディタを再読み込みすると、ノード追加時のスクリプト一覧に
EvasionCalculatorが表示されるはずです。
手順②:プレイヤーにアタッチする
まずはプレイヤーのシーン構成例です。
Player (CharacterBody2D) ├── Sprite2D ├── CollisionShape2D ├── DamageReceiver (Node) ※ダメージ処理コンポーネント(仮) └── EvasionCalculator (Node) ※今回のコンポーネント
Player 本体のスクリプト例:
extends CharacterBody2D
@export var agility: int = 15 # プレイヤーの敏捷ステータス
@onready var evasion: EvasionCalculator = $EvasionCalculator
@onready var damage_receiver = $DamageReceiver
func _ready() -> void:
# owner.agility を使って欲しいので、コンポーネント側のフラグをONにする
evasion.auto_fetch_agility_from_owner = true
# 回避成功/失敗のシグナルを受信して、ログやエフェクトを出す
evasion.evasion_success.connect(_on_evasion_success)
evasion.evasion_failed.connect(_on_evasion_failed)
func receive_attack(attack_info: Dictionary) -> void:
## どこか別のノード(敵など)から呼ばれる想定の関数。
## attack_info には "damage" や "attacker" などを詰めておく。
var is_evaded := evasion.should_evade(attack_info)
if is_evaded:
# 回避成功なら、ダメージ処理は行わずに終了
return
# 命中したので、実際のダメージ処理コンポーネントに委譲
damage_receiver.apply_damage(attack_info)
func _on_evasion_success(context: Dictionary) -> void:
print("[Player] Attack evaded! context=", context)
# ここで回避エフェクト再生など
# play_dodge_animation()
func _on_evasion_failed(context: Dictionary) -> void:
print("[Player] Hit! context=", context)
# ここで被弾エフェクト再生など
# play_hit_animation()
ポイント:
Playerは「回避の仕組み」をほとんど知らず、evasion.should_evade()の結果だけを見ています。- Agility の値は
Player側に持たせていて、EvasionCalculatorはowner.agilityを読むだけです。 - ダメージ処理(HP 減少など)は
DamageReceiverという別コンポーネントに分離しています。
手順③:敵にもそのまま再利用する
敵のシーン構成例:
EnemyGoblin (CharacterBody2D) ├── Sprite2D ├── CollisionShape2D ├── DamageReceiver (Node) └── EvasionCalculator (Node)
EnemyGoblin.gd の例:
extends CharacterBody2D
@export var agility: int = 5 # ゴブリンはちょっとだけ素早い
@onready var evasion: EvasionCalculator = $EvasionCalculator
@onready var damage_receiver = $DamageReceiver
func _ready() -> void:
evasion.auto_fetch_agility_from_owner = true
evasion.evasion_success.connect(_on_evasion_success)
evasion.evasion_failed.connect(_on_evasion_failed)
func receive_attack(attack_info: Dictionary) -> void:
var is_evaded := evasion.should_evade(attack_info)
if is_evaded:
return
damage_receiver.apply_damage(attack_info)
func _on_evasion_success(context: Dictionary) -> void:
print("[EnemyGoblin] dodged! context=", context)
# 回避時のSEやアニメーション
func _on_evasion_failed(context: Dictionary) -> void:
print("[EnemyGoblin] got hit! context=", context)
# 被弾時のSEやアニメーション
プレイヤーとほぼ同じコードで、Agility の値だけ変えれば OK ですね。
「回避ロジックをどこに書くか」で悩む必要はなく、とりあえず EvasionCalculator をアタッチしておけばよい状態になります。
手順④:動く床やギミックにも「回避」を生やす
例えば「攻撃を受けると確率で壊れる床」を作りたい場合も、同じコンポーネントを使い回せます。
BreakableFloor (StaticBody2D) ├── Sprite2D ├── CollisionShape2D ├── DamageReceiver (Node) └── EvasionCalculator (Node)
extends StaticBody2D
@export var agility: int = 0 # 床なので基本は 0。特別な床だけ上げてもOK
@onready var evasion: EvasionCalculator = $EvasionCalculator
@onready var damage_receiver = $DamageReceiver
func _ready() -> void:
evasion.auto_fetch_agility_from_owner = true
evasion.evasion_success.connect(_on_evasion_success)
evasion.evasion_failed.connect(_on_evasion_failed)
func receive_attack(attack_info: Dictionary) -> void:
if evasion.should_evade(attack_info):
return
damage_receiver.apply_damage(attack_info)
func _on_evasion_success(context: Dictionary) -> void:
# 「攻撃を受けたけど壊れなかった」みたいな演出
print("[BreakableFloor] resisted the attack.")
func _on_evasion_failed(context: Dictionary) -> void:
print("[BreakableFloor] destroyed!")
queue_free()
ノードの種類(CharacterBody2D / StaticBody2D など)と関係なく、EvasionCalculator をポンと付けるだけで回避判定を共有できるのが、コンポーネント指向の気持ちよさですね。
メリットと応用
EvasionCalculator をコンポーネントとして切り出すことで、次のようなメリットがあります。
- シーン構造がシンプル
プレイヤーや敵のメインスクリプトから、回避ロジックを追い出せるので、「移動」「入力」「AI」「ダメージ」「回避」がそれぞれ別コンポーネントに分かれ、見通しが良くなります。 - 継承ツリーに縛られない
プレイヤーも敵もギミックも、ベースクラスがバラバラでも同じEvasionCalculatorを使い回せます。「共通の親クラスを作るか…」と悩む必要がなくなります。 - バランス調整が楽
base_evasion_rateやevasion_per_agilityをインスペクタから調整できるので、「この敵だけ回避高め」「このボスはほぼ当たる」などを、コードを書き換えずに調整できます。 - テストがしやすい
debug_print_evasion()やuse_random_seedを使えば、回避率の確認やリプレイ再現がしやすくなります。ユニットテスト的にshould_evade()を叩くことも簡単です。
さらに、別コンポーネントとの組み合わせで応用範囲が広がります。
- 「命中率(Accuracy)」コンポーネントと組み合わせて、「命中側 vs 回避側」の対決にする
- 「状態異常」コンポーネントから
max_total_evasionを一時的に下げて、鈍足・スタンなどの影響を表現 - 「装備」コンポーネントから
agilityを加算して、回避ビルドを作る
すべてを一つの巨大なスクリプトに押し込まず、小さな責務ごとのコンポーネントを合成してキャラクターを組み立てると、プロジェクトが大きくなっても破綻しにくくなります。
改造案:攻撃ごとに回避補正をかける
例えば「重い攻撃は避けづらい」「必中攻撃は回避できない」といった仕様を入れたい場合は、context に evasion_modifier を渡して、最終回避率に掛けるように改造できます。
func should_evade(context := null) -> bool:
var rate := get_evasion_rate()
# attack_info などから回避補正を読み取る(無ければ 1.0)
var modifier := 1.0
if typeof(context) == TYPE_DICTIONARY and context.has("evasion_modifier"):
modifier = float(context.evasion_modifier)
rate = clamp(rate * modifier, 0.0, 1.0)
var roll := randf()
var is_evaded := roll < rate
if is_evaded:
emit_signal("evasion_success", context)
else:
emit_signal("evasion_failed", context)
return is_evaded
これで、攻撃側は
var attack_info = {
"damage": 30,
"evasion_modifier": 0.5 # この攻撃は回避しづらい(回避率半減)
}
target.receive_attack(attack_info)
のように補正をかけられます。
「回避判定」という一つの責務にフォーカスしたコンポーネントにしておくと、こうした改造も局所的に済むので、全体の見通しを保ったままゲーム性を盛っていけますね。
