【Cocos Creator】アタッチするだけ!ScenePreloader (シーン先読み)の実装方法【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】ScenePreloader の実装:アタッチするだけで次のシーンを裏で先読みロードする汎用スクリプト

重いステージシーンを切り替えるとき、ロード画面で数秒待たされるのを避けたい場面は多いです。この記事では、任意のノードにアタッチするだけで、指定したシーンを裏で先読み(プリロード)しておき、必要なタイミングで素早くシーン遷移できる汎用コンポーネント ScenePreloader を実装します。
外部の GameManager などには一切依存せず、インスペクタの設定だけで完結するように設計します。


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

実現したい機能

  • 指定したシーン(または複数シーン)を、バックグラウンドでプリロードする。
  • プリロードの進捗(0〜1)を取得し、プログレスバーやラベルに反映できるようにする。
  • 一定のタイミング条件(時間経過・ボタン押下・自動)で、プリロード済みシーンへ遷移できるようにする。
  • 外部スクリプトに依存せず、インスペクタの設定だけで挙動をコントロールできる。
  • 防御的な実装として、設定ミス(シーン名未設定など)をログで知らせる。

設計アプローチ

Cocos Creator 3.8 では、director.preloadScenedirector.loadScene を使うことで、シーンのプリロードと実際のシーン遷移を分けて行えます。本コンポーネントはそれらをラップし、「いつ」「どのシーンを」「どういう条件でロードするか」をプロパティで柔軟に指定できるようにします。

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

今回の ScenePreloader では、以下のようなプロパティを用意します。

  • targetSceneName: string
    – プリロードしたいシーン名(Project → Scene フォルダにあるシーンの名前)を指定。
    – 空の場合は警告を出し、処理を行わない。
  • autoPreloadOnStart: boolean
    true: start() のタイミングで自動的にプリロードを開始。
    false: スクリプトから startPreload() を呼んだときのみプリロードする。
  • preloadDelay: number
    autoPreloadOnStart が有効な場合、start() から何秒後にプリロードを開始するか。
    – 0 なら即時開始。
  • autoLoadAfterPreload: boolean
    true: プリロード完了後、自動的に loadScene でシーン遷移する。
    false: 自動遷移せず、スクリプトから loadPreloadedScene() を呼ぶまで待機。
  • minShowTime: number
    – 自動遷移時に、現在のシーンを最低限何秒間は表示しておくか。
    – ローディング画面が一瞬で切り替わるのを防ぎたい場合などに使用。
    – 0 なら即時遷移可能。
  • logDetail: boolean
    true: プリロード開始・進捗・完了・エラーなどの詳細ログを出力。
    false: 必要最低限のログのみ。
  • exposeProgressToInspector: boolean
    true: インスペクタ上で現在の進捗(progress)を表示する(読み取り専用)。
    – UI バインドなどで進捗を確認するときに便利。

内部状態(インスペクタには出さない)

  • _isPreloading: boolean – 現在プリロード中かどうか。
  • _isPreloaded: boolean – プリロードが完了しているかどうか。
  • _progress: number – 0〜1 の進捗値。
  • _startTime: number – このコンポーネントが有効になった時間。
  • _preloadStartTime: number – プリロードを開始した時間。

この状態をもとに、update() 内で経過時間をチェックし、minShowTime を満たしたタイミングでシーン遷移を行います。


TypeScriptコードの実装


import { _decorator, Component, director } from 'cc';
const { ccclass, property } = _decorator;

/**
 * ScenePreloader
 * 任意のノードにアタッチするだけで、指定したシーンをバックグラウンドでプリロードし、
 * 条件に応じて素早くシーン遷移できる汎用コンポーネント。
 */
@ccclass('ScenePreloader')
export class ScenePreloader extends Component {

    @property({
        tooltip: 'プリロードしたいシーン名(Assets > Scene にあるシーンの名前)。\n空の場合は処理されません。'
    })
    public targetSceneName: string = '';

    @property({
        tooltip: 'true の場合、start() 時に自動でプリロードを開始します。'
    })
    public autoPreloadOnStart: boolean = true;

    @property({
        tooltip: 'autoPreloadOnStart が true のとき、start() から何秒後にプリロードを開始するか。',
        min: 0
    })
    public preloadDelay: number = 0;

