【Cocos Creator】アタッチするだけ!LoadingSpinner (ロード待機)の実装方法【TypeScript】

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

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

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

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

Unity 6 C#スクリプト 100本ノック

新品価格
¥1,230から
(2025/12/29 10:28時点)

Cocos Creator100本ノックTypeScriptで書く!

新品価格
¥1,250から
(2025/12/29 10:32時点)

【Cocos Creator 3.8】LoadingSpinner の実装:アタッチするだけで「画面右下に表示されるロード中の回転アイコン」を実現する汎用スクリプト

このコンポーネントは、非同期ロード中などに「右下でクルクル回るローディングアイコン」を簡単に表示するためのものです。任意のノードに LoadingSpinner をアタッチするだけで、画面サイズに合わせて自動的に右下に配置され、指定したスプライトを回転させる UI を作れます。外部の GameManager やシングルトンに依存せず、このスクリプト単体で完結します。


コンポーネントの設計方針

今回の要件を整理すると、以下のようになります。

  • 非同期ロードなど「ロード中」の間に、右下で回転するアイコンを表示したい。
  • どのシーンでも再利用できるように、他のカスタムスクリプトには依存しない。
  • UI の Canvas / Camera 構成が多少違っても、基本的に「画面右下」に来てほしい。
  • 回転速度や位置オフセット、表示・非表示の制御をインスペクタから簡単に調整したい。

外部依存をなくすために、以下のような設計を採用します。

  • 回転アニメーションは、このコンポーネント自身の updatenode.angle を更新するだけで完結。
  • 見た目は「このノードにアタッチされた Sprite(または任意の UI ノード)」をそのまま回転させる。
  • 画面右下への配置は、UITransform と親 Canvas のサイズから算出し、アンカーを右下に固定することで実現。
  • Canvas や Camera は外部スクリプトに頼らず、親階層を辿って自動的に Canvas を探す。
  • ロード中かどうかのフラグは、インスペクタからオン・オフできる isSpinning と、公開メソッド show() / hide() で制御。

インスペクタで設定可能なプロパティ

LoadingSpinner で用意する @property は次の通りです。

  • isSpinning: boolean
    • デフォルト: true
    • このフラグが true のときだけスピナーが回転します。
    • エディタ上でチェックを外すと停止した状態で表示されます。
  • autoHideWhenStopped: boolean
    • デフォルト: true
    • isSpinning = false のときにノード自体を active = false にして非表示にします。
    • 「停止してもアイコンは残しておきたい」場合はオフにします。
  • rotationSpeed: number
    • デフォルト: 180(度/秒)
    • スピナーの回転速度。正の値で時計回り、負の値で反時計回りに回転します。
    • 例: 360 にすると 1 秒で 1 回転、90 なら 4 秒で 1 回転。
  • useCanvasBottomRight: boolean
    • デフォルト: true
    • オンの場合:親 Canvas の右下を基準に自動配置します(UI 用)。
    • オフの場合:現在のローカル座標をそのまま使い、位置制御は行いません(自由配置)。
  • marginRight: number
    • デフォルト: 40(ピクセル)
    • 画面の右端からどれだけ内側に寄せるか。
    • useCanvasBottomRighttrue のときのみ有効。
  • marginBottom: number
    • デフォルト: 40(ピクセル)
    • 画面の下端からどれだけ上に寄せるか。
    • useCanvasBottomRighttrue のときのみ有効。
  • lockToUIScale: boolean
    • デフォルト: true
    • 親 Canvas の UITransform のスケールを無視し、スピナー自身のスケールを (1,1,1) に保とうとします。
    • UI の解像度設定によってスケールが変わる環境で、表示サイズを安定させたい場合に有効です。
  • initialVisible: boolean
    • デフォルト: true
    • シーン開始時にスピナーを表示しておくかどうか。
    • 非同期ロードが終わってから明示的に show() したい場合は false にします。

見た目となる Sprite 自体は、このコンポーネントがアタッチされたノードに自由に設定して構いません。Sprite コンポーネントが存在しない場合でも、単に「空のノードが回転する」だけで動作しますが、ログで警告を出しておきます。


TypeScriptコードの実装


import { _decorator, Component, Node, UITransform, Canvas, Sprite, warn, log, Vec3 } from 'cc';
const { ccclass, property } = _decorator;

/**
 * LoadingSpinner
 * 任意の UI ノードにアタッチするだけで、
 * 画面右下に配置される回転アイコン(ローディングスピナー)を実現するコンポーネント。
 *
 * 外部の GameManager などには依存せず、このスクリプト単体で完結します。
 */
