Godot 4で「タイヤをずっと回しておきたい」「コインをクルクル回したい」「敵のレーダーだけ常に回転させたい」みたいな処理、つい親ノードのスクリプトにベタ書きしていませんか?
たとえば…

  • プレイヤーのスクリプトに「タイヤ回転」のコードを書き足す
  • 敵のスクリプトにも「レーダー回転」をコピペ
  • アイテムにも「見た目用の回転アニメーション」を追加

こうやっていくと、どんどん「でかいスクリプト」+「深いノード階層」になっていきます。
しかも「回転スピード変えたい」「今だけ止めたい」といった調整のたびに、あちこちのスクリプトをいじる羽目になります。

そこで登場するのが、今回のコンポーネント RotateForever です。
「回転させたいノードにポン付けするだけ」で、親や特定の子ノードを永続的に回転させるシンプルなコンポーネントにしてみました。
継承ではなく「合成(Composition)」で、どのノードにも同じ回転ロジックを再利用できるようにしていきましょう。

【Godot 4】回転アニメは全部こいつに任せよう!「RotateForever」コンポーネント

フルコード:RotateForever.gd


extends Node
class_name RotateForever
## 親、または親の特定の子ノードを回転させ続けるコンポーネント。
## 任意のノードにアタッチして使います。
##
## 想定用途:
## - 車のタイヤをずっと回転させる
## - コインやアイテムをくるくる回す
## - 敵のレーダーやアンテナだけ常に回転させる

@export_group("Target")
## 回転させる対象を「親ノード」か「親の子ノード」から選ぶ
@export_enum("Parent", "ParentChild") var target_mode: int = 0

## target_mode が "ParentChild" のとき、
## 親ノードの子ノードの中から、この名前のノードを探して回転させる
@export var child_node_name: StringName = &""

@export_group("Rotation")
## 1秒あたりの回転角度(度数法)。正で反時計回り、負で時計回り。
## 2Dならだいたい 90〜360 あたりが使いやすいです。
@export var degrees_per_second: float = 180.0

## 3Dノードを回すかどうか(true: 3D / false: 2D)
## 自動判別もできますが、明示的にした方が事故が少ないのでフラグにしています。
@export var is_3d: bool = false

## ゲーム開始時から自動で回転を開始するかどうか
@export var auto_start: bool = true

## 回転を一時停止するフラグ。コードから制御したいときに使います。
var paused: bool = false

## 実際に回転させる対象ノード(Node2D or Node3D)
var _target_node: Node = null


func _ready() -> void:
    # 対象ノードを解決してキャッシュする
    _resolve_target()

    # auto_start が false の場合、paused を true にしておく
    paused = not auto_start


func _process(delta: float) -> void:
    if paused:
        return
    if _target_node == null:
        return

    # delta に応じて回転量を計算(度数法)
    var delta_degrees := degrees_per_second * delta

    if is_3d:
        _rotate_3d(delta_degrees)
    else:
        _rotate_2d(delta_degrees)


func _resolve_target() -> void:
    ## 親ノードから、回転させる対象を取得する
    var parent := get_parent()
    if parent == null:
        push_warning("RotateForever: 親ノードが存在しません。シーンに追加されてから使ってください。")
        return

    match target_mode:
        0: # "Parent"
            _target_node = parent
        1: # "ParentChild"
            if String(child_node_name) == "":
                push_warning("RotateForever: target_mode=ParentChild ですが child_node_name が空です。親を対象にします。")
                _target_node = parent
            else:
                if parent.has_node(NodePath(child_node_name)):
                    _target_node = parent.get_node(NodePath(child_node_name))
                else:
                    push_warning("RotateForever: 親に指定された子ノード '%s' が見つかりませんでした。親を対象にします。" % child_node_name)
                    _target_node = parent
        _:
            _target_node = parent

    # 2D/3D の型チェック(軽いガード)
    if is_3d:
        if not (_target_node is Node3D):
            push_warning("RotateForever: is_3d=true ですが、対象ノードは Node3D ではありません。回転は行われません。")
            _target_node = null
    else:
        if not (_target_node is Node2D):
            push_warning("RotateForever: is_3d=false ですが、対象ノードは Node2D ではありません。回転は行われません。")
            _target_node = null


func _rotate_2d(delta_degrees: float) -> void:
    ## Node2D 用の回転処理
    var node2d := _target_node as Node2D
    if node2d == null:
        return
    node2d.rotation_degrees += delta_degrees


func _rotate_3d(delta_degrees: float) -> void:
    ## Node3D 用の回転処理(Y軸回転を想定)
    ## 必要に応じて軸を変えたい場合は、改造案のセクションも参照してください。
    var node3d := _target_node as Node3D
    if node3d == null:
        return

    # Y軸を中心に回転(ラジアンに変換してから rotate_y を呼ぶ)
    var delta_radians := deg_to_rad(delta_degrees)
    node3d.rotate_y(delta_radians)


# --- 公開API ------------------------------------------------------------

## 回転を開始する(paused を false にする)
func start() -> void:
    paused = false


## 回転を一時停止する(paused を true にする)
func stop() -> void:
    paused = true


## 回転方向を反転する(degrees_per_second をマイナスにする)
func invert_direction() -> void:
    degrees_per_second = -degrees_per_second


## 対象ノードを再解決する。
## ランタイムでノード構造を差し替えたときなどに呼び出してください。
func refresh_target() -> void:
    _resolve_target()

使い方の手順

