前言:游戏上线后,我们常常还会需要更新,如新增玩法,活动等,这种动态的更新资源我们称为游戏的热更新。热更新一般只适用于脚本语言,因为脚本不需要编译,是一种解释性语言,而如C++语言是很难热更新的,其代码只要有改动就需要重新链接编译(接口统一,用动态库可以实现,不过太不灵活了)。
本章将讲讲用Cocos-lua引擎怎么实现热更新,其实Cocos自带也封装了热更新模块(AssetsManager,AssetsManagerEx),不过我没用自带的那套,自己封装了一套,其基本思路原理是一致的。
热更新基本思路
-
登入游戏先向服务端请求当前游戏版本号信息,与本地版本号比较,如果相同则说明没有资源需要更新直接进入游戏,而如果不相同,则说明有资源需要更新进入第2步。
-
向服务端请求当前所有资源的列表(资源名+MD5),与本地资源列表比较,找出需要更新的资源。
-
根据找出的需要更新资源,向服务端请求下载下来。(目前发现更新资源很多时,一个个循环向服务端请求可能中途会出错,所以最好是以zip包的形式一次性发送过来,客服端只请求一次)
热更新注意点
- 1,程序加载某个文件原理:首先一个程序加载本地硬盘某一文件最终加载的路径都是绝对全路径。而我们之所以还可以写相对路径也能找到对应的文件是因为还有一个搜索路径,搜索路径是用一个容器存储的,相对路径是这样得到全路径的 = 搜索路径(全路径) + 相对路径。就是只要加入到这个搜索路径中的路径,以后要加载这里面的文件就只需给文件名就可以了,前面的路径它会自动去搜索路径循环遍历查找。所以程序里我们一般不写绝对路径,而是把前面的全路径加入到搜索路径,之后只需写后面的相对路径就能查找到了。
2,手游安装到手机上后,其安装目录是只读属性,以后是不能修改的。所以热更新的资源是没法下载到以前目录的,那么就得自己创建一个手机上可读写的目录,并将资源更新到这个目录。接下来还一个问题就是更新目录与以前资源目录不一致了,那么游戏中是怎么优先使用更新目录里的资源的呢?其实只要将创建的可读写目录路径添加到搜索路径中并将其插入到最前面即可,代码里统一是绝对路径。
文件的操作我们使用cocos封装的FileUtils类,其中一些相关函数如:fullPathForFilename:返回全路径,cocos加载文件最后都是通过它转换到全路径加载的,addSearchPath:添加到搜索路径,getWritablePath:返回一个可读写的路径。下面是Lua代码:
--创建可写目录与设置搜索路径
self.writeRootPath = cc.FileUtils:getInstance():getWritablePath() .. "_ClientGame2015_"
if not (cc.FileUtils:isDirectoryExist(self.writeRootPath)) then
cc.FileUtils:createDirectory(self.writeRootPath)
end
local searchPaths = cc.FileUtils:getSearchPaths()
table.insert(searchPaths,1,self.writeRootPath .. '/')
table.insert(searchPaths,102);">2,0);">'/res/')
table.insert(searchPaths,102);">3,0);">'/src/')
cc.FileUtils:setSearchPaths(searchPaths)
-
@H_301_78@1
@H_301_78@2
@H_301_78@3
@H_301_78@4
@H_301_78@5
@H_301_78@6
@H_301_78@7
@H_301_78@8
@H_301_78@9
@H_301_78@10
-
我封装的这套热更新本地需要两个配置文件,一个记录版本信息与请求url,一个记录所有资源列表。这两个配置文件都是json格式,cocos自带json.lua解析库, json.decode(js):将js转lua表,json.encode(table):将lua表转js。配置表如下:
-
还发现一个lua io文件操作的坑,local fp = io.open(fullPath,’r’);这些操作在ios可以但android上却不支持。所以热更新文件读写还得我们c++自己封装再tolua使用(扩展FileUtils类)。然而,c++与lua传递字符串时又有一个坑,c,c++的字符串,如果是const char* 这种,那么遇到二进制的字节0,就认为结束。如果是std::string与lua这种,则有一个单独的变量来表示长度,遇到二进制的字节0也不会结束。而图片数据里面很可能会有很多0字节,那么lua与c++交互是不能直接接收完整的。解决办法是:
c++这边接收字符串这样接收:
char *p = “abc\0def”;
size_t len = 7;
std::string str = std::string(p + len);lua这边修改tolua交互代码:
lua接收c++返回字符串:使用lua_pushlstring(),我看它是用的lua_pushstring()这个,接收不完整遇0结束。
c++接收lua返回字符串:使用lua_tolstring(),同上。
热更新源代码
分为逻辑层与UI层,UI层是异步加载的,所以不能把这个模块当场景切换,用addChild添加到已有场景上就是。
逻辑层:
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 = --开始游戏
StartUpdate = --开始更新
AssetsProgress = --资源更新中
AssetsFinish = 4,0);">--资源更新完成
}
self.callback = nil --外部回调
self.status = self.EventType.None
function UpdateLogicLayer:onEnter()
function UpdateLogicLayer:onExit()
function UpdateLogicLayer:init(callback)
self.callback = callback
--创建可写目录与设置搜索路径
self.writeRootPath = cc.FileUtils:getInstance():getWritablePath() .. 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,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)
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,0);">"xhr.status is: ",xhr.status)
end
xhr:registerScriptHandler(onReadyStateChange)
xhr:send()
--查找更新资源
function UpdateLogicLayer:findUpdateRes()
new()
xhr.responseType = 4
xhr:then
self.resConfigInfo = json.decode(xhr.response)
self.updateResTable = self:findUpdateResTable()
self:downloadRes()
else
print("findUpdateRes = xhr.readyState is:",136);">end
xhr:registerScriptHandler(onReadyStateChange)
xhr:send('filename=/res_config.lua')
--查找需要更新资源表(更新与新增,没考虑删除)
function UpdateLogicLayer:findUpdateResTable()
local clientResTable = nil
local serverResTable = self.resConfigInfo
'resConfig.json')
'*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
'local configFile error!(res_config_local or res_config_server)')
end
return addResTable
--下载更新资源
function UpdateLogicLayer:downloadRes()
local fileName = self.updateResTable[self.updateResProgress]
if fileName new()
xhr:function onReadyStateChange()
then
self:localWriteRes(fileName,xhr.response)
else
print("downloadRes = xhr.readyState is:",xhr.status)
end
xhr:registerScriptHandler(onReadyStateChange)
xhr:'filename=' .. fileName)
else
--资源更新完成
open(self.writeRootPath .. '/res/project.manifest',0);">'w')
then
local js = json.encode(self.manifest)
fp:write(js)
io.close(fp)
end
'/res/resConfig.json',0);">'w')
local js = json.encode(self.resConfigInfo)
fp:end
--更新完成开始游戏
self.status = self.EventType.AssetsFinish
self:noticeEvent()
print('22开始游戏啊啊啊啊啊啊啊啊啊啊啊啊啊啊!!!!!!')
end
--资源本地写入
function UpdateLogicLayer:localWriteRes(resName,resData)
local lenthTable = {}
local tempResName = resName
local maxLength = string.len(tempResName)
local tag = string.find(tempResName,0);">'/')
while tag do
if tag ~= 1 then
table.insert(lenthTable,tag)
end
tempResName = string.sub(tempResName,tag + '/')
local sub = 0
for key,var in ipairs(lenthTable) do
sub = sub + var
end
if sub ~= 0 local temp = string.sub(resName,sub + 1)
local pathName = self.writeRootPath .. temp
if not (cc.FileUtils:getInstance():isDirectoryExist(pathName)) then
cc.FileUtils:getInstance():createDirectory(pathName)
end
self.updateResPath = self.writeRootPath .. resName
open(self.updateResPath,0);">'w')
then
fp:write(resData)
io.close(fp)
self.status = self.EventType.AssetsProgress
self:noticeEvent()
print("countRes = ",self.updateResProgress,0);">"nameRes = ",resName)
self.updateResProgress = self.updateResProgress + 1
self:downloadRes()
'downloadRes write error!!')
end
function UpdateLogicLayer:noticeEvent()
if self.callback then
self.callback(self,self.status)
'callback is nil')
end
return UpdateLogicLayer
-
@H_301_78@1
@H_301_78@2
@H_301_78@3
@H_301_78@4
@H_301_78@5
@H_301_78@6
@H_301_78@7
@H_301_78@8
@H_301_78@9
@H_301_78@10
@H_301_78@11
@H_301_78@12
@H_301_78@13
@H_301_78@14
@H_301_78@15
@H_301_78@16
@H_301_78@17
@H_301_78@18
@H_301_78@19
@H_301_78@20
@H_301_78@21
@H_301_78@22
@H_301_78@23
@H_301_78@24
@H_301_78@25
@H_301_78@26
@H_301_78@27
@H_301_78@28
@H_301_78@29
@H_301_78@30
@H_301_78@31
@H_301_78@32
@H_301_78@33
@H_301_78@34
@H_301_78@35
@H_301_78@36
@H_301_78@37
@H_301_78@38
@H_301_78@39
@H_301_78@40
@H_301_78@41
@H_301_78@42
@H_301_78@43
@H_301_78@44
@H_301_78@45
@H_301_78@46
@H_301_78@47
@H_301_78@48
@H_301_78@49
@H_301_78@50
@H_301_78@51
@H_301_78@52
@H_301_78@53
@H_301_78@54
@H_301_78@55
@H_301_78@56
@H_301_78@57
@H_301_78@58
@H_301_78@59
@H_301_78@60
@H_301_78@61
@H_301_78@62
@H_301_78@63
@H_301_78@64
@H_301_78@65
@H_301_78@66
@H_301_78@67
@H_301_78@68
@H_301_78@69
@H_301_78@70
@H_301_78@71
@H_301_78@72
@H_301_78@73
@H_301_78@74
@H_301_78@75
@H_301_78@76
@H_301_78@77
@H_301_78@78
@H_301_78@79
@H_301_78@80
@H_301_78@81
@H_301_78@82
@H_301_78@83
@H_301_78@84
@H_301_78@85
@H_301_78@86
@H_301_78@87
@H_301_78@88
@H_301_78@89
@H_301_78@90
@H_301_78@91
@H_301_78@92
@H_301_78@93
@H_301_78@94
@H_301_78@95
@H_301_78@96
@H_301_78@97
@H_301_78@98
@H_301_78@99
@H_301_78@100
@H_301_78@101
@H_301_78@102
@H_301_78@103
@H_301_78@104
@H_301_78@105
@H_301_78@106
@H_301_78@107
@H_301_78@108
@H_301_78@109
@H_301_78@110
@H_301_78@111
@H_301_78@112
@H_301_78@113
@H_301_78@114
@H_301_78@115
@H_301_78@116
@H_301_78@117
@H_301_78@118
@H_301_78@119
@H_301_78@120
@H_301_78@121
@H_301_78@122
@H_301_78@123
@H_301_78@124
@H_301_78@125
@H_301_78@126
@H_301_78@127
@H_301_78@128
@H_301_78@129
@H_301_78@130
@H_301_78@131
@H_301_78@132
@H_301_78@133
@H_301_78@134
@H_301_78@135
@H_301_78@136
@H_301_78@137
@H_301_78@138
@H_301_78@139
@H_301_78@140
@H_301_78@141
@H_301_78@142
@H_301_78@143
@H_301_78@144
@H_301_78@145
@H_301_78@146
@H_301_78@147
@H_301_78@148
@H_301_78@149
@H_301_78@150
@H_301_78@151
@H_301_78@152
@H_301_78@153
@H_301_78@154
@H_301_78@155
@H_301_78@156
@H_301_78@157
@H_301_78@158
@H_301_78@159
@H_301_78@160
@H_301_78@161
@H_301_78@162
@H_301_78@163
@H_301_78@164
@H_301_78@165
@H_301_78@166
@H_301_78@167
@H_301_78@168
@H_301_78@169
@H_301_78@170
@H_301_78@171
@H_301_78@172
@H_301_78@173
@H_301_78@174
@H_301_78@175
@H_301_78@176
@H_301_78@177
@H_301_78@178
@H_301_78@179
@H_301_78@180
@H_301_78@181
@H_301_78@182
@H_301_78@183
@H_301_78@184
@H_301_78@185
@H_301_78@186
@H_301_78@187
@H_301_78@188
@H_301_78@189
@H_301_78@190
@H_301_78@191
@H_301_78@192
@H_301_78@193
@H_301_78@194
@H_301_78@195
@H_301_78@196
@H_301_78@197
@H_301_78@198
@H_301_78@199
@H_301_78@200
@H_301_78@201
@H_301_78@202
@H_301_78@203
@H_301_78@204
@H_301_78@205
@H_301_78@206
@H_301_78@207
@H_301_78@208
@H_301_78@209
@H_301_78@210
@H_301_78@211
@H_301_78@212
@H_301_78@213
@H_301_78@214
@H_301_78@215
@H_301_78@216
@H_301_78@217
@H_301_78@218
@H_301_78@219
@H_301_78@220
@H_301_78@221
@H_301_78@222
@H_301_78@223
@H_301_78@224
@H_301_78@225
@H_301_78@226
@H_301_78@227
@H_301_78@228
@H_301_78@229
@H_301_78@230
@H_301_78@231
@H_301_78@232
@H_301_78@233
@H_301_78@234
@H_301_78@235
@H_301_78@236
@H_301_78@237
@H_301_78@238
@H_301_78@239
@H_301_78@240
@H_301_78@241
@H_301_78@242
@H_301_78@243
@H_301_78@244
@H_301_78@245
@H_301_78@246
@H_301_78@247
@H_301_78@248
@H_301_78@249
@H_301_78@250
@H_301_78@251
@H_301_78@252
@H_301_78@253
@H_301_78@254
@H_301_78@255
@H_301_78@256
@H_301_78@257
@H_301_78@258
@H_301_78@259
@H_301_78@260
@H_301_78@261
@H_301_78@262
@H_301_78@263
@H_301_78@264
@H_301_78@265
@H_301_78@266
@H_301_78@267
@H_301_78@268
@H_301_78@269
@H_301_78@270
@H_301_78@271
@H_301_78@272
@H_301_78@273
@H_301_78@274
@H_301_78@275
UI层:
--[[ 说明: 1,本地需求配置文件:project.manifest,resConfig.json 2,循环post请求,有时会出现闪退情况,最好改成只发一次zip压缩包形式 3,目前只支持ios,lua io库文件操作在andriod上不行,文件操作c实现(注意lua与c++交互对于char *遇/0结束问题,需要改lua绑定代码) ]]
local UpdateLogicLayer = 'app.views.Assets.UpdateLogicLayer')
local SelectSerAddrLayer = "app.views.Login.SelectSerAddrLayer")
local UpdateUILayer = class("UpdateUILayer",cc.Layer)
function UpdateUILayer:create()
local view = UpdateUILayer.new()
local function onNodeEvent(eventType)
then
view:onEnter()
elseif eventType == then
view:onExit()
end
end
view:registerScriptHandler(onNodeEvent)
view:init()
return view
end
function UpdateUILayer:ctor()
function UpdateUILayer:onEnterfunction UpdateUILayer:onExitfunction UpdateUILayer:initlocal updateLogicLayer = UpdateLogicLayer:create(function(sender,eventType) self:onEventCallBack(sender,eventType) end)
self:addChild(updateLogicLayer)
function UpdateUILayer:onEventCallBackif eventType == sender.EventType.StartGame print("startgame !!!")
local view = SelectSerAddrLayer.new()
self:addChild(view)
elseif eventType == sender.EventType.StartUpdate "startupdate !!!")
self:initAssetsUI()
elseif eventType == sender.EventType.AssetsProgress "assetsprogress !!!")
self:updateAssetsProgress(sender.updateResPath,sender.updateResTable,sender.updateResProgress)
elseif eventType == sender.EventType.AssetsFinish "assetsfinish !!!")
self:updateAssetsFinish(sender.writeRootPath)
end
end
--UI界面初始化
function UpdateUILayer:initAssetsUIlocal assetsLayer = cc.CSLoader:createNode("csb/assetsUpdate_layer.csb")
local visibleSize = cc.Director:getInstance():getVisibleSize()
assetsLayer:setAnchorPoint(cc.p(0.5,102);">0.5))
assetsLayer:setPosition(visibleSize.width/2)
self:addChild(assetsLayer)
self.rootPanel = assetsLayer:getChildByName("Panel_root")
self.widgetTable = {
LoadingBar_1 = ccui.Helper:seekWidgetByName(self.rootPanel,0);">"LoadingBar_1"),Text_loadProgress = ccui.Helper:seekWidgetByName(self.rootPanel,0);">"Text_loadProgress"),Text_loadResPath = ccui.Helper:seekWidgetByName(self.rootPanel,0);">"Text_loadResPath"),Image_tag = ccui.Helper:seekWidgetByName(self.rootPanel,0);">"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('准备更新...')
--资源更新完成
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(()
local view = SelectSerAddrLayer.new()
self:addChild(view)
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