切换语言
切换主题

Cocos Creator 小游戏项目结构:Boot、场景、结算页这样拆

1.9MB
引擎加载体积
Cocos Creator 引擎本身大小
53%
用户流失率
加载超过 3 秒的移动用户放弃比例
2秒内
优化后加载时间
单场景架构优化后的首屏时间
数据来源: CSDN 博客、腾讯云案例

凌晨两点,盯着屏幕上报错的 “scene not found”,我意识到这个小游戏项目的目录结构已经彻底失控了。

三个月前我信心满满开工:菜单一个场景,游戏主界面一个场景,结算页再来一个场景——听起来挺合理的,对吧?结果呢?场景切换时玩家分数莫名清零,loading 动画卡成 PPT,最惨的是某个关卡加载要等 5 秒,用户还没看到游戏画面就流失了一半。

那时候我才明白:小游戏架构不是随便堆几个 scene 文件就完事的。后来我重构了整个项目,改成单场景 + 四层 Layer 的架构,加载时间从 5 秒缩到 2 秒以内,场景切换丝滑得像抹了油。

为什么小游戏推荐单场景架构

说实话,我最早做小游戏的时候压根没想过”架构”这回事。菜单、游戏、结算——每个界面单独一个 scene 文件,简单粗暴。

跑起来才发现问题一堆:

切换开销。每次 director.loadScene(),引擎要销毁旧场景、创建新场景、重新加载资源。这个过程对小游戏来说是灾难——53% 的移动用户会放弃加载超过 3 秒的页面,而多场景切换的等待时间轻松就能超过这个阈值。

状态丢失。玩家打完一关,分数 1200,切换到结算页——数据没了。为什么?场景切换会销毁所有节点,除非你把数据存到全局变量或者常驻节点里(后面会讲怎么做)。

动画中断。菜单有个酷炫的 logo 动画还没播完,用户点了开始按钮,场景切换——动画硬生生被掐断,观感极差。

单场景架构的优势就摆在这:所有 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 目录只放一个文件。Main.scene 是唯一的主场景,里面包含 Canvas、GameManager、各个 Layer 等节点。别再往这个目录塞其他 scene 了。

managers 放管理类脚本。GameManager 用 game.addPersistRootNode() 设置为常驻节点,负责存储分数、关卡进度等数据;UIManager 负责 Layer 之间的切换逻辑。

layers 目录放各层 UI 的逻辑脚本。每个 Layer 对应一个 Prefab(在 Prefabs/UI 目录下),脚本挂到 Prefab 上,然后拖进 Main.scene 的 Canvas 节点下。

bundles 目录是为微信/抖音小游戏准备的。小游戏有首包体积限制,超出 4MB 的部分要用 Asset Bundle 分包加载。核心资源放 core bundle,关卡资源按需加载。

你的项目目录是这样组织的吗?如果不是,可能要考虑调整一下了。

Boot 场景:启动页怎么设计

先说个数据:Cocos Creator 引擎本身的体积大约 1.9MB,加上你的游戏资源,首屏加载时间默认是 3-5 秒。对于小游戏来说,这时间太长了——用户点开游戏,盯着黑屏或者空白页等了 3 秒,很多人直接就关掉了。

Boot Layer 就是用来解决这个问题的。它的职责很简单:

显示加载进度。让用户知道游戏正在加载,而不是卡死了。一个进度条或者百分比数字就行,别搞太花哨——这时候还没加载完图片资源呢。

预加载核心资源。用 resources.preload() 或者 Asset Bundle 的 loadBundle() 把接下来需要的图片、音频先加载好。

品牌展示。放个 logo,做个简单的动画(比如 logo 缩放渐入),顺便给品牌打个广告。

启动流程大概是这样:

引擎初始化 → 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,就可以替换默认的黑色启动页——加个 loading 动画或者品牌图片都行。

具体操作看官方论坛的教程:Creator | 自定义启动页之H5

Boot Layer 设计好了,接下来是整个架构的核心:四层 Layer 如何切换。

四层架构实现:从菜单到结算

这里是最核心的部分。整个主场景的结构是这样的:

Main.scene
├── Canvas (UI容器)
│   ├── BootLayer      → 加载进度、品牌展示
│   ├── MenuLayer      → 开始界面、关卡选择
│   ├── GameLayer      → 游戏主界面
│   └── SettlementLayer → 结算页(分数、时间、继续/重试)
├── GameManager (常驻节点)
│   └─ 存储:分数、关卡进度、玩家设置
└── AudioRoot (音频管理节点)

你看,四个 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() 这个 API 是 Cocos Creator 官方提供的,作用是让某个节点在场景切换时不被销毁。但我们是单场景架构,场景不切换,为什么还要常驻节点?

其实常驻节点在单场景架构里也有用:它可以作为全局的数据中心和事件中心。

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';
}

@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(微信小游戏用的是 wx.setStorageSync,但封装后的 localStorage 也能用)。

总结

说了这么多,最后给一个架构决策清单,方便你对照自己的项目:

如果你在做小游戏

  • 用单场景架构(一个 Main.scene)
  • 所有 UI 层放在 Canvas 下,用 Layer 切换
  • BootLayer 负责加载进度和预加载
  • GameManager 作为常驻节点存储数据
  • 目录结构按 assets/Scripts/managers/layers/Prefabs 组织

如果你在做大型游戏

  • 多场景是必要的(关卡多、资源大)
  • 但 UI 层可以用单场景 + Layer 的思路
  • 用 Asset Bundle 分包加载

这套架构我用在几个小游戏项目里,踩过坑、改过几轮,应该算比较成熟了。有问题的话可以留言,或者去 GitHub 找个示例项目对照看看。

下一篇打算聊聊 Layer 切换的动画效果——淡入淡出、滑动过渡、scale 缩放这些,让切换更丝滑。

搭建 Cocos Creator 小游戏单场景架构

从零开始搭建 Boot、主场景、结算页三层结构的小游戏项目架构

⏱️ 预计耗时: 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: 创建四个 Layer 预制体

    创建 BootLayer、MenuLayer、GameLayer、SettlementLayer 四个 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。

常见问题

小游戏为什么推荐单场景架构?
单场景架构避免了场景切换时的销毁重建开销、状态丢失和动画中断问题。通过控制 Layer 的 active 属性切换界面,状态自然保留,切换更流畅。
场景切换时数据怎么传递?
使用 GameManager 作为常驻节点存储跨界面数据。玩家分数、关卡进度等存在 GameManager 里,结算页通过 gameManager.getResult() 读取数据。
常驻节点放在哪个层级?
常驻节点要放在场景根层级,不能放在 Canvas 下面。Canvas 是 UI 容器,会随 Layer 切换被操作,常驻节点要独立存在。
Boot Layer 的作用是什么?
Boot Layer 负责显示加载进度、预加载核心资源、展示品牌 Logo。让用户知道游戏在加载而不是卡死,提升等待体验。
微信小游戏首包超 4MB 怎么办?
使用 Asset Bundle 分包加载。核心资源放 core bundle,关卡资源放 levels bundle,在 Boot Layer 中按需加载,首包只保留必要资源。

8 分钟阅读 · 发布于: 2026年5月19日 · 修改于: 2026年5月19日

评论

使用 GitHub 账号登录后即可评论