ここからは、具体的なシーン例を挙げながら使い方を見ていきましょう。
基本の流れはどのケースでも同じです。

  1. RotateForever.gd をプロジェクトのどこかに置く(例: res://components/RotateForever.gd
  2. 回転させたいノードの子として RotateForever ノードを追加
  3. インスペクターから target_mode / child_node_name / degrees_per_second / is_3d を設定
  4. 必要ならスクリプトから start(), stop(), invert_direction() を呼ぶ

例①:2Dプレイヤーのタイヤだけを回転させる

車っぽいプレイヤーがいて、左右のタイヤだけをクルクル回したいケースです。

PlayerCar (CharacterBody2D)
 ├── BodySprite (Sprite2D)
 ├── TireLeft  (Sprite2D)
 ├── TireRight (Sprite2D)
 ├── CollisionShape2D
 ├── RotateForever_Left  (Node)
 └── RotateForever_Right (Node)
  • RotateForever_LeftRotateForever.gd をアタッチ
  • RotateForever_Right にも同様にアタッチ

それぞれの設定例:

  • RotateForever_Left
    • target_mode = ParentChild
    • child_node_name = "TireLeft"
    • degrees_per_second = -360.0(時計回り)
    • is_3d = false
    • auto_start = true
  • RotateForever_Right
    • target_mode = ParentChild
    • child_node_name = "TireRight"
    • degrees_per_second = -360.0
    • is_3d = false
    • auto_start = true

これで、プレイヤー本体のスクリプトは「移動ロジック」に集中できて、
タイヤの回転は完全に コンポーネント任せ にできます。

例②:2Dコインを常に回転させる(親を直接回転)

コイン自体をそのまま回転させたいケースです。

Coin (Node2D)
 ├── Sprite2D
 ├── CollisionShape2D
 └── RotateForever (Node)

RotateForever の設定:

  • target_mode = Parent(親ノード = Coin を回転)
  • degrees_per_second = 180.0
  • is_3d = false
  • auto_start = true

もし「一定時間だけ回転させたい」などの制御をしたければ、
コインのスクリプトからコンポーネントを操作します。


# Coin.gd(例)
extends Node2D

@onready var rotator: RotateForever = $RotateForever

func _ready() -> void:
    # 3秒後に回転を止める
    await get_tree().create_timer(3.0).timeout
    rotator.stop()

例③:3D敵キャラのレーダーだけを回転させる

3Dの敵キャラで、頭の上についているレーダー(アンテナ)だけを Y 軸回転させたいケースです。

EnemyBot (CharacterBody3D)
 ├── MeshInstance3D
 ├── Radar (Node3D)
 │    └── RadarMesh (MeshInstance3D)
 ├── CollisionShape3D
 └── RotateForever (Node)

RotateForever の設定:

  • is_3d = true
  • target_mode = ParentChild
  • child_node_name = "Radar"
  • degrees_per_second = 90.0
  • auto_start = true

これで、敵の行動ロジック(索敵・追尾など)は EnemyBot のスクリプトに閉じ込めつつ、
レーダーの「見た目の回転」は RotateForever に完全に委譲できます。

メリットと応用

この RotateForever コンポーネントを使うことで、以下のようなメリットがあります。

  • シーン構造がスッキリする
    「回転ロジック」を親スクリプトに書かなくていいので、Player.gd / Enemy.gd / Item.gd が細く保てます。
  • 再利用性が高い
    どんなノードにもポン付けできるので、「とりあえず回してみたい」時に毎回同じコードを書く必要がありません。
  • 継承ツリーが増えない
    「回転するプレイヤー」「回転する敵」みたいな専用サブクラスを作らずに済みます。
    ベースクラスはシンプルなまま、回転だけコンポーネントで合成するスタイルですね。
  • レベルデザインが楽
    レベルデザイナーや自分の未来の自分が、インスペクターの数値をいじるだけで回転スピードや方向を調整できます。

さらに、応用として:

  • スイッチを押したら start() / stop() を呼ぶギミック
  • プレイヤーが近づいたら回転速度を上げる(degrees_per_second を動的に変更)
  • ボスのフェーズ移行に合わせて invert_direction() で回転方向を変える

など、「回転」という要素を簡単にゲームロジックに組み込めます。

改造案:3Dで「任意の軸」に回転させる

デフォルトでは 3D は Y 軸回転に固定していますが、
「X軸に回したい」「斜めの軸で回したい」などのニーズも出てきますよね。
そんなときは、以下のように rotation_axis を追加する改造が考えられます。


@export_group("Rotation 3D")
## 3D回転用の軸ベクトル(例: (0, 1, 0) なら Y軸)
@export var rotation_axis: Vector3 = Vector3.UP

func _rotate_3d(delta_degrees: float) -> void:
    var node3d := _target_node as Node3D
    if node3d == null:
        return

    var delta_radians := deg_to_rad(delta_degrees)

    # 正規化した軸で回転
    var axis := rotation_axis.normalized()
    if axis == Vector3.ZERO:
        return # 軸がゼロだと回転できないのでガード

    node3d.rotate(axis, delta_radians)

こうしておけば、「プロペラは X 軸回転」「惑星は任意の傾きで自転」など、
同じコンポーネントでさらに幅広い表現ができるようになります。

継承ツリーを増やす前に、「これコンポーネントで切り出せないかな?」と一度立ち止まってみると、
プロジェクト後半のメンテナンス性がかなり変わってきます。
RotateForever もその一つとして、ぜひプロジェクトに常駐させておいてください。