言語を切り替える
テーマを切り替える

Cocos Creator ミニゲームのプロジェクト構成:Boot、シーン、リザルト画面の分割方法

1.9MB
エンジン読み込み容量
Cocos Creator エンジン本体のサイズ
53%
ユーザー離脱率
読み込みが 3 秒を超えるモバイルユーザーの離脱率
2秒以内
最適化後の読み込み時間
単一シーンアーキテクチャ最適化後の初回画面表示時間
数据来源: CSDN ブログ、Tencent Cloud ケース

画面に “scene not found” と表示され、このミニゲームのディレクトリ構成が完全に破綻していることに気づきました。

3 か月前、意気込んで開発を始めた頃は、メニューに 1 シーン、ゲーム本編に 1 シーン、リザルト画面にもう 1 シーン——理にかなっているように聞こえますよね? 結果は、シーン切り替えのたびにプレイヤーのスコアが謎のゼロに、loading アニメーションはカクカク。最悪だったのは、あるステージの読み込みに 5 秒もかかり、ゲーム画面を見る前にユーザーの半分が離脱したことです。

そのとき初めて分かりました。ミニゲームのアーキテクチャは、scene ファイルを適当に並べれば済む話ではない。プロジェクト全体を「単一シーン+4 層 Layer」にリファクタリングしたところ、読み込み時間は 5 秒から 2 秒以内に短縮。画面切り替えは驚くほど滑らかになりました。

ミニゲームで単一シーンアーキテクチャが推奨される理由

正直、最初にミニゲームを作ったときは「アーキテクチャ」なんて考えていませんでした。メニュー、ゲーム、リザルト——各画面に別々の scene ファイル。シンプルで分かりやすい。

動かしてみると、問題が次々と出てきます。

切り替えコストdirector.loadScene() のたびに、エンジンは旧シーンを破棄し、新シーンを生成し、リソースを再読み込みします。ミニゲームにとってこれは致命的——読み込みが 3 秒を超えるページでは、モバイルユーザーの 53% が離脱します。マルチシーン構成だと、切り替え待ちだけで簡単にこの閾値を超えます。

状態の消失。1 ステージクリア、スコア 1200、リザルト画面へ切り替え——データが消える。なぜか? シーン切り替えですべてのノードが破棄されるからです。グローバル変数か常駐ノードに保存しない限り、データは残りません(後述します)。

アニメーションの中断。メニューのロゴアニメーションがまだ再生中なのに、ユーザーがスタートを押してシーン切り替え——アニメーションが途中で切れ、見栄えが最悪です。

単一シーンアーキテクチャの利点は明確です。すべての UI 層が同じシーン内にあり、切り替えは Layer の active プロパティを変えるだけ。破棄・再生成のコストがなく、状態も自然に保持され、アニメーションも中断されません。ミニゲームは画面数が少ないことが多く、この構成で十分です。大規模ゲームは別——ステージが多く、リソースも大きいので、マルチシーン+分包読み込みが必要です。ミニゲームなら、単一シーンで足ります。

プロジェクトディレクトリ構成テンプレート

単一シーンを選ぶ理由が分かったところで、ディレクトリの整理方法を見ていきましょう。

何度も失敗してまとめたテンプレートです(Cocos Creator 3.x を例に):