    @property({
        tooltip: 'true の場合、プリロード完了後に自動でシーン遷移します。'
    })
    public autoLoadAfterPreload: boolean = false;

    @property({
        tooltip: '自動シーン遷移時に、現在のシーンを最低何秒間は表示しておくか(秒)。\n0 の場合は即時遷移可能です。',
        min: 0
    })
    public minShowTime: number = 0;

    @property({
        tooltip: '詳細なログ(プリロード開始・進捗・完了など)を出力するかどうか。'
    })
    public logDetail: boolean = false;

    @property({
        tooltip: 'true にすると、現在のプリロード進捗を Inspector 上で確認できます(読み取り専用)。'
    })
    public exposeProgressToInspector: boolean = true;

    @property({
        tooltip: '現在のプリロード進捗(0〜1)。読み取り専用のため、スクリプトからのみ更新されます。',
        readonly: true,
        visible: function (this: ScenePreloader) {
            return this.exposeProgressToInspector;
        }
    })
    public progress: number = 0;

    // 内部状態
    private _isPreloading: boolean = false;
    private _isPreloaded: boolean = false;
    private _preloadError: Error | null = null;
    private _startTime: number = 0;
    private _preloadStartTime: number = 0;

    onLoad() {
        this._startTime = performance.now() / 1000; // 秒に変換
        if (!this.targetSceneName) {
            console.warn('[ScenePreloader] targetSceneName が設定されていません。プリロードは行われません。', this.node.name);
        }
    }

    start() {
        if (this.autoPreloadOnStart) {
            if (!this.targetSceneName) {
                console.warn('[ScenePreloader] autoPreloadOnStart が有効ですが、targetSceneName が空です。', this.node.name);
                return;
            }
            if (this.preloadDelay > 0) {
                // 一定時間待ってからプリロード開始
                this.scheduleOnce(() => {
                    this.startPreload();
                }, this.preloadDelay);
            } else {
                this.startPreload();
            }
        }
    }

    update(deltaTime: number) {
        // 自動ロードが有効で、プリロード済み、かつエラーがない場合のみチェック
        if (this.autoLoadAfterPreload && this._isPreloaded && !this._preloadError) {
            const now = performance.now() / 1000;
            const elapsedSinceStart = now - this._startTime;
            const elapsedSincePreload = now - this._preloadStartTime;

            // minShowTime はコンポーネント開始からの時間で判定するほうが分かりやすいのでこちらでチェック
            if (elapsedSinceStart >= this.minShowTime) {
                // 一度だけロードする
                this.autoLoadAfterPreload = false; // 再度呼ばれないようにフラグを下げる
                this.loadPreloadedScene();
            }
        }
    }

    /**
     * シーンのプリロードを開始します。
     * autoPreloadOnStart が false の場合、他のスクリプトから手動で呼び出せます。
     */
    public startPreload(): void {
        if (!this.targetSceneName) {
            console.warn('[ScenePreloader] startPreload() が呼ばれましたが targetSceneName が空です。', this.node.name);
            return;
        }

        if (this._isPreloading || this._isPreloaded) {
            if (this.logDetail) {
                console.log('[ScenePreloader] すでにプリロード中、またはプリロード済みのため startPreload() は無視されました。', this.node.name);
            }
            return;
        }

        this._isPreloading = true;
        this._isPreloaded = false;
        this._preloadError = null;
        this.progress = 0;
        this._preloadStartTime = performance.now() / 1000;

        if (this.logDetail) {
            console.log(`[ScenePreloader] シーン "${this.targetSceneName}" のプリロードを開始します。`, this.node.name);
        }

        director.preloadScene(
            this.targetSceneName,
            (completedCount: number, totalCount: number, item: any) => {
                // 進捗コールバック
                if (totalCount > 0) {
                    this.progress = completedCount / totalCount;
                } else {
                    this.progress = 0;
                }
                if (this.logDetail) {
                    console.log(`[ScenePreloader] プリロード進捗: ${completedCount}/${totalCount} (${(this.progress * 100).toFixed(1)}%)`, this.node.name);
                }
            },
            (error: Error | null) => {
                // 完了コールバック
                this._isPreloading = false;
                if (error) {
                    this._preloadError = error;
                    console.error(`[ScenePreloader] シーン "${this.targetSceneName}" のプリロードに失敗しました:`, error);
                    return;
                }
                this._isPreloaded = true;
                this.progress = 1;
                if (this.logDetail) {
                    console.log(`[ScenePreloader] シーン "${this.targetSceneName}" のプリロードが完了しました。`, this.node.name);
                }
            }
        );
    }

