Cocos2d-Js熱更新(最完整版本,包括自己做的過程中遇到的坑都在裡面)
最近主要進行遊戲指令碼化相關工作,指令碼化的目的就是為了熱更新,所以就寫個demo研究下熱更新。
cocos版本: 3.12
1、基本思路
- cocos的熱更新主要採用其自帶的AssetsManager,執行AssetsManager後,搜尋路徑增加了jsb.fileUtils.getWritablePath()目錄,並且搜尋級別最優;
- 需要熱更新js不放在project.json中定義,等AssetsManager更新完了,用cc.loader.loadJs動態載入;
- 所以在jsb.fileUtils.getWritablePath()目錄下載的資源和js檔案,與專案目錄保持一致,那麼優先載入新下載的資源和js檔案,再進入遊戲,從而實現熱更新的目的。
2、特性
此為官方說明的特性~~~~
- 多執行緒並行下載支援
- 兩層進度統計資訊:檔案級以及位元組級
- Zip壓縮檔案支援
- 斷點續傳
- 詳細的錯誤報告
- 檔案下載失敗重試支援
3、我的流程
- 新建空的cocos2d-js工程並進行相應的修改用作demo
在res資源目錄下,新建project.manifest檔案,填入初始資訊,內容如下:
{
"packageUrl" : "http://192.168.3.139:8080/hotUpdateService/res/",
"remoteManifestUrl" : "http://192.168.3.139:8080/hotUpdateService/res/project.manifest" ,
"remoteVersionUrl" : "http://192.168.3.139:8080/hotUpdateService/res/version.manifest",
"version" : "1.0.0",
"engineVersion" : "3.12",
"groupVersions" : {
"1" : "1.0.0"
},
"assets" : {
},
"searchPaths" : [
]
}
其中地址為我自己的測試地址,各位看官自己進行相應的替換(這裡有個坑,詳見文末)
src目錄下新建jsList.js檔案,需要動態載入的js檔案都寫在jsFiles這個數組裡,這樣js檔案有增加變化,這個files.js一併更新,方便動態載入,內容如下:
var jsList = [
"src/resource.js",
"src/app.js"
];
src目錄下新建assetsManagerScene.js檔案,內容如下:
/**
* Created by MartinYing on 2016/12/28.
*/
var failCount = 0;
var maxFailCount = 1; //最大錯誤重試次數
/**
* 自動更新js和資源
*/
var AssetsManagerLoaderScene = cc.Scene.extend({
_am:null,
_progress:null,
_percent:0,
run:function(){
cc.log("enter run function ..... ");
if (!cc.sys.isNative) {
this.loadGame();
return;
}
var layer = new cc.Layer();
this.addChild(layer);
this._progress = new cc.LabelTTF.create("update 0%", "Arial", 38);
this._progress.x = cc.winSize.width / 2;
this._progress.y = cc.winSize.height / 2 + 50;
layer.addChild(this._progress);
var storagePath = (jsb.fileUtils ? jsb.fileUtils.getWritablePath() : "./");
cc.log("storagePath is " + storagePath);
this._am = new jsb.AssetsManager("res/project.manifest", storagePath);
this._am.retain();
if (!this._am.getLocalManifest().isLoaded())
//if (true)
{
cc.log("Fail to update assets, step skipped.");
this.loadGame();
}
else
{
var that = this;
var listener = new jsb.EventListenerAssetsManager(this._am, function(event) {
switch (event.getEventCode()){
case jsb.EventAssetsManager.ERROR_NO_LOCAL_MANIFEST:
cc.log("enter ERROR_NO_LOCAL_MANIFEST ..... ");
cc.log("No local manifest file found, skip assets update.");
that.loadGame();
break;
case jsb.EventAssetsManager.UPDATE_PROGRESSION:
cc.log("enter UPDATE_PROGRESSION ..... ");
that._percent = event.getPercent();
cc.log(that._percent + "%");
var msg = event.getMessage();
if (msg) {
cc.log(msg);
}
break;
case jsb.EventAssetsManager.ERROR_DOWNLOAD_MANIFEST:
cc.log("enter ERROR_DOWNLOAD_MANIFEST ..... ");
cc.log("Fail to download manifest file, update skipped.");
that.loadGame();
break;
case jsb.EventAssetsManager.ERROR_PARSE_MANIFEST:
cc.log("enter ERROR_PARSE_MANIFEST ..... ");
cc.log("Fail to download manifest file, update skipped.");
that.loadGame();
break;
case jsb.EventAssetsManager.ALREADY_UP_TO_DATE:
cc.log("enter ALREADY_UP_TO_DATE ..... ");
cc.log("ALREADY_UP_TO_DATE.");
that.loadGame();
break;
case jsb.EventAssetsManager.UPDATE_FINISHED:
cc.log("enter UPDATE_FINISHED ..... ");
cc.log("Update finished.");
that.loadGame();
break;
case jsb.EventAssetsManager.UPDATE_FAILED:
cc.log("enter UPDATE_FAILED ..... ");
cc.log("Update failed. " + event.getMessage());
failCount++;
if (failCount < maxFailCount)
{
that._am.downloadFailedAssets();
}
else
{
cc.log("Reach maximum fail count, exit update process");
failCount = 0;
that.loadGame();
}
break;
case jsb.EventAssetsManager.ERROR_UPDATING:
cc.log("enter ERROR_UPDATING ..... ");
cc.log("Asset update error: " + event.getAssetId() + ", " + event.getMessage());
that.loadGame();
break;
case jsb.EventAssetsManager.ERROR_DECOMPRESS:
cc.log("enter 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.log("enter loadGame function ..... ");
//jsList是jsList.js的變數,記錄全部js。
cc.loader.loadJs(["src/jsList.js"], function(){
cc.loader.loadJs(jsList, function(){
cc.director.runScene(new HelloWorldScene());
});
});
},
updateProgress:function(dt){
cc.log("enter updateProgress function ..... ");
this._progress.string = "update " + this._percent + "%";
},
onExit:function(){
cc.log("AssetsManager::onExit");
this._am.release();
this._super();
}
});
這裡用到了上一步建立的res/project.manifest檔案,目的是用於進行版本對比。
因為下載的檔案是儲存在可寫目錄下,載入的順序發生了變化,所以需要修改根目錄下的main.js和“project.json”兩個檔案。
main.js :
cc.game.onStart = function(){
if(!cc.sys.isNative && document.getElementById("cocosLoading")) //If referenced loading.js, please remove it
document.body.removeChild(document.getElementById("cocosLoading"));
// Pass true to enable retina display, on Android disabled by default to improve performance
cc.view.enableRetina(cc.sys.os === cc.sys.OS_IOS ? true : false);
// Adjust viewport meta
cc.view.adjustViewPort(true);
// Uncomment the following line to set a fixed orientation for your game
// cc.view.setOrientation(cc.ORIENTATION_PORTRAIT);
// Setup the resolution policy and design resolution size
cc.view.setDesignResolutionSize(960, 640, cc.ResolutionPolicy.SHOW_ALL);
// The game will be resized when browser size change
cc.view.resizeWithBrowserSize(true);
var scene = new AssetsManagerLoaderScene();
scene.run()
// //load resources
// cc.LoaderScene.preload(g_resources, function () {
// cc.director.runScene(new HelloWorldScene());
// }, this);
};
cc.game.run();
project.json :
{
"project_type": "javascript",
"debugMode" : 1,
"showFPS" : true,
"frameRate" : 60,
"noCache" : false,
"id" : "gameCanvas",
"renderMode" : 0,
"engineDir":"frameworks/cocos2d-html5",
"modules" : ["cocos2d", "extensions"],
"jsList" : [
"src/assetsManagerScene.js"
]
}
到這一步客戶端基本配置已經完成,這個時候客戶端是可以執行的。
- 新建web工程,用作測試伺服器(我這裡使用Myeclipse新建的web工程,搭建web伺服器這裡不做贅述)
在WebRoot目錄下,新增res資料夾,在其內部分別建立res和src用於對應cocos工程的資原始檔夾和原始檔夾目錄,拷貝app.js到src目錄下,隨便修改一點東西,作為更新內容。
res資料夾目錄下新增配置檔案version.manifest和project.manifest檔案
version.manifest :
{
"packageUrl" : "http://192.168.3.139:8080/hotUpdateService/res/",
"remoteManifestUrl" : "http://192.168.3.139:8080/hotUpdateService/res/project.manifest",
"remoteVersionUrl" : "http://192.168.3.139:8080/hotUpdateService/res/version.manifest",
"version" : "1.0.0",
"groupVersions" : {
"1" : "1.0.1"
},
"engineVersion" : "3.12"
}
project.manifest :
{
"packageUrl" : "http://192.168.3.139:8080/hotUpdateService/res/",
"remoteManifestUrl" : "http://192.168.3.139:8080/hotUpdateService/res/project.manifest",
"remoteVersionUrl" : "http://192.168.3.139:8080/hotUpdateService/res/version.manifest",
"version" : "1.0.0",
"groupVersions" : {
"1" : "1.0.1"
},
"engineVersion" : "3.12",
"assets" : {
"update1" : {
"path" : "src/app.zip",
"md5" : "D7698389FD1CA121DCD896035D67687C",
"compressed" : true ,
"group" : "1"
}
},
"searchPaths" : [
]
}
packageUrl : 遠端資源的下載根路徑。
remoteVersionUrl : 遠端版本檔案的路徑,用來判斷伺服器端是否有新版本的資源。
remoteManifestUrl : 遠端配置檔案的路徑,包含版本資訊以及所有資源資訊。
version : 配置檔案對應的版本。
groupVersions : 是新增的功能欄位,用於做增量更新很方便。
engineVersion : 配置檔案對應的引擎版本。
assets : 所有資源資訊。
key : 升級名稱
path : 鍵代表資源的相對路徑(相對於packageUrl)。
md5 : md5值代表資原始檔的版本資訊。
compressed : [可選項] 如果值為true,檔案被下載後會自動被解壓,目前僅支援zip壓縮格式。
searchPaths : 需要新增到Cocos2d引擎中的搜尋路徑列表。
通過學習底層邏輯,發現更新的邏輯順序是這樣的:
先通過本地的version.manifest和服務端的version.manifest比較,如果本地沒有version.manifest,則會先進行下載,如果本地version低於服務端,那麼就會再去獲取project.manifest。
如果version相同,那麼會比較groupVersions。
如果本地沒有下載過groupVersions中的任何更新,那麼會依次下載升級包。
如果本地下載過1.0.1版本的升級包,那麼就會跳過1.0.1下載屬於1.0.2版本的升級內容。
如果下載失敗,或者沒有網路導致更新失敗的,會繼續使用未更新前的版本。並且下次啟動會繼續嘗試更新。
這是我的web工程目錄
至此,基本的更新邏輯已經完成。
增量更新只是在服務端version.manifest和project.manifest檔案增加相應的描述即可。
我在做的過程中主要遇到了一下幾個問題:
1. 由於公司內部網路比較多,大家接入的都是內網,導致測試手機和web伺服器不在一個網路,這個是個人原因
2. web工程目錄問題,導致檔案下載失敗,錯誤描述如圖:
這個主要原因是,manifest檔案中的packageUrl和path拼接後的地址與web工程的目錄不符合,建議各位看官在web工程搭建完成之後先在網頁訪問下需要下載的檔案,確保拼接後的地址能過夠正常訪問。