GodotでアクションゲームやRPGを作っていると、「クリティカルヒット」を入れたくなりますよね。でも、素直に実装しようとすると…

  • プレイヤー、敵、トラップなど、攻撃をするノードごとに「クリティカル判定ロジック」をコピペしがち
  • 「クリティカル率」「倍率」「エフェクト」などの調整値が、各スクリプトにバラバラに散らばる
  • 将来「クリティカル演出を変えたい」と思ったときに、全スクリプトを探し回るハメになる

典型的なのは、Player.gdEnemy.gd

var is_critical := randf() < crit_rate
var damage := base_damage * (is_critical ? crit_multiplier : 1.0)

みたいな処理を直接書いてしまうパターンですね。これを全員が継承していればまだマシですが、Godotのノード構造と継承ツリーを両方きれいに保つのは、だんだん苦しくなってきます。

そこで、「クリティカル判定」をひとつの独立コンポーネントとして切り出してしまいましょう。攻撃側のノードにペタっとアタッチすれば、どこからでも同じインターフェースで「ダメージ+クリティカル情報」を取得できるようになります。

【Godot 4】ダメージ計算はおまかせ!「CriticalHitter」コンポーネント

ここでは、

  • クリティカル率(%)
  • クリティカル時のダメージ倍率
  • 乱数シード(デバッグ用)

をまとめて管理し、「元のダメージ」から「最終ダメージ+クリティカルフラグ」を返すコンポーネント CriticalHitter を実装します。

フルコード(GDScript / Godot 4)

extends Node
class_name CriticalHitter
##
## CriticalHitter.gd
## 攻撃時にクリティカル判定を行い、ダメージを増幅するコンポーネント。
## ・どの攻撃者ノードにもアタッチ可能(Player, Enemy, Trap, Projectile など)
## ・元のダメージ値を渡すと、クリティカルを考慮した最終ダメージを返す
## ・クリティカル発生時には signal で通知(UIやエフェクト側が反応できる)
##

## クリティカル率(0.0~1.0)
## 例: 0.25 = 25% でクリティカル発生
@export_range(0.0, 1.0, 0.01)
var critical_chance: float = 0.1

## クリティカル時のダメージ倍率
## 例: 2.0 = 2倍ダメージ
@export_range(1.0, 10.0, 0.1)
var critical_multiplier: float = 2.0

## 乱数のシードを固定したい場合に使用
## 0 のときはシード固定なし(通常のランダム)
@export var fixed_seed: int = 0

## クリティカル発生時に通知されるシグナル
## ・final_damage: 実際に適用されたダメージ
## ・base_damage: 元のダメージ
## ・multiplier: 掛かった倍率(通常は critical_multiplier)
signal critical_hit(final_damage: float, base_damage: float, multiplier: float)

## 乱数生成器(必要ならシード固定も可能)
var _rng := RandomNumberGenerator.new()


func _ready() -> void:
	# シードを固定したい場合(リプレイ再現やデバッグ用)
	if fixed_seed != 0:
		_rng.seed = fixed_seed
	else:
		_rng.randomize()


## 元のダメージに対してクリティカル判定を行い、最終ダメージを返す。
##
## 使用例:
##   var result := critical_hitter.apply_critical(10.0)
##   print(result.final_damage, result.is_critical)
##
## 戻り値は Dictionary:
##   {
##     "final_damage": float,   # 実際に適用するダメージ
##     "is_critical": bool,     # クリティカルだったかどうか
##     "multiplier": float      # 掛かった倍率(1.0 or critical_multiplier)
##   }
func apply_critical(base_damage: float) -> Dictionary:
	var is_critical := _rng.randf() < critical_chance
	var multiplier := 1.0
	if is_critical:
		multiplier = critical_multiplier
	
	var final_damage := base_damage * multiplier
	
	# クリティカル時はシグナルを発火して、UIやエフェクトに知らせる
	if is_critical:
		critical_hit.emit(final_damage, base_damage, multiplier)
	
	return {
		"final_damage": final_damage,
		"is_critical": is_critical,
		"multiplier": multiplier,
	}


## クリティカルの結果だけが欲しい場合のヘルパー。
## ダメージ値は変えずに、「クリティカルかどうか」だけ知りたいときに使う。
func is_critical_roll() -> bool:
	return _rng.randf() < critical_chance


## UIなどから「現在のクリティカル設定」を文字列で確認したいとき用。
func get_description() -> String:
	var percent := int(critical_chance * 100.0)
	return "Critical: %d%%, x%.2f" % [percent, critical_multiplier]

使い方の手順

