1. 程式人生 > >Cocos2d-Js熱更新(最完整版本,包括自己做的過程中遇到的坑都在裡面)

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工程目錄
我的web工程目錄如圖
至此,基本的更新邏輯已經完成。

增量更新只是在服務端version.manifest和project.manifest檔案增加相應的描述即可。

我在做的過程中主要遇到了一下幾個問題:
1. 由於公司內部網路比較多,大家接入的都是內網,導致測試手機和web伺服器不在一個網路,這個是個人原因
2. web工程目錄問題,導致檔案下載失敗,錯誤描述如圖:
這裡寫圖片描述

這個主要原因是,manifest檔案中的packageUrl和path拼接後的地址與web工程的目錄不符合,建議各位看官在web工程搭建完成之後先在網頁訪問下需要下載的檔案,確保拼接後的地址能過夠正常訪問。