assets/
├── Scenes/
│   └── Main.scene           # 唯一のメインシーン
├── Scripts/
│   ├── managers/
│   │   ├── GameManager.ts   # ゲーム状態、データ保存
│   │   ├── UIManager.ts     # Layer 切り替えロジック
│   │   └── AudioManager.ts  # 効果音管理
│   ├── layers/
│   │   ├── BootLayer.ts     # 起動画面ロジック
│   │   ├── MenuLayer.ts     # メインメニューロジック
│   │   ├── GameLayer.ts     # ゲーム本編
│   │   └── SettlementLayer.ts  # リザルト画面
│   └── components/
│       ├── PlayerController.ts
│       └── EnemyAI.ts
├── Prefabs/
│   ├── UI/
│   │   ├── BootPanel.prefab    # 起動画面 UI プレハブ
│   │   ├── MenuPanel.prefab    # メニュー UI
│   │   ├── SettlementPanel.prefab
│   ├── Game/
│   │   ├── Player.prefab
│   │   ├── Obstacle.prefab
│   └── Effects/
│       └── Explosion.prefab
├── Resources/
│   ├── textures/             # 動的読み込み画像
│   ├── audio/                # 効果音ファイル
│   └── fonts/
└── bundles/                  # Asset Bundle 分包(WeChat ミニゲーム最適化)
    ├── core/                 # コアリソースパック
    └── levels/               # ステージリソースパック

いくつかのポイント:

Scenes ディレクトリには 1 ファイルだけ。Main.scene が唯一のメインシーンで、Canvas、GameManager、各 Layer などのノードを含みます。このディレクトリに他の scene を入れないでください。

managers に管理クラススクリプトを配置。GameManager は game.addPersistRootNode() で常駐ノードに設定し、スコアやステージ進捗などのデータを保持。UIManager は Layer 間の切り替えロジックを担当します。

layers ディレクトリに各 UI 層のロジックスクリプト。各 Layer は Prefab(Prefabs/UI 配下)に対応し、スクリプトを Prefab にアタッチして Main.scene の Canvas ノード下に配置します。

bundles ディレクトリは WeChat/Douyin ミニゲーム向け。初回パッケージにサイズ制限があり、4MB を超える部分は Asset Bundle で分包読み込みします。コアリソースは core bundle に、ステージリソースは必要に応じて読み込みます。

あなたのプロジェクトもこの構成ですか? 違うなら、見直しを検討してみてください。

Boot シーン:起動画面の設計

まず数字から。Cocos Creator エンジン本体は約 1.9MB、ゲームリソースを足すと初回読み込みはデフォルトで 3〜5 秒。ミニゲームには長すぎます——ユーザーがゲームを開いて、黒画面や空白を 3 秒見つめたら、多くの人はそのまま閉じてしまいます。

Boot Layer はこの問題を解決します。役割はシンプルです。

読み込み進捗の表示。ゲームが読み込まれていることを伝え、フリーズしていないことを示す。プログレスバーかパーセント表示で十分——この段階では画像リソースがまだ読み込まれていません。

コアリソースのプリロードresources.preload() や Asset Bundle の loadBundle() で、これから必要になる画像・音声を先に読み込みます。

ブランド表示。ロゴを置き、シンプルなアニメーション(例:ロゴのスケールフェードイン)でブランドを印象づけます。

起動フローのイメージ:

エンジン初期化 → Boot Layer 表示 → コアリソースプリロード → 読み込み完了 → MenuLayer へ切り替え

コード例(BootLayer.ts):

import { _decorator, Component, Node, resources, ProgressBar } from 'cc';
const { ccclass, property } = _decorator;

@ccclass('BootLayer')
export class BootLayer extends Component {
    @property(ProgressBar)
    progressBar: ProgressBar | null = null;

    start() {
        // コアリソースをプリロード
        this.preloadCoreAssets();
    }

    preloadCoreAssets() {
        resources.preloadDir('textures', (err, items) => {
            if (err) {
                console.error('プリロード失敗:', err);
                return;
            }
            // 読み込み完了、メニューへ切り替え
            this.switchToMenu();
        });
    }

    updateProgress(current: number, total: number) {
        if (this.progressBar) {
            this.progressBar.progress = current / total;
        }
    }

    switchToMenu() {
        // UIManager に MenuLayer への切り替えを指示
        // コードは後述
    }
}

WeChat ミニゲーム初回パッケージ最適化:WeChat ミニゲームに公開する場合、初回パッケージは 4MB 制限があります。超過分は Asset Bundle で分包します。Boot Layer 内で bundle を読み込めます:

