Cocos2d-JS 熱更新

前言
工作需要,在空閒時間看了下Cocos2d-JS的熱更新。對其進行了一個簡單的實現,這裏總結分享一下。


Cocos2d-JS 熱更新

Cocos2d-JS 熱更新是啥?Cocos2d-JS終歸還是一個遊戲引擎,就以遊戲的過程來理解吧。傳統遊戲需要更新人物動畫、地圖場景、遊戲邏輯、背景音樂怎麼辦?新出一個APP放到應用商店等用戶下載,或者好一點遊戲內提示又升級並自行下載完整的新版本APP。

使用Cocos2d-JS的熱更新,那就大不一樣了。它可以做到進入遊戲後下載需要更新的資源甚至是腳本本身,而且更新過程不需要退出遊戲。只要用戶聯網,就能夠保證使用到最新的資源。

這些場景就非常適合使用Hot Fix

  • 想在遊戲中對春節開放新活動,不能保證應用商店能準時過審覈上線

  • 發現一個嚴重的Bug,需要立即修復

  • 需要經常換遊戲資源,提升新鮮感

  • 先放一箇中規中矩的版本過市場審覈,然後繞過審覈自己直接推新內容,哈哈

對於遊戲來說,這個特性是比較重量級的。不過因爲JavaScript的語言特性能支持這一功能,所以Cocos2d-JS能用此特性,而Cocos2d-x無法使用。


特性

以下是官方提供的特性

  • 多線程並行下載支持

  • 兩層進度統計信息:文件級以及字節級

  • Zip壓縮文件支持

  • 斷點續傳

  • 詳細的錯誤報告

  • 文件下載失敗重試支持


Simple

1. manifest配置文件

熱更新需要用到的配置文件有2種,都是以.manifest結尾。

一種是精簡版,一種是完整版。精簡版稱爲version.manifest,完整版稱爲project.manifest

version.manifest:

1
2
3
4
5
6
7
8
9
10
11
{
    "packageUrl" "http://127.0.0.1:8080/JsUpdateServer/res"
    "remoteManifestUrl" "http://127.0.0.1:8080/JsUpdateServer/res/project.manifest"
    "version" "1.0.0",
    "groupVersions" : {
        "1" "1.0.1",
        "2" "1.0.2"
    },
    "engineVersion" "3.3"
}

version.manifest是可選的,其中的所有字段也出現在project.manifest中,並且內容一樣,如果沒有就會從服務端下載完整版。但是如果完整版特別大,那麼這個小版本的優勢就很明顯了。

project.manifest:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
{
    "packageUrl" "http://127.0.0.1:8080/JsUpdateServer/res"
    "remoteManifestUrl" "http://127.0.0.1:8080/JsUpdateServer/res/project.manifest"
    "version" "1.0.0",
    "groupVersions" : {
        "1" "1.0.1",
        "2" "1.0.2"
    },
    "engineVersion" "3.3",
    "assets" : {
        "update1" : {
            "path" "src/app.zip",
            "md5" "41D8E948052B5B714B14F81612CF534D",
            "compressed" true,
            "group" "1"
        }, 
        "update2" : {
            "path" "res/HelloWorld.png",
            "md5" "A0FA3FA681D500575012D5E802F74D50",
            "group" "1"
        },
        "update3" : {
            "path" "res/Bound.png",
            "md5" "E7D4218B02CD0C5BB35ADC55E133DBA2",
            "group" "1"
        },
        "update4" : {
            "path" "src/resource.js",
            "md5" "BA47101EBB65FBFCFB61C4CC57A306CA",
            "group" "2"
        }
    },
    "searchPaths" : [
    "res/"
    ]
}

