Godotでアクションゲームを作っていると、プレイヤーやボスに「スーパーアーマー」を付けたくなる瞬間ってありますよね。「ダメージは受けるけど、ひるまない・吹き飛ばない」あれです。

素直に実装しようとすると、こんな感じになりがちです:

  • プレイヤー / 敵のベースクラスに has_super_armor フラグを追加
  • 各種攻撃・ヒット処理で「もしスーパーアーマーならノックバックしない」分岐を追加
  • さらに状態遷移(State Machine)にも「スーパーアーマー中はこのステートに遷移しない」など条件が増える

結果として…

  • ベースクラスがどんどん肥大化する
  • 「このキャラだけスーパーアーマー仕様を変えたい」と思っても、継承ツリーのどこかをいじらないといけない
  • ノックバック周りのコードがあちこちに散らばる

そこで「継承より合成」です。
ノードに「スーパーアーマー」という独立コンポーネントをアタッチして、KnockbackReceiver の挙動だけをピンポイントで無効化・制御してしまいましょう。

【Godot 4】吹き飛びだけ完封!「SuperArmor」コンポーネント

このコンポーネントの役割はシンプルです:

  • ダメージ処理は一切いじらない(HPはちゃんと減る)
  • KnockbackReceiver がノックバックしようとしたときだけ、それをキャンセル or 軽減する
  • 一時的にON/OFFできる(スキル中だけスーパーアーマー、など)

前提: KnockbackReceiver との連携方針

今回は「コンポーネント同士でゆるく連携する」方針にします。
KnockbackReceiver 側が以下のようなシグナルを持っている想定です:

# 例: KnockbackReceiver 側のシグナル(参考)
signal knockback_requested(direction: Vector2, strength: float)

SuperArmor はこのシグナルを受け取り、必要ならノックバックを止めるだけ。
実際の移動処理は KnockbackReceiver に任せます。


GDScript フルコード: SuperArmor.gd

extends Node
class_name SuperArmor
## SuperArmor コンポーネント
## - ダメージはそのまま
## - KnockbackReceiver からのノックバックだけを無効化 / 軽減する
##
## 想定:
## - 同じノード配下に KnockbackReceiver コンポーネントが存在し、
##   そこから `knockback_requested(direction, strength)` シグナルが emit される。

## 有効 / 無効のトグル
@export var enabled: bool = true:
	set(value):
		enabled = value
		_update_debug_name()

## ノックバックを完全無効化するかどうか
## true  : 一切吹き飛ばない
## false : scale_knockback_rate に応じて軽減
@export var disable_knockback: bool = true

## ノックバックを軽減する倍率(0.0〜1.0 推奨)
## 例: 0.3 なら、ノックバック距離を 30% に縮める
@export_range(0.0, 2.0, 0.05) var scale_knockback_rate: float = 0.0

## 一時的なスーパーアーマー時間(秒)
## 0 の場合は「時間制限なしの常時スーパーアーマー」として扱う
@export_range(0.0, 60.0, 0.1) var duration: float = 0.0

## デバッグ用: スーパーアーマー中かどうかをエディタ上で確認する
@export var show_debug_label: bool = true

## 内部状態
var _time_left: float = 0.0
var _knockback_receiver: Node = null
var _debug_label: Label = null


func _ready() -> void:
	# 同じ親ノード内から KnockbackReceiver を探す
	# (構成に合わせてパスを変えてもOK)
	_knockback_receiver = _find_knockback_receiver()
	if _knockback_receiver:
		# KnockbackReceiver からのノックバック要求をフックする
		if _knockback_receiver.has_signal("knockback_requested"):
			_knockback_receiver.connect(
				"knockback_requested",
				Callable(self, "_on_knockback_requested")
			)
		else:
			push_warning("Found KnockbackReceiver but it has no 'knockback_requested' signal.")
	else:
		push_warning("SuperArmor: KnockbackReceiver not found. This component will do nothing.")

	# 時間制限付きの場合、開始時点でカウントをセット
	if duration > 0.0 and enabled:
		_time_left = duration

	# デバッグラベルを作成(任意)
	if show_debug_label:
		_create_debug_label()

	_update_debug_name()


