ARTICLE DETAIL

资讯详情

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

CocosCreator 大厅子游戏拆包实战:从单工程到 Bundle 按需加载

CocosCreator 大厅子游戏拆包实战:从单工程到 Bundle 按需加载 简介一套面向CocosCreator开发者的项目整合示例主要用于演示如何将大厅与多个独立子游戏组合进同一个工程。内容涵盖模块划分、场景导航、动态加载、独立热更与整包打包等关键环节可以作为搭建多游戏平台或游戏集合的起步模板。资源包内共收录64个文件包括19份逻辑脚本、12份配置文件、12份元数据、9张图片、5张素材图、3个字体文件以及2个场景文件整体体积约7.09兆字节。已有943人学习或下载适合正在学习游戏工程化、热更新与资源管理的开发者。通过分析压缩包内的目录结构与脚本写法可以较快掌握大厅与子游戏之间的路由切换、独立更新配置、内存优化以及发布部署的要点并能够迁移到实际项目中。1. 大厅子游戏是什么为什么 CocosCreator 项目要拆成「大厅 多个子项目」打开任意一个棋牌平台或休闲游戏合集你看到的都是同一套壳一个大厅、十几个玩法入口点进去才真正加载对应子游戏。这就是 CocosCreator 里最常见的「大厅 子游戏」结构。很多人拿到一个子游戏 demo 工程后第一反应是把场景和脚本直接复制进大厅项目结果首包体积爆炸、同名资源互相覆盖、切几次游戏内存就顶不住。这篇笔记会从一个最小 demo 出发讲清楚项目整合的两种思路——单工程多场景与多 Bundle 分包——以及背后的资源边界、构建参数和实机排错方法。适合正在搭游戏合集、棋牌大厅或者要把第三方子项目接进现有工程的 CocosCreator 开发者。2. 项目整合的两种方案单工程多场景与多 Bundle选型先于代码2.1 先分清「逻辑隔离」和「资源隔离」把多个子项目整合进大厅第一件事不是写代码而是想清楚你需要的隔离到什么程度。这里有两个维度逻辑隔离和资源隔离。逻辑隔离指脚本、事件、状态互不干扰子游戏 A 的全局变量不会污染子游戏 B。资源隔离指图片、音频、预制体、场景不被打进同一个首包用户没点进子游戏前这些资源不应该出现在下载列表里。我见过大量整合失败的工程问题几乎都出在只做了逻辑隔离甚至逻辑都没做干净。把两个 demo 工程往一个 assets 里一拖同名脚本互相覆盖、公共变量互相串构建能过一跑就黑屏。做整合之前先把这两个概念分开后面每一步决策都会清楚很多。2.2 单工程多场景方案最快跑通 demo适合中小型项目最常见的做法是一个 CocosCreator 工程大厅一个场景每个子游戏一个场景运行时用director.loadScene切换。这个方案的好处是共享代码方便支付、登录、音频管理等公共底层直接 import 就行调试也快改完直接跑。缺点是资源全部进首包。三五个子游戏还好十几个子游戏时启动耗时和包体大小会线性增长。做内部工具、活动页、快速验证 demo 时这个方案能省掉大量构建配置的时间。import { director } from cc; /** * 进入子游戏单工程多场景方案 * param sceneName 目标场景名需在构建时打进主包 */ export function enterSubGameScene(sceneName: string): void { // loadScene 的第二个参数是完成回调err 不为空时要做 UI 反馈 director.loadScene(sceneName, (err) { if (err) { console.error(切换场景失败: ${sceneName}, err); // 这里应该弹一个 Toast而不是静默失败 } }); }这段代码的逻辑很直接传入场景名director.loadScene负责切换。需要注意两点。第一场景名区分大小写写错大小写不会报编译错误只会在运行时回调里拿到一个 err。第二切换默认会销毁当前场景的所有节点如果大厅的某些数据想保留得放到常驻节点或全局单例里。这种方案的参数只有一个sceneName但它的隐含约束是「场景必须在构建时被包含在 main 包里」。所以单工程多场景比较适合那种确定不拆包、团队也小的项目。2.3 多 Bundle 分包方案子游戏独立成包按需加载当子游戏数量上来了或者运营需要单独更新某个玩法就要把每个子游戏做成独立的 Bundle。Bundle 是 CocosCreator 里的资源打包单元每个 Bundle 可以独立构建、独立上传服务器用户点进子游戏时才从本地或远程拉取。多 Bundle 方案的核心区别在于大厅首包只包含大厅自身和公共依赖子游戏资源各自成包。构建时把子游戏目录勾成 Bundle运行时用assetManager.loadBundle加载。这段代码是进入子游戏的标准动作import { assetManager, director, SceneAsset } from cc; export function enterBundleGame(bundleName: string, sceneName: string): void { // 1. 先查内存里有没有避免重复拉取 const bundle assetManager.getBundle(bundleName); if (bundle) { loadSceneFromBundle(bundle, sceneName); return; } // 2. 没有就动态加载 assetManager.loadBundle(bundleName, (err, loadedBundle) { if (err) { console.error(子包加载失败: ${bundleName}, err); return; } loadSceneFromBundle(loadedBundle, sceneName); }); } function loadSceneFromBundle(bundle: Bundle, sceneName: string): void { // 3. 从 bundle 里显式加载场景资源再交给 director 切换 bundle.load(sceneName, SceneAsset, (sceneErr) { if (sceneErr) { console.error(场景加载失败: ${sceneName}, sceneErr); return; } director.loadScene(sceneName); }); }这里三个参数要解释清楚。bundleName是构建面板里配置的 Bundle 名默认取文件夹名不能乱起sceneName是 bundle 内场景资源的名称不带.scene后缀回调里的err要区分处理拉包失败和场景加载失败是两个不同的失败阶段线上环境得分别打日志。这段代码跟单工程方案的差别在于资源加载从「启动时全部就位」变成了「点击时才拉取」。代价是首次进入子游戏会有加载等待所以大厅入口通常需要一个 loading 状态这个后面章节会补上。2.4 选型对比表与我的判断对比维度单工程多场景多 Bundle 分包首包体积所有子游戏资源全部进首包仅大厅与公共依赖启动耗时随子游戏数量线性增长首包小进入子游戏时按需加载并行开发所有人改一个工程冲突频繁子游戏间目录独立可分开维护独立更新不支持改一个子游戏要发整个包支持子包可单独替换远程文件构建配置零配置每个子目录要勾 Bundle配名称调试成本低改完直接跑高改子包要先重新构建对应 Bundle我一般会这么判断demo 阶段或者子游戏少于 5 个用单工程多场景先把玩法和交互验证了确认要长期运营、持续上新玩法再拆 Bundle。不要一上来就上 Bundle构建配置的复杂度会拖慢早期迭代。反过来项目已经确定要做大厅合集那就别在单工程里堆资源后面拆包的代价比一开始就拆大得多。3. 搭一个最小大厅 demo目录规划、入口脚本与子游戏回传数据3.1 目录与场景规划把子游戏边界先画在 assets 里无论选哪种方案目录结构都要在一开始定死。我通常会用下面这种布局它同时兼容单工程多场景和多 Bundle 两种方案——后面想拆包不用挪目录。assets/ scenes/ # 大厅场景放最外层 Hall.scene bundles/ # 所有子游戏 bundle 的根目录 match3/ # 消消乐子项目 Match3.scene scripts/ res/ racing/ # 竞速子项目 Racing.scene scripts/ res/ scripts/ hall/ # 大厅专属脚本 common/ # 公共底层日志、事件、网络、数据这个结构的核心约束是两条。第一assets/bundles下面每个子目录就是一个子项目目录之间不允许互相引用资源不然构建时会拖泥带水。第二公共脚本放到assets/scripts/common不要塞进任何一个子项目目录否则每个子包都会重复打一份公共代码首包体积立刻变大。如果拿到的是第三方 SDK demo 工程我会先把它整个目录挪到assets/bundles下再删掉 demo 自带的入口场景和公共库只保留玩法相关的资源和脚本。这一步不做好后面所有的资源冲突都会在构建时才暴露。3.2 大厅入口脚本防连点、loading 与场景跳转大厅入口最容易被忽略的是「防连点」。用户快速点两下同一个按钮会触发两次loadBundle轻则重复加载重则两个异步回调竞争同一个场景跳转。所以入口脚本需要加一个简单的锁同时把 loading 状态控制在两个阶段——拉包阶段和场景加载阶段。import { assetManager, director, SceneAsset } from cc; export class HallEntry { private static _loading false; /** * 从大厅进入某个子游戏 * param bundleName Bundle 名构建面板里配置的名称 * param sceneName 子游戏内的场景名不带 .scene 后缀 */ public static enterSubGame(bundleName: string, sceneName: string): void { if (HallEntry._loading) { return; // 防连点上一次跳转还没结束直接忽略 } HallEntry._loading true; HallEntry.showLoading(); const bundle assetManager.getBundle(bundleName); if (bundle) { HallEntry.loadScene(bundle, sceneName); return; } assetManager.loadBundle(bundleName, (err, loadedBundle) { if (err) { HallEntry._loading false; HallEntry.hideLoading(); console.error(子包加载失败: ${bundleName}, err); return; } HallEntry.loadScene(loadedBundle, sceneName); }); } private static loadScene(bundle: Bundle, sceneName: string): void { bundle.load(sceneName, SceneAsset, (err) { HallEntry._loading false; HallEntry.hideLoading(); if (err) { console.error(场景加载失败: ${sceneName}, err); return; } director.loadScene(sceneName); }); } private static showLoading(): void { // 实际项目里这里改成 UI 上的菊花转圈或进度条 } private static hideLoading(): void { // 关闭 loading 节点恢复按钮点击 } }enterSubGame里两个分支要留意getBundle命中说明之前加载过直接复用没命中才走loadBundle。bundle.load的第三参如果不传SceneAsset类型也会工作但传了能让编辑器在开发期就帮你校验资源类型对不对。失败路径里_loading必须复位否则一次失败后整个大厅的所有入口都点不动了这种故障在线上特别容易引发差评。3.3 子游戏回大厅的数据传递事件总线与全局单例子游戏结算后返回大厅需要把分数、解锁进度这些数据带回来。director.loadScene切场景会销毁旧场景节点直接拿组件引用肯定不行。常见做法是三种全局单例、事件总线、本地存储。事件总线适合「通知」型数据数据量大时用单例存事件只负责告知「数据已更新」。type Handler (...args: any[]) void; export class EventBus { private static _events: Mapstring, Handler[] new Map(); public static on(event: string, handler: Handler): void { const list EventBus._events.get(event) ?? []; list.push(handler); EventBus._events.set(event, list); } public static emit(event: string, ...args: any[]): void { const list EventBus._events.get(event); if (!list) return; list.forEach((handler) handler(...args)); } public static off(event: string, handler: Handler): void { const list EventBus._events.get(event); if (!list) return; const index list.indexOf(handler); if (index 0) list.splice(index, 1); } }用法是这样子游戏结算面板里发出事件大厅场景提前监听。// 子游戏结算时发送结果 EventBus.emit(subgame:result, { score: 10240, level: 3 }); // 大厅场景 onLoad 里监听 EventBus.on(subgame:result, (data) { this.latestScoreLabel.string 最新分数${data.score}; });事件名一定要带命名空间前缀比如subgame:result不然子游戏多了之后on(result)这种事件会互相串。另一个容易踩的地方是监听回调里如果引用了大厅 UI 组件而大厅场景销毁后回调还在被 emit就会报空引用。所以大厅场景的onDestroy里必须off掉所有自己注册的 handler。提示子游戏返回大厅后的状态恢复不要全指望事件。结算数据写进全局单例或本地存储事件只负责触发 UI 刷新这样即使事件丢失数据也不会丢。4. Bundle 构建配置与远程加载压缩选项、版本参数和缓存更新4.1 构建面板里的 Bundle 选项与压缩类型把子游戏目录配置成 Bundle位置在构建发布面板里勾选「配置为 Bundle」。这一步有个很常见的误解以为新建一个目录天然就是 Bundle其实必须在构建配置里显式勾选否则构建产物里它只是一个普通资源目录全都会被塞进主包。构建面板里跟 Bundle 直接相关的配置项有这几个配置项作用我一般怎么设配置为 Bundle把目录打成独立分包每个子游戏目录单独勾公共目录不勾Bundle 名称loadBundle时传入的名字保持与目录名一致方便定位压缩类型控制包内 JSON 是否合并场景多的子游戏选 mergeAllJson单场景选 none目标平台与远程地址决定产物是本地还是远程上生产用远程地址配合版本管理压缩类型不同直接影响加载时的请求数量。选none时每个资源独立请求调试方便但请求数多选mergeAllJson会把场景和配置合并成一个 JSON减少网络往返。开发期我建议用none因为报错信息里能看到具体是哪个文件加载失败发布前再改成合并类型可以减少用户弱网下的失败率。构建完成后去 build 输出目录里对比每个 bundle 的目录大小能立刻看出资源隔离是否生效。如果发现某个子包体积异常大多半是把公共资源误放进了子目录或者子游戏之间互相引用了资源。4.2 远程 bundle 的版本拉取version 参数与缓存更新本地 Bundle 只能跟着安装包走运营场景下子游戏需要单独更新就得把 Bundle 放到远程服务器上。assetManager.loadBundle的第二个参数支持传入url和version这两个参数配合能解决大部分更新问题。import { assetManager } from cc; const REMOTE_BASE https://your-cdn.example.com/bundles; /** * 从远程加载指定版本的子包 * param bundleName Bundle 名 * param version 版本号建议用构建产物里的版本信息 */ export function loadRemoteBundle(bundleName: string, version: string): void { const url ${REMOTE_BASE}/${bundleName}; assetManager.loadBundle(bundleName, { url, version }, (err, bundle) { if (err) { console.error(远程子包加载失败: ${bundleName} v${version}, err); return; } // 加载成功接下来走场景加载流程 console.log(子包加载成功: ${bundleName} v${version}); }); }version参数的作用是打破缓存。服务器上同一路径下覆盖同名文件时很多客户端会因为本地缓存而继续用旧包version 变化后引擎拉取的 URL 会带上版本标识强制走新路径。实际项目里我会在大厅启动时请求一个版本接口拿到每个子游戏的最新版本号再传给loadRemoteBundle。远程加载的产物校验也不要省。Cocos Creator 构建远程包时会生成对应的配置和资源发布到服务器时要连目录结构一起传上去不能只传几个文件。传错了最常见的表现是loadBundle回调里 err 信息指向 json 解析失败。4.3 cache 与回退版本出问题后的后悔药远程 Bundle 最怕的不是加载失败而是新版本有问题用户已经拉到了本地。这时候服务器上直接覆盖文件是救不回来的——客户端缓存里躺着的是坏版本。我习惯在服务器上保留最近两个版本的 Bundle 目录用带版本号的子目录区分match3/1.0.1/、match3/1.0.2/。线上发现问题时把版本接口里的version指回旧目录老用户下次启动会拉旧包新用户首次加载也走旧包。这个回退机制比让运营重新发版快得多是做过线上大厅的人都会保留的一道后悔药。这里顺带提醒调试时浏览器缓存会骗人。改完子包重新构建浏览器里看到的还是旧资源表现是「代码改了但行为没变」。排查时先在 DevTools 的 Network 面板里确认请求有没有发出、响应是不是 200再考虑清缓存不要一上来就怀疑构建配置。5. 大厅子游戏整合避坑清单资源冲突、加载失败与内存暴涨 5 条血泪经验5.1 同名资源互相覆盖子游戏 A 的贴图跑到子游戏 B 上现象整合两个子项目后A 游戏的按钮图标出现在 B 游戏的界面上或者预制体上一张贴图引用错乱。 原因外部工程拷贝进 assets 时同名文件被覆盖更隐蔽的是两个子项目里都有bg.png或common.prefab这类通用命名资源管理器在导入时冲突后导入的把先导入的顶掉了。 解决每个子游戏目录内部保持独立命名空间文件名加前缀比如match3_bg.png、racing_bg.png从外部工程导入前先检查 assets 下有没有同名文件有就先改文件名再导入预制体引用已经错乱的只能在资源管理器里重新拖拽关联没有批量修复的捷径。这条规矩在新项目第一天就要立好否则后面查起来是纯体力活。5.2 频繁进出子游戏内存只涨不降现象在大厅和子游戏之间来回切换内存占用一路走高低端机切三五次就卡顿甚至闪退。 原因loadBundle加载的子包默认会留在内存里方便二次进入这是特性不是 bug真正的元凶是场景切换时子游戏里动态创建的资源没有释放或者全局单例里持有子游戏的贴图、预制体引用。脚本里注册的全局事件没off也会让已经销毁的节点无法被 GC 回收。 解决从子游戏返回大厅时调用assetManager.removeBundle(bundle)把不再需要的子包整个卸掉如果还想保留热数据先把数据存到全局单例再释放资源。编辑器自带的内存分析器要在真机上配合使用切一次子游戏采样一次重点盯 Texture2D 和 SpriteFrame 的数量。资源释放逻辑有没有生效看这两个数字最直观。5.3 loadBundle 成功但 loadScene 报场景不存在现象assetManager.loadBundle回调里 err 为空紧接着bundle.load场景时报错提示场景找不到。 原因三种情况最常见。第一构建时场景没被包含进这个 Bundle典型是场景文件放在 bundle 目录外第二场景名拼写错误或大小写不对第三同一个场景名在大厅和子包里都存在引擎解析到了另一个场景。 解决构建完成后去 build 产物里看一眼该 Bundle 目录下有没有对应的场景文件bundle.load时显式传SceneAsset类型让引擎按场景资源去解析场景命名加全局前缀match3_home、racing_select这种彻底避免同名歧义。线上出现这个问题时错误信息往往不够直观最快的方式是本地复现确认构建产物里资源的真实路径。5.4 勾了 Bundle首包体积却没有变小现象子游戏目录在构建面板里已经勾选「配置为 Bundle」构建完后主包依然很大感觉白拆了。 原因最普遍的是公共资源被多个 Bundle 重复引用后又被算进了主包其次是resources目录下堆了一堆通用资源而resources下的东西默认就会进首包还有一种情况是构建没有跑干净增量构建把旧文件留在了产物里。 解决改 Bundle 配置后清一次构建产物再做完整构建不要用增量检查assets/resources目录确认里面只放大厅真正需要立即使用的资源用构建产物里每个 Bundle 的体积对比表验证主包如果包含子游戏的专属贴图说明引用关系没切断。这一步最好写进发布检查清单每次发版都核一遍体积表比肉眼猜靠谱得多。5.5 从子游戏返回大厅大厅状态全丢了现象从子游戏退回大厅大厅场景重新加载滚动位置、玩家头像、弹窗记录全部重置用户要重新操作一遍。 原因director.loadScene切换场景会销毁旧场景的所有节点大厅 UI 框架本身被释放了。单工程多场景方案里这个问题特别明显多 Bundle 方案稍好但同样存在。 解决大厅的核心框架用director.addPersistRootNode挂到常驻节点场景跳转时不销毁或者用一个全局DataCenter保存大厅状态返回大厅时在onLoad里重新恢复 UI。我倾向后者——常驻节点持有 UI 容易造成隐式引用反而不如数据恢复干净。多一个习惯在大厅场景onLoad里做状态恢复onDestroy里做数据落盘这两个生命周期把它当成约定俗成的规矩。6. 验证子包产物与一个实测技巧构建看体积、运行看请求前面五章讲完了怎么做最后补一个我每次集成子游戏都会做的验证动作。这个动作能区分「配置对了」和「以为自己配置对了」。第一个验证是构建产物体积。构建完成后直接看输出目录# 以 web 移动端构建目录为例实际路径以你的构建设置为准 du -sh build/web-mobile/assets/*这一步能看出每个 Bundle 的实际大小也能看出主包有没有混入子游戏资源。我通常会把这个命令的输出留档每次发版对比一次体积异常波动就说明有资源引用关系被改坏了。第二个验证是运行时的请求时机。浏览器模拟环境下打开大厅打开 DevTools 的 Network 面板刷新页面观察首屏加载的请求数量。如果首屏就把所有子包的请求都发出去了说明 Bundle 配置根本没生效正确表现是只有大厅资源的请求点击某个子游戏入口才多出对应的 Bundle 请求。这一步是所有验证里最直观的——配置错了藏不住。第三个技巧是预下载。用户停在大厅时用空闲时间提前把下一个可能进入的子包loadBundle下来等用户点击入口时秒开。注意别在大厅首帧就去预下载所有子包那样会把大厅自己的资源加载带宽抢掉首屏反而更慢。我会在进入大厅后延迟 1 秒只预下载最近玩过的那个子游戏命中率足够高也不影响首屏。做大厅子游戏整合这几年我最大的体会是架构本身不复杂复杂的是边界。目录边界、构建边界、内存边界每一条都要在项目第一天想清楚。早期我也试过省事把所有子游戏堆一起结果每次发版全员陪跑后来老老实实按 Bundle 拆好才把这套流程变成可复用的模板。子项目接入时先定边界、再配构建、最后看请求顺序错了后面全是玄学。希望帮到你。本文还有配套的精品资源点击获取
返回列表