    /**
     * すでにプリロード済みのシーンをロードします。
     * プリロードされていない場合は、通常の loadScene として動作します。
     */
    public loadPreloadedScene(): void {
        if (!this.targetSceneName) {
            console.warn('[ScenePreloader] loadPreloadedScene() が呼ばれましたが targetSceneName が空です。', this.node.name);
            return;
        }

        if (this._preloadError) {
            console.error('[ScenePreloader] プリロード時にエラーが発生しているため、シーン遷移を行いません。', this._preloadError);
            return;
        }

        if (this.logDetail) {
            console.log(`[ScenePreloader] シーン "${this.targetSceneName}" へ遷移します。`, this.node.name);
        }

        director.loadScene(this.targetSceneName, (error?: Error | null) => {
            if (error) {
                console.error(`[ScenePreloader] シーン "${this.targetSceneName}" のロードに失敗しました:`, error);
            } else if (this.logDetail) {
                console.log(`[ScenePreloader] シーン "${this.targetSceneName}" への遷移が完了しました。`);
            }
        });
    }

    /**
     * 現在プリロード中かどうかを返します。
     */
    public get isPreloading(): boolean {
        return this._isPreloading;
    }

    /**
     * プリロードが完了しているかどうかを返します。
     */
    public get isPreloaded(): boolean {
        return this._isPreloaded;
    }

    /**
     * 進捗値(0〜1)を返します。
     */
    public getProgress(): number {
        return this.progress;
    }
}

コードのポイント解説

  • onLoad()
    – コンポーネントの開始時間を記録し、targetSceneName が空の場合は警告ログを出します。
    – ここではまだプリロードは行いません。
  • start()
    autoPreloadOnStarttrue なら、preloadDelay 秒後に startPreload() を呼び出します。
    – シーン名が未設定の場合は警告を出して処理を中止します。
  • update()
    autoLoadAfterPreload が有効で、かつプリロード完了後に、minShowTime 秒以上経過していれば loadPreloadedScene() を一度だけ呼びます。
  • startPreload()
    – プリロードの開始メソッド。インスペクタからの自動起動だけでなく、他のスクリプトからも呼べるように public にしています。
    – すでにプリロード中/完了済みの場合は二重実行を防ぎます。
    director.preloadScene の進捗コールバックで progress を更新し、ログ出力を行います。
  • loadPreloadedScene()
    – プリロード済みシーンへの遷移を行います。
    – プリロードエラーがあれば遷移せず、エラーログを出力します。
    – プリロードされていない場合でも director.loadScene はその場でロードしてくれるので、「最低限動く」挙動になります。
  • getters
    isPreloading / isPreloaded / getProgress() により、他のスクリプトから状態を簡単に参照できます。

使用手順と動作確認

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

  1. エディタの Assets パネルで右クリックします。
  2. Create → TypeScript を選択します。
  3. ファイル名を ScenePreloader.ts にします。
  4. 作成された ScenePreloader.ts をダブルクリックして開き、
    既存のテンプレートコードをすべて削除して、前述の ScenePreloader のコードを丸ごと貼り付けます。
  5. 保存します(Ctrl+S / Cmd+S)。

2. テスト用シーンの準備

ここでは例として、「Title」シーンから「Stage1」シーンへプリロードして遷移する流れを想定します。

  1. Title シーンStage1 シーン を作成しておきます。
    – メニューから File → New Scene で作成し、
    Assets/Scene フォルダに Title.scene, Stage1.scene として保存します。
  2. Title シーンを開きます。

3. ScenePreloader コンポーネントのアタッチ

  1. Hierarchy で、空のノードを作成します。
    – Hierarchy パネルで右クリック → Create → Empty Node
    – ノード名を ScenePreloaderNode などに変更します。
  2. ScenePreloaderNode を選択し、Inspector パネルを開きます。
  3. Inspector の下部で Add Component ボタンをクリックします。
  4. Custom → ScenePreloader を選択してアタッチします。