@ccclass('LoadingSpinner')
export class LoadingSpinner extends Component {

    @property({
        tooltip: 'スピナーを回転させるかどうか。\ntrue のときだけ毎フレーム回転します。',
    })
    public isSpinning: boolean = true;

    @property({
        tooltip: '回転が停止しているときに、このノード自体を非表示(active=false)にします。',
    })
    public autoHideWhenStopped: boolean = true;

    @property({
        tooltip: 'スピナーの回転速度(度/秒)。\n正の値で時計回り、負の値で反時計回りに回転します。',
    })
    public rotationSpeed: number = 180;

    @property({
        tooltip: 'true の場合、親 Canvas の右下を基準に自動配置します。\nfalse の場合、ノードの現在位置を維持します。',
    })
    public useCanvasBottomRight: boolean = true;

    @property({
        tooltip: '画面右端からの余白(ピクセル)。useCanvasBottomRight が true のときのみ有効です。',
        visible() {
            return this.useCanvasBottomRight;
        }
    })
    public marginRight: number = 40;

    @property({
        tooltip: '画面下端からの余白(ピクセル)。useCanvasBottomRight が true のときのみ有効です。',
        visible() {
            return this.useCanvasBottomRight;
        }
    })
    public marginBottom: number = 40;

    @property({
        tooltip: '親 Canvas のスケールの影響を打ち消し、スピナー自身のスケールを (1,1,1) に保ちます。',
    })
    public lockToUIScale: boolean = true;

    @property({
        tooltip: 'シーン開始時にスピナーを表示するかどうか。false の場合、start 時に自動で非表示になります。',
    })
    public initialVisible: boolean = true;

    /** キャッシュ用: 親 Canvas ノード */
    private _canvasNode: Node | null = null;

    /** キャッシュ用: このノードの UITransform */
    private _uiTransform: UITransform | null = null;

    /** キャッシュ用: スピナー画像用 Sprite(存在しない場合もある) */
    private _sprite: Sprite | null = null;

    onLoad() {
        // 必要なコンポーネントの取得とチェック
        this._uiTransform = this.node.getComponent(UITransform);
        if (!this._uiTransform) {
            // UITransform がないと UI の座標計算が難しいので警告
            warn('[LoadingSpinner] このノードに UITransform コンポーネントがありません。UI ノードとして使用する場合は追加してください。');
        }

        this._sprite = this.node.getComponent(Sprite);
        if (!this._sprite) {
            // Sprite がなくても動作はするが、見た目がないので警告
            warn('[LoadingSpinner] Sprite コンポーネントが見つかりません。スピナーとして表示するには Sprite を追加し、スプライトフレームを設定してください。');
        }

        // 親階層から Canvas を探索
        this._canvasNode = this._findParentCanvas();
        if (!this._canvasNode && this.useCanvasBottomRight) {
            warn('[LoadingSpinner] 親階層に Canvas が見つかりません。useCanvasBottomRight を true にしている場合、期待通りの位置にならない可能性があります。');
        }
    }

    start() {
        // 初期表示状態の制御
        this.node.active = this.initialVisible;

        // 初期位置の調整(Canvas がある場合)
        if (this.useCanvasBottomRight) {
            this._applyBottomRightPosition();
        }

        // スケールの調整
        if (this.lockToUIScale) {
            this._applyScaleLock();
        }

        // デバッグログ(必要に応じてコメントアウト可)
        log('[LoadingSpinner] Initialized. isSpinning =', this.isSpinning, ', initialVisible =', this.initialVisible);
    }

    update(dt: number) {
        // スケールロックが有効なら、毎フレームスケールを補正
        if (this.lockToUIScale) {
            this._applyScaleLock();
        }

        // スピナーが停止状態の場合の処理
        if (!this.isSpinning) {
            if (this.autoHideWhenStopped) {
                this.node.active = false;
            }
            return;
        }

        // スピナーが回転中の場合、必ず表示しておく
        if (!this.node.active) {
            this.node.active = true;
        }

        // 回転アニメーション
        const deltaAngle = this.rotationSpeed * dt;
        this.node.angle -= deltaAngle; // Cocos の angle は時計回りが負方向
    }

    /**
     * スピナーを表示し、回転を開始します。
     * (isSpinning = true, node.active = true)
     */
    public show(): void {
        this.isSpinning = true;
        this.node.active = true;
    }

    /**
     * スピナーを停止し、autoHideWhenStopped に応じて非表示にします。
     * (isSpinning = false)
     */
    public hide(): void {
        this.isSpinning = false;
        if (this.autoHideWhenStopped) {
            this.node.active = false;
        }
    }

