cocos2dx lua 熱更新

原理:

每次登陸游戲利用cocos的assetManager從服務(wù)器拉去當(dāng)前最新的兩個(gè)文件。 一個(gè)是version.mainifest,一個(gè)project.mainifest. 這兩個(gè)文件都是xml的描述文件。一個(gè)包含了版本信息,第二個(gè)包含了游戲所有資源的MD5碼。首先通過version文件對(duì)比本地的版本是否相同,如果不相同,再通過跟本地的project文件對(duì)比MD5碼來判斷哪些文件需要重新下載,替換資源。

步驟:

1. 有一個(gè)文件下載的熱更新服務(wù)器,將最新項(xiàng)目資源(res/ src/ 目錄)放入熱更新服務(wù)器中,添加版本信息母文件(version_info.json)和python腳本文件eneateManifest.py(生成project.manifest、version.manifest文件)。

2.version_info.json文件: 主要用來配置信息

{
    "packageUrl" : "http://ip:port/update/MyProj/assets/",
    "remoteManifestUrl" : "http://ip:port/update/MyProj/version/project.manifest",
    "remoteVersionUrl" : "http://ip:port/update/MyProj/version/version.manifest",
    "engineVersion" : "3.3",
    "update_channel" : "Android",
    "bundle" : "2018111701",
    "version" : "1.0.0",
}

3.eneateManifest.py文件: 這個(gè)文件是一個(gè)python。目的是生成對(duì)應(yīng)的version和project文件。project文件可以幫你給每個(gè)資源生成獨(dú)一無二的MD5碼,相當(dāng)于每個(gè)資源的標(biāo)記。下面是一段python文件的代碼。

#coding:utf-8
 
import os
import sys
import json
import hashlib
import subprocess
import getpass
 
username = getpass.getuser()
# 改變當(dāng)前工作目錄
#os.chdir('/Users/' + username + '/Documents/client/MyProj/')
 
assetsDir = {
    #MyProj文件夾下需要進(jìn)行熱跟的文件夾
    "searchDir" : ["src", "res"],
    #需要忽略的文件夾
    "ignorDir" : ["cocos", "framework", ".svn"],
    #需要忽略的文件
    "ignorFile":[".DS_Store"],
}
 
versionConfigFile   = "version/version_info.json"  #版本信息的配置文件路徑
versionManifestPath = "version/version.manifest"    #由此腳本生成的version.manifest文件路徑
projectManifestPath = "version/project.manifest"    #由此腳本生成的project.manifest文件路徑
# projectManifestPath = "/Users/ximi/Documents/client/MyProj/res/version/project.manifest"    #由此腳本生成的project.manifest文件路徑(mac機(jī))
 
class SearchFile:
    def __init__(self):
        self.fileList = []
 
        for k in assetsDir:
            if (k == "searchDir"):
                for searchdire in assetsDir[k]:                 
                    self.recursiveDir(searchdire)
 
    def recursiveDir(self, srcPath):
        ''' 遞歸指定目錄下的所有文件'''
        dirList = []    #所有文件夾  
 
        files = os.listdir(srcPath) #返回指定目錄下的所有文件,及目錄(不含子目錄)
 
        for f in files:         
            #目錄的處理
            if (os.path.isdir(srcPath + '/' + f)):              
                if (f[0] == '.' or (f in assetsDir["ignorDir"])):
                    #排除隱藏文件夾和忽略的目錄
                    pass
                else:
                    #添加非需要的文件夾                                  
                    dirList.append(f)
 
            #文件的處理
            elif (os.path.isfile(srcPath + '/' + f)) and (f not in assetsDir["ignorFile"]):               
                self.fileList.append(srcPath + '/' + f) #添加文件
 
        #遍歷所有子目錄,并遞歸
        for dire in dirList:        
            #遞歸目錄下的文件
            self.recursiveDir(srcPath + '/' + dire)
 
    def getAllFile(self):
        ''' get all file path'''
        return tuple(self.fileList)
 
 
def CalcMD5(filepath):
    """generate a md5 code by a file path"""
    with open(filepath,'rb') as f:
        md5obj = hashlib.md5()
        md5obj.update(f.read())
        return md5obj.hexdigest()
 
 
def getVersionInfo():
    '''get version config data'''
    configFile = open(versionConfigFile,"r")
    json_data = json.load(configFile)
 
    configFile.close()
    # json_data["version"] = json_data["version"] + '.' + str(GetSvnCurrentVersion())
    json_data["version"] = json_data["version"]
    return json_data
 
 
def GenerateVersionManifestFile():
    ''' 生成大版本的version.manifest'''
    json_str = json.dumps(getVersionInfo(), indent = 2)
    fo = open(versionManifestPath,"w")  
    fo.write(json_str)  
    fo.close()
 
 
