最近学习了一下CocosCreator的热更新相关知识,记录下学习过程。
Creator版本:v3.5.2
官方范例学习文档:热更新范例教程 · Cocos Creator
官方范例Demo链接:mirrors_cocos-creator/tutorial-hot-update - Gitee.com
官方热更新原理详细介绍:热更新管理器 · Cocos Creator
建议先把官方文档认真仔细看一遍。
重要工具:
1:官方Demo中的cocos-tutorial-hot-update-master\extensions\hot-update编辑器插件。
2:官方Demo中的version_generator.js脚本。
一、创建HotUpdateDemo工程
把官方范例里面的extensions\hot-update文件夹和version_generator.js脚本复制到工程中,如下图所示:
二、搭建一个简易的热更新界面
三、编辑HotUpdateController.ts脚本
import { _decorator, Component, Node, Label, Asset, resources, game, ProgressBar, Button } from 'cc';
import { JSB, NATIVE } from 'cc/env';
const { ccclass, property } = _decorator;
@ccclass('HotUpdateController')
export class HotUpdateController extends Component {
@property(Label)
lbCheckUpdate:Label = null;
@property(Node)
nodeUpdateDialog:Node = null;
@property(Label)
lbFilesCount:Label = null;
@property(Label)
lbFilesSize:Label = null;
@property(Asset)
manifestUrl: Asset = null;
@property(ProgressBar)
pbFilesCount:ProgressBar = null;
@property(ProgressBar)
pbFilesSize:ProgressBar = null;
@property(Button)
btnUpdate:Button = null;
private hotUpdateAM:jsb.AssetsManager = null;
start() {
this.nodeUpdateDialog.active = false;
if(!NATIVE || !JSB){
this.node.active = false;
return;
}
let storagePath = jsb.fileUtils.getWritablePath() + "blackjack-remote-asset";
this.hotUpdateAM = new jsb.AssetsManager("", storagePath, (versionA: string, versionB: string)=>{
console.log("HotUpdate JS Custom Version 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;
}
});
this.hotUpdateAM.setVerifyCallback(function (path: string, asset: any) {
// When asset is compressed, we don't need to check its md5, because zip file have been deleted.
var compressed = asset.compressed;
// Retrieve the correct md5 value.
var expectedMD5 = asset.md5;
// asset.path is relative path and path is absolute.
var relativePath = asset.path;
// The size of asset file, but this value could be absent.
var size = asset.size;
if (compressed) {
console.log("HotUpdate", "Verification passed : " + relativePath);
return true;
}
else {
console.log("HotUpdate", "Verification passed : " + relativePath + ' (' + expectedMD5 + ')');
return true;
}
});
this.checkUpdate();
this.btnUpdate.node.on(Node.EventType.TOUCH_END, ()=>{this.dealUpdate();}, this)
}
private checkUpdate(){
if (this.hotUpdateAM.getState() === jsb.AssetsManager.State.UNINITED) {
console.log("HotUpdate", "use local manifest");
var url = this.manifestUrl.nativeUrl;
this.hotUpdateAM.loadLocalManifest(url);
}
if (!this.hotUpdateAM.getLocalManifest() || !this.hotUpdateAM.getLocalManifest().isLoaded()) {
this.lbCheckUpdate.string = 'Failed to load local manifest ...';
return;
}
this.hotUpdateAM.setEventCallback((event: jsb.EventAssetsManager)=>{
switch(event.getEventCode()){
case jsb.EventAssetsManager.ERROR_NO_LOCAL_MANIFEST:
this.lbCheckUpdate.string = "No local manifest file found, hot update skipped.";
this.scheduleOnce(()=>{this.node.active = false;}, 1.0);
break;
case jsb.EventAssetsManager.ERROR_DOWNLOAD_MANIFEST:
case jsb.EventAssetsManager.ERROR_PARSE_MANIFEST:
this.lbCheckUpdate.string = "Fail to download manifest file, hot update skipped.";
this.scheduleOnce(()=>{this.node.active = false;}, 1.0);
break;
case jsb.EventAssetsManager.ALREADY_UP_TO_DATE:
this.lbCheckUpdate.string = "Already up to date with the latest remote version.";
this.scheduleOnce(()=>{this.node.active = false;}, 1.0);
break;
case jsb.EventAssetsManager.NEW_VERSION_FOUND:
this.lbCheckUpdate.string = 'New version found, please try to update. (' + Math.ceil(this.hotUpdateAM.getTotalBytes() / 1024) + 'kb)';
this.lbFilesCount.string = "文件数量:" + this.hotUpdateAM.getTotalFiles().toString();
this.lbFilesSize.string = "文件大小:" + Math.ceil(this.hotUpdateAM.getTotalBytes() / 1024).toString() + "kb";
this.nodeUpdateDialog.active = true;
break;
default:
return;
}
});
this.hotUpdateAM.checkUpdate();
}
private dealUpdate(){
if (this.hotUpdateAM.getState() === jsb.AssetsManager.State.UNINITED) {
console.log("HotUpdate", "use local manifest");
var url = this.manifestUrl.nativeUrl;
this.hotUpdateAM.loadLocalManifest(url);
}
if (!this.hotUpdateAM.getLocalManifest() || !this.hotUpdateAM.getLocalManifest().isLoaded()) {
this.lbCheckUpdate.string = 'Failed to load local manifest ...';
return;
}
this.btnUpdate.node.active = false;
this.hotUpdateAM.setEventCallback((event:jsb.EventAssetsManager)=>{
let needRestart = false;
switch (event.getEventCode()) {
case jsb.EventAssetsManager.ERROR_NO_LOCAL_MANIFEST:
this.lbCheckUpdate.string = 'No local manifest file found, hot update skipped.';
break;
case jsb.EventAssetsManager.UPDATE_PROGRESSION:
this.pbFilesSize.progress = event.getPercent();
this.pbFilesCount.progress = event.getPercentByFile();
break;
case jsb.EventAssetsManager.ERROR_DOWNLOAD_MANIFEST:
case jsb.EventAssetsManager.ERROR_PARSE_MANIFEST:
this.lbCheckUpdate.string = 'Fail to download manifest file, hot update skipped.';
break;
case jsb.EventAssetsManager.ALREADY_UP_TO_DATE:
this.lbCheckUpdate.string = 'Already up to date with the latest remote version.';
break;
case jsb.EventAssetsManager.UPDATE_FINISHED:
this.lbCheckUpdate.string = 'Update finished. ' + event.getMessage();
this.pbFilesSize.progress = 1;
this.pbFilesCount.progress = 1;
needRestart = true;
break;
case jsb.EventAssetsManager.UPDATE_FAILED:
this.lbCheckUpdate.string = 'Update failed. ' + event.getMessage();
break;
case jsb.EventAssetsManager.ERROR_UPDATING:
this.lbCheckUpdate.string = 'Asset update error: ' + event.getAssetId() + ', ' + event.getMessage();
break;
case jsb.EventAssetsManager.ERROR_DECOMPRESS:
this.lbCheckUpdate.string = event.getMessage();
break;
default:
break;
}
if (needRestart) {
this.hotUpdateAM.setEventCallback(null!);
// Prepend the manifest's search path
var searchPaths = jsb.fileUtils.getSearchPaths();
var newPaths = this.hotUpdateAM.getLocalManifest().getSearchPaths();
console.log("HotUpdate", JSON.stringify(newPaths));
Array.prototype.unshift.apply(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.
localStorage.setItem('HotUpdateSearchPaths', JSON.stringify(searchPaths));
jsb.fileUtils.setSearchPaths(searchPaths);
console.log("HotUpdate SearchPaths:", searchPaths);
// restart game.
setTimeout(() => {game.restart();}, 1000);
}
});
this.hotUpdateAM.update();
}
}
四、第一次构建(Android)
构建完成之后,会出现build目录。
使用node命令,运行version_generator.js脚本,生成project.manifest和version.manifest文件
根据node命令制作version_generator.bat批处理文件,方便后续.manifest文件生成
双击version_generator.bat,会生成remote-assets目录,目录中会生成project.manifest和version.manifest文件
把 project.manifest和version.manifest文件复制到assets目录下,并挂载到脚本上HotUpdateController.ts脚本上
五、第二次构建并打出v1.0.0版本的apk包,使用模拟器打开,会出现如下三个界面切换
本步骤完成之后,就算完成了第一个包了,后续我们就需要构建更新包放到远程服务器,然后通过网络热更新来给安装到模拟器上的这个包进行更新。
六、修改游戏内容,第三次构建出v1.0.1版本的资源。
将Label控件的string由Hello改成Hello,Cocos,然后重新构建出新的资源
注意:只需要构建,不需要生成apk包。
修改version_generator.bat文件中的版本号为1.0.1,生成1.0.1版本的project.manifest和version.manifest文件。
七、本地搭建一个临时服务器。
在电脑任意位置创建一个HotUpdate/remote-assets目录,并且把HotUpdateDemo\build\android\assets目录下的新构建的资源和HotUpdateDemo\remote-assets下的.manifest文件复制进去,如下图所示:
利用live-server搭建简易服务器,成功之后会在浏览器中能访问到HotUpdate/remote-assets目录中的所有文件。
八、重启模拟器中的安装的app
至此,一个粗略的热更新流程就完成了。
问题一:只有热更新本次生效,重启app之后就不生效了
检查HotUpdateDemo\build\android\assets\mian.js中是否有如下代码:
这段代码是 HotUpdateDemo\extensions\hot-update插件在构建完成之后,自动加到main.js中的,构建完成时,Creator编辑器控制台也会有如下日志:
如果没有,需要检查这个热更新插件为何没有生效。
问题二:搜索路径会随着热更新次数的增加而增多。
if (needRestart) {
this.hotUpdateAM.setEventCallback(null!);
// Prepend the manifest's search path
var searchPaths = jsb.fileUtils.getSearchPaths();
var newPaths = this.hotUpdateAM.getLocalManifest().getSearchPaths();
console.log("HotUpdate", JSON.stringify(newPaths));
Array.prototype.unshift.apply(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.
localStorage.setItem('HotUpdateSearchPaths', JSON.stringify(searchPaths));
jsb.fileUtils.setSearchPaths(searchPaths);
console.log("HotUpdate SearchPaths:", searchPaths);
// restart game.
setTimeout(() => {game.restart();}, 1000);
}
这段代码是官方Demo里面的,主要作用就是把热更新的新资源路径放在2dx引擎搜索路径的最前面,这样新下载的资源就会优先被搜索到从而达到热更新的目的。
但是这段代码会有一个问题,如果searchPaths数组中本来就存在newPaths,这个时候再往数组前面加一个newPaths,会导致搜索路径的前N个是相同的。
通过这段日志输出也可以看出来,当从1.0.0->1.0.1->1.0.2之后,搜索路径里面已经出现了3条一模一样的搜索路径,所以这里在unshift.apply之前需要判断下原数组中是否存在新路径。