ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

CocosCreator大厅子游戏架构:多Bundle工程搭建、通信与构建避坑指南

CocosCreator大厅子游戏架构:多Bundle工程搭建、通信与构建避坑指南 简介面向游戏开发者的CocosCreator大厅子游戏整合demo演示了如何在CocosCreator中构建游戏大厅并接入多个可独立热更的子游戏。这种设计适用于在线游戏平台、多关卡或多种玩法组合的项目。资源共64个文件压缩包约7.09MB以js脚本、json配置、fire场景及png/jpg图片素材为主并包含meta、ttf字体与bat构建脚本覆盖了项目从逻辑、配置到资源组织的完整基础结构。目前已有942人学习浏览适合正在学习Cocos Creator模块化开发、热更新和资源管理的开发者。从资源目录结构可以直观看到大厅与各子游戏分目录存放并配有build、tools等辅助模块对照代码可重点学习场景切换与事件监听、动态加载策略、独立子游戏的热更配置以及整包打包时的资源组织与性能优化思路为后续搭建更复杂的多游戏整合项目提供可复用的参考框架。1. 大厅子游戏架构是什么先想清楚再动手写 demo我在做了一轮 CocosCreator 大厅子游戏笔记 demo 之后最大的感受是这东西卡人的点从来不是引擎 API而是“多项目整合”之后所有工程约定都要自己定。所谓大厅子游戏模式就是开场只加载一个“大厅”场景玩家要玩的子项目可以是一堆休闲小游戏、抽奖转盘、活动页面不常驻内存按需拉取子游戏 bundle进入子游戏退出时再释放回大厅。它解决的是包体膨胀、启动变慢、子项目间代码互相污染的问题。适合做合集型 App、盒子型分发、或者内部活动聚合页的团队。对新手来说先不要急着把全部代码塞进一个场景而是理解大厅和子项目之间存在一条清晰边界否则后续的构建、热更、远程资源管理全部会跟着失控。2. 搭建大厅子游戏工程一份工程还是多份工程三种组织方式2.1 方式一单工程 多 Bundle最稳的 demo 起点常见做法是把“大厅”和每个“子游戏”放在同一个 CocosCreator 工程里通过给文件夹打上 Asset Bundle 标记让构建时自动把子项目拆成独立包。我一般这样组织目录结构assets/ hall/ // 大厅工程:开始场景、公共 UI、公共工具库 subgames/ subgame1/ // 子游戏1,这个目录设为 bundle subgame2/ // 子游戏2,这个目录设为 bundle在 Creator 资源管理器中选中subgames/subgame1文件夹在属性检查器里勾选“配置为 Bundle”bundle 名称建议用小写和下划线比如subgame1。每个 bundle 内有自己的场景、脚本、预制体和资源外部大厅代码不直接 require 子游戏内部的模块。从这份 demo 实践来看这种方式的收益非常直接构建时每个 bundle 独立生成config.json和资源文件主包只保留大厅必需资源。子游戏之间即便都叫GameManager.ts这类烂大街文件名也不会被合并到同一份代码里。模块隔离这件事是靠文件夹边界实现的而不是靠起名玄学。以下是开场加载子游戏的核心代码在 Creator 3.x 环境下运行// hall/scripts/Hall.ts import { _decorator, Component, assetManager, director } from cc; const { ccclass } _decorator; ccclass(Hall) export class Hall extends Component { openSubGame(bundleName: string, scenePath: string) { // 1. 先判断是否已经加载过,避免重复加载耗内存 const existed assetManager.getBundle(bundleName); if (existed) { director.loadScene(scenePath); return; } // 2. 从本地包或远程服务器拉取 bundle assetManager.loadBundle(bundleName, (err, bundle) { if (err) { console.error([hall] load bundle ${bundleName} failed, err); return; } // 3. 场景路径直接传给 director director.loadScene(scenePath); }); } }参数说明bundleName要和构建面板里的 bundle 名称完全一致不是文件夹名而是勾选“配置为 Bundle”后填写的名称scenePath指的是该 bundle 内场景资源在assets下的路径比如subgame1/scenes/GameMain不需要带.scene后缀。这里我没有在 loadBundle 里回调中预加载资源是因为loadBundle完成时bundle 内场景已经可以被引擎定位直接 loadScene 是可行且最简的做法。2.2 方式二多工程预构建适合子项目由不同小组维护当子游戏来自不同团队、或者需要独立迭代和发版时可以再把边界往外推一层大厅项目和每个子游戏项目分开各自构建最终产物统一放到远程服务器大厅启动后按 URL 加载远程 bundle。这个方案我没在大范围线上项目里用满但在多团队协作的 demo 推演里跑通过。核心步骤是每个子游戏工程独立构建构建目标平台选同一个比如 web-mobile 或 Android构建结果里找到对应 bundle 目录单独上传到服务器的remote/subgame1路径。大厅启动时用assetManager.loadBundle({ url: https://cdn.example.com/remote/subgame1 })加载远程包。// 多工程方式下,大厅直接用远程地址加载 assetManager.loadBundle({ url: https://cdn.example.com/remote/subgame1, priority: 2 }, (err, bundle) { if (err) { // 远程加载失败时可以降级到本地兜底包 console.error([hall] remote bundle error, fallback to local, err); assetManager.loadBundle(subgame1_local, (err2, localBundle) { if (err2) return; director.loadScene(subgame1/scenes/GameMain); }); return; } director.loadScene(subgame1/scenes/GameMain); });参数含义url指远端 bundle 的 config.json 所在目录priority控制加载队列的优先级数字越大越优先。这里有个容易翻车的细节远程 bundle 的 URL 里不能把config.json写进去只写到 bundle 目录。比如目录下有config.json、index.js、资源文件配置 URL 就写到.../subgame1这层。多工程方案能减少工程体积、避免无关资源互相引用但代价是公共代码会被重复打进每个子游戏包资源无法跨项目共享。如果你的大厅和子游戏要共用一套 UI 基类、网络层、登录模块就必须把这些公共代码放进大厅包里子游戏通过全局对象访问否则每个子游戏体积会噌噌往上飙。2.3 方式三先别做“全场景常驻”模块分包更符合真实开发很多人第一次接到“大厅子游戏”需求时会直接在一个场景里把所有子游戏的内容堆进去按钮隐藏起来靠切换 Canvas 显隐来模拟“进入子游戏”。这种方案在 demo 里看着没问题但一旦子游戏资源达到上百 MB首屏加载会直接劝退用户。子游戏应该被当成可以被动态挂载和卸载的模块而不是被当成大厅场景里的一堆子节点。我建议只把“公共逻辑”放到大厅 bundle子游戏的业务逻辑和美术资源全部留在子 bundle。判断依据很简单如果一段代码或一个图集只有某个子游戏用那就放进该子游戏目录如果多个子游戏都会用再放到公共目录。这样构建后主包能保持轻量子游戏也能独立得到清理和释放。3. 大厅与子游戏之间的通信事件总线、返回唤起和资源释放3.1 用事件总线解耦别直接引用子游戏类大厅和子游戏如果直接互相 new 类或者引用对方节点就会破坏 bundle 的卸载边界。常见做法是让两者只依赖事件不在代码里强引用对方模块。我在 demo 里维护了一个独立的事件模块// hall/scripts/events/HallBus.ts import { EventTarget } from cc; // 全局唯一事件总线,大厅和子游戏都通过它通信 export const hallBus new EventTarget();子游戏内部可以在任意地方发事件// subgame1/scenes/GameMain.ts import { _decorator, Component } from cc; import { hallBus } from hall/scripts/events/HallBus; // 注意:该引用只在类型层面,不构建进子游戏 const { ccclass } _decorator; ccclass(GameMain) export class GameMain extends Component { start() { // 向大厅上报子游戏初始化完成 hallBus.emit(subgame-ready, { name: subgame1 }); } onResultBtn() { // 把子游戏结果回传给大厅 hallBus.emit(subgame-result, { score: 100, level: 3 }); } }这里用一个独立模块做事件广播比直接用director的全局事件或找人“挂到一个公共节点”要干净。hallBus.emit的第二个参数是事件载荷可以是对象或数组。需要特别提醒的是子游戏代码里 import 大厅的HallBus模块在构建时会把这段逻辑合并到子包里但这不会破坏场景卸载因为HallBus本身只是个无状态的EventTarget实例没有持有任何纹理或场景节点它天然就是安全的。3.2 监听方记得用 off否则返回大厅会收到重复回调这是最常被忽视的坑。大厅里的监听代码如果只有 on没有在子游戏结束时 off那么第二次进入同一个子游戏时同一个事件回调会注册两次子游戏发一次消息大厅就执行两遍表现为弹窗出现两次、计分翻倍、甚至场景重复加载。建议的写法是在大厅场景的onDestroy里统一解绑或者把监听器集中在一个 manager 中每次进入子游戏前先 off 再 on// 大厅挂载的节点组件里 private onSubGameReady (data: any) { console.log([hall] subgame ready, data); } onEnable() { hallBus.on(subgame-ready, this.onSubGameReady, this); } onDisable() { hallBus.off(subgame-ready, this.onSubGameReady, this); }一种轻微翻车情况是你觉得返回大厅时要带着子游戏数据于是把一个 scene 里的节点引用放进了全局事件载荷然后在子游戏释放后去访问那个节点。结果肯定是访问到已经销毁的节点控制台报Invalid reference之类的错。所以事件载荷里只传普通数据不要传节点、组件、预制体这类引擎对象。3.3 退出子游戏的关键动作停场景、释放 bundle、还原大厅我把“返回大厅”这个动作拆成三步第一步让子游戏场景停止运行第二步释放子游戏 bundle 的常驻资源第三步把大厅场景切回来。注意不能省略第一步因为当前场景如果没有完全切出直接释放 bundle 可能会出现正在使用的资源被提前释放黑屏随之而来。// 子游戏内点返回按钮 backToHall() { // 通知大厅清理监听 hallBus.emit(back-to-hall); // 保存本次子游戏的 bundle 名,切场景前记一下 const bundleName subgame1; // 释放场景 director.loadScene(hall/scenes/HallMain, () { // 等待新场景首帧渲染后再释放 bundle,避免释放动作打断场景切换 const bundle assetManager.getBundle(bundleName); if (bundle) { bundle.releaseAll(); assetManager.removeBundle(bundle); } }); }releaseAll()会把该 bundle 内的所有资源和脚本持有内容标记为释放removeBundle则把 bundle 实例从 assetManager 里移除。以后再次打开这个子游戏需要重新走loadBundle。这里我特意用了loadScene的第二个参数回调确保新场景已经激活再释放资源。否则可能在场景切换的瞬态里渲染管线还引用着旧纹理导致中间帧出现花屏或闪黑。这也是我自己第一次做大厅子游戏 demo 时真实踩过的坑后来改成“先切场景成功回调后释放”才稳定。4. 构建打包时的关键参数场景、Bundle 名和远程路径4.1 构建面板“参与构建场景”怎么勾主场景只留大厅打开构建发布面板后最上方会列出所有“参与构建场景”。在这里最容易犯的错是把子游戏场景也勾进去导致子游戏内容被打进主包大厅首包体积立刻膨胀。正确做法是只勾选大厅的启动场景。子游戏 bundle 里即使有场景也无需出现在“参与构建场景”列表中因为loadBundle加载的是整包bundle 里的场景是随着 bundle 被引擎读取的。我长期使用的约定是勾选字段只包含hall/scenes/HallMain。每个子游戏有且只有一个主场景命名为GameMain.scene放在子游戏 bundle 目录下。子游戏 bundle 目录不勾选任何“启动场景”相关选项。构建时还要记得在“构建任务”里设置包名、应用名称、版本号。远程加载场景时版本号和 bundle 的配置加载没有直接关系但本地缓存策略需要靠 URL 里的版本目录区分这点后面单独说。4.2 bundle 文件夹的三个参数名称、压缩、目标平台把subgames/subgame1勾选为 bundle 后属性检查器里会出现几个关键项Bundle 名称构建后的目录名也是loadBundle时填的标识。压缩类型默认为 non可选 jpg、png、webp 等。这里不是通用压缩而是纹理压缩格式如果子游戏有透明 UI建议别乱选容易出黑底。目标平台默认对所有平台生效如果只想给某个平台打这个 bundle就勾选限定平台。我在 demo 里通常设置bundleNameDistance: subgame1构建输出是assets/subgame1/这样的结构。这里的名称一旦定了后续代码里到处引用就不要再去改目录名。改名的代价是所有loadBundle入口、配置表、版本管理脚本全部要一起改。建议在工程根目录建一个bundle-config.json把 bundle 名称集中管理起来{ subgames: [ { bundleName: subgame1, entryScene: subgame1/scenes/GameMain, remoteUrl: 1.0.0 }, { bundleName: subgame2, entryScene: subgame2/scenes/GameMain, remoteUrl: 1.0.0 } ] }这个 json 不是引擎要求的功能是工程约定。因为 Creator 没有内置“大厅到子游戏的入口表”如果不做一张表后续新增子游戏时就要改大厅代码非常不工程化。把入口配置独立出来等于给大厅留了一个“随时可以加子项目”的位置。4.3 远程包路径目录版本号比文件名散落更靠谱如果子游戏包放进远程服务器最简单的做法是把 bundle 目录放到带版本号的目录下remote/ 1.0.0/ subgame1/ config.json index.js ... 1.0.1/ subgame1/ config.json index.js ...大厅加载远程 bundle 时URL 指向https://你的域名/remote/1.0.0/subgame1。每次子游戏更新只上传新版本目录并让大厅配置文件里的 version 指向新路径。不推荐覆盖同一个目录因为客户端缓存会让旧版本文件被错误复用出现各种难排查的资源和代码不一致。构建时还要留意目标平台。如果构建的是 web 平台远程 bundle 的跨域取决于服务器 CORS 配置如果构建的是原生平台本地文件系统路径和远程路径的拼接规则不同。原生平台下loadBundle的 URL 参数建议用完整协议头否则可能按本地路径解析。我见过不少项目在浏览器里调试通过打 Android 包后远程 bundle 加载不出来多数就是 URL 少写了协议头。5. 避坑大厅 子项目落地时的五个真实血泪经验5.1 坑一子游戏资源进了主包首包体积完全没降现象构建后主包和“不拆包”时几乎一样大子游戏 bundle 存在但很小。原因子游戏目录里的资源被大厅场景或其他公共资源引用了。Cocos Creator 构建时默认的处理逻辑是被多个 bundle 引用的资源可能被提升到主包这样会导致拆包失去意义。常见元凶是子游戏里的公共图集、公共脚本、共享 prefab混放到了大厅资源目录。解决把子游戏目录当作一个“黑匣子”外部不要直接引用它里面的任何资源。实在需要共用的 UI 组件或工具函数必须复制一份到公共目录或者提取成 npm 模块。每次构建后检查“构建发布”面板里的日志看主包大小变化如果明显异常就去查资源依赖。5.2 坑二多个子游戏共用同名 prefab 或场景加载时场景引用被彼此覆盖现象先打开子游戏 A退出后打开子游戏 B结果加载的是 A 的资源配置。原因两个 bundle 里有同名的资源和脚本引擎在缓存查找时出现歧义。比如都有assets/resources/Prefabs/Item.prefab但因为不是同一个 bundle所有引用在最终构建时可能出现 UUID 冲突。解决给子游戏目录加唯一前缀命名比如subgame1_、subgame2_文件名也尽量带上标识。关键资源不要使用resources目录名因为resources是全局资源目录进构建时会被主包处理掉bundle 隔离失效。我在代码里统一用bundle.get(path)来取资源避免 Copy 资源以后路径混乱。5.3 坑三返回大厅黑屏或白屏但控制台没有报错现象子游戏切换到大厅场景时只有 UI 正常3D 节点全部消失或者整个画面变黑。原因最常见的是场景切换后某个节点引用了子游戏 bundle 里的材质或纹理而该资源被释放了。场景切换回大厅后渲染器还在尝试绘制旧资源但资源已经销毁导致黑屏。解决在返回大厅时先不要马上释放 bundle给一帧渲染缓冲时间。另外检查大厅场景有没有定义“常驻节点”如果大厅组件持有子游戏创建的节点引用那么释放时会出现悬挂引用。我习惯是在场景切换完成后用director.getScene()重新拿到当前场景的根节点确保引用的不再是旧场景内容。5.4 坑四子游戏脚本编译报错提示找不到公共类现象子游戏代码里引用了大厅的公共方法本地编辑器运行正常构建后子游戏 bundle 单独运行时报错。原因子游戏 bundle 构建时会把引用的公共代码一起打包进去但代码路径和公共代码实际所在位置不一致尤其在多工程开发的时候。另一个常见情况是大厅公共代码在子游戏工程里不存在导致构建某个模块时直接找不到符号。解决让子游戏只依赖一个稳定的接口层这个接口层由大厅提供子游戏里用声明文件或最小 stub 类代替不要直接引用大厅内部实现。如果实在绕不开最简单的处理是把公共逻辑放在大厅包的全局window或globalThis上子游戏运行时动态获取。5.5 坑五远程 bundle 加载不报错也不触发回调现象loadBundle的 success 回调没走error 也没走整个流程悬停。原因远程服务器返回了非 JSON 内容或者 URL 写到了config.json本身导致引擎解析 bundle 配置失败。因为配置解析是异步的有时错误被吞在底层逻辑里外部拿不到回调。解决先用浏览器直接访问https://你的域名/remote/1.0.0/subgame1/config.json确认返回的是合法 JSON。然后检查 URL 是不是多了config.json后缀或者少了目录分隔符。我在开发环境里通常准备一个本地 mock 服务器把远程目录映射到本地静态文件这样调试时很快能定位是网络问题还是配置问题。6. 让大厅子游戏从 demo 长成生产骨架每次只改一处就能加游戏我目前习惯在组件里只做“分发动作”真正的 bundle 清单全部交给配置表这样以后每新增一个子游戏不需要动大厅代码。大厅启动时先加载一份subgame_list.json[ { id: game1, name: 消消乐, bundleName: sg_game1, entry: sg_game1/scenes/GameMain, version: 1.0.0, remoteUrl: https://cdn.example.com/remote/sg_game1/1.0.0 }, { id: game2, name: 斗地主, bundleName: sg_game2, entry: sg_game2/scenes/GameMain, version: 1.2.0, remoteUrl: https://cdn.example.com/remote/sg_game2/1.2.0 } ]大厅拿到这个列表后在 UI 上渲染出按钮列表点击时直接按列表字段去加载。新增游戏 构建子游戏 - 上传远程目录 - 在列表 json 加一条记录。这里我保留了多年前端还是套壳开发留下的习惯哪怕是一个 demo也把配置和数据分离否则每次改需求都要经过“翻代码 - 改常量 - 重新出包”这个过程非常低效。版本管控也要跟上。子游戏每个构建产出一个带版本号的目录服务器保留两个版本即可防止客户端缓存的旧版本拉不到资源。返程时把 URL 里的版本和version字段做对比不需要整包热更因为子游戏本身是 bundle新版本直接替换远程文件客户端下次加载自然获取新版。如果遇到“强制更新”需求就在配置表里加一个force: true字段大厅加载时看到这个字段就提示玩家回大厅刷新列表。我现在的习惯是每次打出来的包本地先起一个http-server模拟远程目录访问一遍确认能从大厅切到子游戏、能正常返回然后才传服务器。这一步救过我很多次因为在本地双击index.html和通过 http 服务访问是两码事bundle 加载和场景切换都会受到协议影响。希望这套整合大厅和多个子项目的思路能帮你把前期的坑跳过一大半。本文还有配套的精品资源点击获取
返回列表