Godot 4のコンポーネント指向開発シリーズ、今回はアクションゲームの手触りを劇的に変える**「KnockbackReceiver (ノックバック受付)」**です。
なぜ「ノックバック」が必要なのか?(効果の説明)
単に「HPを減らす」だけでは、ゲームはただの数字の削り合いになってしまいます。このコンポーネントを導入することで、以下の3つの重要な効果が生まれます。
- 「重み」と「痛み」の表現 (Game Feel)
- 攻撃を受けた瞬間にキャラクターが弾き飛ばされることで、「痛そう!」「強い攻撃を食らった!」という感覚が視覚と操作感の両方でプレイヤーに伝わります。
- ピンチの演出 (Risk Management)
- 操作不能(スタン)状態で後ろに飛ばされるため、「穴に落ちるかもしれない」「連続で攻撃されるかもしれない」という緊張感が生まれます。
- 戦闘の駆け引き (Spacing)
- 敵を突き放して距離を取ったり、逆に敵に壁際に追い詰められたりと、位置取りの戦略性が生まれます。
このコンポーネントは、**「外部からの衝撃を受け取り、親の制御を奪って吹き飛ばす」**という役割を一手に引き受けます。
このコンポーネントは、外部(敵や罠)から関数を呼び出されると、指定された時間だけ**「スタン状態(操作不能)」**に移行し、物理演算に従って親ノードを弾き飛ばします。
1. コンポーネントのコード (Full Code)
以下のコードをコピーして、KnockbackReceiver.gd という名前で保存してください。
GDScript
class_name KnockbackReceiver
extends Node
## 外部からの衝撃(ノックバック)を受け付け、親を弾き飛ばすコンポーネント
## 親ノードは CharacterBody2D を想定しています。
# --- シグナル ---
# スタン状態が変化した時に通知(入力コンポーネントのON/OFFに使用)
signal stun_changed(is_stunned: bool)
# --- 設定パラメータ ---
@export var friction: float = 1000.0 ## ノックバック後の減速摩擦力
# --- 内部変数 ---
var _parent: CharacterBody2D
var _knockback_velocity: Vector2 = Vector2.ZERO
var _is_stunned: bool = false
func _ready() -> void:
_parent = get_parent() as CharacterBody2D
if not _parent:
push_error("KnockbackReceiver: 親が CharacterBody2D ではありません。")
set_physics_process(false)
func _physics_process(delta: float) -> void:
# スタン中でなければ何もしない
if not _is_stunned:
return
# ノックバック速度を親に適用
_parent.velocity = _knockback_velocity
_parent.move_and_slide()
# 摩擦で徐々に減速させる(地面をズザザ…と滑る動き)
_knockback_velocity = _knockback_velocity.move_toward(Vector2.ZERO, friction * delta)
## 外部からこの関数を呼ぶことでノックバックが発生します
## force_vector: 吹き飛ばす方向と強さ (例: Vector2(200, -200))
## duration: 操作不能になる時間 (秒)
func apply_knockback(force_vector: Vector2, duration: float = 0.2) -> void:
# 1. スタン開始
_is_stunned = true
_knockback_velocity = force_vector
stun_changed.emit(true) # 他のコンポーネントに入力禁止を通知
# 2. 指定時間待つ
# タイマーノードを作らず、非同期処理で待機
await get_tree().create_timer(duration).timeout
# 3. スタン解除
_is_stunned = false
stun_changed.emit(false) # 入力許可を通知
2. 使い方チュートリアル
このコンポーネントの真価は、「操作コンポーネント(KeyboardMover)」と連携して、プレイヤーの自由を奪う点にあります。
手順①:プレイヤーへのアタッチ
- プレイヤー(
CharacterBody2D)の子ノードとしてKnockbackReceiverを追加します。 - 同階層に、移動用の
KeyboardMover(第1回で作成したもの)があるとします。
シーン構成図:
Player (CharacterBody2D)
├── Sprite2D
├── CollisionShape2D
├── KeyboardMover (移動担当)
└── KnockbackReceiver (被ダメージ・衝撃担当) <-- これを追加
手順②:シグナルで入力を無効化する(重要!)
ノックバック中はプレイヤーがキー入力で動けないようにする必要があります。
Godotのエディタ上で、シグナルを接続します。
- シーンドックで
KnockbackReceiverを選択。 - 右側の「ノード」タブ >
stun_changed(is_stunned)シグナルをダブルクリック。 - 接続先に
KeyboardMoverを選びます。 - 「高度な設定 (Advanced)」 を開き、メソッド名に
set_physics_processと入力します(※)。- 解説: これにより、
stun_changed(true)が来るとKeyboardMoverの物理処理が停止し、移動できなくなります。falseが来ると再開します。
- 解説: これにより、
※注意: もしシグナル接続画面で
set_physics_processが候補に出ない、または難しい場合は、KeyboardMover.gdに以下の小さな関数を追加して、そこに接続してください。# KeyboardMover.gd に追記 func set_active(is_stunned: bool) -> void: # スタン中(true)なら処理停止(false)、スタン解除(false)なら処理再開(true) set_physics_process(not is_stunned)
手順③:敵や罠から衝撃を与える
これで準備完了です。あとは「誰か」がこのコンポーネントを叩く必要があります。
例えば、「トゲの罠」のスクリプトは以下のようになります。
# Trap.gd (トゲ罠の例)
extends Area2D
func _on_body_entered(body: Node2D):
# ぶつかった相手が KnockbackReceiver を持っているか探す
var knocker = body.get_node_or_null("KnockbackReceiver")
if knocker:
# プレイヤーから見て「罠の反対側」へ弾き飛ばす計算
# (body.position - global_position) で「罠→プレイヤー」の向きになる
var direction = (body.global_position - global_position).normalized()
var power = 500.0 # 吹き飛ばす強さ
# 関数呼び出し! (0.4秒間 操作不能にする)
knocker.apply_knockback(direction * power, 0.4)
手順④:実行
トゲ罠を作り、プレイヤーで突っ込んでみてください。
**「操作が一瞬効かなくなり、後ろへズザーッと弾き飛ばされる」**挙動になれば成功です!
3. 応用:演出の強化
この KnockbackReceiver は、「ダメージを受けた状態」の管理ハブとして非常に優秀です。
色を変える(被ダメージ点滅)
KnockbackReceiver.gd の apply_knockback 関数内に、色を変える処理を一行足すだけで、ダメージ演出になります。
func apply_knockback(force, duration):
# ... (前略)
_parent.modulate = Color.RED # 赤くする
await get_tree().create_timer(duration).timeout
_parent.modulate = Color.WHITE # 元に戻す
# ... (後略)
アニメーションを変える
同様に、スタン開始時に AnimationPlayer.play("hurt") を呼び出せば、「痛がるポーズ」をとりながら吹き飛ぶことができます。
このように、「動かす処理」と「動かされる処理」を別のコンポーネントに分けることで、複雑なアクションゲームの制御が驚くほど整理されます。
