昨天有一个小伙伴问我热更新相关的问题,不过up主之前没有专门研究过这块,刚好今天抽空研究了一下。
发现网上很多的教程都比较老了,教程中用到的不少api已经发生变化,对着操作的话会出现不少问题,所以今天写一个简易的教程,记录一下自己的研究结果,希望可以帮助到需要的朋友。
up主对这块研究的也不是很深入,文中内容仅做抛砖引玉作用,主要是为了帮助刚接触到的朋友,如果哪里说的有问题,欢迎留言指正。
1、cocos creator v2.2.2
2、creator热更新插件(creator应用商店下载)
3、静态资源服务器(这里我使用了anywhere npm包)
4、安卓真机/模拟器
5、Android打包环境
相信想要了解热更新的朋友,打包环境应该都已经有了,篇幅原因,以下就不做讲解了
热更新指的是在游戏运行时,在不需要用户手动更新新版本安装包的情况下,动态的对图片、音频、或游戏脚本等资源进行更新。
一般热更新主要用于原生APP,像H5或是微信小程序这类web应用,因为没有安装包的概念,本身就是存在于web端的,所以并不需要进行热更新。
简单来说,热更新主要就是做两个事情:1、下载新资源;2、覆盖使用新逻辑和资源。在Cocos 中的热更新方式类似于 Web 网页的更新模式:
服务端保存最新版本的完整资源(开发者可以随时更新服务器)
客户端发送请求和服务端版本进行比对获得差异列表
从服务端下载所有新版本中有改动的资源文件
用新资源覆盖旧缓存以及应用包内的文件
Cocos 默认的热更新机制并不是基于补丁包更新的机制,传统的热更新经常对多个版本之间分别生成补丁包,按顺序下载补丁包并更新到最新版本。Cocos 的热更新机制通过直接比较最新版本和本地版本的差异来生成差异列表并更新。
这样即可天然支持跨版本更新,比如本地版本为 A,远程版本是 C,则直接更新 A 和 C 之间的差异,并不需要生成 A 到 B 和 B 到 C 的更新包,依次更新。所以,在这种设计思路下,新版本的文件以离散的方式保存在服务端,更新时以文件为单位下载。

热更新基本流程
1、新建一个creator工程,场景中包含一个label和两个button,如下:

2、安装热更新插件


安装完毕之后,进行参数填写


版本号:默认可以从1.0.0填起
资源服务器url:填写对应的静态资源服务器地址,这里我在本机地址启动了一个静态资源服务器,大家可根据实际情况进行填写
build项目资源文件目录:填写构建出来的项目对应地址(即发布路径,如下图标识部分路径所示)

接下来点击生成按钮生成对应的Manifest文件,

点击之后文件会在项目根目录的packVersion中生成,


图中压缩包里的内容就是我们需要的热更文件了,将里面的内容解压到我们的静态资源服务器对应路径下即可。
接下来将Manifest文件导入assert目录

导入之后,会在asserts目录下看到对应的文件

