一、挂载热更新脚本
场景Canvas里面增加脚本HotUpdate.js,对应检查更新和更新两个按钮的功能。
cc.Class({
extends: cc.Component,
properties: {
manifestUrl: {
type: cc.Asset,
default: null
},
updateUI:{
default:null,
type:cc.Node,
},
_updating: false,
_canRetry: false,
_storagePath: '',
progressBar:{
default:null,
type:cc.ProgressBar,
},
},
checkCb: function (event) {
cc.log('Code: ' + event.getEventCode());
switch (event.getEventCode())
{
case jsb.EventAssetsManager.ERROR_NO_LOCAL_MANIFEST:
cc.log("No local manifest file found, hot update skipped.") ;
break;
case jsb.EventAssetsManager.ERROR_DOWNLOAD_MANIFEST:
case jsb.EventAssetsManager.ERROR_PARSE_MANIFEST:
cc.log("Fail to download manifest file, hot update skipped.")
break;
case jsb.EventAssetsManager.ALREADY_UP_TO_DATE:
cc.log("Already up to date with the latest remote version.")
break;
case jsb.EventAssetsManager.NEW_VERSION_FOUND:
cc.log('New version found, please try to update. (' + this._am.getTotalBytes() + ')')
break;
default:
return;
}
this._am.setEventCallback(null);
this._checkListener = null;
this._updating = false;
},
updateCb: function (event) {
var needRestart = false;
var failed = false;
switch (event.getEventCode())
{
case jsb.EventAssetsManager.ERROR_NO_LOCAL_MANIFEST:
cc.log('No local manifest file found, hot update skipped.')
failed = true;
break;
case jsb.EventAssetsManager.UPDATE_PROGRESSION:
this.show();
// cc.log("大小百分比:",event.getPercent())
// cc.log("文件百分比:",event.getPercentByFile())
// cc.log("文件进度:",event.getDownloadedFiles() + ' / ' + event.getTotalFiles())
// cc.log("大小进度:",event.getDownloadedBytes() + ' / ' + event.getTotalBytes())
var title = this.updateUI.getChildByName("title").getComponent(cc.Label)
title.string = parseInt(event.getDownloadedBytes()) + ' / ' + parseInt(event.getTotalBytes());
var progressText = this.updateUI.getChildByName("progressText").getComponent(cc.Label);
progressText.string = event.getPercent().toFixed(2)*100+"%";
// var progressBar = this.updateUI.getChildByName("progress")
this.progressBar.progress = event.getPercent();
cc.log("showPercent = ",(event.getPercent()).toFixed(1),event.getPercent())
var msg = event.getMessage();
if (msg) {
cc.log(event.getPercent()/100 + '% : ' + msg);
}
break;
case jsb.EventAssetsManager.ERROR_DOWNLOAD_MANIFEST:
case jsb.EventAssetsManager.ERROR_PARSE_MANIFEST:
cc.log('Fail to download manifest file, hot update skipped.')
failed = true;
break;
case jsb.EventAssetsManager.ALREADY_UP_TO_DATE:
cc.log('Already up to date with the latest remote version.')
failed = true;
break;
case jsb.EventAssetsManager.UPDATE_FINISHED:
cc.log('Update finished. ' + event.getMessage())
needRestart = true;
break;
case jsb.EventAssetsManager.UPDATE_FAILED:
cc.log('Update failed. ' + event.getMessage())
this._updating = false;
this._canRetry = true;
break;
case jsb.EventAssetsManager.ERROR_UPDATING:
cc.log('Asset update error: ' + event.getAssetId() + ', ' + event.getMessage())
break;
case jsb.EventAssetsManager.ERROR_DECOMPRESS:
cc.log(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();
console.log("newPaths:",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.
cc.sys.localStorage.setItem('HotUpdateSearchPaths', JSON.stringify(searchPaths));
jsb.fileUtils.setSearchPaths(searchPaths);
cc.audioEngine.stopAll();
cc.game.restart();
cc.log("重启游戏")
}
},
retry: function () {
if (!this._updating && this._canRetry) {
this._canRetry = false;
cc.log('Retry failed Assets...');
this._am.downloadFailedAssets();
}
},
checkUpdate: function () {
if (!cc.sys.isNative){
return;
}
if (this._updating) {
cc.log('Checking or updating ...')
return;
}
if (this._am.getState() === jsb.AssetsManager.State.UNINITED) {
// Resolve md5 url
var url = this.manifestUrl.nativeUrl;
if (cc.loader.md5Pipe) {
url = cc.loader.md5Pipe.transformURL(url);
}
this._am.loadLocalManifest(url);
}
if (!this._am.getLocalManifest() || !this._am.getLocalManifest().isLoaded()) {
cc.log('Failed to load local manifest ...')
return;
}
this._am.setEventCallback(this.checkCb.bind(this));
this._am.checkUpdate();
this._updating = true;
},
hotUpdate: function () {
if (!cc.sys.isNative){
return;
}
if (this._am && !this._updating) {
this._am.setEventCallback(this.updateCb.bind(this));
if (this._am.getState() === jsb.AssetsManager.State.UNINITED) {
// Resolve md5 url
var url = this.manifestUrl.nativeUrl;
if (cc.loader.md5Pipe) {
url = cc.loader.md5Pipe.transformURL(url);
}
this._am.loadLocalManifest(url);
}
this._failCount = 0;
this._am.update();
this._updating = true;
}
},
show: function () {
if (this.updateUI.active === false) {
this.updateUI.active = true;
}
},
// use this for initialization
onLoad: function () {
// Hot update is only available in Native build
if (!cc.sys.isNative) {
return;
}
this._storagePath = ((jsb.fileUtils ? jsb.fileUtils.getWritablePath() : '/') + 'myCatchFish-assets');
cc.log('Storage path for remote asset : ' + this._storagePath);
// Setup your own version compare handler, versionA and B is versions in string
// if the return value greater than 0, versionA is greater than B,
// if the return value equals 0, versionA equals to B,
// if the return value smaller than 0, versionA is smaller than B.
this.versionCompareHandle = function (versionA, versionB) {
cc.log("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;
}
};
// Init with empty manifest url for testing custom manifest
this._am = new jsb.AssetsManager('', this._storagePath, this.versionCompareHandle);
var panel = this.panel;
// Setup the verification callback, but we don't have md5 check function yet, so only print some message
// Return true if the verification passed, otherwise return false
this._am.setVerifyCallback(function (path, asset) {
// 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) {
cc.log("Verification passed : " + relativePath)
return true;
}
else {
cc,log("Verification passed : " + relativePath + ' (' + expectedMD5 + ')')
return true;
}
});
cc.log('Hot update is ready, please check or directly update.')
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);
cc.log("Max concurrent tasks count have been limited to 2")
}
},
onDestroy: function () {
if (this._updateListener) {
this._am.setEventCallback(null);
this._updateListener = null;
}
}
});
对应界面显示
二、准备旧版本apk
1.从官网热更新范例里面下载version_generator.js脚本,需要修改的地方为,主要设置version参数为旧版本号
修改路径
上面的src和res对应的是jsb-link发布路径下面的目录
打开终端,cd到version_generator.js目录下面,运行命令(在这之前需要安装node环境)
node version_generator.js
也可以不修改version_generator.js文件,直接命令行传递参数
node version_generator.js -v 1.0.0 -u http://your-server-address/hot-update/remote-assets/ -s native/package/ -d assets/
运行成功终端显示
然后我们在assets目录下面会多出两个文件
现在,可以构建发布Android工程了,cocos creator里面【项目】-> 【构建发布】,选择发布平台为Android,记得不要勾选MD5Cache,否则热更新失效,【构建】
注意,构建完成后,jsb-link目录下面的main.js会刷新,这时候要在main.js第一行增加搜索路径设置的逻辑和更新中断修复代码,否则,热更新成功后,退出游戏重新进入游戏会恢复旧版本的代码
// 在 main.js 的开头添加如下代码
(function () {
if (typeof window.jsb === 'object') {
var hotUpdateSearchPaths = localStorage.getItem('HotUpdateSearchPaths');
if (hotUpdateSearchPaths) {
var paths = JSON.parse(hotUpdateSearchPaths);
jsb.fileUtils.setSearchPaths(paths);
var fileList = [];
var storagePath = paths[0] || '';
var tempPath = storagePath + '_temp/';
var baseOffset = tempPath.length;
if (jsb.fileUtils.isDirectoryExist(tempPath) && !jsb.fileUtils.isFileExist(tempPath + 'project.manifest.temp')) {
jsb.fileUtils.listFilesRecursively(tempPath, fileList);
fileList.forEach(srcPath => {
var relativePath = srcPath.substr(baseOffset);
var dstPath = storagePath + relativePath;
if (srcPath[srcPath.length] == '/') {
cc.fileUtils.createDirectory(dstPath)
}
else {
if (cc.fileUtils.isFileExist(dstPath)) {
cc.fileUtils.removeFile(dstPath)
}
cc.fileUtils.renameFile(srcPath, dstPath);
}
})
cc.fileUtils.removeDirectory(tempPath);
}
}
}
})();
编译->运行(需要先打开模拟器才会自动安装运行apk),至此,旧版本apk完成。
三、生成新版本热更新文件,部署到服务器上
1、修改version_generator.js里面的版本号(1.0.1)
2、修改工程内容,(可以往resource文件夹里面增加一些图片文件,增加场景,然后在新场景中防入新增的图片)
3、运行node version_generator.js命令,得到新的version.manifest和project.manifest两个文件,cocos creator【构建】->【编译】,得到新版本的jsb-link目录
注意:这里构建完也要像旧版本工程那样在main.js第一行增加搜索路径设置的逻辑和更新中断修复代码
4、添加热更新包
新建热更新目录remote-assets,将jsb-link目录下的res和src,还有上面生成的两个manifest文件复制到remote-assets下
5.搭建下载服务器(可以用python搭建简易服务器和nodejs+express或者其他服务器),这里用nodejs+express搭建一个简单服务器,将热更新目录放到服务器上的hot-update/remote-assets目录下面
cd到根目录,运行nmp start命令,服务器启动,我们验证一下服务器下载是否成功,请求一下version_generator.js里面配置的远程资源路径
http://192.168.0.103:3000/hot-update/remote-assets/project.manifest
请求结果:
说明服务器资源可以正常下载
四、打开旧版本测试更新
点击检查更新按钮,日志显示发现新版本,点击更新按钮,弹出更新窗口更新,更新完毕重启游戏进入新版本界面
五、构建完自动更新main.js文件
1.在工程packages目录下面增加 hot-update 编辑器插件
main.js
'use strict';
var Fs = require("fire-fs");
var Path = require("fire-path");
var inject_script = `
(function () {
if (typeof window.jsb === 'object') {
var hotUpdateSearchPaths = localStorage.getItem('HotUpdateSearchPaths');
if (hotUpdateSearchPaths) {
var paths = JSON.parse(hotUpdateSearchPaths);
jsb.fileUtils.setSearchPaths(paths);
var fileList = [];
var storagePath = paths[0] || '';
var tempPath = storagePath + '_temp/';
var baseOffset = tempPath.length;
if (jsb.fileUtils.isDirectoryExist(tempPath) && !jsb.fileUtils.isFileExist(tempPath + 'project.manifest.temp')) {
jsb.fileUtils.listFilesRecursively(tempPath, fileList);
fileList.forEach(srcPath => {
var relativePath = srcPath.substr(baseOffset);
var dstPath = storagePath + relativePath;
if (srcPath[srcPath.length] == '/') {
cc.fileUtils.createDirectory(dstPath)
}
else {
if (cc.fileUtils.isFileExist(dstPath)) {
cc.fileUtils.removeFile(dstPath)
}
cc.fileUtils.renameFile(srcPath, dstPath);
}
})
cc.fileUtils.removeDirectory(tempPath);
}
}
}
})();
`;
module.exports = {
load: function () {
// 当 package 被正确加载的时候执行
},
unload: function () {
// 当 package 被正确卸载的时候执行
},
messages: {
'editor:build-finished': function (event, target) {
var root = Path.normalize(target.dest);
var url = Path.join(root, "main.js");
Fs.readFile(url, "utf8", function (err, data) {
if (err) {
throw err;
}
var newStr = inject_script + data;
Fs.writeFile(url, newStr, function (error) {
if (err) {
throw err;
}
Editor.log("SearchPath updated in built main.js for hot update");
});
});
}
}
};
package.json
{
"name": "hot-update",
"version": "0.0.1",
"description": "用于热更新插件",
"author": "Cocos Creator",
"main": "main.js"
}
这样就不用每次构建都要手动插入一段增加搜索路径设置的逻辑和更新中断修复代码了。
<完>