Cocos Creator热更新
一,添加热更新需要的文件
1. 在项目根目录添加 version_generator.js 文件
version_generator.js 内容如下:
/**
* 此模块用于热更新工程清单文件的生成
*/
var fs = require('fs');
var path = require('path');
var crypto = require('crypto');
var manifest = {
//服务器上资源文件存放路径(src,res的路径)
packageUrl: 'http://192.168.200.117:8000/XiaoMing/remote-assets/',
//服务器上project.manifest路径
remoteManifestUrl: 'http://192.168.200.117:8000/XiaoMing/remote-assets/project.manifest',
//服务器上version.manifest路径
remoteVersionUrl: 'http://192.168.200.117:8000/XiaoMing/remote-assets/version.manifest',
version: '1.0.0',
assets: {},
searchPaths: []
};
//生成的manifest文件存放目录
var dest = 'assets/';
//项目构建后资源的目录
var src = 'build/jsb-link/';
/**
* 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');
});
注意:以下几个地方,你可能需要根据自己的需求修改,本文 第三节第一点 会指出各个参数对应的情况。
image2. 添加热更新组件,并挂在热更新脚本
这里我简单新建了一个helloWorld工程
添加了两个button,check用于检测是否有更新,update用于更新资源;
在canvas上挂载HotUpdate脚本,注意:这时ManifestUrl为空。
给button check添加点击事件,绑定checkForUpdate()
给button update添加点击事件,绑定hotUpdate()
HotUpdate.js
/**
* 负责热更新逻辑的组件
*/
cc.Class({
extends: cc.Component,
properties: {
manifestUrl: cc.RawAsset, //本地project.manifest资源清单文件
_updating: false,
_canRetry: false,
_storagePath: ''
},
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.hotUpdate();
break;
default:
return;
}
cc.eventManager.removeListener(this._checkListener);
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:
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.');
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) {
cc.eventManager.removeListener(this._updateListener);
this._updateListener = null;
this._updating = false;
}
if (needRestart) {
cc.eventManager.removeListener(this._updateListener);
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));
cc.eventManager.addListener(this._checkListener, 1);
this._am.checkUpdate();
this._updating = true;
},
hotUpdate: function () {
if (this._am) {
this._updateListener = new jsb.EventListenerAssetsManager(this._am, this.updateCb.bind(this));
cc.eventManager.addListener(this._updateListener, 1);
if (this._am.getState() === jsb.AssetsManager.State.UNINITED) {
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;
// }
},
// 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() : '/') + 'xiaoming-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;
}
};
// Init with empty manifest url for testing custom manifest
this._am = new jsb.AssetsManager('', this._storagePath, this.versionCompareHandle);
if (!cc.sys.ENABLE_GC_FOR_NATIVE_OBJECTS) {
this._am.retain();
}
// 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");
}
// this.checkUpdate();
},
onDestroy: function () {
if (this._updateListener) {
cc.eventManager.removeListener(this._updateListener);
this._updateListener = null;
}
if (this._am && !cc.sys.ENABLE_GC_FOR_NATIVE_OBJECTS) {
this._am.release();
}
}
});
3. 添加点击事件
4. 构建项目,生成原生资源文件
image注意: 我们选择的模板是 "default" ,发布路径为 "./build" ,发布后的项目资源相对路径为:build/jsb-default
5. 修改main.js(可省略)
2018/08/23测试发现,构建项目生成的main.js中,已包含判断,不用修改,也可以成功
根据官方文档提示,每次构建项目后,都需要修改main.js,那我们就直接复制官方demo根目录main.js的内容覆盖原有内容。
main.js 内容如下:
main.js
(function () {
if (window.cc && cc.sys.isNative) {
var hotUpdateSearchPaths = cc.sys.localStorage.getItem('HotUpdateSearchPaths');
if (hotUpdateSearchPaths) {
jsb.fileUtils.setSearchPaths(JSON.parse(hotUpdateSearchPaths));
}
}
'use strict';
function boot () {
var settings = window._CCSettings;
window._CCSettings = undefined;
if ( !settings.debug ) {
// retrieve minified raw assets
var rawAssets = settings.rawAssets;
var assetTypes = settings.assetTypes;
for (var mount in rawAssets) {
var entries = rawAssets[mount];
for (var uuid in entries) {
var entry = entries[uuid];
var type = entry[1];
if (typeof type === 'number') {
entry[1] = assetTypes[type];
}
}
}
}
// init engine
var canvas;
if (cc.sys.isBrowser) {
canvas = document.getElementById('GameCanvas');
}
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.view.resizeWithBrowserSize(true);
// UC browser on many android devices have performance issue with retina display
if (cc.sys.os !== cc.sys.OS_ANDROID || cc.sys.browserType !== cc.sys.BROWSER_TYPE_UC) {
cc.view.enableRetina(true);
}
//cc.view.setDesignResolutionSize(settings.designWidth, settings.designHeight, cc.ResolutionPolicy.SHOW_ALL);
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);
}
// qq, wechat, baidu
cc.view.enableAutoFullScreen(
cc.sys.browserType !== cc.sys.BROWSER_TYPE_BAIDU &&
cc.sys.browserType !== cc.sys.BROWSER_TYPE_WECHAT &&
cc.sys.browserType !== cc.sys.BROWSER_TYPE_MOBILE_QQ
);
}
// init assets
cc.AssetLibrary.init({
libraryPath: 'res/import',
rawAssetsBase: 'res/raw-',
rawAssets: settings.rawAssets,
packedAssets: settings.packedAssets
});
var launchScene = settings.launchScene;
// load scene
if (cc.runtime) {
cc.director.setRuntimeLaunchScene(launchScene);
}
cc.director.loadScene(launchScene, null,
function () {
if (cc.sys.isBrowser) {
// show canvas
canvas.style.visibility = '';
var div = document.getElementById('GameDiv');
if (div) {
div.style.backgroundImage = '';
}
}
cc.loader.onProgress = null;
// play game
// cc.game.resume();
console.log('Success to load scene: ' + launchScene);
}
);
};
// jsList
var jsList = settings.jsList;
var bundledScript = settings.debug ? 'project.dev.js' : 'project.js';
if (jsList) {
jsList.push(bundledScript);
}
else {
jsList = [bundledScript];
}
// anysdk scripts
if (cc.sys.isNative && cc.sys.isMobile) {
jsList = jsList.concat(['jsb_anysdk.js', 'jsb_anysdk_constants.js']);
}
jsList = jsList.map(function (x) { return 'src/' + x; });
var option = {
//width: width,
//height: height,
id: 'GameCanvas',
scenes: settings.scenes,
debugMode: settings.debug ? cc.DebugMode.INFO : cc.DebugMode.ERROR,
showFPS: settings.debug,
frameRate: 60,
jsList: jsList,
groupList: settings.groupList,
collisionMatrix: settings.collisionMatrix,
renderMode: 0
};
cc.game.run(option, onStart);
}
if (window.document) {
var splash = document.getElementById('splash');
splash.style.display = 'block';
var cocos2d = document.createElement('script');
cocos2d.async = true;
cocos2d.src = window._CCSettings.debug ? 'cocos2d-js.js' : 'cocos2d-js-min.js';
var engineLoaded = function () {
document.body.removeChild(cocos2d);
cocos2d.removeEventListener('load', engineLoaded, false);
boot();
};
cocos2d.addEventListener('load', engineLoaded, false);
document.body.appendChild(cocos2d);
}
else if (window.jsb) {
require('src/settings.js');
require('src/jsb_polyfill.js');
boot();
}
})();
二,服务器搭建(仅局域网内可访问)
1. 编写服务器脚本
server.py
import SimpleHTTPServer
import SocketServer
PORT = 8000
Handler = SimpleHTTPServer.SimpleHTTPRequestHandler
httpd = SocketServer.TCPServer(("", PORT), Handler)
print "serving at port", PORT
httpd.serve_forever()
2. 启动服务
打开命令行
切换目录到 server.py 所在目录下
输入下方命令,启动服务:
python -m server 8000
启动完成后,服务器的地址即: http://192.168.200.117:8000
对应的根目录即为server.py所在的目录下,我们在服务器根目录下,新建HotUpdate文件夹,用于存储新版本资源,通过网页访问如下:
详细参见官方文档给出的地址:https://docs.python.org/2/library/simplehttpserver.html
三,生成旧版清单文件
1. 修改version_generator.js
packageUrl:服务器存放资源文件(src res)的路径
remoteManifestUrl:服务器存放资源清单文件(project.manifest)的路径
remoteVersionUrl:服务器存放version.manifest的路径
dest:要生成的manifest文件存放路径
src:项目构建后的资源目录
2. 根据构建后的资源目录,执行version_generator.js,生成manifest清单文件
打开cmd,切换到当前项目根目录下,执行下方命令:
//官方给出的命令格式
>node version_generator.js -v 1.0.0 -u http://your-server-address/tutorial-hot-update/remote-assets/ -s native/package/ -d assets/
//我的命令
>node version_generator.js -v 1.0.0 -u http://192.168.200.117:8000/HotUpdate/ -s build/jsb-default/ -d assets
//由于我们version_generator文件中,都配置好了参数
//因此可以简单调用以下命令即可
>node version_generator.js
- -v: 指定 Manifest 文件的主版本号。
- -u: 指定服务器远程包的地址,这个地址需要和最初发布版本中 Manifest 文件的远程包地址一致,否则无法检测到更新。
- -s: 本地原生打包版本的目录相对路径。
- -d: 保存 Manifest 文件的地址。
生成的Manifest文件目录,如下:
PS:如果version_generator.js中的配置都正确,特别是版本号,可以直接执行 node version_generator.js。
3. 并绑定到热更新脚本上
如果指定的Manifest文件生成的目录不在assets下,则需将project.manifest复制到assets目录下
并将project.manifest绑定到HotUpdate.js热更新脚本上
4. 打包旧版本
构建项目->编译
在真机上运行build/jsb-default/simulator目录下的apk
1.0.0版本运行如下:
三,生成新版本
1. 更改代码,更改version_generator.js中的版本号
修改logo图片,1.0.1版本,本地运行结果,如下:
2. 构建项目 && 重新生成资源清单文件
构建项目:此步骤和生成旧版本中一样,这里就不截图啦。
重新生成资源清单文件:修改version_generator.js中的版本号后,可以直接调用以下命令:
>node version_generator.js
3. 将manifest文件以及src,res拷贝到服务器
4. 运行旧版本
运行结果,点击 检测更新 后,很快app重启,logo变成了head.png。
成功!
由于这里检测到新版本后,就开始自动更新。
你可以修改这部分逻辑,检测到有新版后,弹窗提示是否需要更新。
后面再继续研究 大厅+子游戏的模式,以及不重启加载子游戏方案...