基於Quick-cocos2d-x的資源更新方案

昨天寫了一篇關於更新方案的理論 遊戲開發:經過路徑搜索優先級來進行補丁升級(從端游到手遊) 今天繼續細化一下
因爲新項目採用的是Quick-cocos2d-x,那我就直接給出我基於Quick-cocos2d-x-master( > 2.2.3 rc) 的更新方案吧

此更新方案要解決如下幾個問題
1、資源、代碼在線更新
2、framework、update模塊自更新
3、玩家安裝新版本後,減小沒必要要的更新
4、更新中斷的處理
5、Quick-cocos2d-x中,趕上的問題

作到上面幾點後,我以爲整個更新方案應該沒有大問題了。
在說更新流程前,我先說說安裝包的內容
首先,咱們這裏會涉及一個大版本號,大版本號的意思,就是C++部分的版本號,若是有變更,這個版本號纔會動。 以提示用戶去APPSTORE下載新的版本
其他的版本號,只是一個顯示版本號,能夠根據遊戲內容來區分。

每個安裝包自己,包含了全部遊戲資源。即一個新版本發佈後,玩家是不須要更新的。點開即玩。
安裝包內部帶了一個文件列表,內容以下




local flist = {
  core = 1
  version = "1.0.1"
  update_md5 = "xxxxxx"
  framework_md5 = "xxxxxx"
  files = {
       {path="ui/shop/shop_close_btn.png",md5="xxxxxx",size="30"},
       {path="ui/army/army_tip.png",md5="xxxxxx",size="20"}
  }      
}
return flist



這是一個LUA文件,之因此使用LUA文件,是爲了在LUA中使用dofile方便讀取。而files裏面,列出了全部包內的文件。 core就是咱們剛剛提到的大版本號。
上面的 path,是相對於res的路徑,且帶完整目錄和文件後綴

資源服務器上也有一份一樣的資源列表。

服務器和安裝包中的結構以下
res/flist 資源列表
res/update.bin 這個是update模塊本身的打包
res/framework.bin這個是quick-framework的打包
res/game.bin這個是遊戲邏輯的打包
res/.....  其它遊戲資源
更新流程大體以下
一、從服務器取得版本列表(flist)
二、檢查update的md5值,看是否有更新,若是有更新,則下載update.bin,從新載入,並退到main(退出以前,注意清除對某些的引用),再次從新進入
三、檢查framework的md5值,看是否有更新,若是有更新,則下載framework.bin,並提示用戶從新啓動
四、讀取本地安裝目錄的版本列表文件(flist)
五、比對服務器版本列表和本地安裝目錄列表中的大版本號,若是大版本號不同,則提示用戶去APPSTORE上下載。
六、讀取upd目錄的版本列表文件(flist),若是flist文件不存在,或者flist中存放的core與安裝目錄列表中的不一致(表示用戶安裝了新版本),則清除整個upd目錄,並將本地安裝目錄的flist內容,寫入upd目錄

七、對比服務器列表與本地列表中的version(由上一步能夠獲得),若是version相同,則認爲數據是不須要更新的
八、若是version不一樣,則與服務器的flist對行md5差別對比,獲得須要更新的文件
九、遍歷須要更新的文件列表,若upd存在,則效驗其MD5值,若是MD5值與服務器的相同,則從待更新列表中移除(這一步,是爲了應對上一次更新過程當中,玩家中途退出的狀況)
十、逐個更新文件,每一個文件更新完畢後,再次效驗其MD5碼,若是MD5碼效驗失敗,則從新下載此文件
十一、待全部文件更新完畢,重寫upd文件中的flist
十二、進入遊戲

資源的下載是OK了,咱們如何來作更新呢,如何可以使程序加載到正確的資源。爲了正確更新資源,咱們能夠經過路徑搜索來實現

--add update path
CCFileUtils:sharedFileUtils():addSearchPath(device.writablePath.."upd/")
--add res path for install
CCFileUtils:sharedFileUtils():addSearchPath("res/")

