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自身也封裝了熱更新的模組AssetsManager
、AssetsManagerEx
。
AssetsManager
採用的是升級包的管理方式,首先進行版本號對比,然後根據URL獲取對應的升級包,解壓升級包,設定資源載入路徑,通過載入writepath
目錄下最新檔案的方式來實現更新。問題是當涉及跳版本更新,或只有一個檔案被改動時,使用者就要下載前面全部的升級內容,升級包會越來越大。
AssetsManagerEx
是AssetsManager
的加強版,不同的是不再使用升級包的方式,而是採用單個檔案拉取的方式。首先獲取本地更新配置,之後與伺服器的更新配置比對,得出差異檔案,之後單個拉取差異檔案。當本地版本大於伺服器版本時,會清理掉本地更新快取。AssetsManagerEx
也有尚未解決的問題,例如多個更新序列無法並行,只能順序啟動。另外版本後期隨著專案龐大配置檔案幾乎包含了所有的檔案資訊,對比檔案時間的耗時會越來越長。
參考