    /**
     * 親階層から Canvas ノードを探して返します。
     */
    private _findParentCanvas(): Node | null {
        let current: Node | null = this.node;
        while (current) {
            const canvas = current.getComponent(Canvas);
            if (canvas) {
                return current;
            }
            current = current.parent;
        }
        return null;
    }

    /**
     * Canvas の右下を基準に、marginRight / marginBottom を反映した位置にノードを移動します。
     */
    private _applyBottomRightPosition(): void {
        if (!this._canvasNode) {
            return;
        }
        const canvasTransform = this._canvasNode.getComponent(UITransform);
        if (!canvasTransform) {
            warn('[LoadingSpinner] 親 Canvas に UITransform がありません。位置の自動調整が行えません。');
            return;
        }

        const canvasWidth = canvasTransform.width;
        const canvasHeight = canvasTransform.height;

        // アンカーを右下に固定(0,0 が左下、1,1 が右上)
        if (this._uiTransform) {
            this._uiTransform.anchorX = 1;
            this._uiTransform.anchorY = 0;
        }

        // Canvas のローカル座標系で右下から margin を引いた位置に配置
        const x = canvasWidth / 2 - this.marginRight;
        const y = -canvasHeight / 2 + this.marginBottom;
        this.node.setPosition(x, y, 0);
    }

    /**
     * 親 Canvas のスケールを打ち消し、スピナー自身のスケールを (1,1,1) に保ちます。
     */
    private _applyScaleLock(): void {
        if (!this._canvasNode) {
            return;
        }
        const parentScale = this._canvasNode.getScale();
        const sx = parentScale.x === 0 ? 1 : 1 / parentScale.x;
        const sy = parentScale.y === 0 ? 1 : 1 / parentScale.y;
        const sz = parentScale.z === 0 ? 1 : 1 / parentScale.z;
        this.node.setScale(new Vec3(sx, sy, sz));
    }
}

コードのポイント解説

  • onLoad
    • UITransformSpritegetComponent で取得し、存在しない場合は warn を出しています。
    • 親階層から Canvas を探索し、右下配置やスケール補正に使うためにキャッシュしています。
  • start
    • initialVisible に応じて node.active を初期化します。
    • useCanvasBottomRighttrue の場合に _applyBottomRightPosition() で右下に自動配置します。
    • lockToUIScaletrue の場合に _applyScaleLock() でスケールを補正します。
  • update
    • lockToUIScale が有効なら、毎フレームスケール補正を行い、UI 解像度の変化にも追従します。
    • isSpinningfalse のとき:
      • autoHideWhenStoppedtrue なら node.active = false にして非表示。
      • 処理を抜けて回転は行いません。
    • isSpinningtrue のとき:
      • ノードが非表示なら active = true に戻します。
      • rotationSpeed * dt だけ angle を変化させて回転させます。
  • show / hide メソッド
    • 外部から簡単に spinner.show() / spinner.hide() で制御できるようにしています。
    • 他のカスタムスクリプトに依存せず、「ボタンのクリックイベント」や「非同期ロード完了コールバック」から直接呼び出せます。
  • _applyBottomRightPosition
    • 親 Canvas の幅・高さから、右下基準で marginRight, marginBottom を引いた位置を算出します。
    • アンカーを (1,0)(右下)に設定することで、スピナーの中心ではなく「右下の角」を基準に位置調整できるようにしています。
  • _applyScaleLock
    • 親 Canvas のスケールを取得し、その逆数をこのノードに設定することで、結果的に見た目のスケールが 1 倍になるようにしています。
    • Canvas のスケールが (1,1,1) であれば、そのまま (1,1,1) になります。

使用手順と動作確認

ここからは、実際に Cocos Creator 3.8.7 のエディタ上でこのコンポーネントを使う手順を説明します。

1. スクリプトファイルの作成

  1. Assets パネルで右クリックします。
  2. Create > TypeScript を選択します。
  3. ファイル名を LoadingSpinner.ts にします。
  4. 作成された LoadingSpinner.ts をダブルクリックして開き、先ほどの TypeScript コード全体を貼り付けて保存します。

