敵の弾を跳ね返すギミックって、つい「弾のスクリプト側で if 反射板に当たったら…」みたいな条件分岐を増やしがちですよね。さらに「プレイヤーの弾」「敵の弾」「ボス専用弾」など種類が増えると、弾クラスの継承ツリーが太り、条件分岐も地獄化しがちです。
Godot 4 でも、Area2D の body_entered / area_entered シグナルを直接つないで、「この弾は敵弾か?」「どっちの陣営か?」を都度判定する実装はよく見かけます。ただ、それだと 反射ギミックを増やすたびに弾のコードを触ることになってしまい、レベルデザインのたびにスクリプトを開く羽目になります。
そこで登場するのが、今回のコンポーネント 「Reflector (反射板)」 です。反射板側に「コンポーネント」としてロジックを閉じ込めておき、敵弾の所属の書き換えと反射方向の計算を、継承なし&弾のコード最小限で実現してしまいましょう。
【Godot 4】敵弾を味方に寝返らせろ!「Reflector」コンポーネント
このコンポーネントは、ざっくり言うと:
- 敵弾(
Area2D)が触れたら - 弾の「所属(owner / faction など)」を自分側に書き換え
- 進行方向を反転または反射ベクトルで跳ね返す
という仕事をしてくれる 汎用反射板 です。
前提となる弾(Projectile)インターフェイスについて
Reflector は「どんな弾クラスにも無条件で対応」…とはいきません。
とはいえ、弾側に必要なのは最低限の インターフェイス(約束事) だけにしてあります。
Reflector が期待している「弾」の条件は以下です:
Area2Dであること- 以下のメソッドを持っていること(名前だけ合わせれば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- その
ShieldAreaにReflector.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形態でだけシールドを有効にする」といった
ギミックを、シーンインスタンスとエクスポート変数の調整だけで実現できます。 - 継承ツリーからの解放
BaseProjectile→EnemyProjectile→ReflectableEnemyProjectile…
みたいなツリーを増やさず、「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 をきっかけに「合成で攻める」設計に寄せてみてください。
