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.gdをres://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 = trueduration = 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コンポーネント」などと組み合わせていくと、継承に頼らない柔軟なキャラクター設計ができるようになります。
ぜひ自分のプロジェクト流にカスタマイズしてみてください。