def GenerateProjectManifestFile():
    searchfile = SearchFile()
    fileList = list(searchfile.getAllFile())
    project_str = {}
    project_str.update(getVersionInfo())
    dataDic = {}
    for f in fileList:      
        dataDic[f] = {"md5" : CalcMD5(f)}
        print f
 
    project_str.update({"assets":dataDic})
    json_str = json.dumps(project_str, sort_keys = True, indent = 2)
 
    fo = open(projectManifestPath,"w")  
    fo.write(json_str)  
    fo.close()
 
if __name__ == "__main__":
    GenerateVersionManifestFile()
    GenerateProjectManifestFile()

生成version.manifest如下

{
  "packageUrl": "http://ip:port/update/MyProj/assets/", 
  "engineVersion": "3.3", 
  "version": "1.0.0", 
  "remoteVersionUrl": "http://ip:port/update/MyProj/version/version.manifest", 
  "remoteManifestUrl": "http://ip:port/update/MyProj/version/project.manifest"
}

生成project.manifest如下

{
    "assets": {
        "src/packages/mvc/init.lua": {
            "md5": "6b9173481a1300c5e737ad5885ebef00"
        }, 
        "src/protobuf.lua": {
            "md5": "f790fe35eb179a4341ff41d94e488a5d"
        }
        ...
    }, 
    "packageUrl": "http://ip:port/update/MyProj/assets/", 
    "engineVersion": "3.3", 
    "version": "1.0.0", 
    "remoteVersionUrl": "http://ip:port/update/MyProj/version/version.manifest", 
    "remoteManifestUrl": "http://ip:port/update/MyProj/version/project.manifest"
}