func _process(delta: float) -> void:
	# 時間制限付きスーパーアーマーの残り時間を減らす
	if enabled and duration > 0.0:
		_time_left -= delta
		if _time_left <= 0.0:
			enabled = false
			_time_left = 0.0

	# デバッグラベル更新
	if _debug_label:
		var text := ""
		if enabled:
			if duration > 0.0:
				text = "SA: ON (%.2f)" % _time_left
			else:
				text = "SA: ON"
		else:
			text = "SA: OFF"
		_debug_label.text = text


func _find_knockback_receiver() -> Node:
	## 親ノード配下から KnockbackReceiver らしきノードを探す
	## - 同じシーン内でコンポーネントを並べて使う想定
	if not owner:
		return null

	for child in owner.get_children():
		if child == self:
			continue
		# 名前かクラス名でそれっぽいものを探す
		if "KnockbackReceiver" in child.name or child.get_class() == "KnockbackReceiver":
			return child
	return null


func _on_knockback_requested(direction: Vector2, strength: float) -> void:
	## KnockbackReceiver からノックバック要求が来たときに呼ばれる
	## - enabled なら、ノックバックを止める or 軽減する
	## - 実際の移動処理は KnockbackReceiver 側で行う前提
	if not enabled:
		return

	if disable_knockback:
		# 完全に吹き飛びを無効化する
		# ここでは KnockbackReceiver 側に「0で実行して」と伝える形にする
		if _knockback_receiver and _knockback_receiver.has_method("apply_knockback_override"):
			_knockback_receiver.apply_knockback_override(direction, 0.0)
		# もし override メソッドがない場合は、シグナル接続順や処理順で
		# KnockbackReceiver 側が「0なら無視する」実装にしておきましょう
		return

	# 軽減モード
	var new_strength := strength * scale_knockback_rate
	if _knockback_receiver and _knockback_receiver.has_method("apply_knockback_override"):
		_knockback_receiver.apply_knockback_override(direction, new_strength)


func activate(duration_sec: float = -1.0) -> void:
	## 外部からスーパーアーマーをONにするためのヘルパー
	## duration_sec >= 0 の場合、その時間だけ有効にする
	enabled = true
	if duration_sec >= 0.0:
		duration = duration_sec
	if duration > 0.0:
		_time_left = duration


func deactivate() -> void:
	## スーパーアーマーをOFFにする
	enabled = false
	_time_left = 0.0


func _create_debug_label() -> void:
	## 親ノードに小さなラベルを生やして状態を表示する
	if not owner:
		return
	_debug_label = Label.new()
	_debug_label.name = "SuperArmorDebugLabel"
	_debug_label.modulate = Color(0.2, 1.0, 0.2, 0.8)
	_debug_label.scale = Vector2(0.6, 0.6)
	_debug_label.z_index = 9999
	owner.add_child(_debug_label)
	_debug_label.owner = owner


func _update_debug_name() -> void:
	## エディタのインスペクタで分かりやすくするための名前変更
	if not is_inside_tree():
		return
	var suffix := enabled ? "[SA:ON]" : "[SA:OFF]"
	name = "SuperArmor " + suffix

KnockbackReceiver 側の参考実装

連携のイメージが掴みやすいように、最低限の KnockbackReceiver 例も載せておきます。
(すでに自作している場合は、シグナル名と apply_knockback_override の部分だけ合わせればOKです。)

extends Node
class_name KnockbackReceiver

signal knockback_requested(direction: Vector2, strength: float)

## 実際に適用されるノックバック強さ
var _current_knockback: Vector2 = Vector2.ZERO

func request_knockback(direction: Vector2, strength: float) -> void:
	## 攻撃側などから呼ばれる入口
	emit_signal("knockback_requested", direction, strength)
	# SuperArmor などから override されていなければ、そのまま適用
	if _current_knockback == Vector2.ZERO:
		_current_knockback = direction.normalized() * strength


func apply_knockback_override(direction: Vector2, strength: float) -> void:
	## SuperArmor からの上書き用
	if strength <= 0.0:
		_current_knockback = Vector2.ZERO
	else:
		_current_knockback = direction.normalized() * strength


func apply_to_body(body: CharacterBody2D, delta: float) -> void:
	## CharacterBody2D などに対してノックバックを適用する処理
	if _current_knockback == Vector2.ZERO:
		return
	body.velocity += _current_knockback
	# フレーム毎に減衰させるなどの処理はお好みで
	_current_knockback = _current_knockback.move_toward(Vector2.ZERO, 200.0 * delta)

