Godot で「何かの周りをぐるぐる回す」動きって、ついそのノード専用のスクリプトを書いてしまいがちですよね。
- プレイヤーの周りを回るオプション武器
- ボスの周りを回るシールド
- 惑星の周りを回る衛星
こういうのを毎回それぞれのシーンにベタ書きしていくと、
- 「この敵だけちょっと半径を変えたい」→ コピペ地獄
- 「回転スピードを全部まとめて調整したい」→ 各シーンを開いてチマチマ修正
- 「やっぱりターゲットの追従方式を変えたい」→ 全部のスクリプトを書き換え…
と、典型的な「継承+ベタ書き」のつらさが出てきます。
そこで今回は、どんなノードにもポン付けできる「周回移動コンポーネント」を用意して、
- ターゲットは
@exportでインスペクタから指定 - 半径・速度・開始角度・回転方向も全部
@exportで調整 - ローカル座標/グローバル座標の切り替えもワンタッチ
といった「合成(Composition)スタイル」で、周回移動だけを独立コンポーネントにしてしまいましょう。
【Godot 4】ターゲットの周りをクルクル周回!「OrbitMovement」コンポーネント
フルコード(GDScript / Godot 4)
extends Node
class_name OrbitMovement
## ターゲットの周囲を円運動で周回させるコンポーネント
##
## どんな2Dノードにもアタッチでき、指定したターゲットの周りを
## ぐるぐる回り続けます。
##
## 惑星の衛星、プレイヤーのオプション、ボスの周回シールドなどにどうぞ。
@export_group("Orbit Settings")
## 周回の中心となるターゲットノード
## Node2D系(Node2D, CharacterBody2D, Area2D など)を想定
@export var target: Node2D
## 周回する半径(ピクセル)
@export var radius: float = 64.0
## 角速度(ラジアン/秒)
## 例: 2 * PI で 1秒で一周
@export var angular_speed: float = PI
## 初期角度(ラジアン)
## 0 ならターゲットの右側からスタート
@export var start_angle: float = 0.0
## 回転方向を反転するかどうか
## true にすると反時計回り → 時計回りに反転
@export var reverse_direction: bool = false
@export_group("Coordinate Mode")
## ターゲットのローカル座標系で周回させるかどうか
## true: target のローカル座標に追従(親ごと動いても相対位置を維持)
## false: グローバル座標で周回(ワールド固定の円軌道)
@export var use_local_space: bool = true
@export_group("Debug")
## デバッグ用: エディタ上で円軌道のガイドを表示するか
@export var draw_gizmo: bool = true
## 現在の角度(ラジアン)
var _angle: float
## ターゲットの初期基準位置(ローカル or グローバル)
var _origin_position: Vector2
func _ready() -> void:
# 初期角度を設定
_angle = start_angle
if not target:
push_warning("OrbitMovement: target が設定されていません。インスペクタで設定してください。")
return
# ターゲットの基準位置を保存
if use_local_space:
_origin_position = target.position
else:
_origin_position = target.global_position
# 初回位置を即座に更新
_update_orbit_position(0.0)
func _process(delta: float) -> void:
if not target:
return
# 角度を進める
var direction := reverse_direction ? -1.0 : 1.0
_angle += angular_speed * direction * delta
# 角度を 0〜2π の範囲に正規化(オーバーフロー防止)
_angle = wrapf(_angle, 0.0, TAU)
_update_orbit_position(delta)
func _update_orbit_position(delta: float) -> void:
# ターゲットの現在の基準位置を取得
var center: Vector2 = use_local_space \
? target.position \
: target.global_position
# 円軌道上のオフセットを計算
var offset := Vector2(
cos(_angle) * radius,
sin(_angle) * radius
)
# 最終的な位置を決定
var orbit_pos := center + offset
# このコンポーネントを付けたノードを周回させる
var node2d := _get_owner_as_node2d()
if node2d:
if use_local_space:
# target と同じ親を持っている前提のローカル座標周回
node2d.position = orbit_pos
else:
node2d.global_position = orbit_pos
else:
push_warning("OrbitMovement: 親ノードが Node2D 系ではありません。周回移動できません。")
func _get_owner_as_node2d() -> Node2D:
# このコンポーネントの「親」を周回させたいので、親を取得
# (シーン構成図で OrbitMovement を子ノードとして付ける想定)
var parent := get_parent()
if parent is Node2D:
return parent
return null
func _draw() -> void:
# エディタ上でのガイド表示
if Engine.is_editor_hint() and draw_gizmo and target:
var center: Vector2 = use_local_space \
? target.position \
: target.global_position
# このノードから見たローカル座標に変換
var local_center := (get_parent() as Node2D).to_local(center) if get_parent() is Node2D else center
draw_circle(local_center, radius, Color(0.2, 0.8, 1.0, 0.5))
draw_line(
local_center,
local_center + Vector2(radius, 0.0),
Color(0.2, 0.8, 1.0, 0.8),
1.5
)
func _notification(what: int) -> void:
# エディタでパラメータが変わったときにもガイドを更新
if what == NOTIFICATION_TRANSFORM_CHANGED or what == NOTIFICATION_VISIBILITY_CHANGED:
if Engine.is_editor_hint():
queue_redraw()
使い方の手順
ここでは、代表的な3パターンの例で使い方を見ていきます。
例1: プレイヤーの周りを回る「オプション武器」
ノード構成例:
Player (CharacterBody2D)
├── Sprite2D
├── CollisionShape2D
└── Option (Node2D)
├── Sprite2D
└── OrbitMovement (Node)
- まず
OrbitMovement.gdをプロジェクトに保存し、上記のコードを貼り付けます。 - プレイヤーシーンに
Option(Node2D)を追加し、その子としてOrbitMovement(Node)を追加します。 OrbitMovementノードにこのスクリプトをアタッチし、インスペクタで:targetにPlayer(CharacterBody2D)をドラッグ&ドロップradiusに 48 ~ 96 あたりの値angular_speedにPI(2秒で一周)などuse_local_spaceはtrueのまま(プレイヤーに追従させる)
- ゲームを実行すると、
Optionがプレイヤーの周りをぐるぐる周回します。
例2: ボスの周りを回る「シールドオーブ」
ノード構成例:
Boss (CharacterBody2D)
├── Sprite2D
├── CollisionShape2D
├── ShieldOrb1 (Area2D)
│ ├── Sprite2D
│ ├── CollisionShape2D
│ └── OrbitMovement (Node)
└── ShieldOrb2 (Area2D)
├── Sprite2D
├── CollisionShape2D
└── OrbitMovement (Node)
手順:
- ボスシーンに
ShieldOrb1,ShieldOrb2などのオーブを追加し、それぞれの子にOrbitMovementノードを付けます。 - 各
OrbitMovementのtargetをBossに設定します。 start_angleをオーブごとに変えます(例:ShieldOrb1:start_angle = 0.0ShieldOrb2:start_angle = PI
- 同じ半径・角速度でも、開始角度をずらすだけで「等間隔に並んだ周回シールド」が簡単に作れます。
例3: 惑星の周りを回る「衛星(グローバル座標版)」
ワールド全体を1つのシーンにまとめるようなケースでは、ターゲットと衛星の親が違うことも多いですね。その場合は use_local_space = false にして、グローバル座標での周回にします。
ノード構成例:
World (Node2D) ├── Planet (Node2D) │ └── Sprite2D ├── Moon (Node2D) │ ├── Sprite2D │ └── OrbitMovement (Node) └── UI (CanvasLayer)
手順:
Moonの子にOrbitMovementを追加し、スクリプトをアタッチ。targetにPlanetを指定。use_local_spaceをfalseに変更(グローバル座標での周回)。- これで、
PlanetとMoonの親が違っても、惑星の周りをきちんと周回するようになります。
メリットと応用
この OrbitMovement コンポーネントを使うと、周回ロジックをすべて1か所に閉じ込められるので、シーン設計がかなりスッキリします。
- どのノードにもポン付けできる
親がNode2D系であれば、プレイヤーでも敵でも弾でも UI アイコンでも、同じコンポーネントで周回させられます。 - シーン構造が浅く保てる
「周回する敵クラス」「周回する弾クラス」みたいな継承ツリーを作らず、OrbitMovementをアタッチするだけでOK。
深いノード階層や複雑な継承関係から解放されます。 - パラメータ調整が楽
半径・速度・開始角度をインスペクタからいじるだけで、レベルデザイナも直感的に配置&調整ができます。 - 挙動の一括変更が簡単
周回ロジックを変えたくなったら、このコンポーネントのスクリプトだけを修正すれば、全シーンに一括で反映されます。
さらに応用として、例えば「ターゲットとの距離を徐々に縮めるホーミング周回」なども簡単に追加できます。
例えば、以下のような関数を足すと「半径を時間経過で変化させる」ことができます:
## 半径を時間経過で変化させる簡易アニメーション
## 例: _process 内から呼び出して、徐々にターゲットに近づく / 離れる
func animate_radius(delta: float, shrink_speed: float = 10.0, min_radius: float = 16.0) -> void:
# shrink_speed ピクセル/秒で半径を縮める
radius = max(min_radius, radius - shrink_speed * delta)
_process の末尾で animate_radius(delta) を呼べば、「ターゲットに少しずつ近づきながら周回するオーブ」になります。こういう改造も、周回ロジックが1コンポーネントにまとまっているおかげで、安全かつ簡単に追加できますね。
継承にロジックを詰め込むより、「動きごとにコンポーネントを分けて合成する」方が、長期的には圧倒的に楽なので、ぜひこのスタイルで周回移動を使い回してみてください。