4.游戲客戶端: 利用cocos assetManager來從服務(wù)器獲取文件并且進(jìn)行資源的替換(這里所謂的替換并不是真正的替換,利用了Fileutils->searchPath() 設(shè)置資源文件讀取的優(yōu)先級(jí)。也就是老資源和代碼并沒有刪除,而是舍棄不用。


--region *.lua
--Date
 
local AssetsManager = class("AssetsManager",function ()
    return cc.LayerColor:create(cc.c4b(20, 20, 20, 220))
end)
 
function AssetsManager:ctor()
    self:onNodeEvent("exit", handler(self, self.onExitCallback))
    self:initUI()
    self:setAssetsManage()
end
 
function AssetsManager:onExitCallback()
    self.assetsManagerEx:release()
end
 
function AssetsManager:initUI()
 
    local hintLabel = cc.Label:createWithTTF("正在更新...", CONFIG.TTF_FONT_2, 20)
        :addTo(self)
        :move(600, 80)
 
    local progressBg = display.newSprite("sprites/hyd_progress_bg.png")    
        :addTo(self)
        :move(600, 40)
 
    self.progress = cc.ProgressTimer:create(display.newSprite("sprites/hyd_progress.png"))
        :addTo(progressBg)
        :move(380, 19)
    self.progress:setType(cc.PROGRESS_TIMER_TYPE_BAR)
    self.progress:setBarChangeRate(cc.p(1, 0))
    self.progress:setMidpoint(cc.p(0.0, 0.5))
    self.progress:setPercentage(0) 
 
    --觸摸吞噬
    self.listener = cc.EventListenerTouchOneByOne:create()
    self.listener:setSwallowTouches(true)
    local onTouchBegan = function (touch, event)
        return true
    end
 
    self.listener:registerScriptHandler(onTouchBegan, cc.Handler.EVENT_TOUCH_BEGAN)
    cc.Director:getInstance():getEventDispatcher():addEventListenerWithSceneGraphPriority(self.listener, self)   
end
 
function AssetsManager:setAssetsManage()
    --創(chuàng)建可寫目錄與設(shè)置搜索路徑
    local storagePath = cc.FileUtils:getInstance():getWritablePath() .. "NewRes/" 
    local resPath = storagePath.. '/res/'
    local srcPath = storagePath.. '/src/'
    if not (cc.FileUtils:getInstance():isDirectoryExist(storagePath)) then         
        cc.FileUtils:getInstance():createDirectory(storagePath)
        cc.FileUtils:getInstance():createDirectory(resPath)
        cc.FileUtils:getInstance():createDirectory(srcPath)
    end
    local searchPaths = cc.FileUtils:getInstance():getSearchPaths() 
    table.insert(searchPaths, 1, storagePath)  
    table.insert(searchPaths, 2, resPath)
    table.insert(searchPaths, 3, srcPath)
    cc.FileUtils:getInstance():setSearchPaths(searchPaths)
 
    self.assetsManagerEx = cc.AssetsManagerEx:create("version/project.manifest", storagePath)    
    self.assetsManagerEx:retain()
 
    local eventListenerAssetsManagerEx = cc.EventListenerAssetsManagerEx:create(self.assetsManagerEx, 
       function (event)
           self:handleAssetsManagerEvent(event)
       end)
 
    local dispatcher = cc.Director:getInstance():getEventDispatcher()
    dispatcher:addEventListenerWithFixedPriority(eventListenerAssetsManagerEx, 1)
 
    --檢查版本并升級(jí)
    self.assetsManagerEx:update()
end
 
function AssetsManager:handleAssetsManagerEvent(event)    
    local eventCodeList = cc.EventAssetsManagerEx.EventCode    
 
    local eventCodeHand = {
 
        [eventCodeList.ERROR_NO_LOCAL_MANIFEST] = function ()
            print("發(fā)生錯(cuò)誤:本地資源清單文件未找到")
        end,
 
        [eventCodeList.ERROR_DOWNLOAD_MANIFEST] = function ()
            print("發(fā)生錯(cuò)誤:遠(yuǎn)程資源清單文件下載失敗")  --資源服務(wù)器沒有打開,
            self:downloadManifestError()
        end,
 
        [eventCodeList.ERROR_PARSE_MANIFEST] = function ()
             print("發(fā)生錯(cuò)誤:資源清單文件解析失敗")
        end,
 
        [eventCodeList.NEW_VERSION_FOUND] = function ()
            print("發(fā)現(xiàn)找到新版本")
        end,
 
        [eventCodeList.ALREADY_UP_TO_DATE] = function ()
            print("已經(jīng)更新到服務(wù)器最新版本")            
            self:updateFinished()
        end,
 
        [eventCodeList.UPDATE_PROGRESSION]= function ()
            print("更新過程的進(jìn)度事件")
            self.progress:setPercentage(event:getPercentByFile())
        end,
 
        [eventCodeList.ASSET_UPDATED] = function ()
            print("單個(gè)資源被更新事件")
        end,
 
        [eventCodeList.ERROR_UPDATING] = function ()
            print("發(fā)生錯(cuò)誤:更新過程中遇到錯(cuò)誤")
        end,
 
        [eventCodeList.UPDATE_FINISHED] = function ()
            print("更新成功事件")
            self:updateFinished()
        end,
 
        [eventCodeList.UPDATE_FAILED] = function ()
            print("更新失敗事件")
        end,
 
        [eventCodeList.ERROR_DECOMPRESS] = function ()
            print("解壓縮失敗")
        end
    }
    local eventCode = event:getEventCode()    
    if eventCodeHand[eventCode] ~= nil then
        eventCodeHand[eventCode]()
    end  
end
 
function AssetsManager:updateFinished()
    self:setVisible(false)
    self.listener:setEnabled(false)
end
 
function AssetsManager:downloadManifestError()
    self:setVisible(false)
    self.listener:setEnabled(false)
end
 
return AssetsManager
 
 
--endregion

Android apk 安裝后在手機(jī)中還是以apk存在,apk 不可寫入和刪除,所以熱更新下載的最新資源都存在緩存中,并添加緩存目錄為最高優(yōu)先級(jí)搜索目錄,加載資源時(shí)從最高優(yōu)先級(jí)目錄中加載從而起到替換更新的作用。

cocos2dx中有一個(gè)熱更新類AssetsManagerEx,用這個(gè)類實(shí)現(xiàn)熱更功能時(shí)需要有兩個(gè)文件,project.manifest以及version.manifest。這里主要是project.manifest文件

Cocos自身也封裝了熱更新的模塊AssetsManager、AssetsManagerEx。

AssetsManager采用的是升級(jí)包的管理方式,首先進(jìn)行版本號(hào)對(duì)比,然后根據(jù)URL獲取對(duì)應(yīng)的升級(jí)包,解壓升級(jí)包,設(shè)置資源加載路徑,通過加載writepath目錄下最新文件的方式來實(shí)現(xiàn)更新。問題是當(dāng)涉及跳版本更新,或只有一個(gè)文件被改動(dòng)時(shí),用戶就要下載前面全部的升級(jí)內(nèi)容,升級(jí)包會(huì)越來越大。

AssetsManagerEx是AssetsManager的加強(qiáng)版,不同的是不再使用升級(jí)包的方式,而是采用單個(gè)文件拉取的方式。首先獲取本地更新配置,之后與服務(wù)器的更新配置比對(duì),得出差異文件,之后單個(gè)拉取差異文件。當(dāng)本地版本大于服務(wù)器版本時(shí),會(huì)清理掉本地更新緩存。AssetsManagerEx也有尚未解決的問題,例如多個(gè)更新序列無法并行,只能順序啟動(dòng)。另外版本后期隨著項(xiàng)目龐大配置文件幾乎包含了所有的文件信息,對(duì)比文件時(shí)間的耗時(shí)會(huì)越來越長。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容