(熱更新相關)CocoaChina 2013春季開發者大會:《大掌門》歐陽劉彬--基於Cocos2d-x引擎開發經驗分享

《大掌門》歐陽劉彬分享的內容同樣是與Cocos2D-X和跨平臺開發有關,在演講中他詳細分享了爲什麼會選擇Lua。


歐陽劉彬:首先感謝CocoaChina的邀請,跟大家分享一下我們《大掌門》在遊戲開發過程當中使用Cocos2D所開發的一些經驗。剛纔凌聰講的內容 感覺已經是一個比較完整的、系統的東西了,我們本身在剛開始做的時候,我覺得他們那邊應該是有一個比較強大開發團隊在下面做了一些支撐的事情。我們做的一 些事情,其實大部分他剛纔已經提到了,我們做的可能不像他們那麼系統,但是也有一些東西是跟他很像的一個過程,我就我們這邊的一些經驗來跟大家再做一些分享。

我們爲什麼選Cocos2D-X,其實最大的原因就是跨平臺。當時爲什麼選擇Lua呢?大概我們從去年春天的時候開始做這個事情,我去年春天也來了 CocoaChina的一個會,當時我們選一個腳本,覺得比較合適的可能還是用Lua,那時候已經出來了一段時間了。它的一些特點其實剛纔也已經提到過 了,我們覺得它,支持一些動態更新,它的容錯性比別的好,我們找一個靠譜的C++程序員說實話還是比較難的,我們在開發速度上會比其他的語言要快一些。


我們的一些基本情況,其實我們用Cocos2D是比較老的一個版本了,我們大概在去年3、4月份就開始做,基於上面做了一些改進,後面因爲在開發的過程當 中,我們覺得一個底層的框架已經選好了之後就儘量不要去動它,因爲這個說實話是一個傷筋動骨的事情,但是我們會繼續用Cocos2D的框架,我們會選擇 2.0的版本,老的那一套東西一直在上面做。我們做的一些事情沒有那麼系統,但是也做了一些。首先我們在上面做了我們的一些事件管理,做了一些底層的UI 控件,做了我們的網絡通信模塊和加解密的模塊。我們的一個特點就是說,我們所有的遊戲邏輯都是用Lua去實現的,我們在Lua裏面也去實現了一些MVC的 框架等等,但是對於一些性能要求比較高的東西,比如說加解密這種東西可能還會在C上實現,在Lua裏面去用新的接口。


首先我要分享的就是我們版本更新的機制,其實剛纔凌聰也提到了,我們爲什麼要用Lua,就說它能夠走遊戲內更新的方式更新版本,比如說我們拿App Store來舉例,你去提交一個版本,我們以往的經驗,首次提交的話可能需要一個月的時間,後續的更新審覈可能5個工作日,也就一週過去了。這個提交從審 核裏面如果一個月過了,當然大家相當高興了。中間的過程當中其實經常會遇到一些非技術的原因把這個打回來,比如說你的截圖,或者是一些信息寫的不對,中間 來回打會可能也會耽誤時間,這個審覈週期太長了,我們覺得是一件讓人不太好接受的事情,尤其是一些非技術原因。

 

再有一個問題就是,像我們的遊戲版本,我們遊戲的安裝包現在說實話,從最開始的50兆到現在變成100兆已經比較大了。假設我們去實現的話,如果我發現遊戲裏面一個比較重要的Bug,我去修復這個 Bug,提交之後玩家再去更新,按照現有的機制,玩家就是下一個完整的安裝包下來。國內其實像凌聰剛剛提到了,在Android市場有一定的新的機制,但 是我瞭解在Android的4.0上面才支持這種方式,在比較低的版本上還是不支持增量的更新。

 

在騰訊這邊確實是做得比較好,它的QQ遊戲大廳是支持增量 的更新的。我剛纔提到的,就是說更新一個版本,我們加一個什麼玩法有一個全新的包,這種帶來的問題就是每次更新都要下一個完整的包,用戶的體驗是非常差 的,對於用戶整個留存都會有影響。我們的解決方案就是,所有的遊戲邏輯都去用Lua實現,我們實現一套遊戲內更新的機制,每次遊戲升級的時候,基本上只要 更新自己的Lua文件就行了,這樣就可以及時、隨時的更新,我們不需要等任何人的審覈。還有就是快速,我們走增量更新的方式,只需要去下載我們改動過的一 些資源和代碼。在這裏也可以插一句,剛纔也人問到蘋果基於這個審覈的條款,在我的理解,實際上蘋果只是給自己設一個底線。

 

