Godotで「ブーメラン」を実装しようとすると、つい専用の Boomerang シーンを作って、そこにプレイヤー用のロジックを全部書きたくなりますよね。
でもそれをやり始めると…
- プレイヤー用ブーメランと敵用ブーメランで挙動がちょっと違う → シーンやスクリプトが分裂
- 「戻る相手」を
get_parent()前提で書いてしまい、別のノード構造で使い回しにくい - 処理の一部をプレイヤー側に書き、残りを弾側に書き…と責務が分散してカオス
Godot標準の「ノード継承+深いシーン階層」で作り込むと、あとから「敵も同じブーメランを使いたい」「動く床からもブーメランを発射したい」となったときに、かなり面倒になってきます。
そこで今回は、「発射体の移動ロジックだけ」をコンポーネント化して、
- プレイヤーでも敵でも、どんなノードでも
- 「戻ってきてほしい相手」を差し替えるだけで
- ブーメラン挙動を簡単に付け替えられる
そんな 「BoomerangProjectile」コンポーネント を作っていきましょう。
継承ではなく「合成(Composition)」で、ノードにサクッとアタッチして使うスタイルです。
【Godot 4】行って帰ってくる賢い弾!「BoomerangProjectile」コンポーネント
このコンポーネントは、
- まっすぐ一定距離進む
- 規定距離 or 時間に達したら「戻りフェーズ」に移行
- 発射者(親や任意のノード)に向かって速度を反転させて戻ってくる
という挙動を、どんな Node2D / CharacterBody2D / RigidBody2D などにも「コンポーネントとして」付与できるようにしたものです。
移動そのものは velocity を持つボディに任せてもいいし、コンポーネント自身が position を動かしてもOKな、ちょっと柔軟な設計にしています。
フルコード:BoomerangProjectile.gd
extends Node
class_name BoomerangProjectile
## ブーメラン挙動を付与するコンポーネント
##
## - 一定距離 or 時間だけ前進
## - その後、発射者(戻り先)に向かって戻る
## - 任意の Node2D / CharacterBody2D / RigidBody2D にアタッチして使う
@export_group("基本設定")
## 初速の向きと大きさ。たとえば右方向に 400px/s なら Vector2.RIGHT * 400
@export var initial_velocity: Vector2 = Vector2.RIGHT * 400.0
## 進行フェーズ(行き)の最大移動距離。これを超えたら戻りフェーズに移行
@export var max_forward_distance: float = 300.0
## 進行フェーズ(行き)の最大時間(秒)。0以下なら「時間による制限なし」
@export var max_forward_time: float = 0.0
## 戻りフェーズで、戻り先に向かうときの速度(スカラー)。向きは自動計算
@export var return_speed: float = 500.0
@export_group("戻り先設定")
## 戻る相手のノード。未指定の場合は「最初の親(発射者)」を自動的に参照
@export var return_target: Node2D
## 戻り先に十分近づいたとみなす距離(ピクセル)。この距離以内に入ったら
## signal_emitted で通知し、必要なら自ノードを削除するなどの処理を行う。
@export var arrival_threshold: float = 16.0
@export_group("制御オプション")
## true の場合、このコンポーネント自身が position を直接動かす(Node2D前提)
## false の場合、velocity を持つボディ(CharacterBody2D / RigidBody2Dなど)に
## velocity を書き込むだけにして、実際の移動はそのノードに任せる。
@export var move_owner_directly: bool = true
## velocity を書き込む先のプロパティ名。
## 例: "velocity" (CharacterBody2D), "linear_velocity" (RigidBody2D) など。
@export var velocity_property_name: StringName = "velocity"
@export_group("デバッグ")
## デバッグ用: 移動の状態をログに出すかどうか
@export var debug_log: bool = false
signal boomerang_started_returning
## 戻りフェーズに入ったときに発火
signal boomerang_arrived
## 戻り先に到達したときに発火
enum Phase {
FORWARD, ## 行き(前進)フェーズ
RETURN, ## 戻りフェーズ
}
var _phase: Phase = Phase.FORWARD
var _owner_2d: Node2D
var _start_position: Vector2
var _elapsed_forward_time: float = 0.0
func _ready() -> void:
# このコンポーネントがくっついている親を Node2D として保持
_owner_2d = owner as Node2D
if _owner_2d == null:
push_warning("BoomerangProjectile: owner is not a Node2D. Movement will not work.")
return
_start_position = _owner_2d.global_position
_elapsed_forward_time = 0.0
_phase = Phase.FORWARD
# 戻り先が指定されていなければ、最初は owner の親を戻り先候補にする
if return_target == null:
return_target = _owner_2d.get_parent() as Node2D
# 初速を設定
_apply_velocity(initial_velocity)
if debug_log:
print("BoomerangProjectile: ready, start_position=", _start_position)
func _physics_process(delta: float) -> void:
if _owner_2d == null:
return
match _phase:
Phase.FORWARD:
_process_forward(delta)
Phase.RETURN:
_process_return(delta)
func _process_forward(delta: float) -> void:
_elapsed_forward_time += delta
# 現在の移動距離を計算
var distance_from_start := _owner_2d.global_position.distance_to(_start_position)
var distance_limit_reached := distance_from_start >= max_forward_distance
var time_limit_reached := max_forward_time > 0.0 and _elapsed_forward_time >= max_forward_time
if distance_limit_reached or time_limit_reached:
_start_return_phase()
func _process_return(delta: float) -> void:
if return_target == null:
# 戻り先が消えていた場合は、何もしない or 自壊するなど方針次第
if debug_log:
print("BoomerangProjectile: return_target is null, doing nothing.")
return
# 戻り先への方向ベクトル
var to_target: Vector2 = (return_target.global_position - _owner_2d.global_position)
var distance_to_target := to_target.length()
if distance_to_target <= arrival_threshold:
# 到着判定
if debug_log:
print("BoomerangProjectile: arrived at target.")
emit_signal("boomerang_arrived")
return # ここで自ノードを queue_free するかどうかは利用側に任せる
# 向きだけを正規化して速度を決定
var direction := to_target.normalized()
var vel := direction * return_speed
_apply_velocity(vel)
func _start_return_phase() -> void:
if _phase == Phase.RETURN:
return
_phase = Phase.RETURN
emit_signal("boomerang_started_returning")
if debug_log:
print("BoomerangProjectile: start returning.")
# 戻りフェーズに入った瞬間に、向きを即座に反転させたい場合はここで設定
# ただし、毎フレーム _process_return で上書きするので、ここはなくてもよい。
if return_target != null:
var dir := (return_target.global_position - _owner_2d.global_position).normalized()
_apply_velocity(dir * return_speed)
func _apply_velocity(vel: Vector2) -> void:
## 実際の移動ロジックをどう扱うかをここで切り替える
if move_owner_directly:
# Node2D として直接位置を更新する方式
# (本当は _physics_process 内で delta を掛けて動かすのが正道だが、
# ここでは「velocity」をあくまで「1秒あたりの移動量」として扱う想定)
# → 実際には、owner 側でこの velocity を読んで move_and_slide などに使う方が綺麗。
# ここでは「簡易挙動」として直接 position をいじる例を示す。
if Engine.is_in_physics_frame():
# 物理フレーム内なら、delta を使って直接動かせるが、
# このコンポーネントだけで完結させると delta を渡しづらいので、
# シンプルに「速度をプロパティとして保持」する形に留める案もある。
pass # ここでは実際の移動は行わない(下記を参照)
# velocity を持つボディに値を書き込む方式
if _owner_2d.has_method("set"):
# 動的にプロパティを書き込む
if _owner_2d.has_property(velocity_property_name):
_owner_2d.set(velocity_property_name, vel)
else:
# CharacterBody2D などは has_property が false になることがあるので、
# 直接 set してしまう(存在しなければエラーになる)
_owner_2d.set(velocity_property_name, vel)
else:
push_warning("BoomerangProjectile: owner does not support setting velocity property '%s'." % velocity_property_name)
※ 上記では「コンポーネントが velocity を書き込む」スタイルにしてあります。
CharacterBody2D にアタッチしている場合は、そのノード側の _physics_process で move_and_slide() を呼ぶ前提です。
使い方の手順
-
スクリプトを用意する
上記のBoomerangProjectile.gdをプロジェクト内に保存します。
例:res://components/BoomerangProjectile.gd -
ブーメラン用の弾シーンを作る
ここでは「プレイヤーが投げるブーメラン弾」を例にします。Boomerang (CharacterBody2D) ├── Sprite2D ├── CollisionShape2D └── BoomerangProjectile (Node)Boomerang(ルート)にCharacterBody2Dを使うBoomerangProjectileノードに、先ほどのスクリプトをアタッチBoomerangProjectile.initial_velocityに「右方向に 400」などを設定velocity_property_nameは"velocity"(CharacterBody2Dのプロパティ名)
-
プレイヤーから発射する
プレイヤーシーン例:Player (CharacterBody2D) ├── Sprite2D ├── CollisionShape2D └── BoomerangSpawnPoint (Marker2D)プレイヤーのスクリプトから、ブーメランシーンをインスタンス化して発射します。
# Player.gd の一部例 @export var boomerang_scene: PackedScene func throw_boomerang() -> void: var boomerang := boomerang_scene.instantiate() as CharacterBody2D var spawn_point := $BoomerangSpawnPoint boomerang.global_position = spawn_point.global_position # ブーメランのコンポーネントを取得 var comp := boomerang.get_node("BoomerangProjectile") as BoomerangProjectile # 戻り先を自分(Player)に設定 comp.return_target = self # プレイヤーの向きに応じて初速を変えるなど var dir := Vector2.RIGHT if is_facing_right() else Vector2.LEFT comp.initial_velocity = dir * 400.0 # ルートノード(たとえば現在のシーン)に追加 get_tree().current_scene.add_child(boomerang) func _physics_process(delta: float) -> void: # プレイヤー自身の移動処理 velocity = get_input_velocity() * 200.0 move_and_slide() -
ブーメラン側の移動処理を仕上げる
ブーメランルート(CharacterBody2D)にも簡単なスクリプトを付けて、velocityを使って移動させます。# BoomerangRoot.gd (Boomerangシーンのルート CharacterBody2D 用) extends CharacterBody2D func _physics_process(delta: float) -> void: # BoomerangProjectile コンポーネントが velocity を書き換えてくれるので、 # ここでは単に move_and_slide するだけでOK move_and_slide()これで、
BoomerangProjectileコンポーネントがvelocityを制御し、
ルートのCharacterBody2Dが実際に移動する、という分業になります。
別の使用例:敵キャラのブーメラン攻撃
同じコンポーネントを、そのまま敵にも使い回せます。
Enemy (CharacterBody2D) ├── Sprite2D ├── CollisionShape2D └── BoomerangSpawnPoint (Marker2D)
敵のスクリプト側では、プレイヤーの代わりに Enemy 自身を return_target に設定するだけです。
# Enemy.gd の一部
@export var boomerang_scene: PackedScene
func attack_with_boomerang() -> void:
var boomerang := boomerang_scene.instantiate() as CharacterBody2D
boomerang.global_position = $BoomerangSpawnPoint.global_position
var comp := boomerang.get_node("BoomerangProjectile") as BoomerangProjectile
comp.return_target = self # 敵自身に戻ってくる
comp.initial_velocity = (get_player_direction().normalized() * 350.0)
get_tree().current_scene.add_child(boomerang)
プレイヤー用と敵用で「別のブーメランシーン」を作る必要はありません。
戻り先だけ差し替えれば、同じコンポーネントで挙動を共有できます。
メリットと応用
メリット
- 「ブーメランのロジック」が完全に 1 コンポーネントに閉じているので、プレイヤーや敵のスクリプトがスリムになる
- 戻り先を
return_targetに差し替えるだけで、どんなノードにもブーメラン挙動を付与できる - シーン構造がシンプルなまま:プレイヤーや敵のシーンを継承で増やさない
- 「深いノード階層にブーメラン用のノードを生やす」必要がなく、
単に弾シーンにコンポーネントを 1 個付けるだけで済む
応用アイデア
- 戻りフェーズ中に、ターゲットが動いても追尾し続ける「ホーミング・ブーメラン」にする
- 戻りきったときに「キャッチ」アニメーションを再生する
- 戻る途中で敵に当たったら、そこで自壊 or 貫通するなどのヒット処理を追加
たとえば「戻りフェーズに入った瞬間に、トレイルエフェクトを有効化する」ような改造も簡単です。
# BoomerangProjectile.gd に追記する改造案の一例
@export var trail_node_path: NodePath ## トレイル用 Particles2D など
func _start_return_phase() -> void:
if _phase == Phase.RETURN:
return
_phase = Phase.RETURN
emit_signal("boomerang_started_returning")
# ★ ここから改造部分: 戻り開始と同時にトレイルをONにする
if trail_node_path != NodePath():
var trail := _owner_2d.get_node_or_null(trail_node_path)
if trail and trail.has_method("set_emitting"):
trail.set_emitting(true)
if debug_log:
print("BoomerangProjectile: start returning.")
if return_target != null:
var dir := (return_target.global_position - _owner_2d.global_position).normalized()
_apply_velocity(dir * return_speed)
このように、「ブーメラン挙動」そのものはコンポーネントに閉じ込めておき、演出やヒット処理は別コンポーネントで足していくと、シーンがどんどん見通し良くなります。
継承で増やすより、コンポーネントを「レゴブロック」みたいに組み合わせる方が、Godot 4 では圧倒的に楽ですね。