assetManager.loadBundle('levels', (err, bundle) => {
    if (err) return;
    console.log('ステージパック読み込み完了');
});

H5 プラットフォームのカスタム起動画面:Cocos Creator には build-templates 機構があり、H5 公開後の起動画面をカスタマイズできます。プロジェクトルートに build-templates/web-mobile を作成し、カスタム index.html を置けば、デフォルトの黒い起動画面を置き換えられます——loading アニメーションやブランド画像を追加できます。

詳細は公式フォーラムのチュートリアルを参照:Creator | 自定义启动页之H5

Boot Layer が整ったら、次はアーキテクチャの核心——4 層 Layer の切り替え方法です。

4 層アーキテクチャの実装:メニューからリザルトまで

ここが最も重要な部分です。メインシーン全体の構造:

Main.scene
├── Canvas (UI コンテナ)
│   ├── BootLayer      → 読み込み進捗、ブランド表示
│   ├── MenuLayer      → スタート画面、ステージ選択
│   ├── GameLayer      → ゲーム本編
│   └── SettlementLayer → リザルト画面(スコア、時間、続ける/リトライ)
├── GameManager (常駐ノード)
│   └─ 保存:スコア、ステージ進捗、プレイヤー設定
└── AudioRoot (音声管理ノード)

4 つの Layer はすべて同じ Canvas 配下にあります。切り替え時は特定 Layer の active プロパティを変えるだけ——他の Layer は存在したままなので、状態も失われず、アニメーションも中断されません。

Layer 切り替えの核心ロジックは UIManager にあります:

// UIManager.ts
import { _decorator, Component, Node, tween, Vec3 } from 'cc';
const { ccclass, property } = _decorator;

@ccclass('UIManager')
export class UIManager extends Component {
    private layers: Map<string, Node> = new Map();
    private currentLayer: string = 'BootLayer';

    onLoad() {
        // すべての Layer ノードを収集
        const canvas = this.node.getChildByName('Canvas');
        if (!canvas) return;

        canvas.children.forEach(child => {
            if (child.name.endsWith('Layer')) {
                this.layers.set(child.name, child);
                child.active = false; // 初期状態はすべて非表示
            }
        });

        // BootLayer を表示
        this.switchLayer('BootLayer');
    }

    switchLayer(targetLayer: string) {
        // 現在の層を非表示
        const current = this.layers.get(this.currentLayer);
        if (current) {
            current.active = false;
        }

        // 目標の層を表示
        const target = this.layers.get(targetLayer);
        if (target) {
            target.active = true;
            // フェードインアニメーション(任意)
            this.playFadeIn(target);
        }

        this.currentLayer = targetLayer;
    }

    playFadeIn(node: Node) {
        // シンプルなスケールフェードイン
        node.setScale(new Vec3(0.9, 0.9, 1));
        tween(node)
            .to(0.2, { scale: new Vec3(1, 1, 1) })
            .start();
    }
}

ポイント:Map ですべての Layer 参照を保持すれば、切り替えのたびに getChildByName() で探す必要がなく、効率が上がります。

リザルト画面へのデータ受け渡し:1 ステージクリア、スコア 1200、プレイ時間 45 秒——このデータをリザルト画面へどう渡す?

答えは GameManager です。

GameManager は常駐ノードで、ゲーム全体を通じてライフサイクルが続きます。GameLayer でステージをクリアしたら、スコアと時間を GameManager に保存。SettlementLayer へ切り替えたら、GameManager からデータを取得して表示します。

// GameManager.ts(簡易版)
import { _decorator, Component, game } from 'cc';
const { ccclass, property } = _decorator;

@ccclass('GameManager')
export class GameManager extends Component {
    public currentScore: number = 0;
    public currentLevel: number = 1;
    public playTime: number = 0;

    onLoad() {
        // 常駐ノードに設定
        game.addPersistRootNode(this.node);
    }

