敵の弾を跳ね返すギミックって、つい「弾のスクリプト側で if 反射板に当たったら…」みたいな条件分岐を増やしがちですよね。さらに「プレイヤーの弾」「敵の弾」「ボス専用弾」など種類が増えると、弾クラスの継承ツリーが太り、条件分岐も地獄化しがちです。

Godot 4 でも、Area2Dbody_entered / area_entered シグナルを直接つないで、「この弾は敵弾か?」「どっちの陣営か?」を都度判定する実装はよく見かけます。ただ、それだと 反射ギミックを増やすたびに弾のコードを触ることになってしまい、レベルデザインのたびにスクリプトを開く羽目になります。

そこで登場するのが、今回のコンポーネント 「Reflector (反射板)」 です。反射板側に「コンポーネント」としてロジックを閉じ込めておき、敵弾の所属の書き換えと反射方向の計算を、継承なし&弾のコード最小限で実現してしまいましょう。

【Godot 4】敵弾を味方に寝返らせろ!「Reflector」コンポーネント

このコンポーネントは、ざっくり言うと:

  • 敵弾(Area2D)が触れたら
  • 弾の「所属(owner / faction など)」を自分側に書き換え
  • 進行方向を反転または反射ベクトルで跳ね返す

という仕事をしてくれる 汎用反射板 です。


前提となる弾(Projectile)インターフェイスについて

Reflector は「どんな弾クラスにも無条件で対応」…とはいきません。
とはいえ、弾側に必要なのは最低限の インターフェイス(約束事) だけにしてあります。

Reflector が期待している「弾」の条件は以下です:

  1. Area2D であること
  2. 以下のメソッドを持っていること(名前だけ合わせればOK)
    • func get_velocity() -> Vector2 現在の移動ベクトルを返す
    • func set_velocity(new_velocity: Vector2) -> void 新しい移動ベクトルをセット
    • func set_owner_faction(faction: String) -> void 弾の所属を設定

この 3 つさえ実装しておけば、弾クラスの実装は自由です。
「継承より合成」らしく、Reflector は「弾の内部実装」を一切知らず、公開されたメソッドだけを叩くようにしています。


Reflector.gd フルコード


extends Area2D
class_name Reflector
"""
Reflector (反射板) コンポーネント
- 敵弾(Area2D)が触れたときに、弾の所属を自分側に変更しつつ、進行方向を反射させる。
- 「弾の実装にベッタリ依存しない」ことを目指したコンポーネント指向デザイン。
"""

@export_group("Basic Settings")
## この反射板が属する陣営名。
## 例: "player", "enemy", "neutral" など。
@export var faction: String = "player"

## 反射時に速度に掛ける倍率。
## 1.0: 速度そのまま / 1.2: ちょっと加速 / 0.8: 減速して返す
@export_range(0.0, 10.0, 0.05)
var speed_multiplier: float = 1.0

## 法線ベクトルを使った「きれいな反射」をするかどうか。
## false の場合は単純にベクトルを反転させるだけ。
@export var use_surface_normal: bool = true

## 法線ベクトルの向き。use_surface_normal = true のときのみ使用。
## 反射板の見た目に合わせて調整してください。
@export var surface_normal: Vector2 = Vector2.LEFT

@export_group("Filter")
## どの陣営の弾を反射対象にするか。
## 空文字列("")の場合は、全ての弾を反射対象にする。
@export var target_faction: String = "enemy"

## 1フレーム内で同じ弾を何度も反射しないためのクールダウン時間(秒)。
@export_range(0.0, 1.0, 0.01)
var reflect_cooldown: float = 0.05

## デバッグログを出すかどうか。
@export var debug_log: bool = false


# 内部状態: 直近で反射した弾を記録しておき、連続反射を防ぐ
var _recently_reflected: Dictionary = {} # { projectile: expire_time }


func _ready() -> void:
    # Area2D のシグナルを自動接続しておく
    area_entered.connect(_on_area_entered)


func _process(delta: float) -> void:
    # クールダウンが切れた弾を辞書から掃除
    if _recently_reflected.is_empty():
        return

    var now := Time.get_ticks_msec() / 1000.0
    for projectile in _recently_reflected.keys():
        if _recently_reflected[projectile] <= now:
            _recently_reflected.erase(projectile)


func _on_area_entered(area: Area2D) -> void:
    # ここで「弾っぽい Area2D かどうか」をゆるく判定する
    if not _is_projectile_candidate(area):
        return

    # クールダウン中なら何もしない
    if _recently_reflected.has(area):
        return

    # 所属チェック: 弾側が get_owner_faction を持っている場合のみ判定
    if target_faction != "":
        if "get_owner_faction" in area:
            var current_faction := area.get_owner_faction()
            if current_faction != target_faction:
                # 対象外の陣営の弾ならスルー
                return

    _reflect_projectile(area)