假設device.writablePath的目錄是 /data/data/com.ooxx.game1/  那,第一個目錄就是/data/data/com.ooxx.game1/upd/ 第二個目錄是 res/
在ANDROID和IOS上,若是它檢查是以 / 開頭的,則認爲是絕對路徑,直接與文件名合併,生成對應的完整路徑
若是不是以 / 開頭的,那在IOS上的工做原理和WINDOWS一個樣,在ANDROID上,他會先檢查是否以assets開頭,若是不是,則強加上 "assets/" 並去APK裏面搜索
總之,上面的兩個路徑,是在任何地方都適合的。

以上就是我綜合了陽光七月,yezehui200,GcvqrNq等人的更新方案而得出的本身的更新流程,上面的流程,幾乎解決了本文開頭就提出的問題。
下面我來講說我在實現這一方案中,趕上的問題
第一次趕上的問題,是執行安裝目錄中的flist. 由於dofile會認一個絕對路徑,我在WINDOWS上是很OK的,可是在ANDROID上死活都不行,即便我硬編碼 dofile("assets/res/flist") 其緣由是由於,在ANDROID上,讀取資源是從APK壓縮包中讀取的。
後來我只有經過一個比較矬的方法來優美地解決 就是使用CCFileUtils:sharedFileUtils:getFileData(「res/flist」) ,將獲得的數據寫入存儲卡上,再dofile
因爲upd目錄下是沒有res文件夾的,所以,咱們能夠保證,這貨取得的確定是安裝包下的文件路徑。 這樣就解決了dofile在ANDROID上的問題。
而要想取安裝上下的資源,就只能像這樣 dofile(device.writablePath.."upd/flist")
總之,在添加了多路徑搜索後,對路徑的使用就要格外當心。

第二個問題,就是我在測試crypto.md5file的時候,發現,在ANDROID上,若是咱們要取一個APK中的文件時,是會失敗的,緣由就是在C++實現裏,它使用了fopen來打開文件,這在ANDROID上是作不到讀取APK中文件的。 好在這個需求不須要了。

第三個問題,因爲沒有使用AssetManager,所以,目前尚未實現單個文件的進度條,到時候可能會參考一下AssetManager的實現 

