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

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

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

深夜2時、画面に表示された “scene not found” エラーを見つめながら、このミニゲームプロジェクトのディレクトリ構成が完全に制御不能になっていることに気づきました。

3ヶ月前、私は自信満々でプロジェクトを開始しました。メニュー用のシーン、ゲームメイン画面用のシーン、決済画面用のシーンと、それぞれ別のシーンを用意するのは合理的に思えました。しかし結果はどうでしょう?シーン切り替え時にプレイヤーのスコアがなぜかリセットされ、ローディングアニメーションはスライドショーのようにカクカクし、最悪なのはあるレベルの読み込みに5秒もかかり、ユーザーがゲーム画面を見る前に半分が離脱してしまいました。

その時ようやく理解しました。ミニゲームのアーキテクチャは、単にシーンファイルをいくつか配置するだけでは済まないことを。その後、プロジェクト全体をリファクタリングし、単一シーン + 4層 Layer アーキテクチャに変更しました。読み込み時間は5秒から2秒以内に短縮され、シーン切り替えは油を塗ったように滑らかになりました。

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

正直に言うと、最初にミニゲームを開発した時は「アーキテクチャ」なんて考える余裕もありませんでした。メニュー、ゲーム、決済と、各画面に別々のシーンファイルを用意するだけ。シンプルで分かりやすい。

しかし、実際に動かしてみると問題が山積みでした。

切り替えのオーバーヘッドdirector.loadScene() を呼び出すたびに、エンジンは古いシーンを破棄し、新しいシーンを作成し、リソースを再読み込みする必要があります。この処理はミニゲームにとって致命的です。53%のモバイルユーザーは、読み込みに3秒以上かかるページを放棄するというデータがあり、複数シーンの切り替えは簡単にこの閾値を超えてしまいます。

状態消失。プレイヤーがレベルをクリアし、スコア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 分包(微信ミニゲーム最適化)
    ├── core/                 # コアリソースパッケージ
    └── levels/               # レベルリソースパッケージ

いくつかの重要なポイント:

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

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

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

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

あなたのプロジェクトディレクトリはこのように整理されていますか?もし違うなら、調整を検討すべきかもしれません。

Boot シーン:起動画面のデザイン方法

まずデータを1つ紹介します。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 に切り替えるよう指示
        // コードは後で紹介
    }
}

微信ミニゲームの初期パッケージ最適化:ゲームを微信ミニゲームプラットフォームにリリースする場合、初期パッケージの制限は 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 を配置することで、デフォルトの黒い起動画面を置き換えられます。ローディングアニメーションやブランド画像を追加することも可能です。

詳細な操作は公式フォーラムのチュートリアルを参照してください: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() で検索する必要がなくなり、効率が向上します。

決済画面のデータ受け渡し:プレイヤーがレベルをクリアし、スコア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 の責任

プレイヤーデータの保存——スコア、レベル進捗、最高記録、アンロックしたレベルリスト。

設定データの保存——効果音オン/オフ、音楽オン/オフ、言語選択。

グローバルイベントの提供——例えば「レベル完了」イベント。各 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' | 'ja';
}

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

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

    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 をサポートしています(微信ミニゲームは wx.setStorageSync を使用しますが、カプセル化された localStorage も使用可能です)。

まとめ

ここまで説明してきましたが、最後にアーキテクチャ決定のチェックリストをまとめます。自分のプロジェクトと照らし合わせてみてください:

ミニゲームを開発している場合

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

大型ゲームを開発している場合

  • マルチシーンは必要(レベルが多く、リソースが大きい)
  • しかし UI レイヤーは単一シーン + Layer の考え方を適用可能
  • Asset Bundle で分包読み込み

このアーキテクチャは、いくつかのミニゲームプロジェクトで使用し、試行錯誤を重ねて改良してきました。問題がある場合はコメントを残すか、GitHub でサンプルプロジェクトを探して確認してみてください。

次回は Layer 切り替えのアニメーション効果について解説する予定です。フェードイン/フェードアウト、スライド遷移、スケール拡大/縮小など、切り替えをよりスムーズにする方法を紹介します。

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 は読み込み進捗の表示、コアリソースのプリロード、ブランドロゴの表示を担当します。ユーザーにゲームが読み込まれていることを知らせ、フリーズしていないことを示し、待ち時間の体験を向上させます。
微信ミニゲームの初期パッケージが4MBを超えたらどうする?
Asset Bundle で分包読み込みを使用します。コアリソースは core bundle に、レベルリソースは levels bundle に配置し、Boot Layer で必要に応じて読み込みます。初期パッケージには必要なリソースのみを保持します。

4 min read · 公開日: 2026年5月19日 · 更新日: 2026年5月19日

コメント

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