如果遊戲只是對於一些正常的功能 邏輯的更新,走腳本的方式的話應該還是可以的,只不過你如果把遊戲整個更新一遍,變成一個完全不同的遊戲,那肯定是不行的,我認爲蘋果這種條款,可能是作 爲它自己的一個底線放在那兒。


我們整個遊戲更新的過程,其實也是類似的,我們的後臺服務器分爲了版本管理的服務器、資源服務器、遊戲邏輯服務器。首先我們的遊戲啓動之後,第一次會帶着 自己的版本向我們的服務器版本去問,就是我現在的版本號是一,最新的版本號是多少。如果沒有更新的話,後續它直接往遊戲邏輯服務器發起後續的通信需求,但 是如果有更新的話,我們的版本服務器會告訴他最新的版本已經到了2.0了,會返回一些需要更新的文件列表回來,然後我們手機端就會從資源服務器把這些所有 需要的文件全部下載下來,然後在自己這裏實現一些資源的重載,整個腳本的一些Reload,接下來再往我們的服務器做通信的時候,就已經變成一個最新的版 本了。


我們如果實現了這種遊戲自己更新的機制的話,我們就要考慮到一個問題,就是我們客戶端上面的那些代碼文件和一些資源文件怎麼去放。我們一個簡單的想法就是 說,我們的遊戲邏輯都是拿Lua實現的,我們的代碼可能就是直接放一個Lua文件在手機上面。它從V1更新到V2的話,我們就從資源服務器直接下載一份最 新的Lua文件把它覆蓋掉。但是這樣有一個問題,我這個PPT裏面其實有一些忽略,在我們的後臺有一些版本的管理系統,我們對於每一個版本都有資源、後臺 代碼和數值的配置都是有保存的,我們隨時可以從後臺界面上去做切換,把我們的遊戲升級到某一個版本,我們也支持遊戲多版本的並存。比如說我們開發人員在測 一個版本,但是玩家玩的是另外一個版本,我們自己測試好之後再切換上線。所以我們一套東西之後,你就會考慮到一個問題,就是說假設我們測試不夠充分,一個 版本放出去之後,你可能突然發現一個重大的Bug,你可能要麼給它進行修復,如果很快的找不到解決辦法,你只能馬上把版本回復回去。第一種方式就是說,如 果客戶端只放一個版本的話,我的回滾希望更快,就是說代碼在之前,唯一的代碼在你手上實際上是有的,希望服務器返回某一個狀態,你立刻自己就能夠切回到 V1的版本上去,而不需要重新把那些代碼再重新Download一遍。


還有一個辦法就是每個版本都通過一個版本號,比如說這是我V1的版本,我增量後續更新的時候,可能會把V1先拷貝成V2,然後再去做增量更新。比如說V1 到V2有哪些文件變了,比如有兩個文件,A.Lua或者是B.Lua,我可能會實現增量的更新,但是我的V1的B點Lua和V2的B點Lua實際上是沒有 變的,它們會佔用額外的存儲空間,會有浪費。所以我們最後的方案,也結合我們後臺的那套版本管理的系統,就是實現的方案是把我們所有的資源和我們的腳本, 包括我們的數值配置,我們的腳本都算是一種資源,我們做md5重命名,通過在我們的手機端、遊戲後臺、資源管理服務器都會有一個數據庫去管理這種每一個版 本下面所有的資源的映射關係,就是原始的文件名和最後打包之後會存下來,每一個版本都會存下來。接下來真正遊戲邏輯運行起來之後,需要通過一個自定義的 Lua Loader去查找這些文件。


