惯例先上官方文档:
http://docs.cocos.com/creator/manual/zh/advanced-topics/hot-update.html
http://docs.cocos.com/creator/manual/zh/advanced-topics/assets-manager.html
参考:
Cocos Creator热更新
cocoscreator热更新-小白教程
本人所用的cocos creator版本为 2.0.8
具体步骤如下:
1.打开cc创建一个helloworld工程
2.新建脚本HotUpdate.js添加到场景的Canvas上。
3.在场景中添加两个进度条,三个Label,三个按钮,分别拖到到HotUpdate.js上。同时,绑定"检查版本"和"更新"按钮的点击事件。
HotUpdate.js如下:
cc.Class({
extends: cc.Component,
properties: {
byteLabel: cc.Label,
fileLabel: cc.Label,
byteProgress: cc.ProgressBar,
fileProgress: cc.ProgressBar,
infoLabel: cc.Label,
manifestUrl: {
type: cc.Asset,
default: null
},
},
checkCb: function (event) {
cc.log('Code: ' + event.getEventCode());
switch (event.getEventCode())
{
case jsb.EventAssetsManager.ERROR_NO_LOCAL_MANIFEST:
this.infoLabel.string = "No local manifest file found, hot update skipped.";
break;
case jsb.EventAssetsManager.ERROR_DOWNLOAD_MANIFEST:
case jsb.EventAssetsManager.ERROR_PARSE_MANIFEST:
this.infoLabel.string = "Fail to download manifest file, hot update skipped.";
break;
case jsb.EventAssetsManager.ALREADY_UP_TO_DATE:
this.infoLabel.string = "Already up to date with the latest remote version.";
break;
case jsb.EventAssetsManager.NEW_VERSION_FOUND:
this.infoLabel.string = 'New version found, please try to update.';
// this.panel.checkBtn.active = false;
this.fileProgress.progress = 0;
this.byteProgress.progress = 0;
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:
this.infoLabel.string = 'No local manifest file found, hot update skipped.';
failed = true;
break;
case jsb.EventAssetsManager.UPDATE_PROGRESSION:
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) {
this.infoLabel.string = 'Updated file: ' + msg;
// cc.log(event.getPercent()/100 + '% : ' + msg);
}
break;
case jsb.EventAssetsManager.ERROR_DOWNLOAD_MANIFEST:
case jsb.EventAssetsManager.ERROR_PARSE_MANIFEST:
this.infoLabel.string = 'Fail to download manifest file, hot update skipped.';
failed = true;
break;
case jsb.EventAssetsManager.ALREADY_UP_TO_DATE:
this.infoLabel.string = 'Already up to date with the latest remote version.';
failed = true;
break;
case jsb.EventAssetsManager.UPDATE_FINISHED:
this.infoLabel.string = 'Update finished. ' + event.getMessage();
needRestart = true;
break;
case jsb.EventAssetsManager.UPDATE_FAILED:
this.infoLabel.string = 'Update failed. ' + event.getMessage();
// this.panel.retryBtn.active = true;
this._updating = false;
this._canRetry = true;
break;
case jsb.EventAssetsManager.ERROR_UPDATING:
this.infoLabel.string = 'Asset update error: ' + event.getAssetId() + ', ' + event.getMessage();
break;
case jsb.EventAssetsManager.ERROR_DECOMPRESS:
this.infoLabel.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();
console.log(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();
}
},
loadCustomManifest: function () {
console.log("=========================loadCustomManifest=============================")
if (this._am.getState() === jsb.AssetsManager.State.UNINITED) {
var manifest = new jsb.Manifest(customManifestStr, this._storagePath);
this._am.loadLocalManifest(manifest, this._storagePath);
this.infoLabel.string = 'Using custom manifest';
}
},
retry: function () {
if (!this._updating && this._canRetry) {
// this.panel.retryBtn.active = false;
this._canRetry = false;
this.infoLabel.string = 'Retry failed Assets...';
this._am.downloadFailedAssets();
}
},
checkUpdate: function () {
if (this._updating) {
this.infoLabel.string = 'Checking or updating ...';
return;
}
if (this._am.getState() === jsb.AssetsManager.State.UNINITED) {
// Resolve md5 url
var url = this.manifestUrl.nativeUrl;
console.log("==========================================");
console.log(url);
if (cc.loader.md5Pipe) {
url = cc.loader.md5Pipe.transformURL(url);
}
this._am.loadLocalManifest(url);
}
if (!this._am.getLocalManifest() || !this._am.getLocalManifest().isLoaded()) {
this.infoLabel.string = 'Failed to load local manifest ...';
return;
}
this._am.setEventCallback(this.checkCb.bind(this));
this._am.checkUpdate();
this._updating = true;
},
hotUpdate: function () {
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.panel.updateBtn.active = false;
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() : '/') + 'helloworld-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);
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;
}
};
var _this = this;
// 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) {
_this.infoLabel.string = "Verification passed : " + relativePath;
return true;
}
else {
_this.infoLabel.string = "Verification passed : " + relativePath + ' (' + expectedMD5 + ')';
return true;
}
});
this.infoLabel.string = '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);
this.infoLabel.string = "Max concurrent tasks count have been limited to 2";
}
this.fileProgress.progress = 0;
this.byteProgress.progress = 0;
},
onDestroy: function () {
if (this._updateListener) {
this._am.setEventCallback(null);
this._updateListener = null;
}
}
});
此时还没有生成project.manifest文件所以Manifest Url先留空,如下图:
4.构建项目生成res和src
5.下载官方示例中 version_generator.js文件,放置于hellowrold工程根目录
并执行如下代码生成两个manifest文件
node version_generator.js -v 1.0.0 -u http://192.168.123.115:8080/hotUpdate/ -s build/jsb-default/ -d assets/
6.将第四步生成的res、src以及第5步生成的两个manifest文件,一共4个文件放入hotUpdate文件夹,并放入服务器容器中,我这边用的是tomcat,如下图:
启动tomcat,确保能够访问
7.将project.manifest文件绑定到Manifest Url上,
绑定之后,项目->构建发布—>构建
构建完成之后在main.js中添加如下代码
if (cc.sys.isNative) {
var hotUpdateSearchPaths = cc.sys.localStorage.getItem('HotUpdateSearchPaths');
if (hotUpdateSearchPaths) {
jsb.fileUtils.setSearchPaths(JSON.parse(hotUpdateSearchPaths));
}
}
最终main.js如下(每次构建都需要修改此处):
// QQPlay window need to be inited first
if (false) {
BK.Script.loadlib('GameRes://libs/qqplay-adapter.js');
}
window.boot = function () {
if (cc.sys.isNative) {
var hotUpdateSearchPaths = cc.sys.localStorage.getItem('HotUpdateSearchPaths');
if (hotUpdateSearchPaths) {
jsb.fileUtils.setSearchPaths(JSON.parse(hotUpdateSearchPaths));
}
}
var settings = window._CCSettings;
window._CCSettings = undefined;
if ( !settings.debug ) {
var uuids = settings.uuids;
var rawAssets = settings.rawAssets;
var assetTypes = settings.assetTypes;
var realRawAssets = settings.rawAssets = {};
for (var mount in rawAssets) {
var entries = rawAssets[mount];
var realEntries = realRawAssets[mount] = {};
for (var id in entries) {
var entry = entries[id];
var type = entry[1];
// retrieve minified raw asset
if (typeof type === 'number') {
entry[1] = assetTypes[type];
}
// retrieve uuid
realEntries[uuids[id] || id] = entry;
}
}
var scenes = settings.scenes;
for (var i = 0; i < scenes.length; ++i) {
var scene = scenes[i];
if (typeof scene.uuid === 'number') {
scene.uuid = uuids[scene.uuid];
}
}
var packedAssets = settings.packedAssets;
for (var packId in packedAssets) {
var packedIds = packedAssets[packId];
for (var j = 0; j < packedIds.length; ++j) {
if (typeof packedIds[j] === 'number') {
packedIds[j] = uuids[packedIds[j]];
}
}
}
}
function setLoadingDisplay () {
// Loading splash scene
var splash = document.getElementById('splash');
var progressBar = splash.querySelector('.progress-bar span');
cc.loader.onProgress = function (completedCount, totalCount, item) {
var percent = 100 * completedCount / totalCount;
if (progressBar) {
progressBar.style.width = percent.toFixed(2) + '%';
}
};
splash.style.display = 'block';
progressBar.style.width = '0%';
cc.director.once(cc.Director.EVENT_AFTER_SCENE_LAUNCH, function () {
splash.style.display = 'none';
});
}
var onStart = function () {
cc.loader.downloader._subpackages = settings.subpackages;
cc.view.enableRetina(true);
cc.view.resizeWithBrowserSize(true);
if (!false && !false) {
if (cc.sys.isBrowser) {
setLoadingDisplay();
}
if (cc.sys.isMobile) {
if (settings.orientation === 'landscape') {
cc.view.setOrientation(cc.macro.ORIENTATION_LANDSCAPE);
}
else if (settings.orientation === 'portrait') {
cc.view.setOrientation(cc.macro.ORIENTATION_PORTRAIT);
}
cc.view.enableAutoFullScreen([
cc.sys.BROWSER_TYPE_BAIDU,
cc.sys.BROWSER_TYPE_WECHAT,
cc.sys.BROWSER_TYPE_MOBILE_QQ,
cc.sys.BROWSER_TYPE_MIUI,
].indexOf(cc.sys.browserType) < 0);
}
// Limit downloading max concurrent task to 2,
// more tasks simultaneously may cause performance draw back on some android system / browsers.
// You can adjust the number based on your own test result, you have to set it before any loading process to take effect.
if (cc.sys.isBrowser && cc.sys.os === cc.sys.OS_ANDROID) {
cc.macro.DOWNLOAD_MAX_CONCURRENT = 2;
}
}
// init assets
cc.AssetLibrary.init({
libraryPath: 'res/import',
rawAssetsBase: 'res/raw-',
rawAssets: settings.rawAssets,
packedAssets: settings.packedAssets,
md5AssetsMap: settings.md5AssetsMap
});
var launchScene = settings.launchScene;
// load scene
cc.director.loadScene(launchScene, null,
function () {
if (cc.sys.isBrowser) {
// show canvas
var canvas = document.getElementById('GameCanvas');
canvas.style.visibility = '';
var div = document.getElementById('GameDiv');
if (div) {
div.style.backgroundImage = '';
}
}
cc.loader.onProgress = null;
console.log('Success to load scene: ' + launchScene);
}
);
};
// jsList
var jsList = settings.jsList;
if (false) {
BK.Script.loadlib();
}
else {
var bundledScript = settings.debug ? 'src/project.dev.js' : 'src/project.js';
if (jsList) {
jsList = jsList.map(function (x) {
return 'src/' + x;
});
jsList.push(bundledScript);
}
else {
jsList = [bundledScript];
}
}
var option = {
id: 'GameCanvas',
scenes: settings.scenes,
debugMode: settings.debug ? cc.debug.DebugMode.INFO : cc.debug.DebugMode.ERROR,
showFPS: !false && settings.debug,
frameRate: 60,
jsList: jsList,
groupList: settings.groupList,
collisionMatrix: settings.collisionMatrix,
}
cc.game.run(option, onStart);
};
// main.js is qqplay and jsb platform entry file, so we must leave platform init code here
if (false) {
BK.Script.loadlib('GameRes://src/settings.js');
BK.Script.loadlib();
BK.Script.loadlib('GameRes://libs/qqplay-downloader.js');
var ORIENTATIONS = {
'portrait': 1,
'landscape left': 2,
'landscape right': 3
};
BK.Director.screenMode = ORIENTATIONS[window._CCSettings.orientation];
initAdapter();
cc.game.once(cc.game.EVENT_ENGINE_INITED, function () {
initRendererAdapter();
});
qqPlayDownloader.REMOTE_SERVER_ROOT = "";
var prevPipe = cc.loader.md5Pipe || cc.loader.assetLoader;
cc.loader.insertPipeAfter(prevPipe, qqPlayDownloader);
window.boot();
}
else if (window.jsb) {
var isRuntime = (typeof loadRuntime === 'function');
if (isRuntime) {
require('src/settings.js');
require('src/cocos2d-runtime.js');
require('jsb-adapter/engine/index.js');
}
else {
require('src/settings.js');
require('src/cocos2d-jsb.js');
require('jsb-adapter/jsb-engine.js');
}
window.boot();
}
修改完成后,编译,编译完成后,Android Studio打开运行到手机上,此时点击检查版本按钮,会提示无更新。