func _is_projectile_candidate(area: Area2D) -> bool:
    # 必要なメソッド群を持っているかどうかで判定
    var required_methods := [
        "get_velocity",
        "set_velocity",
        "set_owner_faction"
    ]

    for m in required_methods:
        if not (m in area):
            return false

    return true


func _reflect_projectile(projectile: Area2D) -> void:
    # 弾の現在速度を取得
    var v: Vector2 = projectile.get_velocity()
    if v == Vector2.ZERO:
        # 静止している弾は反射させようがないのでスキップ
        return

    var new_velocity := v

    if use_surface_normal:
        # 反射板の法線ベクトルを正規化して使用
        var n := surface_normal.normalized()
        if n == Vector2.ZERO:
            n = Vector2.LEFT
        # 物理的な反射: v' = v - 2 (v・n) n
        new_velocity = v - 2.0 * v.dot(n) * n
    else:
        # 単純反転: 真逆の方向へ
        new_velocity = -v

    # 速度倍率を適用
    new_velocity *= speed_multiplier

    # 弾の所属をこの反射板の faction に変更
    projectile.set_owner_faction(faction)

    # 弾の進行方向を更新
    projectile.set_velocity(new_velocity)

    # 必要であれば弾側に「反射イベント」を通知する(任意実装)
    if "on_reflected" in projectile:
        projectile.on_reflected(self, new_velocity)

    # クールダウン登録
    var now := Time.get_ticks_msec() / 1000.0
    _recently_reflected[projectile] = now + reflect_cooldown

    if debug_log:
        print("[Reflector] projectile=%s reflected. new_velocity=%s, new_faction=%s"
            % [projectile.name, new_velocity, faction])

サンプル: Projectile(敵弾)クラスの最小実装

上の Reflector が動作するための「最低限の弾クラス」の例も載せておきます。
自分のプロジェクトの弾クラスに、同名メソッドを追加してもOKです。


extends Area2D
class_name SimpleProjectile
"""
Reflector と連携するための最低限の弾クラス例。
- get_velocity / set_velocity / set_owner_faction / get_owner_faction を実装。
"""

@export var speed: float = 300.0
@export var initial_velocity: Vector2 = Vector2.RIGHT * 300.0

## 所属陣営。 "enemy" / "player" など。
@export var owner_faction: String = "enemy"

var _velocity: Vector2


func _ready() -> void:
    _velocity = initial_velocity


func _physics_process(delta: float) -> void:
    position += _velocity * delta


# === Reflector から呼ばれるインターフェイス ===

func get_velocity() -> Vector2:
    return _velocity


func set_velocity(new_velocity: Vector2) -> void:
    _velocity = new_velocity


func set_owner_faction(faction: String) -> void:
    owner_faction = faction


func get_owner_faction() -> String:
    return owner_faction


# 任意: Reflector があれば呼んでくれるコールバック
func on_reflected(reflector: Node, new_velocity: Vector2) -> void:
    # 見た目の向きを変えるなど、好きな処理をここに書ける
    rotation = new_velocity.angle()

使い方の手順

手順① Reflector コンポーネントをシーンに追加

まずは、反射板として使いたいノードに Reflector.gd をアタッチします。
壁やシールドなど、Area2D ベースのノードに付けるのが基本です。

ReflectWall (Area2D)
 ├── CollisionShape2D
 └── Reflector (script)
  • ReflectWall はただの Area2D(見た目は任意)
  • 子に CollisionShape2D を置いて当たり判定を作る
  • ReflectWall 自体に Reflector.gd をアタッチする

