// cocos creator
首先是官方文檔
http://docs.cocos.com/creator/manual/zh/advanced-topics/hot-update.html
http://docs.cocos.com/creator/manual/zh/advanced-topics/assets-manager.html
熱更新的原理
客戶端存在一個(gè)project.manifest文件忿墅,該文件包含幾個(gè)信息:
- packageUrl:url,服務(wù)器更新數(shù)據(jù)包根目錄城舞;
- remoteManifestUrl:url钧唐, [可選項(xiàng)]服務(wù)器上project.manifest文件的url忙灼,
- remoteVersionUrl:url,服務(wù)器上version.mainifest文件的url钝侠;
- version:x.x.x,項(xiàng)目版本该园;
-
assets:{},資源列表;
key : 資源的相對路徑(相對于資源根目錄)
md5 : md5 值代表資源文件的版本信息
compressed: [可選項(xiàng)] 如果值為 true帅韧,文件被下載后會自動被解壓里初,目前僅支持 zip 壓縮格式
size: [可選項(xiàng)] 文件的字節(jié)尺寸,用于快速獲取進(jìn)度信息 - searchPaths:" ",搜索路徑
客戶端通過本地的project.manifest中url忽舟,可以獲取服務(wù)器上project.manifest文件双妨,比較兩者的version屬性,如果客戶端的version比服務(wù)器低叮阅,則啟動更新刁品。
更新的內(nèi)容:assets是文件列表,里面列出了項(xiàng)目中的完整資源浩姥,每個(gè)資源都有md5表示挑随,客戶端根據(jù)本地project.manifest中的assets列表和服務(wù)器的assets列表對比,下載不同的資源到臨時(shí)文件夾勒叠,如果最后所有資源都正常兜挨,則把臨時(shí)文件夾的內(nèi)容替換到本地緩存文件夾中竞阐,并且修改優(yōu)先搜索路徑為該文件夾。所以重啟游戲之后的使用的資源優(yōu)先從緩存文件夾中搜索暑劝。
需要環(huán)境
- nodejs
- cocoscreator
官方案例
-
下載官方范例,解壓颗搂。該案例已經(jīng)把客戶端和服務(wù)器的資源都打包好了担猛,更新包在remote-assets文件夾中。
image.png 服務(wù)器--我使用的是nodejs丢氢。
1 .新建一個(gè)文件夾nodejs傅联,在nodejs中新建hotUpdate文件夾,在把官方案例中的remote-assets復(fù)制到hotUpdate文件夾中疚察。
2 .在nodejs中新建一個(gè)js腳本蒸走,腳本內(nèi)容如下
var express = require('express');
var path = require('path');
var app = express();
app.use(express.static(path.join(__dirname, 'hotUpdate')));
app.listen(80);
3.在nodejs文件夾下執(zhí)行node app.js命令,啟動服務(wù)器貌嫡,可以訪問http://127.0.0.1/remote-assets/project.manifest比驻,如果成功訪問則服務(wù)器啟動成功。
接下來修改manifest文件里面的url岛抄,有三個(gè)文件需要修改别惦,服務(wù)器remote-assets中的兩個(gè)manifest后綴文件,官方項(xiàng)目assets文件夾下的project.manifest
只修改三個(gè)url
修改完成之后夫椭,打開項(xiàng)目掸掸,用模擬器運(yùn)行看看效果。
點(diǎn)擊檢查更新按鈕
點(diǎn)擊立即更新按鈕
因?yàn)槟承┰虿淝铮M器更新完成扰付,重啟不能使用更新的資源,所以需要編譯成原生仁讨,我的電腦不能編譯windows羽莺,就不上圖了。打包成apk自測可以成功更新洞豁。
從零開始
-
制作新版本
該項(xiàng)目有兩個(gè)場景禽翼,作為是新版本,資源存放在服務(wù)器上族跛。刪除一個(gè)場景作為舊版本闰挡,編譯安裝在手機(jī)上。目標(biāo)是使用舊版本熱更新成新版本礁哄,并成功切換場景长酗。
新建一個(gè)項(xiàng)目,新建兩個(gè)場景桐绒,helloworld場景是測試熱更行是否成功的場景(舊版本不存在helloworld夺脾,通過更新可以跳轉(zhuǎn)到該場景)之拨。hotUpdate場景,該場景中添加兩個(gè)進(jìn)度條咧叭,對應(yīng)字節(jié)和文件個(gè)數(shù)兩種進(jìn)度蚀乔,三個(gè)label,對應(yīng)兩種進(jìn)度以及提示信息菲茬,三個(gè)按鈕吉挣,分別為,檢查更新婉弹,更新睬魂,切換場景。
image.png -
腳本 --copy官方示例
新建一個(gè)hotUpdate腳本镀赌,添加在Canvas上氯哮。腳本中增加五個(gè)變量,把場景中對應(yīng)的節(jié)點(diǎn)拖拽上去商佛。喉钢,增加三個(gè)回調(diào)方法,把他綁定在按鈕上良姆。
image.png
cc.Class({
extends: cc.Component,
properties: {
byteLabel: cc.Label,
fileLabel: cc.Label,
byteProgress: cc.ProgressBar,
fileProgress: cc.ProgressBar,
label: cc.Label,
manifestUrl: cc.RawAsset,
},
onLoad() {
var self = this;
this._storagePath = jsb.fileUtils.getWritablePath();
this._am = new jsb.AssetsManager('', this._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) {
self .label.string = "Verification passed : " + relativePath;
return true;
}
else {
self .label.string = "Verification passed : " + relativePath + ' (' + expectedMD5 + ')';
return true;
}
});
this.byteProgress.progress = 0;
this.fileProgress.progress = 0;
},
hotUpdate() {
if (this._am && !this._updating) {
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._am.update();
this._updating = true;
}
},
checkUpdata() {
if (this._updating) {
this.label.string = 'Checking or updating ...';
return;
}
if (this._am.getState() === jsb.AssetsManager.State.UNINITED) {
this._am.loadLocalManifest(this.manifestUrl);
}
if (!this._am.getLocalManifest() || !this._am.getLocalManifest().isLoaded()) {
this.label.string = '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;
},
changeScene() {
cc.director.loadScene('helloworld');
},
checkCb: function (event) {
switch (event.getEventCode()) {
case jsb.EventAssetsManager.ERROR_NO_LOCAL_MANIFEST:
this.label.string = "No local manifest file found, hot update skipped.";
break;
case jsb.EventAssetsManager.ERROR_DOWNLOAD_MANIFEST:
case jsb.EventAssetsManager.ERROR_PARSE_MANIFEST:
this.label.string = "Fail to download manifest file, hot update skipped.";
break;
case jsb.EventAssetsManager.ALREADY_UP_TO_DATE:
this.label.string = "Already up to date with the latest remote version.";
break;
case jsb.EventAssetsManager.NEW_VERSION_FOUND:
this.label.string = 'New version found, please try to update.';
this.fileProgress.progress = 0;
this.byteProgress.progress = 0;
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:
this.label.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.label.string = 'Updated file: ' + msg;
}
break;
case jsb.EventAssetsManager.ERROR_DOWNLOAD_MANIFEST:
case jsb.EventAssetsManager.ERROR_PARSE_MANIFEST:
this.label.string = 'Fail to download manifest file, hot update skipped.';
failed = true;
break;
case jsb.EventAssetsManager.ALREADY_UP_TO_DATE:
this.label.string = 'Already up to date with the latest remote version.';
failed = true;
break;
case jsb.EventAssetsManager.UPDATE_FINISHED:
this.label.string = 'Update finished. ' + event.getMessage();
needRestart = true;
break;
case jsb.EventAssetsManager.UPDATE_FAILED:
this.label.string = 'Update failed. ' + event.getMessage();
this._updating = false;
this._canRetry = true;
break;
case jsb.EventAssetsManager.ERROR_UPDATING:
this.label.string = 'Asset update error: ' + event.getAssetId() + ', ' + event.getMessage();
break;
case jsb.EventAssetsManager.ERROR_DECOMPRESS:
this.label.string = 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;
var searchPaths = jsb.fileUtils.getSearchPaths();
var newPaths = this._am.getLocalManifest().getSearchPaths();
Array.prototype.unshift(searchPaths, newPaths);
cc.sys.localStorage.setItem('HotUpdateSearchPaths', JSON.stringify(searchPaths));
jsb.fileUtils.setSearchPaths(searchPaths);
cc.game.restart();
}
},
});
-
打包資源
首先構(gòu)建原生
image.png
構(gòu)建完成在項(xiàng)目中會生成原生項(xiàng)目出牧,熱更新需要的資源是res,src兩個(gè)文件夾以及里面的內(nèi)容歇盼。
把nodejs--hotUpdate--remote-assets文件夾下所有東西都刪除舔痕,把res,src復(fù)制到nodejs--hotUpdate--remote-assets文件夾下豹缀。
image.png
構(gòu)建完原生項(xiàng)目伯复,打開官方項(xiàng)目文件夾,拷貝出 version_generator.js 文件到helloworld項(xiàng)目的根目錄(如下圖)邢笙,并在helloworld根目錄打開命令窗口啸如,執(zhí)行命令,node version_generator.js -v 1.7.0 -u http://127.0.0.1/remote-assets/ -s build/jsb-default/ -d assets/
在assets文件夾下會多出兩個(gè)文件氮惯,把他們復(fù)制到nodejs--hotUpdate--remote-assets文件夾下叮雳。
最終遠(yuǎn)程資源如圖
在nodejs文件夾下執(zhí)行node app.js命令,啟動服務(wù)器妇汗,可以訪問http://127.0.0.1/remote-assets/project.manifest帘不,如果成功訪問則服務(wù)器啟動成功。
- 制作舊版本
刪除helloworld場景杨箭,腳本寞焙,texture文件夾,以及之前生成的manifest文件。保存之后構(gòu)建項(xiàng)目捣郊,構(gòu)建選項(xiàng)不要變動
在helloworld根目錄打開命令窗口辽狈,執(zhí)行命令,node version_generator.js -v 1.0.0 -u http://127.0.0.1/remote-assets/ -s build/jsb-default/ -d assets/注意這里的-v 1.0.0呛牲,之前是1.7.0刮萌,舊版本版本號要小于新版本
在assets生成兩個(gè)manifest文件。把project.manifest拖到屬性檢查器上面娘扩。
重新構(gòu)建原生平臺着茸,在main.js加下面這段代碼,然后編譯
if (cc.sys.isNative) {
var hotUpdateSearchPaths = cc.sys.localStorage.getItem('HotUpdateSearchPaths');
if (hotUpdateSearchPaths) {
jsb.fileUtils.setSearchPaths(JSON.parse(hotUpdateSearchPaths));
}
}
現(xiàn)在已經(jīng)完成了舊版本的制作畜侦。這是我在安卓手機(jī)上的效果。