3、代码部分
// HotUpdate.js
cc.Class({
extends: cc.Component,
properties: {
manifestUrl: cc.Asset,
_updating: false,
_canRetry: false,
_storagePath: '',
label: {
default: null,
type: cc.Label
},
},
checkCb(event) {
cc.log('Code: ' + event.getEventCode());
switch (event.getEventCode()) {
case jsb.EventAssetsManager.ERROR_NO_LOCAL_MANIFEST:
this.label.string = '本地文件丢失';
break;
case jsb.EventAssetsManager.ERROR_DOWNLOAD_MANIFEST:
case jsb.EventAssetsManager.ERROR_PARSE_MANIFEST:
this.label.string = '下载远程mainfest文件错误';
break;
case jsb.EventAssetsManager.ALREADY_UP_TO_DATE:
this.label.string = '已经是最新版本';
break;
case jsb.EventAssetsManager.NEW_VERSION_FOUND:
this.label.string = '有新版本发现,请点击更新';
// this.hotUpdate();
break;
default:
return;
}
this._am.setEventCallback(null);
this._checkListener = null;
this._updating = false;
},
updateCb(event) {
var needRestart = false;
var failed = false;
switch (event.getEventCode()) {
case jsb.EventAssetsManager.ERROR_NO_LOCAL_MANIFEST:
this.label.string = '本地版本文件丢失,无法更新';
failed = true;
break;
case jsb.EventAssetsManager.UPDATE_PROGRESSION:
let percent = parseInt(event.getPercent() * 100);
if (Number.isNaN(percent)) percent = 0;
this.label.string = '更新进度:' + percent;
break;
case jsb.EventAssetsManager.ERROR_DOWNLOAD_MANIFEST:
case jsb.EventAssetsManager.ERROR_PARSE_MANIFEST:
this.label.string = '下载远程版本文件失败';
failed = true;
break;
case jsb.EventAssetsManager.ALREADY_UP_TO_DATE:
this.label.string = '当前为最新版本';
failed = true;
break;
case jsb.EventAssetsManager.UPDATE_FINISHED:
this.label.string = '更新完成. ' + event.getMessage();
needRestart = true;
break;
case jsb.EventAssetsManager.UPDATE_FAILED:
this.label.string = '更新失败. ' + event.getMessage();
this._updating = false;
this._canRetry = true;
break;
case jsb.EventAssetsManager.ERROR_UPDATING:
this.label.string = '资源更新错误: ' + event.getAssetId() + ', ' + event.getMessage();
break;
case jsb.EventAssetsManager.ERROR_DECOMPRESS:
this.label.string = event.getMessage();
break;
default:
break;
}
if (failed) {
this._am.setEventCallback(null);
this._updateListener = null;
this._updating = false;
}
if (needRestart) {
this._am.setEventCallback(null);
this._updateListener = null;
// Prepend the manifest's search path
var searchPaths = jsb.fileUtils.getSearchPaths();
var newPaths = this._am.getLocalManifest().getSearchPaths();
cc.log(JSON.stringify(newPaths));
Array.prototype.unshift(searchPaths, newPaths);
// This value will be retrieved and appended to the default search path during game startup,
// please refer to samples/js-tests/main.js for detailed usage.
// !!! Re-add the search paths in main.js is very important, otherwise, new scripts won't take effect.
cc.sys.localStorage.setItem('HotUpdateSearchPaths', JSON.stringify(searchPaths));
jsb.fileUtils.setSearchPaths(searchPaths);
cc.audioEngine.stopAll();
cc.game.restart();
}
},
retry() {
if (!this._updating && this._canRetry) {
this._canRetry = false;
this.label.string = '重现获取失败资源...';
this._am.downloadFailedAssets();
}
},
checkUpdate() {
if (this._updating) {
this.label.string = '检查更新中...';
return;
}
if (this._am.getState() === jsb.AssetsManager.State.UNINITED) {
this._am.loadLocalManifest(this.manifestUrl);
}
if (!this._am.getLocalManifest() || !this._am.getLocalManifest().isLoaded()) {
this.label.string = '本地manifest加载失败...';
return;
}
this._am.setEventCallback(this.checkCb.bind(this));
this._am.checkUpdate();
this._updating = true;
},
hotUpdate() {
if (this._am && !this._updating) {
this._am.setEventCallback(this.updateCb.bind(this));
if (this._am.getState() === jsb.AssetsManager.State.UNINITED) {
this._am.loadLocalManifest(this.manifestUrl);
}
this._failCount = 0;
this._am.update();
this._updating = true;
}
},
// use this for initialization
onLoad() {
if (!cc.sys.isNative) {
return;
}
this._storagePath = ((jsb.fileUtils ? jsb.fileUtils.getWritablePath() : '/') + 'remote-asset');
cc.log('Storage path for remote asset : ' + this._storagePath);
this.versionCompareHandle = (versionA, versionB) => {
this.label.string = 'Compare: version A is ' + versionA + ', version B is ' + versionB;
var vA = versionA.split('.');
var vB = versionB.split('.');
for (var i = 0; i < vA.length; ++i) {
var a = parseInt(vA[i]);
var b = parseInt(vB[i] || 0);
if (a === b) {
continue;
}
else {
return a - b;
}
}
if (vB.length > vA.length) {
return -1;
}
else {
return 0;
}
};
// Init with empty manifest url for testing custom manifest
this._am = new jsb.AssetsManager('', this._storagePath, this.versionCompareHandle);
this._am.setVerifyCallback((path, asset) => {
var compressed = asset.compressed;
var expectedMD5 = asset.md5;
var relativePath = asset.path;
var size = asset.size;
if (compressed) {
this.label.string = 'Verification passed : ' + relativePath;
return true;
} else {
this.label.string = 'Verification passed : ' + relativePath + ' (' + expectedMD5 + ')';
return true;
}
});
this.label.string = '热更新组件加载完毕,请手动点击检测按钮';
if (cc.sys.os === cc.sys.OS_ANDROID) {
// Some Android device may slow down the download process when concurrent tasks is too much.
// The value may not be accurate, please do more test and find what's most suitable for your game.
this._am.setMaxConcurrentTask(2);
// this.label.string = 'Max concurrent tasks count have been limited to 2';
}
//检查更新
// this.checkUpdate();
},
onDestroy() {
if (this._updateListener) {
this._am.setEventCallback(null);
this._updateListener = null;
}
}
});
4、组件绑定



5、测试热更
1)前面的部分完成之后,重新对项目进行构建
2)build\jsb-default\main.js开头添加如下代码(划重点!)
// 在 main.js 的开头添加如下代码
(function () {
if (typeof window.jsb === 'object') {
var hotUpdateSearchPaths = localStorage.getItem('HotUpdateSearchPaths');
if (hotUpdateSearchPaths) { jsb.fileUtils.setSearchPaths(JSON.parse(hotUpdateSearchPaths));
}
} })();
3)编译出apk包并安装

4)修改场景中的内容,并重新构建
这里我随便加了一个色块进去



生成热更资源的时候版本号提升一位即可,搞定之后把新的资源替换掉静态资源服务器里的老资源
5)测试结果



https://docs.cocos.com/creator/manual/zh/advanced-topics/hot-update.html
https://docs.cocos.com/creator/manual/zh/advanced-topics/assets-manager.html
https://blog.csdn.net/huanghuipost/article/details/102569135