這是我們打包的流程,首先會打包的時候把這些文件算一個碼然後進行重命名,同時會生成一個資源的數據庫,接下來把這些資源的數據庫和腳本文件放到這個包裏 面去。然後遊戲邏輯裏面,比如說我們的Lua代碼去裝載一個文件的話,語法就是Require一個A,就會去查找A.Lua這個文件。我們現在面臨的問題 就是,這個A.Lua實際上已經不存在了,我們已經重命名了,我們需要有一個機制,讓Lua再去Require這個A的時候要找到一個文件,所以我們需要 自己實現一個Lua  Loader,實際上Lua那個引擎裏面是有相應的方法的,我們可以在那個引擎裏面註冊一個自己的Loader,在它被觸發之後,首先根據我們要裝載的文 件,比如說A,在我們的List裏面去查這個文件真實的文件名是什麼。比如說我們的客戶端現在是V1的版本,它去V1的裏面去查到的文件是這個,那麼他可 能就會最終度曲的文件就是這個文件,再最終通過Lua的Loadbuffer這個函數轉進來,再走後續的流程。如果是V2的版本,就找到另外一個文件,最 終回進入到這個Loadbuffer當中來。這樣的話,我們在整個遊戲邏輯裏面有一個變量去標識當前客戶端的版本是1還是2,但是實際上他們版本1和版本 2的代碼可能都會在我們的手機上有,可以很快速的去切換。像我們一個沒有變化的B點Lua,就以我們剛纔提到的這種方式去實現的話,他們查的話是不同的, 但是找到的文件是同一個。


接下來大家就會想到,遊戲邏輯如果去用腳本實現的話,那你的代碼怎麼加密?像JS一般有混淆器。但是這種方式總之還是能有人去反編譯的,如果我們有一些比 較核心的業務邏輯放在前端的代碼裏面,我們不希望被別人很輕易的看到。其實我們如果在打包流程當中已經做了這些事情,我們已經有了自己自定義的Lua Loader的話,加解密這個事情就很容易做了,我們在打包的過程當中,在重命名之前做一次加密的操作,然後在我們的遊戲邏輯運行起來之後,我們再把一個 文件的內容讀出來之後,再去調用Loadbuffer再去做加密就OK了。


我們用這種方式去管理腳本和資源之後,這個文件就會有一個問題,就是說我們整個代碼可能有70多個Lua文件,有200多個配置文件,這些文件如果以文本 的方式去加密之後,APK的安裝包和IPK的安裝包都是最基礎的安裝包,這些文件在加密之後你再去壓縮沒有壓抑的,因爲加密之後二進制流是沒有規則的,你 用Zip去壓是是沒有任何意義的,帶來的問題就是安裝包體積會變大。還有一個需要考慮的就是說,我們這種增加一個加解密的環節,對於我們遊戲運行起來的時 候會有什麼性能的影響?所以我們做了一些改進。就是說其實這個想法也挺簡單的,就是說你加密之前先把文件壓縮了再加密,這個事情直接就讓這個加密對於壓縮 這個影響已經完全沒有了,你先壓縮再加密的話,本身這個文件就已經比較小了。在我們遊戲運行的時候,把它先在我們的Lua Loader裏面先解密,再解壓,然後再調用Loadbuffer這個函數。


我們做一些對比,剛纔提到了我們的安裝包如果直接使用的話,這個圖(PPT)有三列,第一列是我們的原始文件,我們的文件是一個Zip格式,在安裝包裏面 的大小可能就是1.5M,如果我們把配置文件和腳本去做加密處理的話,然後再放到我們的安裝包裏面去,它就變大到8M,就是整個安裝包就大了不少。但是如 果我們後面再通過第三種方式,就是先壓縮再加密這種方式的話,安裝包的體積可能比以前的原始文件直接放進去稍微大一點點,基本上大不了多少。所以這樣的 話,壓縮、加密這種對於遊戲安裝包的影響就已經可以完全沒有了。