    // GameLayer から呼び出してデータを保存
    saveResult(score: number, time: number) {
        this.currentScore = score;
        this.playTime = time;
    }

    // SettlementLayer から呼び出してデータを取得
    getResult() {
        return {
            score: this.currentScore,
            time: this.playTime
        };
    }
}

SettlementLayer 表示時に GameManager から直接読み取ります:

// SettlementLayer.ts
onEnable() {
    const gameManager = find('GameManager')?.getComponent(GameManager);
    if (!gameManager) return;

    const result = gameManager.getResult();
    this.scoreLabel.string = `スコア: ${result.score}`;
    this.timeLabel.string = `プレイ時間: ${result.time}秒`;
}

これでデータ受け渡しは解決です。複雑なイベントシステムは不要——GameManager がすべて受け持ちます。

常駐ノード:Layer 間のデータ受け渡し

上でも触れましたが、常駐ノードについてもう少し詳しく説明します。

game.addPersistRootNode() は Cocos Creator 公式 API で、シーン切り替え時にノードを破棄しないようにします。単一シーン構成ではシーン自体は切り替わりませんが、常駐ノードは依然として有用——グローバルなデータセンター兼イベントハブとして機能します。

GameManager の役割

プレイヤーデータの保存——スコア、ステージ進捗、最高記録、解放済みステージ一覧。
設定データの保存——効果音 ON/OFF、BGM ON/OFF、言語選択。
グローバルイベントの提供——例:「ステージクリア」イベントを各 Layer が購読して反応。

注意点

常駐ノードは Canvas の下に置けません。Canvas は UI コンテナで、Layer の active 状態に連動して操作されます。常駐ノードは独立し、シーンのルート階層に配置します。

常駐ノードは手動で作成します。Main.scene に空ノード “GameManager” を作成し、GameManager スクリプトをアタッチ。onLoad()game.addPersistRootNode(this.node) を呼び出します。

常駐ノードは慎重に使いましょう。すべてを詰め込まず、画面間で本当に共有が必要なデータだけを保存。各 Layer 固有の UI 状態(ボタンの表示有無など)は GameManager に入れず、Layer 自身で管理します。

完全なデータ構造の例:

// GameManager.ts(完全版)
interface PlayerData {
    highestScore: number;
    unlockedLevels: number[];
    currentLevel: number;
}

interface GameSettings {
    soundEnabled: boolean;
    musicEnabled: boolean;
    language: 'zh' | 'en';
}

@ccclass('GameManager')
export class GameManager extends Component {
    private playerData: PlayerData = {
        highestScore: 0,
        unlockedLevels: [1],
        currentLevel: 1
    };

    private settings: GameSettings = {
        soundEnabled: true,
        musicEnabled: true,
        language: 'zh'
    };

    onLoad() {
        game.addPersistRootNode(this.node);
        // ローカルストレージからデータを読み込み
        this.loadData();
    }

    loadData() {
        const saved = localStorage.getItem('playerData');
        if (saved) {
            this.playerData = JSON.parse(saved);
        }
    }

    saveData() {
        localStorage.setItem('playerData', JSON.stringify(this.playerData));
    }

    // getters と setters...
}

ここでは localStorage の読み書きも追加し、プレイヤーデータをローカルに永続化しています。ミニゲーム各プラットフォームは localStorage をサポートします(WeChat ミニゲームは wx.setStorageSync ですが、ラップ後の localStorage も使えます)。

まとめ

最後に、アーキテクチャ判断のチェックリストです。自分のプロジェクトと照らし合わせてみてください。

ミニゲームを作っている場合

  • 単一シーンアーキテクチャ(Main.scene 1 つ)
  • すべての UI 層を Canvas 下に置き、Layer で切り替え
  • BootLayer で読み込み進捗とプリロードを担当
  • GameManager を常駐ノードとしてデータを保存
  • ディレクトリは assets/Scripts/managers/layers/Prefabs で整理

