前言:遊戲上線後,咱們經常還會須要更新,如新增玩法,活動等,這種動態的更新資源咱們稱爲遊戲的熱更新。熱更新通常只適用於腳本語言,由於腳本不須要編譯,是一種解釋性語言,而如C++語言是很難熱更新的,其代碼只要有改動就須要從新連接編譯(接口統一,用動態庫能夠實現,不過太不靈活了)。
本章將講講用Cocos-lua引擎怎麼實現熱更新,其實Cocos自帶也封裝了熱更新模塊(AssetsManager, AssetsManagerEx),不過我沒用自帶的那套,本身封裝了一套,其基本思路原理是一致的。android
登入遊戲先向服務端請求當前遊戲版本號信息,與本地版本號比較,若是相同則說明沒有資源須要更新直接進入遊戲,而若是不相同,則說明有資源須要更新進入第2步。ios
向服務端請求當前全部資源的列表(資源名+MD5),與本地資源列表比較,找出須要更新的資源。c++
根據找出的須要更新資源,向服務端請求下載下來。(目前發現更新資源不少時,一個個循環向服務端請求可能中途會出錯,因此最好是以zip包的形式一次性發送過來,客服端只請求一次)json
--建立可寫目錄與設置搜索路徑
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)
我封裝的這套熱更新本地須要兩個配置文件,一個記錄版本信息與請求url,一個記錄全部資源列表。這兩個配置文件都是json格式,cocos自帶json.lua解析庫, json.decode(js):將js轉lua表,json.encode(table):將lua錶轉js。配置表以下:
ruby
還發現一個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++交互是不能直接接收完整的。解決辦法是:markdown
c++這邊接收字符串這樣接收:
char *p = 「abc\0def」;
size_t len = 7;
std::string str = std::string(p + len);applua這邊修改tolua交互代碼:
lua接收c++返回字符串:使用lua_pushlstring(),我看它是用的lua_pushstring()這個,接收不完整遇0結束。
c++接收lua返回字符串:使用lua_tolstring(),同上。異步
分爲邏輯層與UI層,UI層是異步加載的,因此不能把這個模塊當場景切換,用addChild添加到已有場景上就是。函數
邏輯層:post
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
UI層:
--[[ 說明: 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