前言
工作需要,在空閒時間看了下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
|
{ "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
|
{ "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
|
{ "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工程中的文件替換你的文件,通用的引擎工程太大沒有上傳。
其他可以使用到的教程