前言:游戏上线后,我们常常还会需要更新,如新增玩法,活动等,这种动态的更新资源我们称为游戏的热更新。热更新一般只适用于脚本语言,因为脚本不需要编译,是一种解释性语言,而如C++语言是很难热更新的,其代码只要有改动就需要重新链接编译(接口统一,用动态库可以实现,不过太不灵活了)。
本章将讲讲用Cocos-lua引擎怎么实现热更新,其实Cocos自带也封装了热更新模块(AssetsManager, AssetsManagerEx),不过我没用自带的那套,自己封装了一套,其基本思路原理是一致的。
热更新基本思路
登入游戏先向服务端请求当前游戏版本号信息,与本地版本号比较,如果相同则说明没有资源需要更新直接进入游戏,而如果不相同,则说明有资源需要更新进入第2步。
向服务端请求当前所有资源的列表(资源名+MD5),与本地资源列表比较,找出需要更新的资源。
根据找出的需要更新资源,向服务端请求下载下来。(目前发现更新资源很多时,一个个循环向服务端请求可能中途会出错,所以最好是以zip包的形式一次性发送过来,客服端只请求一次)
热更新注意点
1,程序加载某个文件原理:首先一个程序加载本地硬盘某一文件最终加载的路径都是绝对全路径。而我们之所以还可以写相对路径也能找到对应的文件是因为还有一个搜索路径,搜索路径是用一个容器存储的,相对路径是这样得到全路径的 = 搜索路径(全路径) + 相对路径。就是只要加入到这个搜索路径中的路径,以后要加载这里面的文件就只需给文件名就可以了,前面的路径它会自动去搜索路径循环遍历查找。所以程序里我们一般不写绝对路径,而是把前面的全路径加入到搜索路径,之后只需写后面的相对路径就能查找到了。2,手游安装到手机上后,其安装目录是只读属性,以后是不能修改的。所以热更新的资源是没法下载到以前目录的,那么就得自己创建一个手机上可读写的目录,并将资源更新到这个目录。接下来还一个问题就是更新目录与以前资源目录不一致了,那么游戏中是怎么优先使用更新目录里的资源的呢?其实只要将创建的可读写目录路径添加到搜索路径中并将其插入到最前面即可,代码里统一是绝对路径。
文件的操作我们使用cocos封装的FileUtils类,其中一些相关函数如:fullPathForFilename:返回全路径,cocos加载文件最后都是通过它转换到全路径加载的,addSearchPath:添加到搜索路径,getWritablePath:返回一个可读写的路径。下面是Lua代码:
1
2
3
4
5
6
7
8
9
10
|
<code
class
=
"hljs"
ruby=
""
>--创建可写目录与设置搜索路径
self.writeRootPath = cc.FileUtils:getInstance():getWritablePath() .. _ClientGame2015_
if
not (cc.FileUtils:getInstance():isDirectoryExist(self.writeRootPath)) then
cc.FileUtils:getInstance():createDirectory(self.writeRootPath)
end
local searchPaths = cc.FileUtils:getInstance():getSearchPaths()
table.insert(searchPaths,
1
,self.writeRootPath ..
'/'
)
table.insert(searchPaths,
2
,self.writeRootPath ..
'/res/'
)
table.insert(searchPaths,
3
,self.writeRootPath ..
'/src/'
)
cc.FileUtils:getInstance():setSearchPaths(searchPaths)</code>
|
我封装的这套热更新本地需要两个配置文件,一个记录版本信息与请求url,一个记录所有资源列表。这两个配置文件都是json格式,cocos自带json.lua解析库, json.decode(js):将js转lua表,json.encode(table):将lua表转js。配置表如下:
还发现一个lua io文件操作的坑,local fp = io.open(fullPath,’r’);这些操作在iZ喎�"http://www.2cto.com/kf/ware/vc/" target="_blank" class="keylink">vc7/J0tS1q2FuZHJvaWTJz8i0srvWp7PWoaPL+dLUyMi4/NDCzsS8/rbB0LS7ubXDztLDx2MrK9fUvLq34tew1Nl0b2x1Ycq508MowKnVuUZpbGVVdGlsc8DgKaGjyLu2+KOsYysr0+tsdWG0q7Xd19a3+7SuyrHT1tPQ0ru49r/To6xjo6xjKyu1xNfWt/u0rqOsyOe5+8rHY29uc3QgY2hhciog1eLW1qOsxMfDtNP2tb22/r341sa1xNfWvdowo6y+zcjPzqq94cr4oaPI57n7ysdzdGQ6OnN0cmluZ9PrbHVh1eLW1qOs1PLT0NK7uPa1pbbAtcSx5MG/wLSx7cq+s6S2yKOs0/a1vbb+vfjWxrXE19a92jDSsrK7u+G94cr4oaO2+M28xqzK/b7dwO/D5rrcv8nE3Lvh09C63LbgMNfWvdqjrMTHw7RsdWHT62MrK727u6XKx7K7xNzWsb3TvdPK1c3q1fu1xKGjveK+9rDst6jKx6O6PC9wPg0KPGJsb2NrcXVvdGU+DQoJPHA+Yysr1eKx373TytXX1rf7tK7V4tH5vdPK1aO6PGJyIC8+DQoJY2hhciAqcCA9ICZsZHF1bzthYmNkZWYmcmRxdW87OzxiciAvPg0KCXNpemVfdCBsZW4gPSA3OzxiciAvPg0KCXN0ZDo6c3RyaW5nIHN0ciA9IHN0ZDo6c3RyaW5nKHAgKyBsZW4pOzwvcD4NCgk8cD5sdWHV4rHf0N64xHRvbHVhvbu7pbT6wuujujxiciAvPg0KCWx1Yb3TytVjKyu3tbvY19a3+7Suo7rKudPDbHVhX3B1c2hsc3RyaW5nKCmjrM7Sv7TL/MrH08O1xGx1YV9wdXNoc3RyaW5nKCnV4rj2o6y908rVsrvN6tX70/YwveHK+KGjPGJyIC8+DQoJYysrvdPK1Wx1Ybe1u9jX1rf7tK6jusq508NsdWFfdG9sc3RyaW5nKCkszazJz6GjPC9wPg0KPC9ibG9ja3F1b3RlPg0KPGgyIGlkPQ=="热更新源代码">热更新源代码
分为逻辑层与UI层,UI层是异步加载的,所以不能把这个模块当场景切换,用addChild添加到已有场景上就是。
逻辑层:
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
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
|
<code
class
=
"hljs"
livecodeserver=
""
>
require(
'common.json'
)
local UpdateLogicLayer =
class
(UpdateLogicLayer, cc.Node)
function UpdateLogicLayer:create(callback)
local view = UpdateLogicLayer.
new
()
local function onNodeEvent(eventType)
if
eventType == enter then
view:onEnter()
elseif eventType == exit then
view:onExit()
end
end
view:registerScriptHandler(onNodeEvent)
view:init(callback)
return
view
end
function UpdateLogicLayer:ctor()
self.writeRootPath = nil --手机可写路径
self.manifest = nil --配置表信息(json->table)
self.resConfigInfo = nil --资源列表(json->table)
self.updateResTable = nil --需要更新资源表
self.updateResProgress =
1
--更新进度
self.updateResPath = nil --当前更新资源路径
self.EventType = {
None =
0
, --初始化状态
StartGame =
1
, --开始游戏
StartUpdate =
2
, --开始更新
AssetsProgress =
3
, --资源更新中
AssetsFinish =
4
, --资源更新完成
}
self.callback = nil --外部回调
self.status = self.EventType.None
end
function UpdateLogicLayer:onEnter()
end
function UpdateLogicLayer:onExit()
end
function UpdateLogicLayer:init(callback)
self.callback = callback
--创建可写目录与设置搜索路径
self.writeRootPath = cc.FileUtils:getInstance():getWritablePath() .. _ClientGame2015_
if
not (cc.FileUtils:getInstance():isDirectoryExist(self.writeRootPath)) then
cc.FileUtils:getInstance():createDirectory(self.writeRootPath)
end
local searchPaths = cc.FileUtils:getInstance():getSearchPaths()
table.insert(searchPaths,
1
,self.writeRootPath ..
'/'
)
table.insert(searchPaths,
2
,self.writeRootPath ..
'/res/'
)
table.insert(searchPaths,
3
,self.writeRootPath ..
'/src/'
)
cc.FileUtils:getInstance():setSearchPaths(searchPaths)
--配置信息初始化
local fullPath = cc.FileUtils:getInstance():fullPathForFilename(
'project.manifest'
)
local fp = io.open(fullPath,
'r'
)
if
fp then
local js = fp:read(
'*a'
)
io.close(fp)
self.manifest = json.decode(js)
else
print(
'project.manifest read error!'
)
end
--版本比较
self:cmpVersions()
end
--版本比较
function UpdateLogicLayer:cmpVersions()
--Post
local xhr = cc.XMLHttpRequest:
new
()
xhr.responseType =
4
--json类型
xhr:open(POST, self.manifest.versionUrl)
local function onReadyStateChange()
if
xhr.readyState ==
4
and (xhr.status >=
200
and xhr.status <
207
) then
local localversion = self.manifest.version
self.manifest = json.decode(xhr.response)
if
self.manifest.version == localversion then
--开始游戏
self.status = self.EventType.StartGame
self:noticeEvent()
print(
'11开始游戏啊啊啊啊啊啊啊啊啊啊啊啊啊啊!!!!!!'
)
else
--查找需要更新的资源并下载
self.status = self.EventType.StartUpdate
self:noticeEvent()
self:findUpdateRes()
end
else
print(cmpVersions = xhr.readyState is:, xhr.readyState, xhr.status is: ,xhr.status)
end
end
xhr:registerScriptHandler(onReadyStateChange)
xhr:send()
end
--查找更新资源
function UpdateLogicLayer:findUpdateRes()
local xhr = cc.XMLHttpRequest:
new
()
xhr.responseType =
4
xhr:open(POST, self.manifest.tableResUrl)
local function onReadyStateChange()
if
xhr.readyState ==
4
and (xhr.status >=
200
and xhr.status <
207
) then
self.resConfigInfo = json.decode(xhr.response)
self.updateResTable = self:findUpdateResTable()
self:downloadRes()
else
print(findUpdateRes = xhr.readyState is:, xhr.readyState, xhr.status is: ,xhr.status)
end
end
xhr:registerScriptHandler(onReadyStateChange)
xhr:send(
'filename=/res_config.lua'
)
end
--查找需要更新资源表(更新与新增,没考虑删除)
function UpdateLogicLayer:findUpdateResTable()
local clientResTable = nil
local serverResTable = self.resConfigInfo
local fullPath = cc.FileUtils:getInstance():fullPathForFilename(
'resConfig.json'
)
local fp = io.open(fullPath,
'r'
)
if
fp then
local js = fp:read(
'*a'
)
fp:close(fp)
clientResTable = json.decode(js)
else
print(
'resConfig.json read error!'
)
end
local addResTable = {}
local isUpdate =
true
if
clientResTable and serverResTable then
for
key1, var1 in ipairs(serverResTable)
do
isUpdate =
true
for
key2, var2 in ipairs(clientResTable)
do
if
var2.name == var1.name then
if
var2.md5 == var1.md5 then
isUpdate =
false
end
break
end
end
if
isUpdate ==
true
then
table.insert(addResTable,var1.name)
end
end
else
print(
'local configFile error!(res_config_local or res_config_server)'
)
end
return
addResTable
end
--下载更新资源
function UpdateLogicLayer:downloadRes()
local fileName = self.updateResTable[self.updateResProgress]
if
fileName then
local xhr = cc.XMLHttpRequest:
new
()
xhr:open(POST, self.manifest.downloadResUrl)
local function onReadyStateChange()
if
xhr.readyState ==
4
and (xhr.status >=
200
and xhr.status <
207
) then
self:localWriteRes(fileName,xhr.response)
else
print(downloadRes = xhr.readyState is:, xhr.readyState, xhr.status is: ,xhr.status)
end
end
xhr:registerScriptHandler(onReadyStateChange)
xhr:send(
'filename='
.. fileName)
else
--资源更新完成
local fp = io.open(self.writeRootPath ..
'/res/project.manifest'
,
'w'
)
if
fp then
local js = json.encode(self.manifest)
fp:write(js)
io.close(fp)
end
local fp = io.open(self.writeRootPath ..
'/res/resConfig.json'
,
'w'
)
if
fp then
local js = json.encode(self.resConfigInfo)
fp:write(js)
io.close(fp)
end
--更新完成开始游戏
self.status = self.EventType.AssetsFinish
self:noticeEvent()
print(
'22开始游戏啊啊啊啊啊啊啊啊啊啊啊啊啊啊!!!!!!'
)
end
end
--资源本地写入
function UpdateLogicLayer:localWriteRes(resName, resData)
local lenthTable = {}
local tempResName = resName
local maxLength = string.len(tempResName)
local tag = string.find(tempResName,
'/'
)
while
tag
do
if
tag ~=
1
then
table.insert(lenthTable,tag)
end
tempResName = string.sub(tempResName,tag +
1
,maxLength)
tag = string.find(tempResName,
'/'
)
end
local sub =
0
for
key, var in ipairs(lenthTable)
do
sub = sub + var
end
if
sub ~=
0
then
local temp = string.sub(resName,
1
,sub +
1
)
local pathName = self.writeRootPath .. temp
if
not (cc.FileUtils:getInstance():isDirectoryExist(pathName)) then
cc.FileUtils:getInstance():createDirectory(pathName)
end
end
self.updateResPath = self.writeRootPath .. resName
local fp = io.open(self.updateResPath,
'w'
)
if
fp then
fp:write(resData)
io.close(fp)
self.status = self.EventType.AssetsProgress
self:noticeEvent()
print(countRes = , self.updateResProgress,nameRes = ,resName)
self.updateResProgress = self.updateResProgress +
1
self:downloadRes()
else
print(
'downloadRes write error!!'
)
end
end
function UpdateLogicLayer:noticeEvent()
if
self.callback then
self.callback(self,self.status)
else
print(
'callback is nil'
)
end
end
return
UpdateLogicLayer
</code>
|
UI层:
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
111
112
113
114
115
116
117
118
119
120
121
|
<code
class
=
"hljs"
lua=
""
>--[[
说明:
1
,本地需求配置文件:project.manifest, resConfig.json
2
,循环post请求,有时会出现闪退情况,最好改成只发一次zip压缩包形式
3
,目前只支持ios,lua io库文件操作在andriod上不行,文件操作c实现(注意lua与c++交互对于
char
*遇/
0
结束问题,需要改lua绑定代码)
]]
local UpdateLogicLayer = require(
'app.views.Assets.UpdateLogicLayer'
)
local SelectSerAddrLayer = require(app.views.Login.SelectSerAddrLayer)
local UpdateUILayer =
class
(UpdateUILayer, cc.Layer)
function UpdateUILayer:create()
local view = UpdateUILayer.
new
()
local function onNodeEvent(eventType)
if
eventType == enter then
view:onEnter()
elseif eventType == exit then
view:onExit()
end
end
view:registerScriptHandler(onNodeEvent)
view:init()
return
view
end
function UpdateUILayer:ctor()
end
function UpdateUILayer:onEnter()
end
function UpdateUILayer:onExit()
end
function UpdateUILayer:init()
local updateLogicLayer = UpdateLogicLayer:create(function(sender,eventType) self:onEventCallBack(sender,eventType) end)
self:addChild(updateLogicLayer)
end
function UpdateUILayer:onEventCallBack(sender,eventType)
if
eventType == sender.EventType.StartGame then
print(startgame !!!)
local view = SelectSerAddrLayer.
new
()
self:addChild(view)
elseif eventType == sender.EventType.StartUpdate then
print(startupdate !!!)
self:initAssetsUI()
elseif eventType == sender.EventType.AssetsProgress then
print(assetsprogress !!!)
self:updateAssetsProgress(sender.updateResPath,sender.updateResTable,sender.updateResProgress)
elseif eventType == sender.EventType.AssetsFinish then
print(assetsfinish !!!)
self:updateAssetsFinish(sender.writeRootPath)
end
end
--UI界面初始化
function UpdateUILayer:initAssetsUI()
local assetsLayer = cc.CSLoader:createNode(csb/assetsUpdate_layer.csb)
local visibleSize = cc.Director:getInstance():getVisibleSize()
assetsLayer:setAnchorPoint(cc.p(
0.5
,
0.5
))
assetsLayer:setPosition(visibleSize.width/
2
,visibleSize.height/
2
)
self:addChild(assetsLayer)
self.rootPanel = assetsLayer:getChildByName(Panel_root)
self.widgetTable = {
LoadingBar_1 = ccui.Helper:seekWidgetByName(self.rootPanel ,LoadingBar_1),
Text_loadProgress = ccui.Helper:seekWidgetByName(self.rootPanel ,Text_loadProgress),
Text_loadResPath = ccui.Helper:seekWidgetByName(self.rootPanel ,Text_loadResPath),
Image_tag = ccui.Helper:seekWidgetByName(self.rootPanel ,Image_tag),
}
self.widgetTable.Image_tag:setVisible(
false
)
self.widgetTable.LoadingBar_1:setPercent(
1
)
self.widgetTable.Text_loadProgress:setString(
'0%'
)
self.widgetTable.Text_loadResPath:setString(
'准备更新...'
)
end
--资源更新完成
function UpdateUILayer:updateAssetsFinish(writePaht)
self.widgetTable.Text_loadResPath:setString(
'资源更新完成...'
)
self.widgetTable.Text_loadProgress:setString(
'100%'
)
self:runAction(cc.Sequence:create(cc.DelayTime:create(
1
),
cc.CallFunc:create(function()
local view = SelectSerAddrLayer.
new
()
self:addChild(view)
end)
))
end
--资源更新中
function UpdateUILayer:updateAssetsProgress(resPath, updateResTable, updateResProgress)
self.widgetTable.Text_loadResPath:setString(resPath)
local percentMaxNum = #updateResTable
local percentNum = math.floor((updateResProgress / percentMaxNum) *
100
)
self.widgetTable.LoadingBar_1:setPercent(percentNum)
self.widgetTable.Text_loadProgress:setString(percentNum ..
'%'
)
end
return
UpdateUILayer
|