CocosCreator热更新的最佳实践

前言

热更新的本质很简单,说白了就是把游戏资源放到远程服务器,然后客户端在需要时下载远程资源,使用远程资源替换本地文件。

一、Creator官方提供的热更新方案

Creator官方提供了热更新方案,详细信息请参考文档热更新
这里简单介绍一下:

  • project.manifest文件
    用来比对本地和远程的资源差异,确定哪些文件需要下载。

    • 本地和远程各有一份 project.manifest 文件,project.manifest 文件中包含了所有资源的简略信息,比如文件名、大小、MD5等。
    • 通过比对本地和远程的project.manifest 中文件的md5,来确定哪些文件需要下载。
  • version.manifest文件
    减少流量消耗,减少计算量

    • 由于project.manifest可能比较大,所以官方还提供了一份version.manifest 文件,有用的信息其实只有资源版本号
    • 通过比对本地project.manifest和远程version.manifest 中的资源版本号,来确定是否需要下载project.manifest以及是否需要做文件比对
  • 生成热更新资源流程

    1. 构建项目
    2. 使用构建后的资源生成 project.manifestversion.manifest 文件
    3. 上传资源到CDN
  • 客户端更新的流程 (每次都会下载version.manifest)

    1. 初始化AssetsManager,加载本地project.manifest文件
    2. 下载远程的version.manifest文件,比对资源版本号,不需要更新跳过,需要更新继续下一步
    3. 下载远程的project.manifest文件,比对文件MD5
    4. 下载差异文件到本地,设置文件搜索路径
    5. 重启游戏

二、踩坑经历

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 文件。看起来没问题,但实际上有个时间窗口问题:

  1. 玩家 A 在 10:00 开始检测更新, 发现远程资源版本号是2,需要更新 100 个文件
  2. 玩家 A 开始下载,已经下载了 50 个文件
  3. 这时候 10:10 我们发了资源版本,把CDN上的资源全部覆盖了
  4. 玩家 A 继续下载剩下的 50 个文件,但这些文件已经是版本 3 的了
  5. 最终玩家 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 打包流程:

  1. 构建Creator项目
  2. 修改构建后的main.js文件,添加热更代码
  3. 生成project.manifest文件
  4. 原生平台打包(Android、iOS、鸿蒙)

3.3.2 热更新资源上传:

  1. 构建Creator项目
  2. 生成project.manifestversion.manifest 文件
  3. 先按资源版本号上传资源文件到CDN
  4. 再上传project.manifest 文件到CDN
  5. 最后上传version.manifest 文件到CDN

3.3.3 游戏热更新流程:

  1. 启动游戏
  2. 创建AssetsManager实例
  3. 下载远程version.manifest文件,比对资源版本号,不需要更新跳过,需要更新继续下一步
  4. 读取本地project.manifest文件,使用3下载的version.manifest文件中的地址,替换本地project.manifest文件中的地址
  5. 重新加载LocalManifest
  6. 调用AssetsManagerupdate方法,开始更新
  7. 更新完成,设置搜索路径,重启游戏

四、总结

跨游戏版本更新,只需要更新对应版本文件夹下的version.manifest文件,指向最新版本的资源即可。

  • 不要修改已发布热更新资源
  • 动态修改manifest是关键
  • 热更的资源按游戏版本隔离
  • 使用脚本自动化构建、热更流程,防止人为操作失误

这篇文章分享的方案不一定是最优的,但起码是我在实际项目中验证过的,基本能覆盖常见场景。如果你也在做热更新,希望能帮你少踩点坑。

有问题随时交流,大家一起进步。

五、扩展阅读

4赞

热更新后要重启游戏感觉体验不是很好,有没有办法解决这个问题呢?

没有,需要重载代码
也不是要用户退出游戏,重新打开,这个影响不大吧

重启不是事,但重启经常崩溃掉就是事了

为什么会崩溃呢

你看腾讯的王者荣耀热更完了还提示要重启游戏呢,真想玩的不会在乎这个时间的,不想玩的该跑早跑了