所有字段具體含義,官方文檔的定義還是比較詳細,只是少了groupVersions這個新字段:

  • packageUrl : 遠程資源的下載根路徑。

  • remoteVersionUrl : 遠程版本文件的路徑,用來判斷服務器端是否有新版本的資源。

  • remoteManifestUrl : 遠程配置文件的路徑,包含版本信息以及所有資源信息。

  • version : 配置文件對應的版本。

  • groupVersions : 是新增的功能字段,用於做增量更新很方便。

  • engineVersion : 配置文件對應的引擎版本。

  • assets : 所有資源信息。

    • key : 升級名稱

    • path : 鍵代表資源的相對路徑(相對於packageUrl)。

    • md5 : md5值代表資源文件的版本信息。

    • compressed : [可選項] 如果值爲true,文件被下載後會自動被解壓,目前僅支持zip壓縮格式。

  • searchPaths : 需要添加到Cocos2d引擎中的搜索路徑列表。

以我的配置爲例,一個完整的更新流程是這樣的:

先通過本地的version.manifest和服務端的version.manifest比較,如果本地version低於服務端,那麼就會再去獲取project.manifest。

如果version相同,那麼會比較groupVersions。

如果本地沒有下載過groupVersions中的任何更新,那麼會依次下載升級包。

如果本地下載過1.0.1版本的升級包,那麼就會跳過1.0.1下載屬於1.0.2版本的升級內容。

如果下載失敗,或者沒有網絡導致更新失敗的,會繼續使用未更新前的版本。並且下次啓動會繼續嘗試更新。

2. 創建後臺

用一種你喜歡的方式起一個後臺服務。我樸實無華的用IDE(Eclipse for JavaEE)建立了一個空的Web工程,用Tomcat起了一個後臺。

從剛纔的配置文件可以看出,Web工程名爲JsUpdateServer。

WebContent的根目錄下新建一個index.html文件,隨便在裏邊寫些東西,用來驗證我的後臺已經起來。

Run起來之後,在瀏覽器裏輸入http://localhost:8080/JsUpdateServer/index.html看看是不是能夠看到你剛纔寫的內容?

到這裏你的後臺已經OK了~

3. 創建Cocos2d-JS工程

新建一個Cocos2d-JS工程,可以用專用的IDE(cocos Code IDE),官網有下載,也有如何配置的教程,這裏就不多說了。

res目錄下,新建一個project.manifest文件。這個是初始版本信息,之後的更新會用到這個文件。內容如下:

1
2
3
4
5
6
7
8
9
10
11
    "packageUrl" "http://127.0.0.1:8080/JsUpdateServer/res"
    "remoteManifestUrl" "http://127.0.0.1:8080/JsUpdateServer/res/project.manifest"
    "version" "1.0"
    "engineVersion" "3.3"
    "assets" : {
    },
    "searchPaths" : [ 
    
}

在src目錄下,新建一個jsList.js文件,內容如下:

1
2
3
4
var jsList = [ 
              "src/resource.js"
              "src/app.js" 
              ]

這個文件裏包括的是需要加載的js文件路徑,將會在其他地方加載。

src目錄下,新建一個assetsManagerScene.js文件,這裏就是熱更新的主要邏輯了。代碼如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
var failCount = 0; 
var maxFailCount = 1;   //最大錯誤重試次數
/** 
 * 自動更新js和資源 
 */ 