第四個問題,目錄建立問題, 好比 ui/shop/ 咱們直接使用 lfs.mkdir(device.writablePath.."upd/ui/shop/) 是不會成功的,須要一級級向下建立,目前沒有找到一次性搞定的方案,說不定使用os.execute的mkdir帶參數,能夠搞定 

最後我說說我測試資源服務器的方法。
測試資源服務器最簡單的方法,就是網上下載一個nginx,解壓,解壓後,找到html目錄,把資源扔進去,點nginx.exe啓動,瀏覽器輸入127.0.0.1,你會發現welcome nginx 輸入127.0.0.1/1.png (假設你html目錄下有這個圖片),你會在瀏覽器裏看到圖片。
剩下的,就愛怎麼整怎麼整了。

若是是手機測試,最好是把內網的防火牆關了,不然鏈接不上。


代碼尚未在IOS上測試過,以及update和framework的測試尚未OK,等一切都OK後。定會奉上源碼html


又是12點半了,對於一個程序員來講,這是一個黃金時間,精力旺盛,我想,是最適合整理和分享一些思路的時候了。
自從上次寫了 基於Quick-cocos2d-x的資源更新方案
一直以爲有些什麼地方不對。思考了許久以後,發現對於framework和update自身模塊的更新,是有必定的問題的。
對於update自身模塊的更新來講,檢查是否有更新,如有更新,則使用require "main"從新來過。 是很是合理。
而對於framework的更新,就不是那麼容易了。 前一篇文章中,我提到若檢查到framework有更新,則提示用戶重啓客戶端,看似很不經意的操做,卻給了用戶思考要不要再次打開此遊戲的機會。
對於這種思考機會,咱們是不能給用戶的。 所以,無縫更新纔是硬道理。

強勢插入--------目前我使用的是 quick-cocos2d-x 2.2.3rc 版本-----------------

在思索良久後,我發現對於update模塊的實現,是徹底能夠脫離quick框架的。update模塊所須要的scene,writablePath,md5file等功能,都不是quick提供的,僅僅是quick提供了一層封裝。所以,咱們可使用純cocos2d-lua的API來實現update模塊。這樣一來,framework.init就能夠不在update模塊裏調用,那當咱們在update中更新了framework模塊時,咱們徹底不須要擔憂require重入問題。 由於在真正進入遊戲前,咱們的framework中的任何模塊,都尚未被require過。
因爲天天晚上只有1多小時的編程時間,因此,這事兒我又花了好幾天。其間也趕上了很多問題。
我先來講說新版的更新流程
在前一文章的基礎上,咱們修改前三個步驟
一、從服務器取得update模塊,與本地進行比較
二、檢查update的md5值,看是否有更新,若是有更新,則下載update.bin,從新載入,並退到main(退出以前,注意清除對某些的引用),再次從新進入
三、若是本地update的值與服務器相同,表示update模塊沒有更新,則去下載文件列表 flist
.....後面的步驟不變

這樣修改完畢後,咱們就能夠徹底實現update模塊的自更新,以及framework的自更新了。 整個過程不須要從新啓動。
下面來講說這幾天趕上的問題
一、在ANDROID上讀取APK內文件的MD5值
因爲CCCrypto::md5file是用fopen讀取文件內容,再交給MD5計算器得出MD5值。 因此,在APK裏是不能夠的,後來,我把fopen改爲了CCFileUtils::getFileData獲得解決
二、flist是一個LUA文件,若是處於APK中,則沒法使用dofile,若是是從網上下載,也沒法使用dofile
爲了保持兼容,在讀取APK和執行網上下載來的flist時,我使用了loadstring(str)() 這樣就解決了全部問題
三、遞歸建立文件和清空文件夾
一開始使用了os.execute來執行mkdir,rm等命令,但一但執行命令時控制檯有輸出,那整個LUA的LOG就亂了。 後來改成了lfs手工建立和遍歷刪除來解決。工做良好
四、compile_scripts.bat打包
我須要將update打一個包,main.lua不打包,其他的app和config.lua打一個包
因爲compile_script.bat是按文件夾進行模塊剔除的,因此,我只好調整如下結構
scripts
   +--launcher_src
       +--launcher.lua
   app
   +---MyApp
   +---config.lua
   +---scenes
 --main.lua
能夠看出,我將更新模塊單獨放入了一個文件夾,將config.lua移到了app下面。 差點忘了說,個人update模塊,也是沒有依賴config.lua的。
這樣,我就能夠打出app和luancher兩個包了,陽光七月的打包後綴是*.bin,我用了png,總之,這個後綴是可有可無的,打包我寫了兩個腳本
build_app.bat
內容:%QUICK_COCOS2DX_ROOT%/bin/compile_scripts.bat -i scripts/app_src -p app -o res/app.png
build_lchr.bat
內容:%QUICK_COCOS2DX_ROOT%/bin/compile_scripts.bat -i scripts/lchr_src -p lchr -o res/lchr.png

這樣當咱們想要運行程序的時候,只須要打包一下就能夠了。

而考慮到不少時候,咱們改了代碼就想直接按F5,不想去打包……。 這裏有兩個方案。
一是處理一下PLAYER的F5事件,自動執行上面兩個腳本。
二是處理一下main.lua,使它支持打包和非打包模式,目前,我是經過修改main.lua來作的

LOAD_FROM_BIN = true
APP_NAME = "dota"
APP_PACKAGE_ROOT = ""

if LOAD_FROM_BIN then
    APP_PACKAGE_ROOT = "app"
    CCLuaLoadChunksFromZIP("res/lchr.png")
    package.loaded["lchr.launcher"] = nil
    require("lchr.launcher")
else
    APP_PACKAGE_ROOT = "app_src"
    package.loaded["lchr_src.launcher"] = nil
    require("lchr_src.launcher")
end

寫到這裏,已經不知道本身還要說些什麼了。雖然目前功能不夠完善,但已經能夠發碼了。但願可以給你們一個參考。
nginx

目前的版本存在如下問題
一、沒有在update模塊加載以前(main.lua中)添加搜索路徑,致使update模塊老是加載到安裝路徑下的

二、main.lua默認認爲不更新的,可是目前代碼過多,很難保證穩定性。所以,下一個版本,會將main.lua中的代碼移到luncher裏。

main.lua僅保持
local filemgr = CCFileUtils:sharedFileUtiles()
local writablePath = CCFileUtils:sharedFileUtiles():getWritablePath()
filemgr:addSearchPath(writablePath.."upd/")
filemgr:addSearchPath("res/")
CCLoadChunksFromZIP("lchr.png")
require "lchr.launcher"




這個代碼,幾乎不會由於需求有任何改動。
但這樣只能在打包模式下執行更新模塊。
不過,更新模塊作好後,通常狀況下是不須要去改動的, 所以,launcher內部,依然能夠選擇是採用ZIP方式來加載遊戲代碼,仍是直接運行遊戲代碼。
PS:我也想過,將main.lua進行打包,這樣main.lua也能夠進行代碼更新了。 可是,上面的代碼中,若是說非要有什麼須要動的話,那可能就是路徑了。
就算把main.lua打包,在C++代碼裏進行加載,路徑也是須要的,所以,若是路徑改變了,C++代碼也會變。 這是一個無解的問題,勢必致使從新發布安裝包。
所以,上面的實現和將main.lua打包爲ZIP放入C++代碼里加載是等同的,不須要再糾結。


若是以爲UPDATE模塊負責的東西過多,那能夠將目前的launcher拆分。
咱們的launcher能夠拆分爲loader和update兩個包,loader負責是否採用ZIP加載,以及是否須要進行update檢查等工做。 但總的來講,也都差強人意,想要進行更新的模塊,就須要打包。 所以,不如將那些代碼打入到一個包。
這樣更新過程當中能夠減小一個步驟,也減小了錯誤機會
三、關於framework
羣裏有人問道:framework總共才100KB,爲毛在更新的時候要區分
在先前七月的文章中,區分framework的緣由,是update模塊依賴了framework,所以,當update檢測到framework須要更新時,要消除require過的模塊,從新再require一次。 由於lua中的require是一個MAP(固然,在LUA中是一個table,不要摳字眼),若是require發現一個東西已經require了,那他是不會再次require的。 這是一個策略上的優化,可讓你在代碼裏放心使用require,而沒必要擔憂別人已經require了。 同時,這在代碼更新中,帶來的就是一個問題,他會致使你新的代碼沒法執行。
我在思索好久後,決定從新消除update模塊對framework的引用,所以,我使用了cocos2d-lua原生的API來實現了個人update,這樣,framework的代碼在個人更新方案中,就是一個普通的代碼,不須要作任何的特殊處理。


四、在個人方案中,使用了 CCLoadChunksFromZIP來加載個人 res/app.png,這也是一個問題,首先, res是隻有安裝目錄纔有的路徑。 對於已經更新的模塊,應該是 writablePath.."upd/app.png"纔對。 這是一個BUG。 其次,在這以前,我應該LOAD一次framework纔對。
但若是我像下面這樣作
CCLoadChunksFromZIP("framework")
CCLoadChunksFromZIP("app.png")


可能有人會問,若是某一天,我想把framework拆開,或者想把app拆分爲不一樣的模塊怎麼辦?
哦哈哈,這就是典型的想多了。
其實,咱們在這裏,CCLoadChunksFromZIP("app.png")便可。
而後,在MyApp.lua裏面(這是app的入口) 使用CCLoadChunksFromZIP加載全部你想要的模塊。
MyApp.lua的開頭大概會像這樣


在頭上強加這段
CCLoadChunksFromZIP("framwork")
CCLoadChunksFromZIP("module1")
CCLoadChunksFromZIP("module2")
CCLoadChunksFromZIP("module3")
CCLoadChunksFromZIP("module4")
CCLoadChunksFromZIP("module5")


--這是原始文件的開始
require("app.config")
require("framework.init")


--這裏能夠初始化一些全局的模塊,好比上面的全部模塊都是「單件類「,那咱們能夠在這裏所有require一次。


五、目前使用的是直線流程,並無採用狀態機制來作模塊更新,這種方式下,徹底不具有容錯能力
六、沒有實現先前提到的存儲空間不足的提示問題。


這原本應該是寫在本系列文章的第三篇中的,但我怕想法有漏洞,先寫在這裏,讓你們拍磚
程序員

相關文章
相關標籤/搜索