Godot 4のコンポーネント指向開発シリーズ、今回はアクションゲームの手触りを劇的に変える**「KnockbackReceiver (ノックバック受付)」**です。

なぜ「ノックバック」が必要なのか?(効果の説明)

単に「HPを減らす」だけでは、ゲームはただの数字の削り合いになってしまいます。このコンポーネントを導入することで、以下の3つの重要な効果が生まれます。

  1. 「重み」と「痛み」の表現 (Game Feel)
    • 攻撃を受けた瞬間にキャラクターが弾き飛ばされることで、「痛そう!」「強い攻撃を食らった!」という感覚が視覚と操作感の両方でプレイヤーに伝わります。
  2. ピンチの演出 (Risk Management)
    • 操作不能(スタン)状態で後ろに飛ばされるため、「穴に落ちるかもしれない」「連続で攻撃されるかもしれない」という緊張感が生まれます。
  3. 戦闘の駆け引き (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)」と連携して、プレイヤーの自由を奪う点にあります。

手順①:プレイヤーへのアタッチ

  1. プレイヤー(CharacterBody2D)の子ノードとして KnockbackReceiver を追加します。
  2. 同階層に、移動用の KeyboardMover(第1回で作成したもの)があるとします。

シーン構成図:

Player (CharacterBody2D)
 ├── Sprite2D
 ├── CollisionShape2D
 ├── KeyboardMover (移動担当)
 └── KnockbackReceiver (被ダメージ・衝撃担当) <-- これを追加

手順②:シグナルで入力を無効化する(重要!)

ノックバック中はプレイヤーがキー入力で動けないようにする必要があります。

Godotのエディタ上で、シグナルを接続します。

  1. シーンドックで KnockbackReceiver を選択。
  2. 右側の「ノード」タブ > stun_changed(is_stunned) シグナルをダブルクリック。
  3. 接続先に KeyboardMover を選びます。
  4. 「高度な設定 (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.gdapply_knockback 関数内に、色を変える処理を一行足すだけで、ダメージ演出になります。

func apply_knockback(force, duration):
    # ... (前略)
    _parent.modulate = Color.RED # 赤くする
    await get_tree().create_timer(duration).timeout
    _parent.modulate = Color.WHITE # 元に戻す
    # ... (後略)

アニメーションを変える

同様に、スタン開始時に AnimationPlayer.play("hurt") を呼び出せば、「痛がるポーズ」をとりながら吹き飛ぶことができます。

このように、「動かす処理」と「動かされる処理」を別のコンポーネントに分けることで、複雑なアクションゲームの制御が驚くほど整理されます。