コンポーネントスクリプトを用意する
上記の CriticalHitter.gd をプロジェクト内(例: res://components/CriticalHitter.gd)に保存します。
class_name CriticalHitter を定義しているので、スクリプトをアタッチするときに検索してそのまま選択できます。

攻撃を行うノードに CriticalHitter をアタッチ
例として「プレイヤー」が近接攻撃を持っているケースを考えます。

Player (CharacterBody2D)
├── Sprite2D
├── CollisionShape2D
└── CriticalHitter (Node)

Player(CharacterBody2D)に子ノードとして Node を追加

その NodeCriticalHitter.gd をアタッチ

インスペクタで critical_chancecritical_multiplier を調整

攻撃処理から CriticalHitter を呼び出す
たとえばプレイヤーの攻撃コードを以下のようにします。


# Player.gd (一例)
extends CharacterBody2D

@onready var critical_hitter: CriticalHitter = $CriticalHitter

var base_attack_damage: float = 10.0

func attack(target: Node) -> void:
# 1. 元のダメージを決める
var base_damage := base_attack_damage

# 2. クリティカル判定を適用
var result := critical_hitter.apply_critical(base_damage)

# 3. ターゲットに最終ダメージを適用
if target.has_method("take_damage"):
target.take_damage(result.final_damage)

# 4. デバッグ用ログ(必要なら)
if result.is_critical:
print("CRITICAL! Damage: ", result.final_damage)
else:
print("Normal hit: ", result.final_damage)


このように、「クリティカル判定」はすべて CriticalHitter 側に閉じ込めておき、攻撃側はシンプルに「元ダメージを渡して結果を受け取る」だけにしておくのがポイントです。


派手な数字やエフェクトをシグナルで鳴らす
クリティカル時にだけ「金色のダメージテキスト」や「専用エフェクト」を出したい場合は、critical_hit シグナルを使います。

# 例: Player.gd で CriticalHitter のシグナルに接続
func _ready() -> void:
$CriticalHitter.critical_hit.connect(_on_critical_hit)

func _on_critical_hit(final_damage: float, base_damage: float, multiplier: float) -> void:
# ここで派手なエフェクトやダメージテキストを生成
# 例: ダメージテキスト生成用のシーンをインスタンスして表示
var crit_label := preload("res://ui/CriticalDamageLabel.tscn").instantiate()
crit_label.text = "%d!" % int(final_damage)
get_tree().current_scene.add_child(crit_label)

このように、演出側のノードにロジックを漏らさず、コンポーネントからのイベントとして扱うと、責務の分離がきれいに保てます。


別の使用例:敵やトラップにも簡単に流用

同じ CriticalHitter を敵にも使えます。

EnemyGoblin (CharacterBody2D)
 ├── Sprite2D
 ├── CollisionShape2D
 └── CriticalHitter (Node)
# EnemyGoblin.gd
extends CharacterBody2D

@onready var critical_hitter: CriticalHitter = $CriticalHitter
var base_attack_damage: float = 4.0

func attack_player(player: Node) -> void:
	var result := critical_hitter.apply_critical(base_attack_damage)
	if player.has_method("take_damage"):
		player.take_damage(result.final_damage)

トラップ(トゲ床など)でも同様です。

SpikeTrap (Area2D)
 ├── CollisionShape2D
 └── CriticalHitter (Node)
# SpikeTrap.gd
extends Area2D

@onready var critical_hitter: CriticalHitter = $CriticalHitter
var base_damage: float = 2.0

func _on_body_entered(body: Node) -> void:
	if not body.has_method("take_damage"):
		return
	
	var result := critical_hitter.apply_critical(base_damage)
	body.take_damage(result.final_damage)

どのノードも「apply_critical を呼ぶだけ」で統一されているので、後から仕様変更しても安心です。

メリットと応用

メリット

  • ロジックの一元化:クリティカル率や倍率の計算ロジックが1か所にまとまるので、ゲームバランス調整が楽になります。
  • シーン構造がシンプル:プレイヤーや敵のスクリプトから「クリティカル周りのゴチャっとした処理」が消え、攻撃処理が読みやすくなります。
  • 再利用性が高い:プレイヤー、敵、トラップ、弾丸など、攻撃を行うあらゆるノードにそのまま使い回せます。
  • 継承ツリーを汚さないCriticalHitter を継承ベースの「共通攻撃クラス」に押し込める必要がなく、既存のツリーに影響を与えません。

応用(改造案)

例えば、「HPが減るほどクリティカル率が上がるバーサーカー風仕様」にしたい場合、こんな拡張ができます。

## 改造案: HP割合に応じてクリティカル率を動的に変更する例
## attacker_current_hp, attacker_max_hp を渡して使用。
func apply_critical_with_hp(base_damage: float, attacker_current_hp: float, attacker_max_hp: float) -> Dictionary:
	var hp_ratio := clamp(attacker_current_hp / max(attacker_max_hp, 1.0), 0.0, 1.0)
	# HPが減るほどクリティカル率アップ(例: 最低HPで2倍)
	var dynamic_chance := clamp(critical_chance * (2.0 - hp_ratio), 0.0, 1.0)
	
	var is_critical := _rng.randf() < dynamic_chance
	var multiplier := 1.0
	if is_critical:
		multiplier = critical_multiplier
	
	var final_damage := base_damage * multiplier
	
	if is_critical:
		critical_hit.emit(final_damage, base_damage, multiplier)
	
	return {
		"final_damage": final_damage,
		"is_critical": is_critical,
		"multiplier": multiplier,
	}

このように、クリティカルに関する仕様変更や特殊ルールの追加も、コンポーネント内だけで完結します。攻撃側のノードは相変わらず「元ダメージを渡すだけ」で済むので、シーン構造や他のロジックに影響を与えずにどんどん改造していけます。

継承より合成で、クリティカルもスッキリ管理していきましょう。