CocosCreator热更新
JavaScript 热更新
准备热更
前段时间花了点儿时间研究了Cocos Creator的热更新方案,参考着官网的例子来做。基于JavaScript来热更,项目是基于TypeScript过段时间看能不能转为TypeScript的热更方案。
官方例子上有一个名叫version_generator.js来生成版本对比的project.manifest与version.manifest两个文件,现在官方有插件可以直接用插件就可以了。通过:扩展->扩展商店->免费排行->热更新manifest生成工具;先把热更新mainfest生成工具安装上去就可以了。
本次只使用一个场景里的内容作为例子,JavaScript代码如下:
cc.Class({
extends: cc.Component,
properties: {
// 项目里生成的project.mainfest文件赋值给这个属性
manifestUrl: {
type: cc.Asset,
default: null
},
// 用于显示热更新状态与当前更新那个文件
lblHotUpdate: {
type: cc.Label,
default: null
},
// 用于显示图片测试热更新的资源
sprite: {
type: cc.Sprite,
default: null
},
_storagePath: "",
isUpdate: false,
},
update(dt) {
if (this.isUpdate) {
var url = this.manifestUrl.nativeUrl;
if (cc.loader.md5Pipe) {
url = cc.loader.md5Pipe.transformURL(url);
}
this._am.loadLocalManifest(url);
this._am.setEventCallback(this.updateCb.bind(this));
// this.lblHotUpdate.string = 'begin to hot update';
this._am.update();
}
},
onLoad() {
cc.loader.loadRes("bg1.jpg", cc.SpriteFrame, function (err, srpiteFrame) {
this.sprite.spriteFrame = srpiteFrame;
}.bind(this));
if (!cc.sys.isNative) {
this.lblHotUpdate.string = "no supper hot update";
return;
}
this._storagePath = ((jsb.fileUtils ? jsb.fileUtils.getWritablePath() : '/') + 'remote-asset');
var panel = this;
this.versionCompareHandle = function (versionA, versionB) {
panel.lblHotUpdate.string = "JS Custom Version Compare: version A is " + versionA + ', version B is ' + versionB;
// panel.lblHotUpdate.string = "hot update code";
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._am = new jsb.AssetsManager('', this._storagePath, this.versionCompareHandle);
// this._am.setVerifyCallback(function (filePath, asset) {
// var compressed = asset.compressed; // 是否被压缩
// var expectedMD5 = asset.md5; //
// var relativePath = asset.path;
// var size = asset.size;
// if (compressed) {
// panel.lblHotUpdate.string = "校验 : " + relativePath;
// return true;
// } else {
// var resMD5 = JSB_MD5(jsb.fileUtils.getDataFromFile(filePath));
// panel.lblHotUpdate.string = "Verification passed : " + relativePath + ' (' + expectedMD5 + ') result:' + (asset.md5 == resMD5);
// return asset.md5 == resMD5;
// }
// });
this.lblHotUpdate.string = 'Hot update is ready, please check or directly update.';
if (cc.sys.os === cc.sys.OS_ANDROID) {
this._am.setMaxConcurrentTask(2);
this.lblHotUpdate.string = "Max concurrent tasks count have been limited to 2";
}
if (this._am.getState() === jsb.AssetsManager.State.UNINITED) {
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()) {
this.lblHotUpdate.string = 'Failed to load local manifest ...';
return;
}
this._am.setEventCallback(this.checkCb.bind(this));
this._am.checkUpdate();
},
checkCb(event) {
switch (event.getEventCode()) {
case jsb.EventAssetsManager.ERROR_NO_LOCAL_MANIFEST:
this.lblHotUpdate.string = "No local manifest file found, hot update skipped.";
break;
case jsb.EventAssetsManager.ERROR_DOWNLOAD_MANIFEST:
this.lblHotUpdate.string = "Fail to download manifest file, hot update skipped.";
case jsb.EventAssetsManager.ERROR_PARSE_MANIFEST:
this.lblHotUpdate.string = "Fail to parse manifest file, hot update skipped.";
break;
case jsb.EventAssetsManager.ALREADY_UP_TO_DATE:
// this.lblHotUpdate.string = "Already up to date with the latest remote version.";
break;
case jsb.EventAssetsManager.NEW_VERSION_FOUND:
this.lblHotUpdate.string = 'New version found, please try to update.';
break;
default:
return;
}
this._am.setEventCallback(null);
this.isUpdate = true;
},
updateCb(event) {
var needRestart = false;
var failed = false;
switch (event.getEventCode()) {
case jsb.EventAssetsManager.ERROR_NO_LOCAL_MANIFEST:
this.lblHotUpdate.string = 'update No local manifest file found, hot update skipped.';
failed = true;
break;
case jsb.EventAssetsManager.UPDATE_PROGRESSION:
var msg = event.getMessage();
if (msg) {
this.lblHotUpdate.string = 'Updated file: ' + msg;
}
break;
case jsb.EventAssetsManager.ERROR_DOWNLOAD_MANIFEST:
case jsb.EventAssetsManager.ERROR_PARSE_MANIFEST:
this.lblHotUpdate.string = 'update Fail to download manifest file, hot update skipped.';
failed = true;
break;
case jsb.EventAssetsManager.ALREADY_UP_TO_DATE:
this.lblHotUpdate.string = 'update Already up to date with the latest remote version.';
failed = true;
break;
case jsb.EventAssetsManager.UPDATE_FINISHED:
this.lblHotUpdate.string = 'Update finished. ' + event.getMessage();
needRestart = true;
break;
case jsb.EventAssetsManager.UPDATE_FAILED:
// this.lblHotUpdate.string = 'Update failed. ' + event.getMessage();
break;
case jsb.EventAssetsManager.ERROR_UPDATING:
// this.lblHotUpdate.string = 'error: ' + event.getAssetId() + ', ' + event.getMessage();
break;
case jsb.EventAssetsManager.ERROR_DECOMPRESS:
this.lblHotUpdate.string = event.getMessage();
break;
default:
break;
}
if (failed) {
return
}
if (needRestart) {
this._am.setEventCallback(null);
var searchPaths = jsb.fileUtils.getSearchPaths();
var newPaths = this._am.getLocalManifest().getSearchPaths();
Array.prototype.unshift.apply(searchPaths, newPaths);
var hs = JSON.stringify(searchPaths);
this.lblHotUpdate.string = hs;
cc.sys.localStorage.setItem('HotUpdateSearchPaths', hs);
jsb.fileUtils.setSearchPaths(searchPaths);
cc.audioEngine.stopAll();
cc.game.restart();
}
}
});
官网上是手动热更的,我这边是自动热更新。setVerifyCallback用于热更新后文件验证一直出错所以直接把注释掉了与是可以的。
写完后先构建一下然后选择 项目->热更新工具。先在 生成Manifest配置 里填写版本号(先把版本号设置为1.0.0),资源服务器url,build项目资源文件目录(选择是项目根目录下的build/jsb-defaule,选择自己构建的目录),然后点击 生成 按钮。把生成的project.manifest与version.manifest文件复制到根目录assets下。
然后通过Cocos Creator把project.manifest文件设置给manifestUrl这个属性。然后重新打包构建,找到根目录下build/main.js文件最下面改为以下内容:
// QQPlay window need to be inited first
if (false) {
BK.Script.loadlib('GameRes://libs/qqplay-adapter.js');
}
window.boot = function () {
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]];
}
}
}
var subpackages = settings.subpackages;
for (var subId in subpackages) {
var uuidArray = subpackages[subId].uuids;
if (uuidArray) {
for (var k = 0, l = uuidArray.length; k < l; k++) {
if (typeof uuidArray[k] === 'number') {
uuidArray[k] = uuids[uuidArray[k]];
}
}
}
}
}
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;
}
}
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,
}
// init assets
cc.AssetLibrary.init({
libraryPath: 'res/import',
rawAssetsBase: 'res/raw-',
rawAssets: settings.rawAssets,
packedAssets: settings.packedAssets,
md5AssetsMap: settings.md5AssetsMap,
subpackages: settings.subpackages
});
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');
}
cc.macro.CLEANUP_IMAGE_CACHE = true;
window.boot();
}
最后编译为apk文件安装到手机。
测试热更
测试资源热更
把sprite的图片换一下,然后再构建,重新生成热更新文件。把res,src,project.mamifest,version.manifest文件放到配置服务器相应的地方。
在手机上打开刚安装的应用会看到经过图片被改变了。
测试代码热更
把panel.lblHotUpdate.string = "JS Custom Version Compare: version A is " + versionA + ', version B is ' + versionB;代码换成panel.lblHotUpdate.string = "hot update code";然后重新构建生成热更新文件放到服务器上,再在手机上打开安装的应用lblHotUpdate的提示信息就会改变