使い方の手順

手順①: コンポーネントスクリプトを用意する

  • SuperArmor.gdres://components/SuperArmor.gd などに保存
  • KnockbackReceiver.gd も同様に保存

手順②: プレイヤーにアタッチする例

プレイヤーシーンの構成例:

Player (CharacterBody2D)
 ├── Sprite2D
 ├── CollisionShape2D
 ├── KnockbackReceiver (Node)
 └── SuperArmor (Node)

ポイント:

  • Player 本体は「移動」「攻撃」などに集中させる
  • ノックバック関連は KnockbackReceiver に丸投げ
  • スーパーアーマーは SuperArmor に丸投げ

プレイヤーのスクリプト側では、ノックバックはこう扱うだけでOKです:

# Player.gd(抜粋)
@onready var knockback_receiver: KnockbackReceiver = $KnockbackReceiver

func _physics_process(delta: float) -> void:
	# 通常移動処理 ...
	# ノックバックを適用
	knockback_receiver.apply_to_body(self, delta)
	move_and_slide()

手順③: 攻撃ヒット時にノックバックをリクエストする

# 例: SwordHitbox.gd など
func _on_body_entered(body: Node) -> void:
	if not body.has_node("KnockbackReceiver"):
		return
	var receiver: KnockbackReceiver = body.get_node("KnockbackReceiver")
	var dir := (body.global_position - global_position).normalized()
	var strength := 400.0
	receiver.request_knockback(dir, strength)
	# ダメージ処理は別コンポーネント(Health など)に任せるとさらに綺麗です

このとき、SuperArmor が有効であれば、request_knockback が呼ばれてもノックバックはキャンセル or 軽減されます。
ダメージは別のコンポーネント(Health など)で処理するので、HP はちゃんと減る、という状態が作れます。

手順④: スキル中だけスーパーアーマーをONにする

プレイヤーのスクリプトから、スキル発動時に以下のように呼び出します:

# Player.gd(抜粋)
@onready var super_armor: SuperArmor = $SuperArmor

func use_special_attack() -> void:
	# 3秒間だけスーパーアーマー
	super_armor.activate(3.0)
	# 攻撃アニメーション再生など
	$AnimationPlayer.play("special_attack")

ボス敵に常時スーパーアーマーを付けたい場合は、SuperArmor のインスペクタで:

  • enabled = true
  • duration = 0(無制限)
  • disable_knockback = true(完全無効)

と設定しておけばOKです。


メリットと応用

  • シーン構造がスッキリ
    スーパーアーマーの有無をプレイヤー/敵のベースクラスに埋め込まないので、クラス階層が汚れません。
  • 「吹き飛び仕様」だけを差し替え可能
    ある敵は「軽く吹き飛ぶ」、別の敵は「ほぼ動かない」など、SuperArmor のパラメータだけで調整できます。
  • テストがしやすい
    SuperArmor ノードを一時的に削除 or 無効化すれば、通常ノックバック挙動のテストがすぐできます。
  • レベルデザインが楽
    「このステージの敵は全員スーパーアーマー付きにしよう」など、シーンインスタンスにコンポーネントをポン付けするだけでOK。

コンポーネント指向の良さは、「機能を1ノードに閉じ込めて、必要なときだけアタッチする」ことです。
スーパーアーマーのような「一部のキャラだけが持つ特殊能力」は、まさにコンポーネント向きですね。

改造案: HP が一定以下になったら自動でスーパーアーマーON

例えば、ボスの残りHPが30%を切ったら自動でスーパーアーマーを付与する、といった演出も簡単に追加できます。
以下は SuperArmor.gd に追加できるちょっとした改造例です:

## 例: 外部の Health コンポーネントから呼ばれる想定
func on_health_changed(current: float, max_health: float) -> void:
	var ratio := current / max_health
	if ratio <= 0.3 and not enabled:
		# 残り30%以下になったら、永続スーパーアーマーON
		activate()
	elif ratio > 0.3 and enabled and duration == 0.0:
		# 30%を上回ったらOFF(好みに応じて)
		deactivate()

こんなふうに、SuperArmor を「HPコンポーネント」「ステートマシン」「AIコンポーネント」などと組み合わせていくと、継承に頼らない柔軟なキャラクター設計ができるようになります。
ぜひ自分のプロジェクト流にカスタマイズしてみてください。