有一个很常见的需求,在上一篇也提到过。
我们希望所有频繁变动业务都划分为一个个AssetBundle,在游戏过程中需要的时候下载这部分内容,同时如果有要更新的时候,能够在不重启游戏的前提下热更新模块。(基础需求)
一部分AssetBundle在包含在apk内,这些Bundle是用户游戏运行后立即且必要的资源,但同时希望保持在游戏中热更的基础特性。(优化)
本文主要给出解决该需求的具体实现方案。
1. 定义
1.1 热更新和模块更新
对于不在Asset Bundle中的资源更新,以下称为热更新,热更新由原生代码触发执行,不受js脚本影响,热更新完成后启动游戏引擎,这部分更新资源包括main.js、assets目录中本地和内置Bundle、jsb-adapter目录、src目录。
对于划分在Asset Bundle下的资源更新,以下称为模块更新。
1.2 不同的Bundle
根据需求,我们将一些Bundle包含在apk内,并在有更新版本时更新,这类Bundle以下称为灵活Bundle;只存在apk中,且只跟随热更新时更新的,以下称为本地Bundle只存在远程服务器上,在需要时下载或更新的,以下称为远程Bundle。
1.3 资源划分
资源(包括脚本)划分为两个大部分——不频繁变动和频繁变动部分。
不频繁变动的部分为底层架构和常用稳定方法,如对网络接口的封装、时间戳格式化、资源管理器等等,这部分内容和业务基本无关。
频繁变动的部分为业务相关部分,根据业务内容分割成AssetBundle,其中包括大厅、子游戏、活动等。
2. 模块更新
2.1 更新清单结构
{
bundleName1 : {
version : 勾选md5打包生成,用于比对Bundle是否有修改,传入loadBundle参数
ver : 版本号,如果version不同,切远程ver大于本地才更新
url : 远程更新路径(apk中的初始清单上,灵活Bundle填写Bundle Name)
resVer : 热更新版本,非必要,根据具体方案自行决定
},
bundleName2 : {
...
},
...
}
以上结构根据项目实际情况指定。
2.2 下载优化思路
对于灵活Bundle,一开始本地加载,当发现远程有更新版本时,加载远程路径,这时候会在缓存目录中发现,下载了Bundle中的所有文件,即使和本地包内的资源相同,但是因为路径发生变化,Cocos的资源管理器依然认为,我们需要的是不同的文件。
对于远程Bundle,第一次远程加载,当发现有新的版本时,如果路径和之前的不一致,也会将新的资源完全下载下来。我们可以将不同版本的资源放在一个路径下,但是那样会难以管理。所以还是将不同版本的Bundle资源放在不同路径下,方便删除等操作。
我们需要修改Cocos的UrlTransform管线行为。这里简单通俗介绍一下管线和任务,以及我们为什么修改UrlTransform管线。
2.2.1 Cocos管线和任务是什么?
管线是一些方法组合成的数组,任务是这些方法之间传递的一个对象。当我们执行一个任务时,比如加载Bundle,管线开始之前,将我们的参数包装成一个对象,之后传递给第一个方法获取Bundle的真实地址,并把地址存在这个对象身上,再传递给第二个方法去查看本地是否存在缓存,直到运行完数组中的最后一个方法得到最终结果或中途某一方法出错得到一个错误,返回给我们。(以上只是举例,不代表实际存储类型和过程)
2.2.2 Cocos内置的三条管线
transformPiple(转换资源路径管线):获取配置信息 -> 生成请求资源对象 -> 反序列化uuid -> 添加md5 -> 获取子包信息(subpackage)。
loadPiple(加载资源管线):分析参数 -> 转换路径 -> 下载资源 -> 解析资源。
fetchPiple(预加载管线):分析参数 -> 转换路径 -> 下载资源。
2.2.3 TransformPiple
从上可以看出,不管加载方式如何,都一定会走transformPiple,我们在这个管线之后追加方法,如果资源已经存在本地,则将返回路径改为本地,这样,后面的下载过程则会优先在本地寻找,不会实际网络下载。
2.2.4 缓存路径从何而来?
我们确定了在transformPiple修改目标路径的方案,那么我们如何指定新的路径呢?
首先,灵活Bundle存在apk中的assets目录下,我们可以在游戏启动时,先调用一次loadBundle加载灵活Bundle,在bundle._config上包含了这个Bundle中所有的资源信息。代码如下:
let cache = {
}; //灵活bundle本地缓存
let bundleConfig = {
}; //灵活bundle路径配置
cc.assetManager.loadBundle(bundleName,(err,bundle:cc.AssetManager.Bundle)=>{
if(err != nil){
return;
}
bundleConfig[bundle.name] = {