前言
热更新的本质很简单,说白了就是把游戏资源放到远程服务器,然后客户端在需要时下载远程资源,使用远程资源替换本地文件。
一、Creator官方提供的热更新方案
Creator官方提供了热更新方案,详细信息请参考文档热更新。
这里简单介绍一下:
-
project.manifest文件
用来比对本地和远程的资源差异,确定哪些文件需要下载。- 本地和远程各有一份 project.manifest 文件,project.manifest 文件中包含了所有资源的简略信息,比如文件名、大小、MD5等。
- 通过比对本地和远程的project.manifest 中文件的md5,来确定哪些文件需要下载。
-
version.manifest文件
减少流量消耗,减少计算量- 由于project.manifest可能比较大,所以官方还提供了一份version.manifest 文件,有用的信息其实只有资源版本号
- 通过比对本地project.manifest和远程version.manifest 中的资源版本号,来确定是否需要下载project.manifest以及是否需要做文件比对
-
生成热更新资源流程
- 构建项目
- 使用构建后的资源生成 project.manifest 和 version.manifest 文件
- 上传资源到CDN
-
客户端更新的流程 (每次都会下载version.manifest)
- 初始化AssetsManager,加载本地project.manifest文件
- 下载远程的version.manifest文件,比对资源版本号,不需要更新跳过,需要更新继续下一步
- 下载远程的project.manifest文件,比对文件MD5
- 下载差异文件到本地,设置文件搜索路径
- 重启游戏
二、踩坑经历
2.1 远程资源存储路径问题
很多年前第一次做热更新功能时,我们把资源按照游戏版本号放到CDN上
最初的方案是这样的
cdn.example.com/game/1.0.0/ # 游戏版本 1.0.0
├── project.manifest
├── version.manifest
└── remote-assets/
├── assets/
├── src/
└── jsb-adapter/
每次发布热更新,直接在 1.0.0/ 目录下覆盖资源再覆盖 manifest 文件和 version.manifest 文件。看起来没问题,但实际上有个时间窗口问题:
- 玩家 A 在 10:00 开始检测更新, 发现远程资源版本号是
2,需要更新 100 个文件 - 玩家 A 开始下载,已经下载了 50 个文件
- 这时候 10:10 我们发了资源版本,把CDN上的资源全部覆盖了
- 玩家 A 继续下载剩下的 50 个文件,但这些文件已经是版本 3 的了
- 最终玩家 A 本地的资源是版本 2 和版本 3 混合的,文件完整性被破坏,启动游戏时直接崩了
2.2 本地资源污染的问题
从CDN下载的远程资源会保存到本地,开始就直接在可写目录下新建了一个hotupdate文件夹,并没有其他区分机制,导致不同包版本的热更新资源全部放到同一个目录下,跨包版本更新时旧文件没清理干净,运行的是四不像版本,各种诡异 bug。
后来才找到靠谱的方案。这篇文章说说我是怎么踩坑、怎么填坑的。
三、解决方案
先明确两个概念:
- 游戏版本号:指的是通过应用商店发布的包版本,比如 1.0.0、1.1.0、2.0.0
- 资源版本号:指的是同一个游戏版本下的热更的资源版本,这里我们用数字表示,比如 1、2、3、4…,发布包中的资源版本号用0表示初始资源版本
举个例子:游戏版本 1.0.0 可能有多个资源版本(1、2、3…),每次热更新都会递增资源版本号。
3.1 远程资源存储路径问题的解决方案
按照下面这样分布热更新资源,还有另外一个好处
可以提前准备热更新资源,并上传到CDN,但是不上传version.manifest文件
等时间到发布的时间节点,再替换掉远程的version.manifest文件,指向最新版本的资源
上文中2.1中提到的问题,是因为我们直接操作了已经发布了的热更新资源,我们不能保证瞬间上传完成,所以就需要已经发布的资源保证不再修改。
所以我们改成了下面这样的CDN文件布局:
cdn.example.com/game/
└── 1.0.0/ # 游戏版本 1.0.0
├── version.manifest # 固定地址,内容指向最新的资源版本
├── 1/ # 资源版本 1
│ ├── project.manifest
│ └── remote-assets/
├── 2/ # 资源版本 2
│ ├── project.manifest
│ └── remote-assets/
└── 3/ # 资源版本 3
├── project.manifest
└── remote-assets/
每次发布热更新,直接在1.0.0/目录下根据资源版本号新建一个目录,然后上传 project.manifest 和 远程资源 文件。
由于每次都会重新下载version.manifest文件,所以我们把变更后的新地址保存到version.manifest中
{
"packageUrl":"https://cdn.example.com/game/1.0.0/3", // 指向新的资源地址
"remoteManifestUrl":"https://cdn.example.com/game/1.0.0/3/project.manifest", // 指向新的 project.manifest 地址
"remoteVersionUrl":"https://cdn.example.com/game/1.0.0/version.manifest", // 这个地址保持不变
"version":"3" // 新的资源版本号
}
当所有文件上传完成之后,最后替换远程的1.0.0/version.manifest 文件
这样就保证了已经发布的资源不会被修改,也不会被覆盖。
3.1.1 引入了新的问题
玩家热更新时,下载远程的version.manifest文件和project.manifest文件使用的都是本地的project.manifest文件中的地址。
这些地址是不会变化的,指向的远程资源还是老的文件,当然远程version.manifest文件中的地址已经发生变化了。
但是version.manifest目前有用的信息只用到了version字段,也就是资源版本号。
3.1.2 解决问题(动态变更资源的指向)
通过查看官方AssetsManager的源码,发现AssetsManager中其实是提供了动态修改远程地址的问题
下载远程version.manifest文件后,把其中的地址相关的信息,覆盖到本地的project.manifest文件中
// 参数1: 本地project.manifest文件路径,可以传空字符串,或者加载后的资源Asset的nativeUrl属性
// 参数2: 本地存储目录
// 参数3: 版本号比对函数,这里我用的是数字,比较大小就行了
this._am = new native.AssetsManager("", writablePath, (v1, v2) => {
return v1 > v2;
});
// 之后下载远程的version.manifest文件,替换本地project.manifest文件中的地址后,再重新加载LocalManifest
this._am.getLocalManifest().parseJSONString(JSON.stringify(content), manifestRoot);
2.2 本地资源污染解决
这个就很简单了,可以根据游戏版本号组织本地资源的缓存目录,或者跨游戏版本后,清理之前存的本地资源。
// 之前的资源存储路径
let storagePath = `${jsb.fileUtils.getWritablePath()}hotupdate/`;
// 新的资源存储路径
let gameVersion = '1.0.0'; // 游戏版本号
let storagePath = `${jsb.fileUtils.getWritablePath()}hotupdate/${gameVerison}/`;
3.3 完整流程:
3.3.1 打包流程:
- 构建Creator项目
- 修改构建后的
main.js文件,添加热更代码 - 生成
project.manifest文件 - 原生平台打包(Android、iOS、鸿蒙)
3.3.2 热更新资源上传:
- 构建Creator项目
- 生成
project.manifest和version.manifest文件 - 先按资源版本号上传资源文件到CDN
- 再上传
project.manifest文件到CDN - 最后上传
version.manifest文件到CDN
3.3.3 游戏热更新流程:
- 启动游戏
- 创建AssetsManager实例
- 下载远程
version.manifest文件,比对资源版本号,不需要更新跳过,需要更新继续下一步 - 读取本地
project.manifest文件,使用3下载的version.manifest文件中的地址,替换本地project.manifest文件中的地址 - 重新加载
LocalManifest - 调用
AssetsManager的update方法,开始更新 - 更新完成,设置搜索路径,重启游戏
四、总结
跨游戏版本更新,只需要更新对应版本文件夹下的version.manifest文件,指向最新版本的资源即可。
- 不要修改已发布热更新资源
- 动态修改manifest是关键
- 热更的资源按游戏版本隔离
- 使用脚本自动化构建、热更流程,防止人为操作失误
这篇文章分享的方案不一定是最优的,但起码是我在实际项目中验证过的,基本能覆盖常见场景。如果你也在做热更新,希望能帮你少踩点坑。
有问题随时交流,大家一起进步。
五、扩展阅读
- Cocos Creator 热更新教程 - 官方基础教程
- AssetsManager 使用指南 - 热更新管理器 API
- 热更新的完整实现 - 开源项目