creator用了有一年了,一直没有注意热更新!这段刚好有时间,来看看热更的实现!下面我们一起来看看具体的实现办法!
我这里使用的工具如下:
我们要实现热更新,必须要有个理论依据,需要知道热更新的原理,和实现步骤!毕竟没有流程我们不知道如何下手!我们就从官方文档开始一步一步的熟悉大致的流程!
下面是官方的文档,并且举例给了Demo:
从官方文档中我们大概知道了,热更新的原理:
游戏资源来说,也可以在资源服务器上保存一份完整的资源,客户端更新时与服务端进行比对,下载有差异的文件并替换缓存。无差异的部分继续使用包内版本或是缓存文件。这样我们更新游戏需要的就是:
服务端保存最新版本的完整资源(开发者可以随时更新服务器)
客户端发送请求和服务端版本进行比对获得差异列表
从服务端下载所有新版本中有改动的资源文件
用新资源覆盖旧缓存以及应用包内的文件
这就是整个热更新流程的设计思路,当然里面还有非常多具体的细节,后面会结合实际流程进行梳理。这里需要特别指出的是:
Cocos 默认的热更新机制并不是基于补丁包更新的机制,传统的热更新经常对多个版本之间分别生成补丁包,按顺序下载补丁包并更新到最新版本。Cocos 的热更新机制通过直接比较最新版本和本地版本的差异来生成差异列表并更新。 这样即可天然支持跨版本更新,比如本地版本为 A,远程版本是 C,则直接更新 A 和 C 之间的差异,并不需要生成 A 到 B 和 B 到 C 的更新包,依次更新。所以,在这种设计思路下,新版本的文件以离散的方式保存在服务端,更新时以文件为单位下载。
基于这样的项目结构,本篇教程中的热更新思路很简单:
1、基于原生打包目录中的 assets 和 src 目录生成本地 Manifest 文件。
2、创建一个热更新组件来负责热更新逻辑。
3、游戏发布后,若需要更新版本,则生成一套远程版本资源,包含 assets 目录、src 目录和 Manifest 文件,将远程版本部署到服务端。
4、当热更新组件检测到服务端 Manifest 版本不一致时,就会开始热更新
这里也将按照这个思路来实现一个热更新Demo!
这里将分为以下几个部分介绍!
在硬盘中创建一个目录,用来热更新的目录。我这里是D:\hotUpdate_client:
通过上面的地址下载后是一个压缩文件,解压到磁盘中,启动hfs.exe,我这里是hfs_490056.exe!点击箭头根部的按钮,设置端口号为8080!将刚刚的目录拖入hfs左侧空白就会自动生成,此时点击左边列表中的hotUpdate_client文件夹就会看到形如红框中的地址!这个地址就是将来热更的地址!
这样就简单的生成了资源更新服务器了。
任务目标:通过热更新来实现一个切换游戏场景的功能!
先创建一个新项目,添加如下控件:
其中三个按钮check绑定checkUpdate,update绑定hotUpdate,changeScene用来切换场景,这里可以先绑定上,然后隐藏这个按钮!这里绑定事件需要后面添加了脚本后再绑定,保存场景为test,我们通过更新来显示changeScene按钮,并且点击按钮来切换场景!新的场景这里先不制作,等出了包之后再更改做!
首先需要再项目的根目录添加version_generator.js脚本( 官方代码仓库也有),放置如下位置:
这里把代码贴在下面:
/**
* 此模块用于热更新工程清单文件的生成
*/
var fs = require('fs');
var path = require('path');
var crypto = require('crypto');
var manifest = {
//服务器上资源文件存放路径(src,res的路径)http://192.168.1.133:8080/hotUpdate_client/
packageUrl: 'http://192.168.1.133:8080/hotUpdate_client/',
//服务器上project.manifest路径
remoteManifestUrl: 'http://192.168.1.133:8080/hotUpdate_client/project.manifest',
//服务器上version.manifest路径
remoteVersionUrl: 'http://192.168.1.133:8080/hotUpdate_client/remote-assets/version.manifest',
version: '1.0.0',
assets: {},
searchPaths: []
};
//生成的manifest文件存放目录
var dest = 'assets/';
//项目构建后资源的目录
var src = 'build/jsb-default/';
/**
* node version_generator.js -v 1.0.0 -u http://your-server-address/tutorial-hot-update/remote-assets/ -s native/package/ -d assets/
*/
// Parse arguments
var i = 2;
while ( i < process.argv.length) {
var arg = process.argv[i];
switch (arg) {
case '--url' :
case '-u' :
var url = process.argv[i+1];
manifest.packageUrl = url;
manifest.remoteManifestUrl = url + 'project.manifest';
manifest.remoteVersionUrl = url + 'version.manifest';
i += 2;
break;
case '--version' :
case '-v' :
manifest.version = process.argv[i+1];
i += 2;
break;
case '--src' :
case '-s' :
src = process.argv[i+1];
i += 2;
break;
case '--dest' :
case '-d' :
dest = process.argv[i+1];
i += 2;
break;
default :
i++;
break;
}
}
function readDir (dir, obj) {
var stat = fs.statSync(dir);
if (!stat.isDirectory()) {
return;
}
var subpaths = fs.readdirSync(dir), subpath, size, md5, compressed, relative;
for (var i = 0; i < subpaths.length; ++i) {
if (subpaths[i][0] === '.') {
continue;
}
subpath = path.join(dir, subpaths[i]);
stat = fs.statSync(subpath);
if (stat.isDirectory()) {
readDir(subpath, obj);
}
else if (stat.isFile()) {
// Size in Bytes
size = stat['size'];
md5 = crypto.createHash('md5').update(fs.readFileSync(subpath, 'binary')).digest('hex');
compressed = path.extname(subpath).toLowerCase() === '.zip';
relative = path.relative(src, subpath);
relative = relative.replace(/\\/g, '/');
relative = encodeURI(relative);
obj[relative] = {
'size' : size,
'md5' : md5
};
if (compressed) {
obj[relative].compressed = true;
}
}
}
}
var mkdirSync = function (path) {
try {
fs.mkdirSync(path);
} catch(e) {
if ( e.code != 'EEXIST' ) throw e;
}
}
// Iterate res and src folder
readDir(path.join(src, 'src'), manifest.assets);
readDir(path.join(src, 'res'), manifest.assets);
var destManifest = path.join(dest, 'project.manifest');
var destVersion = path.join(dest, 'version.manifest');
mkdirSync(dest);
fs.writeFile(destManifest, JSON.stringify(manifest), (err) => {
if (err) throw err;
console.log('Manifest successfully generated');
});
delete manifest.assets;
delete manifest.searchPaths;
fs.writeFile(destVersion, JSON.stringify(manifest), (err) => {
if (err) throw err;
console.log('Version successfully generated');
});
注意:以下几个地方,你可能需要根据自己的需求修改,后面我们会再介绍!
packageUrl:服务器存放资源文件(src res)的路径
remoteManifestUrl:服务器存放资源清单文件(project.manifest)的路径
remoteVersionUrl:服务器存放version.manifest的路径
dest:要生成的manifest文件存放路径
src:项目构建后的资源目录
然后在asset目录添加热更新组件,并挂载热更新脚本HotUpdate.js
如图所示:
下面是详细内容:
//HotUpdate.js
/**
* 负责热更新逻辑的组件
*/
cc.Class({
extends: cc.Component,
properties: {
manifestUrl: cc.RawAsset, //本地project.manifest资源清单文件
_updating:false,
_canRetry:false,
_storagePath:'',
label: {
default: null,
type: cc.Label
},
fileLabel:cc.Label,
fileProgress:cc.ProgressBar,
byteLabel:cc.Label,
byteProgress:cc.ProgressBar
},
checkCb: function (event) {
var self = this;
cc.log('Code: ' + event.getEventCode());
switch (event.getEventCode()) {
case jsb.EventAssetsManager.ERROR_NO_LOCAL_MANIFEST:
self.label.string = '本地文件丢失';
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.");
self.label.string = '下载远程mainfest文件错误';
break;
case jsb.EventAssetsManager.ALREADY_UP_TO_DATE:
cc.log("Already up to date with the latest remote version.");
self.label.string = '已经是最新版本';
break;
case jsb.EventAssetsManager.NEW_VERSION_FOUND:
cc.log('New version found, please try to update.');
self.label.string = '有新版本发现,请点击更新';
this.fileProgress.progress = 0;
this.byteProgress.progress = 0;
//this.hotUpdate(); 暂时去掉自动更新
break;
default:
return;
}
this._am.setEventCallback(null);
//this._checkListener = null;
this._updating = false;
},
updateCb: function (event) {
var self = this;
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...');
self.label.string = '本地版本文件丢失,无法更新';
failed = true;
break;
case jsb.EventAssetsManager.UPDATE_PROGRESSION:
cc.log(event.getPercent());
cc.log(event.getPercentByFile());
cc.log(event.getDownloadedFiles() + ' / ' + event.getTotalFiles());
cc.log(event.getDownloadedBytes() + ' / ' + event.getTotalBytes());
this.byteProgress.progress = event.getPercent();
this.fileProgress.progress = event.getPercentByFile();
this.fileLabel.string = event.getDownloadedFiles() + ' / ' + event.getTotalFiles();
this.byteLabel.string = event.getDownloadedBytes() + ' / ' + event.getTotalBytes();
var msg = event.getMessage();
if (msg) {
cc.log('Updated file: ' + msg);
this.label.string = 'Updated file: ' + msg;
}
break;
case jsb.EventAssetsManager.ERROR_DOWNLOAD_MANIFEST:
case jsb.EventAssetsManager.ERROR_PARSE_MANIFEST:
cc.log('Fail to download manifest file, hot update skipped.');
self.label.string = '下载远程版本文件失败';
failed = true;
break;
case jsb.EventAssetsManager.ALREADY_UP_TO_DATE:
cc.log('Already up to date with the latest remote version.');
self.label.string = '当前为最新版本';
failed = true;
break;
case jsb.EventAssetsManager.UPDATE_FINISHED:
cc.log('Update finished. ' + event.getMessage());
self.label.string = '更新完成. ' + event.getMessage();
needRestart = true;
break;
case jsb.EventAssetsManager.UPDATE_FAILED:
cc.log('Update failed. ' + event.getMessage());
self.label.string = '更新失败. ' + event.getMessage();
this._updating = false;
this._canRetry = true;
break;
case jsb.EventAssetsManager.ERROR_UPDATING:
cc.log('Asset update error: ' + event.getAssetId() + ', ' + event.getMessage());
self.label.string = '资源更新错误: ' + event.getAssetId() + ', ' + event.getMessage();
break;
case jsb.EventAssetsManager.ERROR_DECOMPRESS:
cc.log(event.getMessage());
self.label.string = event.getMessage();
break;
default:
break;
}
if (failed) {
//cc.eventManager.removeListener(this._updateListener);
this._am.setEventCallback(null);
//this._updateListener = null;
this._updating = false;
}
if (needRestart) {
//cc.eventManager.removeListener(this._updateListener);
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: function () {
if (!this._updating && this._canRetry) {
this._canRetry = false;
cc.log('Retry failed Assets...');
this._am.downloadFailedAssets();
}
},
/* checkForUpdate: function () {
cc.log("start checking...");
if (this._updating) {
cc.log('Checking or updating ...');
return;
}
if (this._am.getState() === jsb.AssetsManager.State.UNINITED) {
this._am.loadLocalManifest(this.manifestUrl);
cc.log(this.manifestUrl);
}
if (!this._am.getLocalManifest() || !this._am.getLocalManifest().isLoaded()) {
cc.log('Failed to load local manifest ...');
return;
}
this._checkListener = new jsb.EventListenerAssetsManager(this._am, this.checkCb.bind(this));
this._checkListener.setEventCallback(this.checkCb.bind(this));
//cc.eventManager.addListener(this._checkListener, 1);
this._am.checkUpdate();
this._updating = true;
}, */
checkForUpdate:function(){
/* if (this._updating) {
cc.log('Checking or updating ...');
return;
} */
//cc.log("加载更新配置文件");
//this._am.loadLocalManifest(this.manifestUrl);
//cc.log(this.manifestUrl);
//this.tipLabel.string = '检查更新';
//cc.log("start checking...");
//var state = this._am.getState()
//if (state=== jsb.AssetsManager.State.UNINITED) {
// Resolve md5 url
console.log('检查更新')
this._am.setEventCallback(this.checkCb.bind(this));
this._failCount = 0;
this._am.checkUpdate();
this._updating = true;
// }
},
hotUpdate: function () {
if (this._am && !this._updating) {
//this._updateListener = new jsb.EventListenerAssetsManager(this._am, this.updateCb.bind(this));
this._am.setEventCallback(this.updateCb.bind(this));
//cc.eventManager.addListener(this._updateListener, 1);
this._am.loadLocalManifest(this.manifestUrl);
this._failCount = 0;
this._am.update();
this._updating = true;
}
},
show: function () {
// if (this.updateUI.active === false) {
// this.updateUI.active = true;
// }
},
changesence:function(){
cc.log("改变场景");
cc.director.loadScene("helloworld");
},
// use this for initialization
onLoad: function () {
var self = this;
// Hot update is only available in Native build
console.log("onloadUpdate");
if (!cc.sys.isNative) {
return;
}
this._storagePath = ((jsb.fileUtils ? jsb.fileUtils.getWritablePath() : '/') + 'remote-asset');
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);
self.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);
// 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;
}
}.bind(this));
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");
}
this._am.loadLocalManifest(this.manifestUrl);
cc.log(this.manifestUrl);
//检查更新
this.checkUpdate()
},
checkUpdate:function() {
console.log('检查更新')
this._am.setEventCallback(this.checkCb.bind(this));
this._failCount = 0;
this._am.checkUpdate();
this._updating = true;
},
onDestroy: function () {
if (this._updateListener) {
//cc.eventManager.removeListener(this._updateListener);
this._am.setEventCallback(null);
//this._updateListener = null;
}
//if (this._am && !cc.sys.ENABLE_GC_FOR_NATIVE_OBJECTS) {
// this._am.release();
//}
}
});
注意: 我们选择的模板是 “default” ,发布路径为 “./build” ,发布后的项目资源相对路径为:build/jsb-default
构建完成后,很多人说需要修改main.js,参照了官方demo,附加搜索路径设置的逻辑,放在main.js的开头,测试发现,构建项目生成的main.js中,已包含判断,不用修改,也可以成功,如图为证:
如下:
if (window.cc && cc.sys.isNative) {
var hotUpdateSearchPaths = cc.sys.localStorage.getItem('HotUpdateSearchPaths');
if (hotUpdateSearchPaths) {
jsb.fileUtils.setSearchPaths(JSON.parse(hotUpdateSearchPaths));
}
}
此时,打开项目->热更新工具(没有的参照博文开头的办法自行添加)
然后执行第一步,生成得到打开如图所示的文件夹:
此时打开看到一个带有版本号的压缩文件,包含如下文件:
此时执行步骤2的导入manifest,就在复制了两个版本文件到asset目录中。到这里还需要在挂在HotUpdate.js的组件上指定manifest,就是上面第二步生成到项目中asset目录中的project.manifest文件。然后构建、编译,打包apk即可,准备好了包,开始准备下一步的热更资源!
这里我们把changescene按钮设为可见,然后创建一个新的场景,添加一个label,如图:
保存场景为helloworld。
打包资源,项目->构建发布->构建,打开热更组件修改版本号为1.0.2,生成->部署!至此热更资源准备完成。
为了方便大家,当然如果有不明白的童鞋也可以在这里点此下载Demo示例!
The End
好了,今天的分享就到这里,如有不足之处,还望大家及时指正,随时欢迎探讨交流!!!
喜欢的朋友们,请收藏、点赞、评论!您的肯定是我写作的不竭动力!