4. プロパティの設定例

Title シーンから Stage1 シーンを先読みして、自動で遷移する例を設定します。

  1. targetSceneName: Stage1
    – 実際に保存しているシーン名と完全一致させてください(拡張子は不要)。
  2. autoPreloadOnStart: true
    – Title シーンが開始されたら自動でプリロードを開始します。
  3. preloadDelay: 0
    – 即座にプリロードを開始します。
    – 例えば 1.5 にすれば、1.5 秒経過後にプリロードを開始します。
  4. autoLoadAfterPreload: true
    – プリロードが終わったら自動で Stage1 シーンに遷移します。
  5. minShowTime: 2
    – Title シーンを最低 2 秒間は表示してから遷移します。
    – プリロードが 0.5 秒で終わっても、2 秒経つまでは Title シーンに留まります。
  6. logDetail: true
    – コンソールにプリロードの開始・進捗・完了ログが出るので動作確認しやすくなります。
  7. exposeProgressToInspector: true
    – 再生中に Inspector から progress の値を確認できます。

5. 実行して動作確認

  1. Title シーンを保存します。
  2. エディタ右上の Play ボタン(または Preview in Browser)で再生します。
  3. 再生中に Console パネルを開き、
    – 「プリロードを開始します」
    – 「プリロード進捗: …」
    – 「プリロードが完了しました」
    といったログが出ていることを確認します(logDetail = true の場合)。
  4. 2 秒経過後、自動で Stage1 シーンに切り替わることを確認します。

6. 手動でシーン遷移させるパターン

自動遷移ではなく、「プリロードだけしておき、ボタンが押されたら遷移」したい場合は次のように設定します。

  • autoLoadAfterPreload: false
  • minShowTime: 任意(自動遷移しないので実質無視されます)

そして、例えばボタンのクリックイベントから以下のように呼び出します。


// 例: ボタンクリックスクリプト内
import { _decorator, Component, Node } from 'cc';
import { ScenePreloader } from './ScenePreloader';
const { ccclass, property } = _decorator;

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

    @property({ tooltip: '同じシーン内の ScenePreloader コンポーネントが付いたノードを指定します。' })
    public preloaderNode: Node | null = null;

    private _preloader: ScenePreloader | null = null;

    start() {
        if (this.preloaderNode) {
            this._preloader = this.preloaderNode.getComponent(ScenePreloader);
            if (!this._preloader) {
                console.error('[GoToStageButton] preloaderNode に ScenePreloader コンポーネントが見つかりません。');
            }
        } else {
            console.warn('[GoToStageButton] preloaderNode が設定されていません。');
        }
    }

    // ボタンのクリックイベントにこのメソッドを登録する
    public onClickGo() {
        if (!this._preloader) {
            console.error('[GoToStageButton] ScenePreloader が取得できていないため、シーン遷移できません。');
            return;
        }

        if (!this._preloader.isPreloaded) {
            console.warn('[GoToStageButton] まだプリロードが完了していません。即時ロードします。');
        }
        this._preloader.loadPreloadedScene();
    }
}

このように、プリロードと遷移のタイミングを完全に分離して制御できます。


まとめ

本記事では、Cocos Creator 3.8 / TypeScript で、任意のノードにアタッチするだけでシーンの先読みを行える汎用コンポーネント ScenePreloader を実装しました。

  • targetSceneName を指定するだけで、重いステージシーンを事前に読み込んでおける
  • autoPreloadOnStart, autoLoadAfterPreload, minShowTime により、ロードタイミングと演出時間を柔軟に調整できる。
  • progress プロパティを利用すれば、プログレスバーやローディングテキストへの連携も容易。
  • 外部の GameManager などに依存せず、このスクリプト単体で完結しているため、どのプロジェクトにも簡単に持ち込める。

ステージ選択画面からの遷移、タイトル → メインゲームへの移行、あるいはマップ間移動など、シーン切り替えのたびに発生する待ち時間を目立たなくする用途で幅広く使えます。
ゲームのレスポンスを改善したいときは、まずこのようなプリロードコンポーネントを導入して、シーン設計を見直してみてください。

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をコピーしました!