1. 程式人生 > >cocos2dx lua 熱更新

cocos2dx lua 熱更新

原理:

        每次登陸游戲利用cocos的assetManager從伺服器拉去當前最新的兩個檔案。 一個是version.mainifest,一個project.mainifest. 這兩個檔案都是xml的描述檔案。一個包含了版本資訊,第二個包含了遊戲所有資源的MD5碼。首先通過version檔案對比本地的版本是否相同,如果不相同,再通過跟本地的project檔案對比MD5碼來判斷哪些檔案需要重新下載,替換資源。 

 

步驟:

1. 有一個檔案下載的熱更新伺服器,將最新專案資源(res/  src/ 目錄)放入熱更新伺服器中,新增版本資訊母檔案(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檔案: 這個檔案是一個python。目的是生成對應的version和project檔案。project檔案可以幫你給每個資源生成獨一無二的MD5碼,相當於每個資源的標記。下面是一段python檔案的程式碼。

#coding:utf-8

import os
import sys
import json
import hashlib
import subprocess
import getpass

username = getpass.getuser()
# 改變當前工作目錄
#os.chdir('/Users/' + username + '/Documents/client/MyProj/')

assetsDir = {
    #MyProj資料夾下需要進行熱跟的資料夾
    "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機)

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來從伺服器獲取檔案並且進行資源的替換(這裡所謂的替換並不是真正的替換,利用了Fileutils->searchPath() 設定資原始檔讀取的優先順序。也就是老資源和程式碼並沒有刪除,而是捨棄不用。

--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()
    --建立可寫目錄與設定搜尋路徑
    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)

    --檢查版本並升級
    self.assetsManagerEx:update()
end

function AssetsManager:handleAssetsManagerEvent(event)    
    local eventCodeList = cc.EventAssetsManagerEx.EventCode    

    local eventCodeHand = {

        [eventCodeList.ERROR_NO_LOCAL_MANIFEST] = function ()
            print("發生錯誤:本地資源清單檔案未找到")
        end,

        [eventCodeList.ERROR_DOWNLOAD_MANIFEST] = function ()
            print("發生錯誤:遠端資源清單檔案下載失敗")  --資源伺服器沒有開啟,
            self:downloadManifestError()
        end,

        [eventCodeList.ERROR_PARSE_MANIFEST] = function ()
             print("發生錯誤:資源清單檔案解析失敗")
        end,

        [eventCodeList.NEW_VERSION_FOUND] = function ()
            print("發現找到新版本")
        end,

        [eventCodeList.ALREADY_UP_TO_DATE] = function ()
            print("已經更新到伺服器最新版本")            
            self:updateFinished()
        end,

        [eventCodeList.UPDATE_PROGRESSION]= function ()
            print("更新過程的進度事件")
            self.progress:setPercentage(event:getPercentByFile())
        end,

        [eventCodeList.ASSET_UPDATED] = function ()
            print("單個資源被更新事件")
        end,

        [eventCodeList.ERROR_UPDATING] = function ()
            print("發生錯誤:更新過程中遇到錯誤")
        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 安裝後在手機中還是以apk存在,apk 不可寫入和刪除,所以熱更新下載的最新資源都存在快取中,並新增快取目錄為最高優先順序搜尋目錄,載入資源時從最高優先順序目錄中載入從而起到替換更新的作用。

 

cocos2dx中有一個熱更新類AssetsManagerEx,用這個類實現熱更功能時需要有兩個檔案,project.manifest以及version.manifest。這裡主要是project.manifest檔案

Cocos自身也封裝了熱更新的模組AssetsManagerAssetsManagerEx

AssetsManager採用的是升級包的管理方式,首先進行版本號對比,然後根據URL獲取對應的升級包,解壓升級包,設定資源載入路徑,通過載入writepath目錄下最新檔案的方式來實現更新。問題是當涉及跳版本更新,或只有一個檔案被改動時,使用者就要下載前面全部的升級內容,升級包會越來越大。

AssetsManagerExAssetsManager的加強版,不同的是不再使用升級包的方式,而是採用單個檔案拉取的方式。首先獲取本地更新配置,之後與伺服器的更新配置比對,得出差異檔案,之後單個拉取差異檔案。當本地版本大於伺服器版本時,會清理掉本地更新快取。AssetsManagerEx也有尚未解決的問題,例如多個更新序列無法並行,只能順序啟動。另外版本後期隨著專案龐大配置檔案幾乎包含了所有的檔案資訊,對比檔案時間的耗時會越來越長。

 

參考

Cocos2dx Lua 熱更新