インスペクタで Reflector のパラメータを設定します:

  • faction: 反射後に弾が属する陣営(例: "player"
  • target_faction: 反射対象にしたい陣営(例: "enemy"
  • use_surface_normal: 物理っぽく反射させたいなら ON
  • surface_normal: 反射板の表面向き(例: 左から右に弾が来るなら Vector2.LEFT

手順② 弾クラスにインターフェイスを実装

すでに自作の弾クラスがある場合は、以下の 4 メソッドを追加するだけで OK です。


func get_velocity() -> Vector2:
    return velocity

func set_velocity(new_velocity: Vector2) -> void:
    velocity = new_velocity

func set_owner_faction(faction: String) -> void:
    owner_faction = faction

func get_owner_faction() -> String:
    return owner_faction

これで Reflector は弾クラスの「中身」を知らずに、公開 API だけを叩く形になります。

手順③ プレイヤーや敵シーンに Reflector を組み込む

例えば、プレイヤーが持つ「反射シールド」として使う場合:

Player (CharacterBody2D)
 ├── Sprite2D
 ├── CollisionShape2D
 ├── ShieldArea (Area2D)
 │    ├── CollisionShape2D
 │    └── Reflector (script)
 └── PlayerController (script)
  • ShieldArea はプレイヤーの前面に位置する Area2D
  • その ShieldAreaReflector.gd をアタッチ
  • faction = "player", target_faction = "enemy" にしておけば、敵弾だけを反射してプレイヤー弾に変換

敵シーンに「跳ね返しバリア」として置く場合も同じノリです:

Enemy (CharacterBody2D)
 ├── Sprite2D
 ├── CollisionShape2D
 ├── ReflectBarrier (Area2D)
 │    ├── CollisionShape2D
 │    └── Reflector (script)
 └── EnemyAI (script)
  • ここでは faction = "enemy", target_faction = "player" に設定
  • プレイヤー弾を跳ね返して敵弾に変換する「カウンター持ちボス」にできます

手順④ 動く床・ギミックシーンでも再利用

Reflector は「ただのコンポーネント」なので、動く床やギミック用シーンにもポン付けできます。

MovingPlatform (Node2D)
 ├── Sprite2D
 ├── CollisionShape2D
 ├── Area2D
 │    ├── CollisionShape2D
 │    └── Reflector (script)
 └── MovingPlatformController (script)
  • 床が移動するロジックは MovingPlatformController に閉じ込める
  • 弾の反射ロジックは Reflector に閉じ込める

こうしておけば、「弾が反射する床」「反射しない床」をシーン単位で簡単に切り替えられます。
床のスクリプトを増やしたり、継承で「ReflectFloor」「NormalFloor」とか作る必要はありません。


メリットと応用

Reflector コンポーネントを導入するメリットはかなりはっきりしています:

  • 弾クラスをこれ以上肥大化させなくていい
    「反射するかどうか」「誰に当たるか」といったロジックを、弾側ではなく 環境側(Reflector) に追い出せます。
  • シーン構造がシンプル
    「弾を反射する壁」「しない壁」を、Reflector を付けるかどうかで切り替えられるので、
    ノード階層はそのまま、コンポーネントの有無だけで挙動を変えられます。
  • レベルデザインが楽
    「この部屋だけ敵弾を反射する」「ボスの第2形態でだけシールドを有効にする」といった
    ギミックを、シーンインスタンスとエクスポート変数の調整だけで実現できます。
  • 継承ツリーからの解放
    BaseProjectileEnemyProjectileReflectableEnemyProjectile
    みたいなツリーを増やさず、「Reflector に対応するためのインターフェイス」だけ実装すればOKです。

コンポーネント指向にしておくと、「弾の挙動」と「反射ギミック」を完全に分離できるので、
どちらか片方を差し替えても、もう片方を壊しにくいのが嬉しいところですね。

簡単な改造案:反射回数に制限をつける

「同じ弾が何度も反射されるとゲームバランスが崩れる」という場合は、弾側に「残り反射回数」を持たせて、Reflector から減らしていくのがおすすめです。

例えば弾クラスにこんなフィールドとメソッドを追加しておき:


# SimpleProjectile.gd 側の例
@export var max_reflect_count: int = 3
var _reflect_count: int = 0

func can_be_reflected() -> bool:
    return _reflect_count < max_reflect_count

func increment_reflect_count() -> void:
    _reflect_count += 1

Reflector 側を少しだけ改造して、反射前にチェック&カウントするようにします:


func _reflect_projectile(projectile: Area2D) -> void:
    # 追加: 反射可能かどうか弾に聞く
    if "can_be_reflected" in projectile and not projectile.can_be_reflected():
        return

    var v: Vector2 = projectile.get_velocity()
    if v == Vector2.ZERO:
        return

    var new_velocity := v
    if use_surface_normal:
        var n := surface_normal.normalized()
        if n == Vector2.ZERO:
            n = Vector2.LEFT
        new_velocity = v - 2.0 * v.dot(n) * n
    else:
        new_velocity = -v

    new_velocity *= speed_multiplier

    projectile.set_owner_faction(faction)
    projectile.set_velocity(new_velocity)

    # 追加: 反射回数を増やす
    if "increment_reflect_count" in projectile:
        projectile.increment_reflect_count()

    if "on_reflected" in projectile:
        projectile.on_reflected(self, new_velocity)

    var now := Time.get_ticks_msec() / 1000.0
    _recently_reflected[projectile] = now + reflect_cooldown

この程度の改造で、「1回だけ跳ね返る弾」「3回までバウンドする弾」などを簡単に作れます。
もちろん Reflector 自体は共通コンポーネントのまま、弾クラスごとに max_reflect_count を変えるだけで挙動を差別化できます。

継承ツリーを増やさず、コンポーネントとシンプルなインターフェイスで機能を積み上げていくと、
後からの拡張やバランス調整がかなり楽になるので、ぜひ Reflector をきっかけに「合成で攻める」設計に寄せてみてください。