前言
工作需要,在空闲时间看了下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"
,
"remoteVersionUrl"
:
"http://127.0.0.1:8080/JsUpdateServer/res/version.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"
,
"remoteVersionUrl"
:
"http://127.0.0.1:8080/JsUpdateServer/res/version.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"
,
"remoteVersionUrl"
:
"http://127.0.0.1:8080/JsUpdateServer/res/version.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 热更新/