首先是官方文檔
http://docs.cocos.com/creator/manual/zh/advanced-topics/hot-update.html
http://docs.cocos.com/creator/manual/zh/advanced-topics/assets-manager.html
熱更新的原理
客戶端存在一個project.manifest文件,該文件包含幾個信息:
- packageUrl:url,服務器更新數據包根目錄;
- remoteManifestUrl:url, [可選項]服務器上project.manifest文件的url,
- remoteVersionUrl:url,服務器上version.mainifest文件的url;
- version:x.x.x,項目版本;
- assets:{},資源列表;
key : 資源的相對路徑(相對於資源根目錄)
md5 : md5 值代表資源文件的版本信息
compressed: [可選項] 如果值為 true,文件被下載后會自動被解壓,目前僅支持 zip 壓縮格式
size: [可選項] 文件的字節尺寸,用於快速獲取進度信息 - searchPaths:" ",搜索路徑
客戶端通過本地的project.manifest中url,可以獲取服務器上project.manifest文件,比較兩者的version屬性,如果客戶端的version比服務器低,則啟動更新。
更新的內容:assets是文件列表,里面列出了項目中的完整資源,每個資源都有md5表示,客戶端根據本地project.manifest中的assets列表和服務器的assets列表對比,下載不同的資源到臨時文件夾,如果最后所有資源都正常,則把臨時文件夾的內容替換到本地緩存文件夾中,並且修改優先搜索路徑為該文件夾。所以重啟游戲之后的使用的資源優先從緩存文件夾中搜索。
需要環境
- nodejs
- cocoscreator
官方案例
-
下載官方范例,解壓。該案例已經把客戶端和服務器的資源都打包好了,更新包在remote-assets文件夾中。
image.png -
服務器--我使用的是nodejs。
1 .新建一個文件夾nodejs,在nodejs中新建hotUpdate文件夾,在把官方案例中的remote-assets復制到hotUpdate文件夾中。

2 .在nodejs中新建一個js腳本,腳本內容如下
var express = require('express'); var path = require('path'); var app = express(); app.use(express.static(path.join(__dirname, 'hotUpdate'))); app.listen(80);
3.在nodejs文件夾下執行node app.js命令,啟動服務器,可以訪問http://127.0.0.1/remote-assets/project.manifest,如果成功訪問則服務器啟動成功。

接下來修改manifest文件里面的url,有三個文件需要修改,服務器remote-assets中的兩個manifest后綴文件,官方項目assets文件夾下的project.manifest


只修改三個url

修改完成之后,打開項目,用模擬器運行看看效果。
點擊檢查更新按鈕

點擊立即更新按鈕

因為某些原因,模擬器更新完成,重啟不能使用更新的資源,所以需要編譯成原生,我的電腦不能編譯windows,就不上圖了。打包成apk自測可以成功更新。
從零開始
-
制作新版本
該項目有兩個場景,作為是新版本,資源存放在服務器上。刪除一個場景作為舊版本,編譯安裝在手機上。目標是使用舊版本熱更新成新版本,並成功切換場景。
新建一個項目,新建兩個場景,helloworld場景是測試熱更行是否成功的場景(舊版本不存在helloworld,通過更新可以跳轉到該場景)。hotUpdate場景,該場景中添加兩個進度條,對應字節和文件個數兩種進度,三個label,對應兩種進度以及提示信息,三個按鈕,分別為,檢查更新,更新,切換場景。
image.png -
腳本 --copy官方示例
新建一個hotUpdate腳本,添加在Canvas上。腳本中增加五個變量,把場景中對應的節點拖拽上去。,增加三個回調方法,把他綁定在按鈕上。
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(); } }, });
- 打包資源
首先構建原生
image.png
構建完成在項目中會生成原生項目,熱更新需要的資源是res,src兩個文件夾以及里面的內容。
把nodejs--hotUpdate--remote-assets文件夾下所有東西都刪除,把res,src復制到nodejs--hotUpdate--remote-assets文件夾下。
image.png
構建完原生項目,打開官方項目文件夾,拷貝出 version_generator.js 文件到helloworld項目的根目錄(如下圖),並在helloworld根目錄打開命令窗口,執行命令,node version_generator.js -v 1.7.0 -u http://127.0.0.1/remote-assets/ -s build/jsb-default/ -d assets/

在assets文件夾下會多出兩個文件,把他們復制到nodejs--hotUpdate--remote-assets文件夾下。

最終遠程資源如圖

在nodejs文件夾下執行node app.js命令,啟動服務器,可以訪問http://127.0.0.1/remote-assets/project.manifest,如果成功訪問則服務器啟動成功。
- 制作舊版本
刪除helloworld場景,腳本,texture文件夾,以及之前生成的manifest文件。保存之后構建項目,構建選項不要變動

在helloworld根目錄打開命令窗口,執行命令,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生成兩個manifest文件。把project.manifest拖到屬性檢查器上面。

重新構建原生平台,在main.js加下面這段代碼,然后編譯
if (cc.sys.isNative) { var hotUpdateSearchPaths = cc.sys.localStorage.getItem('HotUpdateSearchPaths'); if (hotUpdateSearchPaths) { jsb.fileUtils.setSearchPaths(JSON.parse(hotUpdateSearchPaths)); } }

現在已經完成了舊版本的制作。這是我在安卓手機上的效果。


作者:歐特雨
鏈接:https://www.jianshu.com/p/cec263b6b9ac
來源:簡書
簡書著作權歸作者所有,任何形式的轉載都請聯系作者獲得授權並注明出處。