【Cocos Creator 3.8】RotateForever の実装:アタッチするだけで任意ノードを永続回転させる汎用スクリプト
「タイヤだけ回したい」「キャラクターの頭上の星をクルクル回したい」「UIのローディングアイコンを回したい」──そんなときに毎回スクリプトを書くのは面倒です。
本記事では、任意のノードにアタッチするだけで、親ノードまたは指定した子ノードをずっと回転させ続ける汎用コンポーネント RotateForever を実装します。
外部の GameManager などには一切依存せず、このスクリプト単体で完結するように設計します。回転速度や回転方向、対象ノードの指定などはすべてインスペクタから調整可能です。
コンポーネントの設計方針
1. 機能要件の整理
- このコンポーネントをアタッチしたノードを「基準」として扱う。
- 回転させる対象は、次のいずれかから選べるようにする:
- SELF: 自身のノードを回転
- PARENT: 親ノードを回転
- CHILD_BY_NAME: 指定名の子ノードを回転
- 回転の軸は 3D 空間の
x / y / zを個別に指定できるようにする。 - 回転速度(度/秒)、回転方向(正転・逆転)、一時停止/再開の制御ができる。
- エディタ上でも挙動が確認しやすいように、Editor 上での再生時(Play ボタン)に動作する。
- ターゲットノードが見つからない場合は
console.errorで原因を明示し、安全に停止する。
2. 外部依存をなくすためのアプローチ
- ゲーム全体の状態(ポーズ・シーン遷移など)には依存しない。
- すべての設定値は
@property経由でインスペクタから指定する。 - 他のカスタムスクリプトを前提としたコールバックやイベントには登録しない。
- 標準 API のみ(
NodeのsetRotationFromEuler/eulerAnglesなど)で完結させる。
3. インスペクタで設定可能なプロパティ設計
以下のようなプロパティを用意します。
targetMode(列挙型):- どのノードを回転させるかを選択するモード。
- SELF(デフォルト): このコンポーネントが付いているノード自身。
- PARENT: 親ノード。存在しない場合はエラーログ。
- CHILD_BY_NAME: 指定名の子ノード。見つからない場合はエラーログ。
childName(string):targetModeがCHILD_BY_NAMEのときだけ使用。- 回転させたい子ノードの名前。
speed(number):- 回転速度(度/秒)。
- 正の値で指定軸の+方向回転、負の値で逆方向回転。
- 例: 360 にすると 1 秒で 1 回転。
axisX,axisY,axisZ(boolean):- どの軸を回転させるかのフラグ。
- 例:
axisZ = trueで 2D ゲームの一般的なスプライト回転。 - 複数 true にすると、斜め方向の複合回転になる。
useLocalSpace(boolean):true: ローカル座標系のオイラー角を直接変更。false: ワールド座標系のオイラー角を変更。- 通常は
true推奨。
playOnStart(boolean):trueのとき、start()時点で自動的に回転を開始。falseのとき、isPlayingを手動でオンにするまで回転しない。
isPlaying(boolean):- 現在回転中かどうかのフラグ。
- インスペクタから再生中にオン/オフして一時停止と再開をテスト可能。
これらにより、タイヤ、風車、UI アイコンなど、さまざまな用途で再利用できる柔軟なコンポーネントになります。
TypeScriptコードの実装
import { _decorator, Component, Node, Enum, Vec3 } from 'cc';
const { ccclass, property } = _decorator;
/**
* 回転対象の指定モード
*/
enum RotateTargetMode {
SELF = 0,
PARENT = 1,
CHILD_BY_NAME = 2,
}
Enum(RotateTargetMode);
@ccclass('RotateForever')
export class RotateForever extends Component {
@property({
type: RotateTargetMode,
tooltip: '回転させる対象ノードの指定方法\nSELF: このノード自身\nPARENT: 親ノード\nCHILD_BY_NAME: 指定名の子ノード'
})
public targetMode: RotateTargetMode = RotateTargetMode.SELF;
@property({
tooltip: 'targetMode が CHILD_BY_NAME のときに検索する子ノード名'
})
public childName: string = '';
@property({
tooltip: '回転速度(度/秒)。360 で 1 秒/1 回転。負の値で逆回転。'
})
public speed: number = 180;
@property({
tooltip: 'X 軸まわりに回転させるかどうか'
})
public axisX: boolean = false;
@property({
tooltip: 'Y 軸まわりに回転させるかどうか'
})
public axisY: boolean = false;
@property({
tooltip: 'Z 軸まわりに回転させるかどうか(2D では通常 Z のみを使用)'
})
public axisZ: boolean = true;
@property({
tooltip: 'true: ローカル座標系の回転を変更\nfalse: ワールド座標系の回転を変更'
})
public useLocalSpace: boolean = true;
@property({
tooltip: '開始時に自動で回転を開始するかどうか'
})
public playOnStart: boolean = true;
@property({
tooltip: '現在回転中かどうか(実行中にオン/オフして一時停止/再開できます)'
})
public isPlaying: boolean = true;
// 実際に回転させるターゲットノード
private _targetNode: Node | null = null;
// 再利用用の一時ベクトル
private static _tempEuler: Vec3 = new Vec3();
onLoad () {
this._resolveTargetNode();
if (!this.axisX && !this.axisY && !this.axisZ) {
console.warn('[RotateForever] いずれの軸も有効になっていません。axisX/axisY/axisZ のいずれかを true にしてください。', this.node.name);
}
}
start () {
// playOnStart の値で isPlaying を初期化
this.isPlaying = this.playOnStart;
}
update (deltaTime: number) {
if (!this.isPlaying) {
return;
}
if (!this._targetNode) {
// ターゲットが見つからなかった場合は、毎フレームエラーを出さないように早期 return
return;
}
if (!this.axisX && !this.axisY && !this.axisZ) {
// 軸がすべて false なら何もしない
return;
}
const euler = RotateForever._tempEuler;
if (this.useLocalSpace) {
this._targetNode.eulerAngles.clone(euler);
} else {
this._targetNode.worldEulerAngles.clone(euler);
}
// 各軸に対して回転を加算
const deltaAngle = this.speed * deltaTime;
if (this.axisX) {
euler.x += deltaAngle;
}
if (this.axisY) {
euler.y += deltaAngle;
}
if (this.axisZ) {
euler.z += deltaAngle;
}
if (this.useLocalSpace) {
this._targetNode.setRotationFromEuler(euler);
} else {
// worldEulerAngles は直接代入できないため、ワールド回転を変更したい場合は
// ローカル回転に反映されるような構造にしておくことを推奨。
// ここでは簡易的に setRotationFromEuler を使用し、親の影響を受けることに注意。
this._targetNode.setRotationFromEuler(euler);
}
}
/**
* インスペクタから targetMode や childName が変更されたときに
* ターゲットノードを再解決する。
*/
protected onValidate () {
this._resolveTargetNode();
}
/**
* 回転対象ノードを targetMode に基づいて解決する。
*/
private _resolveTargetNode () {
let target: Node | null = null;
switch (this.targetMode) {
case RotateTargetMode.SELF:
target = this.node;
break;
case RotateTargetMode.PARENT:
if (!this.node.parent) {
console.error('[RotateForever] targetMode=PARENT ですが、親ノードが存在しません。ノード構造を確認してください。', this.node.name);
target = null;
} else {
target = this.node.parent;
}
break;
case RotateTargetMode.CHILD_BY_NAME:
if (!this.childName) {
console.error('[RotateForever] targetMode=CHILD_BY_NAME ですが、childName が空です。インスペクタで子ノード名を設定してください。', this.node.name);
target = null;
} else {
const child = this.node.getChildByName(this.childName);
if (!child) {
console.error(`[RotateForever] 指定された子ノード "${this.childName}" が見つかりません。ノード名を確認してください。`, this.node.name);
target = null;
} else {
target = child;
}
}
break;
}
this._targetNode = target;
}
/**
* スクリプトから回転を開始したい場合に呼び出せるヘルパー
*/
public play () {
this.isPlaying = true;
}
/**
* スクリプトから回転を一時停止したい場合に呼び出せるヘルパー
*/
public pause () {
this.isPlaying = false;
}
}
コードのポイント解説
RotateTargetMode列挙型:- インスペクタで「SELF / PARENT / CHILD_BY_NAME」をプルダウンで選べるようにするための Enum。
Enum(RotateTargetMode);を呼び出すことで Cocos のインスペクタに反映されます。
onLoad():_resolveTargetNode()を呼び出し、対象ノードを決定します。- 回転軸が一つも有効でない場合は警告を出します(ミス設定の早期発見)。
start():playOnStartの値でisPlayingを初期化します。- ゲーム開始時に自動再生するかどうかを制御します。
update(deltaTime):isPlayingがfalseなら何もしません(一時停止状態)。- ターゲットノードが無効な場合も何もしません(エラーは
_resolveTargetNode()側で一度だけ出力)。 - 現在のオイラー角(ローカル or ワールド)を取得し、
speed * deltaTimeを軸ごとに加算して、setRotationFromEulerで反映します。
onValidate():- インスペクタで値が変更されたときにエディタ上で呼ばれるフックです。
targetModeやchildNameを変更した際に、即座にターゲットノードを再解決します。
play()/pause():- 他のスクリプトから制御したくなった場合にも使えるようにした簡易ヘルパー。
- 今回の要件では必須ではありませんが、汎用コンポーネントとしてはあると便利です。
使用手順と動作確認
1. スクリプトファイルの作成
- Assets パネルで右クリックします。
Create > TypeScriptを選択します。- 新規スクリプトに RotateForever.ts という名前を付けます。
- ダブルクリックしてエディタ(VS Code など)で開き、先ほどのコードをすべて貼り付けて保存します。
2. テスト用ノードの作成(自分自身を回転)
- Hierarchy パネルで右クリックし、2D なら
Create > UI > Sprite、3D ならCreate > 3D Object > Cubeなど、適当なノードを作成します。 - ノード名を分かりやすく RotatingObject などに変更しておきます。
3. RotateForever コンポーネントのアタッチ
- Hierarchy で先ほど作成した RotatingObject ノードを選択します。
- Inspector パネル下部の Add Component ボタンをクリックします。
- Custom カテゴリの中から RotateForever を選択して追加します。
4. プロパティの設定例(自分自身を Z 軸で回転)
Inspector の RotateForever セクションで、次のように設定してみてください。
- Target Mode:
SELF - Speed:
360(1 秒で 1 回転) - Axis X:
false - Axis Y:
false - Axis Z:
true - Use Local Space:
true - Play On Start:
true - Is Playing:
true(自動的に true になります)
この状態で再生ボタン(▶)を押すと、RotatingObject ノードが Z 軸まわりにクルクル回転し続けるはずです。
5. 親ノードを回転させる(タイヤなどの例)
例えば、自動車ノードの中にタイヤノードがあり、「タイヤスクリプトから車体を回転させたい」ような構造を想定します。
- Hierarchy で次のような構造を作ります:
- Car(親)
- Wheel(子)
- Car(親)
- Wheel ノードを選択し、RotateForever コンポーネントをアタッチします。
- Inspector で次のように設定します:
- Target Mode:
PARENT - Speed:
90(4 秒で 1 回転) - Axis Z:
true(2D 車なら Z 軸) - その他はお好みで。
- Target Mode:
再生すると、Wheel ではなく、親の Car ノードが回転します。
タイヤにスクリプトを付けておきつつ、車体全体を回転させたいときに便利です。
6. 特定の子ノードだけを回転させる(頭上の星など)
キャラクターの頭上にある「星」や「オーラ」など、特定の子ノードだけを回転させたいケースです。
- Hierarchy で次のような構造を作ります:
- Character
- Body
- Star
- Character
- Character ノードを選択し、RotateForever コンポーネントをアタッチします。
- Inspector で次のように設定します:
- Target Mode:
CHILD_BY_NAME - Child Name:
Star(子ノード名と完全一致させる) - Speed:
180 - Axis Z:
true - Play On Start:
true
- Target Mode:
再生すると、Character 全体ではなく、子ノードの Star だけが回転します。
子ノード名が間違っている場合は、コンソールにエラーが出力されるので確認してください。
7. 一時停止と再開のテスト
実行中に回転を止めたり再開したりするには、以下の方法があります。
- インスペクタから手動で切り替える:
- 再生中に対象ノードを選択し、Is Playing のチェックを外すと回転が止まります。
- 再度チェックを入れると回転が再開します。
- 他スクリプトから制御する場合(任意):
- 別のコンポーネントから
getComponent(RotateForever)で取得し、play()/pause()を呼び出します。 - 本記事の要件上、RotateForever 側は他スクリプトに依存していないので、使うかどうかは任意です。
- 別のコンポーネントから
まとめ
今回実装した RotateForever コンポーネントは、
- 回転対象を「自分 / 親 / 特定の子ノード」から柔軟に選べる。
- 回転軸・速度・方向をすべてインスペクタから調整できる。
- 外部の GameManager やシングルトンに一切依存しない、完全に独立した汎用スクリプト。
- ノードにアタッチするだけで即利用できるため、プロトタイピングから本番まで幅広く使い回せる。
タイヤ、風車、UI ローディングアイコン、キャラクターの装飾など、「とりあえず回したい」要素はゲーム中に頻出します。
そうした部分をこのような汎用コンポーネントで切り出しておくと、毎回同じ処理を書く手間を省きつつ、設定だけで挙動を変えられるため、開発効率が大きく向上します。
この RotateForever.ts をプロジェクトの「よく使うユーティリティコンポーネント」としてストックしておけば、今後の Cocos Creator 開発で「回転」に関する処理はほぼこれ一つで済むはずです。




