Cocos2dx-- 资源热更新

前言:游戏上线后,我们常常还会需要更新,如新增玩法,活动等,这种动态的更新资源我们称为游戏的热更新。热更新一般只适用于脚本语言,因为脚本不需要编译,是一种解释性语言,而如C++语言是很难热更新的,其代码只要有改动就需要重新链接编译(接口统一,用动态库可以实现,不过太不灵活了)。
本章将讲讲用Cocos-lua引擎怎么实现热更新,其实Cocos自带也封装了热更新模块(AssetsManager, AssetsManagerEx),不过我没用自带的那套,自己封装了一套,其基本思路原理是一致的。

热更新基本思路

  1. 登入游戏先向服务端请求当前游戏版本号信息,与本地版本号比较,如果相同则说明没有资源需要更新直接进入游戏,而如果不相同,则说明有资源需要更新进入第2步。

  2. 向服务端请求当前所有资源的列表(资源名+MD5),与本地资源列表比较,找出需要更新的资源。

  3. 根据找出的需要更新资源,向服务端请求下载下来。(目前发现更新资源很多时,一个个循环向服务端请求可能中途会出错,所以最好是以zip包的形式一次性发送过来,客服端只请求一次)

热更新注意点

  • 1,程序加载某个文件原理:首先一个程序加载本地硬盘某一文件最终加载的路径都是绝对全路径。而我们之所以还可以写相对路径也能找到对应的文件是因为还有一个搜索路径,搜索路径是用一个容器存储的,相对路径是这样得到全路径的 = 搜索路径(全路径) + 相对路径。就是只要加入到这个搜索路径中的路径,以后要加载这里面的文件就只需给文件名就可以了,前面的路径它会自动去搜索路径循环遍历查找。所以程序里我们一般不写绝对路径,而是把前面的全路径加入到搜索路径,之后只需写后面的相对路径就能查找到了。
    2,手游安装到手机上后,其安装目录是只读属性,以后是不能修改的。所以热更新的资源是没法下载到以前目录的,那么就得自己创建一个手机上可读写的目录,并将资源更新到这个目录。接下来还一个问题就是更新目录与以前资源目录不一致了,那么游戏中是怎么优先使用更新目录里的资源的呢?其实只要将创建的可读写目录路径添加到搜索路径中并将其插入到最前面即可,代码里统一是绝对路径。
    文件的操作我们使用cocos封装的FileUtils类,其中一些相关函数如:fullPathForFilename:返回全路径,cocos加载文件最后都是通过它转换到全路径加载的,addSearchPath:添加到搜索路径,getWritablePath:返回一个可读写的路径。下面是Lua代码:
--创建可写目录与设置搜索路径
    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。配置表如下:

  • 还发现一个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  = 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


Cocos2dx-- 资源热更新_第1张图片

你可能感兴趣的:(cocos2dx,文件系统,资源动态更新)