creator生成的项目是纯粹的数据驱动而非代码驱动,一切皆为数据,包括脚本、图片、音频、动画等,而数据驱动需要一个入口,就是main.js,而setting.js描述了整个资源的配置文件,我们自己编写的脚本,在编译后统一存放在了project.js中。热更新根据这个原理,在服务器存放游戏资源,通过服务端与本地的manifest进行对比,把差异文件下载到某个文件夹里面,在入口文件main.js设置搜索路径为更新的文件夹,这样达到热更新的目的。
新建Hall工程
VersionTip用来提示更新,UpdateTip用来显示更新进度,FinishTip用来提示完成更新并重启。
热更新脚本:
onst hotResDir = "AllGame/Hall"; //更新的资源目录
export class HotUpdateUtil {
private static am;
private static isUpdating = false;
private static checkUpdateListener;
private static updateListener;
private static manifestUrl;
private static checkNewVersionListener: Function;
private static updateProgressListener: Function;
private static updateErrorListener: Function;
private static updateFinishListener: Function;
public static init(manifestUrl: cc.Asset) {
if (!cc.sys.isNative) return;
this.manifestUrl = manifestUrl;
let storagePath = ((jsb.fileUtils ? jsb.fileUtils.getWritablePath() : '/') + hotResDir);
this.am = new jsb.AssetsManager("", storagePath, this.versionCompareHandle);
if (!cc.sys.ENABLE_GC_FOR_NATIVE_OBJECTS) {
this.am.retain();
}
this.am.setVerifyCallback(function (path, asset) {
var compressed = asset.compressed;
var expectedMD5 = asset.md5;
var relativePath = asset.path;
var size = asset.size;
if (compressed) {
cc.log("Verification passed : " + relativePath);
return true;
}
else {
cc.log("Verification passed : " + relativePath + ' (' + expectedMD5 + ')');
return true;
}
});
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");
}
}
//版本对比
private static versionCompareHandle(versionA, versionB): number {
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;
}
}
public static checkUpdate(checkNewVersionCallback?:Function) {
if (this.isUpdating) {
cc.log("正在更新中...");
return;
}
this.checkNewVersionListener = checkNewVersionCallback;
if (this.am.getState() === jsb.AssetsManager.State.UNINITED) {
var url = this.manifestUrl;
cc.log(url);
if (cc.loader.md5Pipe) {
url = cc.loader.md5Pipe.transformURL(url);
}
this.am.loadLocalManifest(url);
}
if (!this.am.getLocalManifest() || !this.am.getLocalManifest().isLoaded()) {
cc.log('Failed to load local manifest ...');
return;
}
this.checkUpdateListener = new jsb.EventListenerAssetsManager(this.am, this.checkCallback.bind(this));
cc.eventManager.addListener(this.checkUpdateListener, 1);
this.am.checkUpdate();
this.isUpdating = true;
}
private static checkCallback(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.');
if (this.checkNewVersionListener) {
this.checkNewVersionListener();
}
break;
default:
return;
}
cc.eventManager.removeListener(this.checkUpdateListener);
this.checkUpdateListener = null;
this.isUpdating = false;
}
public static update(updateProgressListener?:Function,updateErrorListener?:Function,updateFinishListener?:Function) {
if (this.am && !this.isUpdating) {
this.updateProgressListener = updateProgressListener;
this.updateErrorListener = updateErrorListener;
this.updateFinishListener = updateFinishListener;
this.updateListener = new jsb.EventListenerAssetsManager(this.am, this.updateCallback.bind(this));
cc.eventManager.addListener(this.updateListener, 1);
if (this.am.getState() === jsb.AssetsManager.State.UNINITED) {
// Resolve md5 url
var url = this.manifestUrl;
if (cc.loader.md5Pipe) {
url = cc.loader.md5Pipe.transformURL(url);
}
this.am.loadLocalManifest(url);
}
this.am.update();
this.isUpdating = true;
}
}
private static updateCallback(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:
if(this.updateProgressListener){
this.updateProgressListener(event.getDownloadedBytes(),event.getTotalBytes());
}
break;
case jsb.EventAssetsManager.ERROR_DOWNLOAD_MANIFEST:
case jsb.EventAssetsManager.ERROR_PARSE_MANIFEST:
cc.log('Fail to download manifest file, hot update skipped.');
if(this.updateErrorListener){
this.updateFinishListener('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;
if(this.updateFinishListener){
this.updateFinishListener();
}
break;
case jsb.EventAssetsManager.UPDATE_FAILED:
cc.log('Update failed. ' + event.getMessage());
if(this.updateErrorListener){
this.updateErrorListener('Update failed. ' + event.getMessage());
}
this.isUpdating = false;
break;
case jsb.EventAssetsManager.ERROR_UPDATING:
cc.log('Asset update error: ' + event.getAssetId() + ', ' + event.getMessage());
if(this.updateErrorListener){
this.updateErrorListener('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.isUpdating = 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.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);
}
}
}
使用脚本
import { HotUpdateUtil } from "./HotUpdateUtil";
const {ccclass, property} = cc._decorator;
@ccclass
export default class HotUpdate extends cc.Component {
@property(cc.Asset)
manifest:cc.Asset = null;
@property(cc.Node)
newVersionTip:cc.Node = null;
@property(cc.Node)
updateTip:cc.Node = null;
@property(cc.Node)
finishUpdateTip:cc.Node = null;
onLoad(){
HotUpdateUtil.init(this.manifest);
}
start(){
HotUpdateUtil.checkUpdate(()=>{
this.newVersionTip.active = true;
});
}
private updateVersion(){
this.updateTip.active = true;
HotUpdateUtil.update((progress)=>{
let temp = ~~(progress * 100);
this.updateTip.getComponentInChildren(cc.Label).string = "下载中" + temp + "%";
},null,()=>{
this.finishUpdateTip.active = true;
});
}
private restart(){
cc.game.restart();
}
}
生成manifest文件
首先,一开始HotUpdate的manifest是空的:
然后,开始构建项目,不要勾选MD5 Cache,以Android为例,模板为default,构建完成后,在项目的根目录下放文件version_generator.js,里面的路径根据需要修改:
/**
* 此模块用于热更新工程清单文件的生成
*/
var fs = require('fs');
var path = require('path');
var crypto = require('crypto');
var manifest = {
//服务器上资源文件存放路径(src,res的路径)
packageUrl: 'http://192.168.0.136:8000',
//服务器上project.manifest路径
remoteManifestUrl: 'http://192.168.0.136:8000/project.manifest',
//服务器上version.manifest路径
remoteVersionUrl: 'http://192.168.0.136:8000/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');
});
接着cd到工程目录下,执行 node version_generator.js,在assets会自动生成了两个文件,project. manifest和version. manifest,把project. manifest拖给HotUpdate:
然后构建项目,构建成功后,修改mian.js:
(function () {
//添加这段
if ( cc.sys.isNative) {
var hotUpdateSearchPaths = cc.sys.localStorage.getItem('HotUpdateSearchPaths'); //这个key对于HotUpdateUtil
if (hotUpdateSearchPaths) {
jsb.fileUtils.setSearchPaths(JSON.parse(hotUpdateSearchPaths));
}
}
'use strict';
//-------------
function boot () {
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]];
}
}
}
}
// init engine
var canvas;
if (cc.sys.isBrowser) {
canvas = document.getElementById('GameCanvas');
}
if (false) {
var ORIENTATIONS = {
'portrait': 1,
'landscape left': 2,
'landscape right': 3
};
BK.Director.screenMode = ORIENTATIONS[settings.orientation];
initAdapter();
}
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;
if (false) {
BK.Script.loadlib();
}
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
});
if (false) {
cc.Pipeline.Downloader.PackDownloader._doPreload("WECHAT_SUBDOMAIN", settings.WECHAT_SUBDOMAIN_DATA);
}
var launchScene = settings.launchScene;
// load scene
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;
console.log('Success to load scene: ' + launchScene);
}
);
};
// jsList
var jsList = settings.jsList;
if (!false) {
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];
}
}
// anysdk scripts
if (cc.sys.isNative && cc.sys.isMobile) {
// jsList = jsList.concat(['src/anysdk/jsb_anysdk.js', 'src/anysdk/jsb_anysdk_constants.js']);
}
var option = {
//width: width,
//height: height,
id: 'GameCanvas',
scenes: settings.scenes,
debugMode: settings.debug ? cc.DebugMode.INFO : cc.DebugMode.ERROR,
showFPS: (!false && !false) && settings.debug,
frameRate: 60,
jsList: jsList,
groupList: settings.groupList,
collisionMatrix: settings.collisionMatrix,
renderMode: 0
}
cc.game.run(option, onStart);
}
if (false) {
BK.Script.loadlib('GameRes://libs/qqplay-adapter.js');
BK.Script.loadlib('GameRes://src/settings.js');
BK.Script.loadlib();
BK.Script.loadlib('GameRes://libs/qqplay-downloader.js');
qqPlayDownloader.REMOTE_SERVER_ROOT = "";
var prevPipe = cc.loader.md5Pipe || cc.loader.assetLoader;
cc.loader.insertPipeAfter(prevPipe, qqPlayDownloader);
// <plugin script code>
boot();
return;
}
if (false) {
require(window._CCSettings.debug ? 'cocos2d-js.js' : 'cocos2d-js-min.js');
require('./libs/weapp-adapter/engine/index.js');
var prevPipe = cc.loader.md5Pipe || cc.loader.assetLoader;
cc.loader.insertPipeAfter(prevPipe, wxDownloader);
boot();
return;
}
if (window.jsb) {
require('src/settings.js');
require('src/jsb_polyfill.js');
boot();
return;
}
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);
if (typeof VConsole !== 'undefined') {
window.vConsole = new VConsole();
}
boot();
};
cocos2d.addEventListener('load', engineLoaded, false);
document.body.appendChild(cocos2d);
}
})();
修改好main.js后,就可以编辑项目安装到手机上了,接着修改项目,换个图片,保存然后构建,不用选MD5 Cache,构建成功后,修改version_generator.js版本号改为1.0.1,然后执行 node version_generator.js,然后把构建后的src、res和生成的project.manifest、version.manifest放在服务端,比如:
通过mac模拟服务器 python -m SimpleHTTPServer 8000
启动好后,就可以打开App安装测试了。