cocos2d-js熱更新
1. 熱更新基本思路
得到cocoachina論壇上fysp和akira_cn的幫助,理清了遊戲熱更新的思路:
- 執行AssetsManager後,搜尋路徑增加了jsb.fileUtils.getWritablePath()目錄,並且是優先搜尋;
- 需要熱更新js不放在project.json中定義,等AssetsManager更新完了,用cc.loader.load動態載入;
- 所以在jsb.fileUtils.getWritablePath()目錄下載的資源和js檔案,與專案目錄保持一致,那麼優先載入新下載的資源和js檔案,再進入遊戲,從而實現熱更新。
2. AssetsManager
cocos2d-js 3.0 rc0對AssetsManager功能進行了完善增強,支援多執行緒下載、斷點續傳、檔案壓縮、更好的進度資訊以及錯誤重試機制,實現遊戲資原始檔和指令碼檔案的熱更新變的更加方便。
用cocos new MyGame -l js -d /directory/to/project
方式新建一個測試專案,參考sample寫的src/AssetsManager.js:
var __failCount = 0; var AssetsManagerLoaderScene = cc.Scene.extend({ _am:null, _progress:null, _percent:0, _percentByFile:0, run:function(){ if (!cc.sys.isNative) { this.loadGame(); return; } var layer = new cc.Layer(); this.addChild(layer); this._progress = new cc.LabelTTF.create("0%", "Arial", 12); this._progress.x = cc.winSize.width / 2; this._progress.y = cc.winSize.height / 2 + 50; layer.addChild(this._progress); // android: /data/data/com.huanle.magic/files/ var storagePath = (jsb.fileUtils ? jsb.fileUtils.getWritablePath() : "./"); this._am = new jsb.AssetsManager("res/project.manifest", storagePath); this._am.retain(); if (!this._am.getLocalManifest().isLoaded()) { cc.log("Fail to update assets, step skipped."); this.loadGame(); } else { var that = this; var listener = new cc.EventListenerAssetsManager(this._am, function(event) { switch (event.getEventCode()){ case cc.EventAssetsManager.ERROR_NO_LOCAL_MANIFEST: cc.log("No local manifest file found, skip assets update."); that.loadGame(); break; case cc.EventAssetsManager.UPDATE_PROGRESSION: that._percent = event.getPercent(); that._percentByFile = event.getPercentByFile(); cc.log(that._percent + "%"); var msg = event.getMessage(); if (msg) { cc.log(msg); } break; case cc.EventAssetsManager.ERROR_DOWNLOAD_MANIFEST: case cc.EventAssetsManager.ERROR_PARSE_MANIFEST: cc.log("Fail to download manifest file, update skipped."); that.loadGame(); break; case cc.EventAssetsManager.ALREADY_UP_TO_DATE: case cc.EventAssetsManager.UPDATE_FINISHED: cc.log("Update finished."); that.loadGame(); break; case cc.EventAssetsManager.UPDATE_FAILED: cc.log("Update failed. " + event.getMessage()); __failCount ++; if (__failCount < 5) { that._am.downloadFailedAssets(); } else { cc.log("Reach maximum fail count, exit update process"); __failCount = 0; that.loadGame(); } break; case cc.EventAssetsManager.ERROR_UPDATING: cc.log("Asset update error: " + event.getAssetId() + ", " + event.getMessage()); that.loadGame(); break; case cc.EventAssetsManager.ERROR_DECOMPRESS: cc.log(event.getMessage()); that.loadGame(); break; default: break; } }); cc.eventManager.addListener(listener, 1); this._am.update(); cc.director.runScene(this); } this.schedule(this.updateProgress, 0.5); }, loadGame:function(){ cc.loader.loadJs(["src/files.js"], function(err){ cc.loader.loadJs(jsFiles, function(err){ cc.director.runScene(new HelloWorldScene()); }); }); }, updateProgress:function(dt){ this._progress.string = "" + this._percent; }, onExit:function(){ cc.log("AssetsManager::onExit"); this._am.release(); this._super(); } });
修改專案目錄下的main.js:
cc.game.onStart = function(){ cc.view.setDesignResolutionSize(800, 450, cc.ResolutionPolicy.SHOW_ALL); cc.view.resizeWithBrowserSize(true); var scene = new AssetsManagerLoaderScene(); scene.run(); }; cc.game.run();
修改專案目錄下的project.json:
{ "project_type": "javascript", "debugMode" : 1, "showFPS" : true, "frameRate" : 60, "id" : "gameCanvas", "renderMode" : 0, "engineDir":"frameworks/cocos2d-html5", "modules" : ["cocos2d", "extensions"], "jsList" : [ "src/AssetsManager.js" ] }
就留一個AssetsManager.js,其他的js都通過它來載入。
增加一個src/files.js,需要動態載入的js檔案都寫在jsFiles這個數組裡,這樣js檔案有增加變化,這個files.js一併更新,方便動態載入:
var jsFiles = [ "src/app.js", "src/resource.js" ];
專案res目錄增加一個project.manifest檔案,AssetsManager.js裡會用到:
{ "packageUrl" : "http://10.0.128.219/res", "remoteManifestUrl" : "http://10.0.128.219/res/project.manifest", "remoteVersionUrl" : "http://10.0.128.219/res/version.manifest", "version" : "1.0.0", "groupVersions" : { "1" : "1.0.0" }, "engineVersion" : "3.0 rc0", "searchPaths" : [ ] }
這裡主要配置服務端資源下載地址,具體欄位說明,在下面服務端配置裡說明。然後用cocos compile -p android
編譯打包成一個apk安裝包,等配置好服務端更新資源安裝測試。
3. 服務端配置
需要建一個WEB伺服器做下載用,在其WEB目錄http://10.0.128.219/res
(我的測試機),增加version.manifest檔案:
{ "packageUrl" : "http://10.0.128.219/res", "remoteManifestUrl" : "http://10.0.128.219/res/project.manifest", "remoteVersionUrl" : "http://10.0.128.219/res/version.manifest", "version" : "1.0.0", "groupVersions" : { "1" : "1.0.1" }, "engineVersion" : "3.0 rc0" }
測試發現,AssetsManager首先會下載version.manifest檔案,如果有更新的版本,那麼才會去下載project.manifest,然後下載其中描述的資原始檔。
project.manifest如下:
{ "packageUrl" : "http://10.0.128.219/res", "remoteManifestUrl" : "http://10.0.128.219/res/project.manifest", "remoteVersionUrl" : "http://10.0.128.219/res/version.manifest", "version" : "1.0.0", "groupVersions" : { "1" : "1.0.1" }, "engineVersion" : "3.0 rc0", "assets" : { "update1" : { "path" : "src/app.zip", "md5" : "f6bf54e5a0d42c963cc5ae81bf9dc6c6", "compressed" : true, "group" : "1" } }, "searchPaths" : [ ] }
寫法和官方文件裡不太一樣,特別是有個groupVersions欄位,這個欄位來自fysp在cocoachina論壇回答其他網友問題寫的示例,測試發現用來做增量更新很方便,後面再說明。其他欄位的說明官方文件已經很詳細了。
由於客戶端本地project.manifest裡groupVersions的版本資訊比伺服器端的低,所以AssetsManager會下載http://10.0.128.219/res/src/app.zip
到手機的/data/data/org.cocos2dx.hellojavascript/files/src/app.zip
,並且會自動解壓,但不會刪除壓縮包本身。
建議用root過的android手機測試,否則/data/data是沒有許可權檢視。執行客戶端測試程式後用adb連線檢視:
e:\>adb shell [email protected]:/ $ su su [email protected]:/ # ls -l /data/data/org.cocos2dx.hellojavascript/files/src/ ls -l /data/data/org.cocos2dx.hellojavascript/files/src/ -rw-rw-rw- app_65 app_65 2228 2014-07-08 14:23 app.js -rw-rw-rw- app_65 app_65 1552 2014-07-08 14:23 app.zip [email protected]:/ # ls -l /data/data/org.cocos2dx.hellojavascript/files/ ls -l /data/data/org.cocos2dx.hellojavascript/files/ -rw-rw-rw- app_65 app_65 553 2014-07-08 14:23 project.manifest drwxrwxrwx app_65 app_65 2014-07-08 14:23 src -rw-rw-rw- app_65 app_65 307 2014-07-08 14:23 version.manifest
用firefox除錯連上手機,發現app.js資源地址是/data/data/org.cocos2dx.hellojavascript/files/src/app.js,而不是assets/src/app.js,實現了熱更新:
4. 增量更新
修改服務端version.manifest:
{ "packageUrl" : "http://10.0.128.219/res", "remoteManifestUrl" : "http://10.0.128.219/res/project.manifest", "remoteVersionUrl" : "http://10.0.128.219/res/version.manifest", "version" : "1.0.0", "groupVersions" : { "1" : "1.0.1", "2" : "1.0.2" }, "engineVersion" : "3.0 rc0" }
修改服務端project.manifest:
{ "packageUrl" : "http://10.0.128.219/res", "remoteManifestUrl" : "http://10.0.128.219/res/project.manifest", "remoteVersionUrl" : "http://10.0.128.219/res/version.manifest", "version" : "1.0.0", "groupVersions" : { "1" : "1.0.1", "2" : "1.0.2" }, "engineVersion" : "3.0 rc0", "assets" : { "update1" : { "path" : "src/app.zip", "md5" : "f6bf54e5a0d42c963cc5ae81bf9dc6c6", "compressed" : true, "group" : "1" }, "update2" : { "path" : "src/config.zip", "md5" : "5d59789090e4143166430b2cf7b313ff", "compressed" : true, "group" : "2" } }, "searchPaths" : [ ] }
這時在android客戶端測試,已經更新到update1的,只會下載update2的更新,而沒有更新過的,會把update1和update2都下載下來。