苦海无涯,特此记录。
先吐槽一下。官方的文档要么就是古董,要么就是分散。对于我这个新上手的人来说,上来cocosCreator版本就是2.1.2了,而参考文档还是1.X的版本、几年前的说明。
1、准备工作
1、cocos Creator 版本 V2.1.2
2、Android 原生打包环境–>自行百度(主要是sdk,nkd,ant)
3、cocosCreator 热更新插件 《
热更新manifest生成工具
》
4、hfs网络文件服务器 2.3(自行下载、安装。主要用做本地简单服务器的搭建)
2、预备知识
1、阅读一下官方文档
https://docs.cocos.com/creator/manual/zh/advanced-topics/hot-update.html
https://docs.cocos.com/creator/manual/zh/advanced-topics/assets-manager.html
读完之后,是不是就知道更新大致需要的文档了
2、从官方文档中给出的大致过程
3、开始热更新
新建一个cocosCreator 项目。
1、开始还是先搭建一个简单的服务器
1、在硬盘中创建一个目录,用来热更新的目录
2、打开hfs.exe,将刚刚的目录拖入hfs左侧空白就会自动生成
这样就简单的生成了资源更新服务器了。
2、生成Manifest文档和更新资源包(官方是代码生成的,这里我使用的cocosCreator商店的工具)
->1、创建 version_generator.js 文件 (官方代码仓库也有)
var fs = require('fs');
var path = require('path');
var crypto = require('crypto');
var manifest = {
//服务器上资源文件存放路径(src,res的路径)
packageUrl: 'http://192.168.1.120:8080/hotUpdate/',
remoteManifestUrl: 'http://192.168.1.120:8080/hotUpdate/project.manifest',
remoteVersionUrl: 'http://192.168.1.120:8080/hotUpdate/version.manifest',
version: '1.0.2',
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)).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');
});
*****该文件主要作用是生成资源的更新信息,将创建的
version_generator.js 文档
放在主工程目录下
–>2、打开cocosCreator热更新工具
设置当前版本号,
第一次打包版本号可以就从1.0.0开始,
资源服务器url 对应服务器搭建的目录我这里是
http://192.168.1.120:8080/hotUpdate/
build资源路径就是我们在构建的时候生成的build文件模板
构建的时候,我选择的是default,所有在热更工具中build目录也是这个目录。
完成后,点击生成,再打开目录
已经生成版本文件。里面就是版本更新文件,解压后可以看到有四项文件(Manifest文件,res,src)
到这里,需要打包用的初始Manifest文件就已经生成了。
可以将这两个*.manifest文件拷贝到assets目录下
接下来就是创建热更新组件来负责这个更新逻辑了。
打开前面新建的项目,新建一个场景,一个更新逻辑js
1、创建场景,和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
},
},
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.hotUpdate(); 暂时去掉自动更新
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...');
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());
var msg = event.getMessage();
if (msg) {
cc.log('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();
//}
}
});
新建热更新场景,绑定脚本,创建元素三个按钮,一个标签。
将HotUpdate.js绑定到canvas上,将对应的节点添加到上去。
在场景文件中,创建三个按钮,一个标签。并分别绑定按钮事件。
其中切换场景就是查看热更新效果。
这个project就是我们在使用热更新工具生成出来的manifest文件
第二步完成
接下来,再一次构建项目。注意顺序
在目录build\jsb-default中找到main.js的开头处添加代码
if (jsb) {
var hotUpdateSearchPaths = localStorage.getItem('HotUpdateSearchPaths');
if (hotUpdateSearchPaths) {
jsb.fileUtils.setSearchPaths(JSON.parse(hotUpdateSearchPaths));
}
}
这里你可能会疑惑,这个HotUpdateSearchPaths是什么,它是HotUpdate.js中updateCb函数中设置的对应的目录。
这个时候回到刚刚编译的界面,记得不要再构建了,直接编译出包。
等待中。。。
出包后,放到手机上安装。这个包就是当做旧版本包来使用。前面设置的版本是1.0.0
其实到这里,热更新已经完成了。测试一下。。注意顺序
在项目中,修改helloWord场景的元素。然后
构建
(这个时候,不是新包,不需要修改main.js 中的内容),然后打开热更新工具,修改版本号(提高一个版本),生成。将生成的资源拷贝到搭建的服务器热更目录下,解压。
这个时候再打开手机上的apk,就会发现提示新版本下载。
热更到此结束(罗里吧嗦的一大堆)!
如果还是没有成功。那就再检查一下
1、在version_generator.js文件中
2、在构建好的目录中
在该目录下的main.js中没有添加更新后的路径搜索代码
要还是崩。。。那就自己先看日志自己解决吧(其他的日志都在手机上会显示)。
最后提一下
以前很多前辈写的一写文章都是摸爬打滚出来的。很有参考价值。
但是,奈何敌不过CocosCreator自己的版本更新。
所以很多的bug都是没有试用新版本而已。及时关注他们的更新指南。
这是2.0的升级指南:
https://docs.cocos.com/creator/manual/zh/release-notes/upgrade-guide-v2.0.html
里面就改进了监听(主要是有个问题困扰我很久,最后看网友说去看升级指南。。。,然后就看到问题所在了。)
最后感谢这位博主
https://www.cnblogs.com/gao88/p/11632626.html
,给出了一个非常详细的过程。
最后指出一下,本博文只是简单的热更新。在代码中有很多屏蔽的地方,都是奔溃的。我只是让它实现了最基本的热更新。
还没有测试ios下,也没有太多的优化。比如进度条显示、文件显示。
后续再更新吧!
今天先到这里。