我們對比了一下這些腳本通過處理之後,它在遊戲運行時對於性能的一些影響。因爲我們的遊戲邏輯運行起來的時候最開始的啓動階段可能是Require一些少 量的腳本,真正進入遊戲的時候,因爲我們Lua內存一些框架的實現原因,我們是需要幾乎把所有的Lua代碼都要裝載進來,所以你一次性的去裝載700多個 Lua文件的話,實際上是有一些性能的問題,實際上會使你的裝載時間變長,就是在這種加密的方式下。但是對於我這一列後面列的第三種方式,就是說壓縮和加 密的方式下,實際上加載的時間又會降下來。所以可能大家就會困惑,爲什麼我只加密裝載時間變長了,但是我加了一個壓縮再加密,我運行的時候先解密再解壓, 裝載時間反而會降下來了呢?這張圖就是說明了這個原因,大家可以對比一下。我的Lua文件裝載實際上是分了好幾個階段,可能有先讀取文件的內容,然後有解 密,有解壓縮的過程,就是三個顏色代表了。對於原始文件,如果我們直接放到安裝包的話,直接讀原始文件就可以了,只要是能讀出來,不需要解密和解壓縮,總 共1千個文件,我們腳本和配置文件夾都放進來的話。如果只加密的話,讀文件裝載時間可能少一點點,這個可能是測試的一些偏差了,正常來說應該是差不多的, 可能還需要另外一半的時間去做解密的操作。但是如果文件壓縮之後再加密的話,在我們運行的時候先把它讀出來,讀出來的時間因爲本身壓縮之後那個文件會很 小,就是它讀出來之後,第一次裝載內存的Buffer是比較小的,裝載的時間其實比原始的文件還要小一點,接下來解密時間也會變得小很多,因爲文件的大小 也小了很多,所以解密耗的時間也會比較少。最終解壓縮的時間,我們測試的時間上是很少的。


前面提到的都是我們在iOS上做的一些性能的對比。到Android上我們就發現了另外一個問題,這個圖裏面藍色的線就是iOS上所有的腳本和配置文件的 加載時間,基本上就是都很小,慢的也就是2秒,快的可能也就是不到1秒的時間。跑到Android上就變得很誇張,遊戲可能進去之後開始有一點轉圈,然後 過了快10秒鐘纔會把一些界面裝載進來。不管是放原始文件還是放加密的文件還是壓縮加密的這種文件,我們放到APP裏面之後再去直接裝載的話就發現性能有 很大的問題。我們看了一下爲什麼會有這種問題?我們就發現在Android上面你直接從SD卡讀一個文件和直接從APK裏面讀一個文件實際上是有相當大的 差異的。其實對於單個文件來說,如果運行時只是裝載一個文件,從SD卡上讀耗的時間可能是一毫秒,從APK裏面去讀耗的時間是十毫秒。對於一個文件來說, 其實這個時間幾乎沒有什麼關係。但是如果你一次裝載1千個文件出來的話,這個10倍的差距就會放大到比較明顯的一個程度。所以針對這種情況,我們在 Android上面就增加了一個環節,就是我們遊戲安裝完第一次啓動的時候,需要有一個解開我們資源和配置文件的過程。像iOS上就不需要這樣的過程,因 爲iOS上本身安裝包裝進去之後就是一個展開的狀態,不像APK,安裝完了之後就是放在手機的RAM裏面。


我們對於這種展開之後,你再去放到SD卡上後面去運行我們遊戲的話可以看到,一個淺藍色的線就是從Android上,如果從SD卡去加載我們的配置文件和 腳本的話,時間就跟iOS上的加載時間差不多了,比直接從APP裏面加載要好很多,剛纔提到的是我們的版本管理的一些經驗。接下來就是我們用Lua在開發 商利用它的一些靈活性,利用一些豐富的接口,我們去做一些輔助我們定位問題和幫助我們加快開發速度的一些方法。在調用我們Lua的時候,在Lua引擎的文 件裏面,會調一個Lua的函數,或者是執行一個Lua的文件,最終的調用方式都是通過Lua Pcall這個方法,它的文檔裏面其實第四個字段是一個Message Handle,這個Lua的Pcall接下來如果在執行Lua代碼的時候如果出錯了,如果我有設置這個Messager handler的話,我會通過這個去處置我的異常。我不知道現在引擎有沒有這個問題,實際上以前在Pcall的時候是沒有做這種錯誤處理的,第四個參數都 是直接填的,如果我們自己定位一個Message Handle,我們可以運行一個詳細的錯誤的堆棧,我們開發的時候,可以選擇把這個堆棧彈放出來,開發人員可能在模擬器上調試的時候,如果出現什麼錯誤就 可以直接把這個錯誤彈出來了。在我們發佈之後,我們遊戲畢竟不能保證百分之百沒有Bug,如果運行的時候出現一些錯誤,我們能夠把這些邏輯信息捕獲,上傳 到我們的日誌服務器裏面去。Lua還有一個好處,一般來說一個掛掉了之後,可能只是影響當時的那一次調用,一般不會讓你的遊戲直接崩潰掉。