2. テスト用の UI ノードを作成

  1. Hierarchy パネルで、まだ Canvas がない場合は
    • 右クリック → Create > UI > Canvas を選択して Canvas を作成します。
  2. Canvas の子として、スピナー用のノードを作成します。
    • Canvas を右クリック → Create > UI > Sprite を選択します。
    • 作成された Sprite ノードの名前を LoadingSpinnerNode など分かりやすい名前に変更します。
  3. スピナーの見た目となる画像を設定します。
    • Assets に用意した「ぐるぐるアイコン」の画像をインポートします(PNG など)。
    • LoadingSpinnerNode を選択し、Inspector の Sprite コンポーネントの Sprite Frame にその画像をドラッグ&ドロップします。
    • スピナーにしたい円形の画像などを使うとわかりやすいです。

3. LoadingSpinner コンポーネントをアタッチ

  1. Hierarchy で LoadingSpinnerNode を選択します。
  2. Inspector で Add Component ボタンをクリックします。
  3. Custom カテゴリから LoadingSpinner を選択して追加します。
    • もし一覧に見つからない場合は、スクリプトの @ccclass('LoadingSpinner') 名とファイル名が一致しているか確認し、一度エディタを保存/再読み込みしてみてください。

4. プロパティの設定例

LoadingSpinner コンポーネントを選択した状態で、Inspector から以下のように設定してみます。

  • isSpinning: true
  • autoHideWhenStopped: true
  • rotationSpeed: 240
    • 少し速めに回したい場合の例です。
  • useCanvasBottomRight: true
  • marginRight: 40
  • marginBottom: 40
  • lockToUIScale: true
  • initialVisible: true

この設定で、ゲームを再生すると、画面右下にスピナーが表示され、クルクル回転します。

5. シーン再生での動作確認

  1. エディタ上部の「▶」ボタンを押して、シーンを再生します。
  2. Game ビューで、画面右下にスピナーが見えることを確認します。
  3. 回転速度を確認し、必要であれば rotationSpeed を調整します。
    • 例: 360 にすると 1 秒で 1 回転、720 にすると 0.5 秒で 1 回転とかなり速くなります。
  4. 再生中に Inspector から isSpinning のチェックを外してみます。
    • autoHideWhenStopped = true の場合 → スピナーが非表示になります。
    • autoHideWhenStopped = false の場合 → 回転は止まりますが、アイコンは表示されたままになります。

6. 実際のロード処理と連動させる例

外部スクリプトに依存せず、単純に「他のスクリプトからこのコンポーネントを呼び出す」だけで連動できます。例えば、同じノードに別のスクリプトを付けて、ボタン操作で制御する場合:


import { _decorator, Component, Node } from 'cc';
import { LoadingSpinner } from './LoadingSpinner';
const { ccclass, property } = _decorator;

@ccclass('SpinnerTestController')
export class SpinnerTestController extends Component {

    @property(LoadingSpinner)
    spinner: LoadingSpinner | null = null;

    // ボタンのクリックイベントなどから呼ぶ
    public onStartLoading() {
        if (this.spinner) {
            this.spinner.show();
        }
    }

    public onFinishLoading() {
        if (this.spinner) {
            this.spinner.hide();
        }
    }
}

この SpinnerTestController はあくまで例であり、LoadingSpinner 自体はこのスクリプトがなくても完全に動作します。必要な場合だけ、他のスクリプトから show() / hide() を呼ぶ、という使い方ができます。


まとめ

今回作成した LoadingSpinner コンポーネントは、

  • Canvas 上の任意の UI ノードにアタッチするだけで「画面右下で回転するローディングアイコン」を実現できる。
  • 外部の GameManager やシングルトンに一切依存せず、このスクリプト単体で完結している。
  • 回転速度・表示/非表示・右下への自動配置・余白・スケール補正などをすべてインスペクタから調整できる。
  • show() / hide() メソッドにより、任意のロード処理と簡単に連動できる。

このような「アタッチするだけで UI の振る舞いが完結する汎用コンポーネント」を用意しておくと、

  • 各シーンごとにローディング UI を作り直す手間が省ける。
  • デザイナーやレベルデザイナーが、コードを書かずに Inspector から挙動を調整できる。
  • スピナーのデザインを差し替えるだけで、全シーンのロード演出を一括で変更できる。

といったメリットが得られ、Cocos Creator での UI 演出を効率よく統一できます。
このパターンを応用すれば、「左上にフェードインする通知」「中央にポップアップするチュートリアル」なども同様の設計で汎用コンポーネント化できます。まずはこの LoadingSpinner をベースに、プロジェクトごとの共通 UI ライブラリを育ててみてください。

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

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

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

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

Unity 6 C#スクリプト 100本ノック

新品価格
¥1,230から
(2025/12/29 10:28時点)

Cocos Creator100本ノックTypeScriptで書く!

新品価格
¥1,250から
(2025/12/29 10:32時点)

URLをコピーしました!