ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

CocosCreator大厅子游戏整合:从Demo到可维护架构

2026/10/7 23:01:51 拓冰建站 浏览量
CocosCreator大厅子游戏整合:从Demo到可维护架构 简介面向Cocos Creator开发者的大厅与多个子游戏整合项目笔记及示例Demo适合需要实现模块化游戏大厅、独立热更子游戏或多模式游戏集合的开发者参考学习也可作为团队内部分享与项目起步模板。压缩包共64个文件约7.09MB核心包含js逻辑脚本、json配置、meta资源声明与fire场景文件另有png、jpg图片、ttf字体资源和一个bat辅助脚本基本覆盖大厅入口、子游戏场景切换、动态加载、热更新配置和资源管理等模块。已有943人学习或下载文件目录结构清晰便于对照源码笔记逐段阅读。通过学习该Demo可以了解如何将独立子游戏按需打入整包掌握项目结构设计、模块间导航切换、服务器更新接口组织、性能优化与调试发布等关键知识点同时理解资源预加载与内存管理策略掌握Cocos Creator打包发布流程中针对不同平台的配置要点减少多子游戏项目从零搭建时的踩坑成本。1. 大厅子游戏整合一个demo容易一套架构难如果你的项目叫“cocosCreator大厅子游戏笔记demo”那你多半已经看过不少类似的代码包大厅一个场景几个子游戏各占一个场景点击按钮就能跳过去。demo做得再完整也只是把“能跑”这件事证明了一遍。但真正放到线上你会发现脱离demo形态的整合才是大头——包体怎么拆、子游戏怎么进怎么退、资源什么时候该释放、代码怎么不互相污染随便一个问题都能让项目卡在测试阶段两三周。这篇笔记就是围绕“大厅多子项目”的整合方案写的面向的是需要把这个demo变成一个可维护、可上线的业务框架的开发者不看广告只看落地路径。2. 目录与资源规划把 demo 打磨成可拆分架构的关键一仗2.1 先说清一个容易被忽略的前提包体是分“层”的很多做大厅整合的人第一反应是“我把所有子游戏场景都放进包里做热更时不就好了”。这个思路没有错但它回避了一个核心问题首包到底想做到多大。子游戏一旦多起来美术资源、ui图集、动画序列都是吃包体的猛兽。你把它们全塞进首包启动下载时长会直接劝退一批用户。CocosCreator 处理这类问题的标准姿势是 Bundle资源分包。Bundle 本身不是新概念你可以把它理解成“可以单独加载、单独释放的资源集合”。大厅是一个 Bundle每个子游戏是一个独立 Bundle公共依赖放进 resources 或另一个共享 Bundle。启动时只加载大厅 Bundle子游戏 Bundle 在点击入口时才拉取。这样首包体积能压到只包含大厅界面和几个必要入口图子游戏资源按需走远程加载或后续热更新体验上接近“点开即玩”。目录结构上我习惯按“框架、入口、子游戏”三层来分assets/ bundles/ hallBundle/ scenes/ // 大厅场景 prefabs/ // 大厅ui、按钮、弹窗 scripts/ // 只属于大厅的逻辑 gameABundle/ scenes/ prefabs/ scripts/ gameBBundle/ scenes/ prefabs/ scripts/ resources/ commonAssets/ // 公共图集、公共音效、通用字体 scripts/ framework/ // 加载、注册、生命周期管理框架代码 shared/ // 所有子游戏共用的纯逻辑代码这里有个容易被新手忽略的点scripts/framework和scripts/shared千万不要塞进某个子 Bundle 里否则会导致公共代码被每个子游戏各打一份构建产物变大且逻辑分叉。框架代码应该编译进主包或独立 Bundle子游戏 Bundle 只放自己的资源与局部脚本。2.2 Bundle 划分与首包控制的三个可执行参数把资源分包拆到哪个粒度要考虑加载时间、内存占比和构建配置三个维度。太小了会造成大量小文件请求太大会失去按需加载的意义。我的常用经验值供参考一个子游戏的 Bundle 控制在 2–5 MB压缩前达到这个量级就值得继续拆内部资源首包只顾大厅自身公共依赖尽量打成一个共享 Bundle子游戏之间完全不共享的美术资源一定不允许互相引用否则构建时会被重复打进两个 Bundle构建层面在 CocosCreator 的构建发布面板里每个文件夹被标记为 Bundle 后构建时会在配置中自动生成对应的 Bundle 配置。你需要检查一下当前项目的“构建发布 → 浏览器/原生平台 → Bundle 配置”面板确认每个 Bundle 的“压缩类型”是不是符合预期。一般 js 文件选merge_dep图片音频选默认压缩不要自己乱改格式不然会出现资源能加载但解析异常的怪问题。2.3 子游戏的注册表一份 JSON 定清楚所有元信息当子游戏数量变多最忌讳的是把入口按钮写死在代码里。加一个子游戏就要改大厅逻辑再发布一版大厅这违背了“大厅稳定、子游戏可加”的初衷。我一般会维护一份subgame_config.json放进 resources 或远程配置地址格式大致如下{ games: [ { id: 101, name: 捕鱼达人, version: 1.0.0, bundleName: gameABundle, scenePath: scenes/GameA, icon: textures/icons/fishing, isHotUpdate: true }, { id: 102, name: 消消乐, version: 1.2.1, bundleName: gameBBundle, scenePath: scenes/GameB, icon: textures/icons/eliminate, isHotUpdate: false } ] }这段 JSON 的解析逻辑不复杂但它解决的问题非常大新增子游戏时不需要修改大厅代码只需在配置里加一条通过网络下发即可。bundleName对应构建中的 Bundle 名scenePath是子游戏内场景的路径isHotUpdate字段用来区分当前子游戏是走首包、远程 Bundle 还是后续热更补丁。注意version最好与子游戏资源版本号联动否则会出现老客户端加载新资源导致字段不匹配的问题。2.4 公共依赖与去重不把框架代码打进子游戏CocosCreator 构建时有一个容易踩的配置项如果你的子 Bundle 脚本引用了公共目录里的类或组件构建工具在打包时可能自动把相关代码拷贝进子 Bundle。你会在构建后看到子游戏包体里出现一堆你根本没写过的框架代码这正是公共代码没有独立成 Bundle 的结果。框架层建议做成一个frameworkBundle所有子游戏在加载时先依赖主包或框架包代码引用保持在运行时解析构建配置里给框架包勾选“不合并进其他 Bundle”的选项不同版本选项名称略有差异但思路一致让公共代码只出现一次。如果你发现子 Bundle 的构建日志里出现了scripts/framework相关文件说明依赖解析被打断了要回过去检查是否有代码直接 import 了框架路径。正确做法是子游戏代码通过全局入口或事件总线访问框架能力而不是 import 具体类。3. 整合框架落地拿到一份最小可跑的 CocosCreator 大厅代码3.1 框架代码的四个核心接口底层设计不需要过度抽象但至少要保证四个能力加载 Bundle、加载场景、注册子游戏生命周期、释放子游戏。下面这份精简版框架代码是常见做法你可以直接抄去改造成自己的工程// framework/SubgameManager.ts import { AssetManager, director, Director, Scene, warn } from cc; export interface ISubgameContext { gameId: string; onEnter(params?: Recordstring, unknown): void; onExit(): void; } export class SubgameManager { private static _instance: SubgameManager; static get instance(): SubgameManager { if (!this._instance) { this._instance new SubgameManager(); } return this._instance; } private _currentGame: ISubgameContext | null null; async enterGame(config: { bundleName: string; scenePath: string; params?: Recordstring, unknown }): Promisevoid { // 1. 先退出当前子游戏保证同一时刻只有一个子游戏活跃 await this.exitCurrentGame(); // 2. 加载子游戏 Bundle const bundle await this.loadBundle(config.bundleName); if (!bundle) { throw new Error([SubgameManager] bundle 加载失败: ${config.bundleName}); } // 3. 通过 scenePath 实例化子游戏场景 const scene await this.loadSceneFromBundle(bundle, config.scenePath); // 4. 通知子游戏进入并传入参数 const subgameRoot scene.getChildByName(SubgameRoot); if (subgameRoot) { this._currentGame subgameRoot.getComponent(GameEntry) as unknown as ISubgameContext; if (this._currentGame) { this._currentGame.onEnter(config.params); } } } private loadBundle(name: string): PromiseAssetManager.Bundle { return new Promise((resolve, reject) { assetManager.loadBundle(name, (err, bundle) { if (err) { reject(err); } else { resolve(bundle); } }); }); } }这段代码解决的是“统一入口”问题大厅只需要调用SubgameManager.instance.enterGame(config)不用关心具体子游戏里有哪些脚本。注意ISubgameContext接口约定了onEnter和onExit子游戏根节点上的脚本实现这两个方法即可被框架管理。3.2 场景实例化与回调丢失的隐患上面代码里我用了loadSceneFromBundle这个函数需要你自己实现因为director.loadScene默认只能加载当前工程场景列表里的场景不一定能直接加载 Bundle 内场景。CocosCreator 3.x 中Bundle 内场景可以通过资源类型Scene加载后手动切换常见写法如下// framework/sceneLoader.ts import { AssetManager, director, SceneAsset, instantiate, Node, find } from cc; export function loadSceneFromBundle(bundle: AssetManager.Bundle, scenePath: string): PromiseScene { return new Promise((resolve, reject) { bundle.load(${scenePath}, SceneAsset, (err, sceneAsset) { if (err) { reject(err); return; } const scene instantiate(sceneAsset.scene) as Scene; const canvas find(Canvas); if (canvas) { canvas.addChild(scene); } else { reject(new Error(当前场景没有 Canvas请检查大厅场景结构)); } resolve(scene); }); }); }这里有一个关键点我没有使用director.loadScene而是用instantiate把目标场景实例化成节点挂载到大厅的 Canvas 下。这样做的好处是大厅的根节点和常驻节点不会在切换时被销毁返回大厅时只需要把子游戏节点从 Canvas 下移除即可不用重新加载整个大厅场景体验上“回去”很快。缺点是需要你自己管理场景内组件的onLoad和onDestroy调用时机子游戏代码不能依赖场景切换带来的自动生命周期。3.3 子游戏的 GameEntry 脚本约定子游戏端需要做一个入口脚本挂到子游戏场景的根节点上。以下是约定示例// gameABundle/scripts/GameEntry.ts import { Component, Node } from cc; export class GameEntry extends Component { private _params: Recordstring, unknown | null null; onEnter(params?: Recordstring, unknown): void { this._params params; // 根据参数初始化子游戏比如玩家 id、房间号 console.log([GameEntry] 进入子游戏, this._params); } onExit(): void { // 保存进度、清理事件监听、停止定时器 console.log([GameEntry] 退出子游戏); } }onEnter和onExit的调用时机要严格遵循框架层约定进入新子游戏前先调用旧子游戏的onExit。这一步千万别省略否则旧子游戏的全局事件监听会残留导致新子游戏界面被旧逻辑干扰。把这两个方法当作子游戏与大门的契约子游戏内部爱怎么改都行大门只管这两个入口。4. Bundle 动态加载与场景切换资源、回调、进度都在这里排队4.1 加载进度与失败重试的工程化做法在真实的网络环境里远程 Bundle 加载不会像本地那样瞬间完成。如果你只是调assetManager.loadBundle用户点击子游戏入口后会有一个毫无反馈的等待期。需要给加载过程加进度条和超时重试。以下代码可以在enterGame之前插入进度事件。// framework/SubgameManager.ts 增加进度回调 enterGameWithProgress( config: { bundleName: string; scenePath: string; params?: Recordstring, unknown }, onProgress: (percent: number) void ): Promisevoid { return new Promise((resolve, reject) { assetManager.loadBundle( config.bundleName, (finished: number, total: number) { const percent total 0 ? finished / total : 0; onProgress(percent); }, (err, bundle) { if (err) { reject(err); return; } // 继续执行场景加载... resolve(); } ); }); }这里的重点是loadBundle的第二个回调参数即进度回调。在 CocosCreator 3.x 中它的签名与 2.x 不同传入的finished和total单位是请求数量不是字节数。所以进度条显示的百分比仅供参考别拿来做精确到小数点后几位的特效。我一般会做一层“假进度”兜底前 80% 走真实加载回调剩余 20% 在加载完成后做场景实例化的过渡动画时补齐避免用户看到进度条卡在 99% 不动。4.2 远程 Bundle 与热更地址的参数匹配assetManager.loadBundle默认从本地包体或已下载的热更目录读取。如果你把子游戏 Bundle 发到服务器需要先配置远程包地址。CocosCreator 的assetManager有setBundleServer方法或者在项目设置里配置“资源服务器地址”。常见做法是登录后向服务器拉取一份“资源版本表”里面写明了每个 Bundle 的远端地址和版本号客户端拿着这张表动态设置加载路径。一个需要警惕的细节版本表与客户端本地版本不一致时如果你只是把自己实现的版本写进缓存并判断会出现用户A能进子游戏、用户B不能进的诡异情况而且往往只在特定机型上出现。这类问题的根因通常不是加载代码而是地址拼接错误。建议把地址字段做成完整 URL不要用相对路径拼字符串否则服务器升级了端口或路径变更老的缓存配置就会失效。排查时可以先打开浏览器的 Network 面板看加载请求的 URL 是否符合预期排除地址拼接问题再去查 JSON 配置。4.3 卸载时机与内存峰值的“后悔药”子游戏从大厅切换出去后很多人会忘记释放 Bundle。CocosCreator 的assetManager.releaseBundle能释放整个 Bundle 的资源但调用时必须保证该 Bundle 下没有任何场景节点还在使用资源否则会报引用计数错误甚至黑屏。我的一般做法是三步从场景树上移除子游戏根节点并在节点上调用node.destroy()等一帧scheduleOnce后再调用bundle.releaseAll()最后调用assetManager.releaseBundle(bundleName)。第二步之所以要等一帧是因为destroy()后资源引用计数不会立刻变为零立即释放会导致组件回调还在执行时资源已经被移出内存。这个顺序问题我在原生端踩过两次每次都表现为“子游戏退出后大厅点击无响应”或“界面纹理变成白色占位图”。// 退出并释放 Bundle 的顺序控制 async exitAndRelease(bundleName: string): Promisevoid { const rootNode this._currentSubgameRoot; if (rootNode) { rootNode.destroy(); } await this.waitForFrames(1); const bundle assetManager.getBundle(bundleName); if (bundle) { bundle.releaseAll(); assetManager.releaseBundle(bundleName); } } private waitForFrames(n: number): Promisevoid { return new Promise((resolve) { director.once(Director.EVENT_AFTER_UPDATE, () resolve()); }); }这里waitForFrames用Director.EVENT_AFTER_UPDATE事件来实现“下一帧再执行”的效果比setTimeout更可靠。注意如果你在微信小游戏平台帧回调频率可能受前台后台切换影响最好加一个最长时间限制避免用户切后台再回到游戏时回调一直未触发。4.4 生命周期状态机防止子游戏互相覆盖当用户点了一个子游戏加载中又退出去点另一个子游戏加载回调后却把两个子游戏都显示出来的怪事就可能出现。框架层需要一个简单的状态机来避免这种并发操作。enum SubgameState { Idle idle, Loading loading, Running running, Exiting exiting }Idle当前无子游戏活跃Loading正在加载子游戏资源此时所有入口按钮应该被禁置或显示遮罩Running子游戏正常运行中Exiting正在退出和释放此时不允许进入新游戏在enterGame开头判断当前 state如果不是 Idle 或 Running直接拦截并返回。否则先置为 Loading。加载成功后状态变为 Running退出时先置为 Exiting完全释放后再回到 Idle。这个小状态机大约二十行代码但能避免掉很多线上反馈“界面错乱”的疑难杂症。5. 避坑大厅整合子游戏的 5 个高发故障现场5.1 场景加载完成后回调里拿不到 Canvas现象子游戏场景通过instantiate挂到大厅 Canvas 下后子游戏里某个组件的onLoad用find(Canvas)拿到的节点是对的但下一行代码访问它的子节点时返回null。原因场景实例化的节点树在instantiate时已经完整但各组件的onLoad执行顺序不一定保证父节点的子节点全部 mounted 完尤其在挂载时机和addChild之间还有引擎内部的流程时。解决不要在子组件的onLoad里大胆假设整个 Canvas 下的兄弟节点已经就绪。改为在场景根节点挂一个GameEntry由它主动在整个场景脚本start执行完后再向子组件发事件或直接调用方法。如果不改架构也可以用scheduleOnce延迟一帧执行初始化逻辑但要注意微信小游戏上帧间隔不稳定建议用“延迟一帧 判断非空”的双保险。5.2 子游戏退出后纹理和音频占用不释放帧率回不来现象连续进出几个子游戏后游戏帧率明显下降内存面板一路走高。原因子游戏里的动态加载图片、音频通过resources.load或bundle.load加载后没有被记录在退出节点的destroy流程中。如果这些资源是在脚本里直接赋给 SpriteFrame 或 AudioSource引擎不会自动释放它们。解决在子游戏内统一用一个资源管理器记录所有动态加载的资源退出时批量release。不要在子游戏组件里散装处理资源。另外检查子游戏里是否有常驻单例节点比如某个 manager 加到了persistRootNode这个节点会让整个子游戏的核心逻辑永不销毁等同于内存泄漏。我习惯在onExit里打印一份子游戏节点树快照确认没有残留节点挂在persistRootNode下。5.3 远程 Bundle 首次加载失败用户卡在黑屏入口现象用户点击子游戏入口进度条走了一小半就卡死几秒后弹出加载失败提示。重试一次后又能进入。原因服务器 CDN 某些节点首次连接慢或超时时间设置太短或loadBundle失败后没有自动重试机制。如果错误码是资源 404多半是远程路径版本号没对上。解决失败时不要直接提示用户“网络错误”先做一次静默重试第二次失败才弹窗。重试地址建议拼上版本号参数例如https://yourcdn.com/bundles/gameABundle?v101这样能绕过 CDN 的过期缓存。另外把超时时间从默认的 10 秒往上调移动网络下首次建立连接经常超过 15 秒。如果项目是大厅制建议在大厅进入时就预下载热门子游戏 Bundle用户点击时走本地加载成功率会好看很多。5.4 构建产物里出现重复的公共代码现象构建后检查子 Bundle 文件发现里面混入了framework的 js 代码包体比预期大了 40%。原因子游戏场景中直接挂载了来自framework目录组件的 Prefab构建时依赖分析扫描到该组件就把相关框架代码连带打进子 Bundle。解决回到目录规划那一节把框架代码打包成独立的 frameworkBundle并在子游戏构建配置的“排除”列表里显式排除框架路径。更彻底的做法是子游戏内不允许出现任何对框架目录的静态引用所有框架访问都通过全局单例接口。遇到这类问题检查构建日志里是否有 “WARNING: asset ... is referenced by multiple bundles” 的提示有就说明依赖没有理清。5.5 子游戏代码里出现“找不到类”的报错但编辑器预览正常现象编辑器模式一切正常构建到小游戏或原生包后某个子游戏场景打开时控制台报 “Can not find class GameEntry”但代码明明已经写好了。原因CocosCreator 移除了某些构建模式下的非场景引用脚本如果GameEntry没有在场景中被引用构建过程不会把它打进包内。另一种常见场景是脚本放在子 Bundle 的根目录下。解决在子 Bundle 的入口场景里显式把入口脚本挂到根节点上这样构建器会保留它。如果不想挂载可以在settings.py或构建插件里手动添加一个包含该脚本的“启动场景”。还有一种是直接把脚本目录放到构建配置的“包含脚本”列表里。这个坑在微信小游戏和原生平台高发PC 预览反而看不到建议每次发布前在目标端真机自测一次入口场景。6. 进阶把整合做成可维护的长期手艺当你把“能跑”的 demo 变成真正的主宿结构后会发现日常开发里反复做的事变成了三类加子游戏、改子游戏、调加载策略。这里我整理了一个自用的整合验收清单每次发布前过一遍能省掉大量线上返工。第一检查大厅入口配置是否来自远端并且有本地缓存兜底。大厅 UI 要在断网时给用户一个可点击的“刷新”按钮而不是一个空白的子游戏列表。第二子游戏包体大小和加载时间要形成报表每次发新版前对比上一次数据如果某个子游戏包体膨胀超过 30%先查是不是公共资源被打进了子 Bundle。第三退出流程要自动化测试。每进出一次子游戏记录内存峰值和场景节点数如果节点数没有回到进游戏前的水平说明存在节点残留。我用的办法是进出一个子游戏后立即打印director.getScene().children.length对比基准值超过基准值 3 个以上就要查树结构。关于调试技巧我会把 SubgameManager 的每次enterGame、exitGame、loadBundle失败都加一条带SUB前缀的日志。线上用户反馈问题时只要把客户端日志拉回来就能靠日志串出完整的子游戏切换链路定位到具体的加载阶段。这比让用户描述“点了没反应”高效得多。框架层不要舍不得打日志真正的整合问题大多发生在流程时序上有日志才有判断依据。另外一个长期维护的经验子游戏的入口节点一定不要用固定名称去find比如SubgameRoot。一旦子游戏多了某个子游戏改过界面层次结构就可能导致其他子游戏拿到错误节点。我后来改成根据GameEntry组件来查找子游戏根节点而不依赖节点的名字。这个改动很小却让我少接了几次线上问题排查。最后说一点习惯上的事我每次接到一个新子游戏都会先花半小时看它的事件监听、定时器和场景节点挂在谁上面而不是直接看它好不好玩。整合项目里子游戏的质量很大程度体现在它退出时是不是把东西都带走。这个习惯帮我避开了很多后期重构的麻烦。希望这篇笔记里提到的方案和坑能让你在这个方向上少走几趟弯路也希望你能在大厅框架里做出属于自己顺手的工具箱祝顺利。本文还有配套的精品资源点击获取