比如說我們實現了一個自己的錯誤的Handle,這個例子就是我們去改造CCLua文件裏面一個執行Global的一個例子,紅色的三項是我們自己加上去 的,如果你有1G的Handle,你首先要Psuh到State上去,在最終調用的時候,-1就是你馬上要執行的那個Global的指針,-2就是我們自 己的Message Handle。在這個Message Handle裏面的實現,實際上主要是用到了Lua的一個方法,他們可以取得運行時的信息,就是堆棧和一些調用的腳本名稱和行號什麼的。這是我們通過自己 定義的Hanlle可以捕獲的一些錯誤異常,第一行是我們Lua錯誤的Message,下面是Lua錯誤的堆棧信息。我們通過這些東西很容易看到究竟是哪 個模塊,哪個文件的哪一行出了什麼問題,有堆棧的話也很容易看出,爲什麼它會在這些行出現這樣的錯誤。


剛纔凌聰也提到了Lua跟Java跟iOS去做交互的時候有一些比較噁心的東西,他們有的通過自己的方式實現,我們也有自己的一些小的技巧。一些常用的方 式,如果我們有一些Java和Object的接口的話,我們會通過C++導出給Lua用。這樣的問題,尤其是一些像我們的iOS和Java需要有一次 C++的封裝其實就有開發量。再有就是這種接口比較固定的還好,比如說加解密這種可能實現一次就OK了。後來你再去做SDK介入的時候你會發現每一個渠道 都不太一樣,你很難做出一個比較通用的接口暴露給Lua,如果按照之前的這種Tolua++的方式去做的話就很麻煩。比如說我在Java這一層實現打開一 個網址的功能,可能先在Java裏面要實現一個OPenUrl的函數,在C++這邊也要實現一個OperUrl的函數,去調用Java的代碼,再用 Tolua++這個導出,再去調用這個函數。JNI調用的代碼說實話,我的感覺很噁心,很容易寫出Bug,如果這個字段改成份了,需要修改的參數就很大, 需要改很多地方。Java這一層要改,C++和Lua都要改。我們的技巧就是對於一些性能要求不高的封口上,我們就安裝一個doAction這個函數,在 Lua層,就去調用不管是C還是Object C還是Java的接口的時候,可能就是調這個接口。我們在C裏面去實現一個調度,就是在C裏面,我們這個Tring裏面一般會有一個Action的字段, 我們在C這裏面出現相應的處理,可能也都會在這個Tring裏面去做,在Java這邊,首先通過JNI的調用,調到一個Java的 PlatformTool裏面,我們通過這種封裝之後,在Lua這一層去大概一個網址,我可能在Lua的代碼裏面就會這麼寫,首先會把我的參數,比如說 Action是UIO,需要變成一個串,然後再傳給這個doAction的方法。這樣的好處就是說,我的交互,基本上第一次寫好這個doAction之後 就不用變了,我依然要在OC或者是Java這裏去實現我的業務代碼,不需要考慮在C這一層怎麼做中間的角度。調用方式也變得比較一致了,接口後面如果發生 一些變化的話,修改量也會比之前的方式小很多。


上面就是我們在遊戲開發過程當中對於版本更新、代碼加密、錯誤檢查和接口封裝方面的一些經驗。很高興今天能在這裏跟大家分享這些內容,歡迎大家能有一些技 術上的交流。我這邊廣告準備得不充分,我們也是最近招各種各樣的人,包括技術、策劃和美術,我們都歡迎大家來加入我們,去年這個時候我們可能就是一個很成 長中的團隊了,我們也面臨着很多的機遇和挑戰,大家有興趣的話歡迎來聯繫我。


主持人:技術分會場的議程到現在就全部結束了,感謝各位的光臨,下次大會再見!

發佈了26 篇原創文章 · 獲贊 3 · 訪問量 10萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章