【Godot 4】OrbitMovement (周回移動) コンポーネントの作り方

Godot 4ゲーム制作 実践ドリル 100本ノック

新品価格
¥1,250から
(2025/12/13 21:27時点)

脱・初心者!Godot 4 ゲーム開発の「2歩目」

新品価格
¥831から
(2025/12/13 21:39時点)

Godot4ローグライク入門 ~ダンジョン自動生成~

新品価格
¥831から
(2025/12/13 21:44時点)

Godot4& GDScriptではじめる 2Dゲーム開発レシピ

新品価格
¥590から
(2025/12/13 21:46時点)

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)
  1. まず OrbitMovement.gd をプロジェクトに保存し、上記のコードを貼り付けます。
  2. プレイヤーシーンに Option(Node2D)を追加し、その子として OrbitMovement(Node)を追加します。
  3. OrbitMovement ノードにこのスクリプトをアタッチし、インスペクタで:
    • targetPlayer(CharacterBody2D)をドラッグ&ドロップ
    • radius に 48 ~ 96 あたりの値
    • angular_speedPI(2秒で一周)など
    • use_local_spacetrue のまま(プレイヤーに追従させる)
  4. ゲームを実行すると、Option がプレイヤーの周りをぐるぐる周回します。

例2: ボスの周りを回る「シールドオーブ」

ノード構成例:

Boss (CharacterBody2D)
 ├── Sprite2D
 ├── CollisionShape2D
 ├── ShieldOrb1 (Area2D)
 │    ├── Sprite2D
 │    ├── CollisionShape2D
 │    └── OrbitMovement (Node)
 └── ShieldOrb2 (Area2D)
      ├── Sprite2D
      ├── CollisionShape2D
      └── OrbitMovement (Node)

手順:

  1. ボスシーンに ShieldOrb1, ShieldOrb2 などのオーブを追加し、それぞれの子に OrbitMovement ノードを付けます。
  2. OrbitMovementtargetBoss に設定します。
  3. start_angle をオーブごとに変えます(例:
    • ShieldOrb1: start_angle = 0.0
    • ShieldOrb2: start_angle = PI
  4. 同じ半径・角速度でも、開始角度をずらすだけで「等間隔に並んだ周回シールド」が簡単に作れます。

例3: 惑星の周りを回る「衛星(グローバル座標版)」

ワールド全体を1つのシーンにまとめるようなケースでは、ターゲットと衛星の親が違うことも多いですね。その場合は use_local_space = false にして、グローバル座標での周回にします。

ノード構成例:

World (Node2D)
 ├── Planet (Node2D)
 │    └── Sprite2D
 ├── Moon (Node2D)
 │    ├── Sprite2D
 │    └── OrbitMovement (Node)
 └── UI (CanvasLayer)

手順:

  1. Moon の子に OrbitMovement を追加し、スクリプトをアタッチ。
  2. targetPlanet を指定。
  3. use_local_spacefalse に変更(グローバル座標での周回)。
  4. これで、PlanetMoon の親が違っても、惑星の周りをきちんと周回するようになります。

メリットと応用

この 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コンポーネントにまとまっているおかげで、安全かつ簡単に追加できますね。

継承にロジックを詰め込むより、「動きごとにコンポーネントを分けて合成する」方が、長期的には圧倒的に楽なので、ぜひこのスタイルで周回移動を使い回してみてください。

Godot 4ゲーム制作 実践ドリル 100本ノック

新品価格
¥1,250から
(2025/12/13 21:27時点)

脱・初心者!Godot 4 ゲーム開発の「2歩目」

新品価格
¥831から
(2025/12/13 21:39時点)

Godot4ローグライク入門 ~ダンジョン自動生成~

新品価格
¥831から
(2025/12/13 21:44時点)

Godot4& GDScriptではじめる 2Dゲーム開発レシピ

新品価格
¥590から
(2025/12/13 21:46時点)

URLをコピーしました!