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()
使い方の手順
ここからは、具体的なシーン例を挙げながら使い方を見ていきましょう。
基本の流れはどのケースでも同じです。
- RotateForever.gd をプロジェクトのどこかに置く(例:
res://components/RotateForever.gd) - 回転させたいノードの子として RotateForever ノードを追加
- インスペクターから target_mode / child_node_name / degrees_per_second / is_3d を設定
- 必要ならスクリプトから
start(),stop(),invert_direction()を呼ぶ
例①:2Dプレイヤーのタイヤだけを回転させる
車っぽいプレイヤーがいて、左右のタイヤだけをクルクル回したいケースです。
PlayerCar (CharacterBody2D) ├── BodySprite (Sprite2D) ├── TireLeft (Sprite2D) ├── TireRight (Sprite2D) ├── CollisionShape2D ├── RotateForever_Left (Node) └── RotateForever_Right (Node)
- RotateForever_Left に
RotateForever.gdをアタッチ - RotateForever_Right にも同様にアタッチ
それぞれの設定例:
- RotateForever_Left
target_mode = ParentChildchild_node_name = "TireLeft"degrees_per_second = -360.0(時計回り)is_3d = falseauto_start = true
- RotateForever_Right
target_mode = ParentChildchild_node_name = "TireRight"degrees_per_second = -360.0is_3d = falseauto_start = true
これで、プレイヤー本体のスクリプトは「移動ロジック」に集中できて、
タイヤの回転は完全に コンポーネント任せ にできます。
例②:2Dコインを常に回転させる(親を直接回転)
コイン自体をそのまま回転させたいケースです。
Coin (Node2D) ├── Sprite2D ├── CollisionShape2D └── RotateForever (Node)
RotateForever の設定:
target_mode = Parent(親ノード = Coin を回転)degrees_per_second = 180.0is_3d = falseauto_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 = truetarget_mode = ParentChildchild_node_name = "Radar"degrees_per_second = 90.0auto_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 もその一つとして、ぜひプロジェクトに常駐させておいてください。
