Cocos Creator 小游戏项目结构:Boot、场景、结算页这样拆
凌晨两点,盯着屏幕上报错的 “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: 创建项目目录结构
在 assets 下创建 Scenes、Scripts/managers、Scripts/layers、Scripts/components、Prefabs/UI、Prefabs/Game、Resources、bundles 等目录,按职责分离组织文件。 - 2
步骤2: 创建 Main.scene 和 GameManager
创建唯一主场景 Main.scene,在根层级创建 GameManager 节点并挂载脚本,在 onLoad() 中调用 game.addPersistRootNode(this.node) 设置为常驻节点。 - 3
步骤3: 创建四个 Layer 预制体
创建 BootLayer、MenuLayer、GameLayer、SettlementLayer 四个 Prefab,每个 Prefab 挂载对应逻辑脚本,统一放在 Prefabs/UI 目录下。 - 4
步骤4: 实现 UIManager 切换逻辑
在 UIManager.ts 中用 Map 存储所有 Layer 引用,实现 switchLayer(targetLayer) 方法,通过控制 active 属性切换界面,添加淡入动画提升体验。 - 5
步骤5: 实现 Boot Layer 加载流程
在 BootLayer.ts 中调用 resources.preloadDir() 预加载核心资源,更新进度条,加载完成后调用 UIManager 切换到 MenuLayer。
常见问题
小游戏为什么推荐单场景架构?
场景切换时数据怎么传递?
常驻节点放在哪个层级?
Boot Layer 的作用是什么?
微信小游戏首包超 4MB 怎么办?
8 分钟阅读 · 发布于: 2026年5月19日 · 修改于: 2026年5月19日
评论
使用 GitHub 账号登录后即可评论