var AssetsManagerLoaderScene = cc.Scene.extend({ 
_am:null, 
_progress:null, 
_percent:0, 
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", 12); 
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
cc.EventListenerAssetsManager
var listener = new jsb.EventListenerAssetsManager(this._am, function(event) { 
switch (event.getEventCode()){ 
case jsb.EventAssetsManager.ERROR_NO_LOCAL_MANIFEST: 
cc.log("No local manifest file found, skip assets update."); 
that.loadGame(); 
break
case jsb.EventAssetsManager.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: 
case jsb.EventAssetsManager.ERROR_PARSE_MANIFEST: 
cc.log("Fail to download manifest file, update skipped."); 
that.loadGame(); 
break
case jsb.EventAssetsManager.ALREADY_UP_TO_DATE: 
cc.log("ALREADY_UP_TO_DATE."); 
that.loadGame(); 
break
case jsb.EventAssetsManager.UPDATE_FINISHED: 
cc.log("Update finished."); 
that.loadGame(); 
break
case jsb.EventAssetsManager.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("Asset update error: " + event.getAssetId() + ", " + event.getMessage()); 
that.loadGame(); 
break
case jsb.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(){ 
//jsList是jsList.js的變量,記錄全部js。 
cc.loader.loadJs(["src/jsList.js"], function(){ 
cc.loader.loadJs(jsList, function(){ 
cc.director.runScene(new HelloWorldScene()); 
}); 
}); 
},
updateProgress:function(dt){ 
this._progress.string = "update" this._percent + "%"
},
onExit:function(){ 
cc.log("AssetsManager::onExit");
this._am.release(); 
this._super(); 
});

熱更新主要是通過引擎提供的AssetsManager來實現的。

AssetsManagerLoaderScene裏,用到了res/project.manifest,是用來對版本對比的。

下載完成後的資源會被解壓,然後放在jsb.fileUtils.getWritablePath()的路徑下,加到引擎的搜索範圍中,並且他們的優先級是高於APP原本的資源路徑的。所以相同的資源名稱,引擎會優先使用更新路徑下的文件,就達到更新的目的。

下載成功後會執行loadGame方法,裏邊會加載src/jsList.js的所有資源。也就是說通過AssetsManagerLoaderScene來確保熱更新完成後,再加載所有資源。

由於資源和腳本的加載順序發生了改變,所以還要修改根目錄下的main.js和“project.json”。

main.js:

1
2
3
4
5
6
7
8
9
cc.game.onStart = function(){
    cc.view.adjustViewPort(true);
    cc.view.setDesignResolutionSize(800, 450, cc.ResolutionPolicy.SHOW_ALL);
    cc.view.resizeWithBrowserSize(true);
     
    var scene = new AssetsManagerLoaderScene(); 
    scene.run(); 
};
cc.game.run();

將啓動場景改爲我的熱更新場景AssetsManagerLoaderScene。

project.json:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
{
    "project_type":"javascript",
    "debugMode":1,
    "showFPS":true,
    "frameRate":60,
    "id":"gameCanvas",
    "renderMode":0,
    "engineDir":"frameworks/cocos2d-html5",
    "modules":[
        "cocos2d",
        "extensions"
    ],
    "jsList":[
        "src/assetsManagerScene.js" 
    ]
}

將jsList的值改爲src/assetsManagerScene.js,只要加載這個js,其他js會在AssetsManagerLoaderScene中被加載。

到這裏你可以跑起來看下,雖然獲取不到升級文件,但是可以以最原始的版本跑起來。

4. 在後臺配置升級文件

現在到後臺工程的WebContent目錄下添加升級文件。

從我的manifest配置文件你可以看到,我又建立了一個res文件夾,在其中分別建立src和res分別對應Cocos2d-JS工程中的資源、腳本文件夾。

隨意修改一下app.js,將其作爲更新內容使用。總結了幾個要注意的地方。

升級文件的路徑,一定要和配置文件中的path內容一致。

升級文件是.zip,記得compressed改爲true。

添加新的資源文件要記得同步更新resource.js

新增js文件也記得同步更新jsList.js

5. 回到Cocos2d-JS工程

覈對一下project.manifest文件中的地址是不是和後臺一致,內容是否正確。

沒問題的話就可以跑起來了,會發現你的升級內容被下載下來並且更新了。


參考

本文工程後臺&Cocos2d-JS的簡陋源碼戳這裏,Cocos2d-JS工程中的文件替換你的文件,通用的引擎工程太大沒有上傳。

其他可以使用到的教程

資源管理器ASSETS MANAGER

Cocos2d-JS熱更新

來源網址:http://karelgt.com/Cocos2dJS 熱更新/

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章