大規模ゲームを作っている場合

  • マルチシーンが必要(ステージ多・リソース大)
  • ただし UI 層は単一シーン+ Layer の考え方を流用可能
  • Asset Bundle で分包読み込み

このアーキテクチャは、いくつかのミニゲームプロジェクトで実践し、何度も修正を重ねたものです。質問があればコメントをどうぞ。GitHub のサンプルプロジェクトと照らし合わせるのもおすすめです。

次回は Layer 切り替えのアニメーション——フェードイン/アウト、スライド遷移、scale 拡大縮小など、より滑らかな切り替えについて書く予定です。

Cocos Creator ミニゲーム単一シーンアーキテクチャの構築

ゼロから Boot、メインシーン、リザルト画面の 3 層構造を持つミニゲームプロジェクトアーキテクチャを構築

⏱️ 目安時間: 30 分

  1. 1

    ステップ1: プロジェクトディレクトリ構成の作成

    assets 配下に Scenes、Scripts/managers、Scripts/layers、Scripts/components、Prefabs/UI、Prefabs/Game、Resources、bundles などのディレクトリを作成し、責務ごとにファイルを整理します。
  2. 2

    ステップ2: Main.scene と GameManager の作成

    唯一のメインシーン Main.scene を作成し、ルート階層に GameManager ノードを作成してスクリプトをアタッチ。onLoad() で game.addPersistRootNode(this.node) を呼び出して常駐ノードに設定します。
  3. 3

    ステップ3: 4 つの Layer プレハブの作成

    BootLayer、MenuLayer、GameLayer、SettlementLayer の 4 つの Prefab を作成し、各 Prefab に対応するロジックスクリプトをアタッチ。Prefabs/UI ディレクトリに統一して配置します。
  4. 4

    ステップ4: UIManager 切り替えロジックの実装

    UIManager.ts 内で Map ですべての Layer 参照を保存し、switchLayer(targetLayer) メソッドを実装。active プロパティで画面を切り替え、フェードインアニメーションを追加して体験を向上させます。
  5. 5

    ステップ5: Boot Layer 読み込みフローの実装

    BootLayer.ts 内で resources.preloadDir() を呼び出してコアリソースをプリロードし、プログレスバーを更新。読み込み完了後に UIManager を呼び出して MenuLayer に切り替えます。

FAQ

ミニゲームで単一シーンアーキテクチャが推奨される理由は?
単一シーンアーキテクチャは、シーン切り替え時の破棄・再作成のオーバーヘッド、状態消失、アニメーション中断を回避できます。Layer の active プロパティを制御して画面を切り替えるため、状態が自然に保持され、切り替えもスムーズです。
シーン切り替え時にデータはどう受け渡す?
GameManager を常駐ノードとして画面間のデータを保存します。プレイヤーのスコアやレベル進捗などは GameManager に保存し、リザルト画面では gameManager.getResult() でデータを読み取ります。
常駐ノードはどの階層に配置する?
常駐ノードはシーンのルート階層に配置する必要があります。Canvas の下には置けません。Canvas は UI コンテナであり、Layer 切り替えに伴って操作されるため、常駐ノードは独立して存在させます。
Boot Layer の役割は?
Boot Layer は読み込み進捗の表示、コアリソースのプリロード、ブランドロゴの表示を担当します。ゲームが読み込まれていることをユーザーに伝え、フリーズしていないことを示し、待ち時間の体験を向上させます。
WeChat ミニゲームの初回パッケージが 4MB を超えたらどうする?
Asset Bundle で分包読み込みを使用します。コアリソースは core bundle に、ステージリソースは levels bundle に配置し、Boot Layer で必要に応じて読み込みます。初回パッケージには必要なリソースのみを残します。

4分で読めます · 公開日: 2026年5月19日 · 更新日: 2026年6月8日

コメント

GitHubアカウントでログインしてコメントできます