腾讯开源手游热更新方案,Unity3D下的Lua编程
寫在前面
\\xLua是Unity3D下Lua編程解決方案,自2016年初推廣以來,已經(jīng)應用于十多款騰訊自研游戲,因其良好性能、易用性、擴展性而廣受好評?,F(xiàn)在,騰訊已經(jīng)將xLua開源到GitHub。
\\2016年12月末,xLua剛剛實現(xiàn)新的突破:全平臺支持用Lua修復C#代碼bug。
\\目前Unity下的Lua熱更新方案大多都是要求要熱更新的部分一開始就要用Lua語言實現(xiàn),不足之處在于:
\\xLua熱補丁技術(shù)支持在運行時把一個C#實現(xiàn)(函數(shù),操作符,屬性,事件,或者整個類)替換成Lua實現(xiàn),意味著你可以:
\\這個新特性iOS,Android,Window,Mac都測試通過了,目前在做一些易用性優(yōu)化。
\\那么,騰訊開源的xLua究竟是怎樣的技術(shù)?它是為何如此設(shè)計的?更令人關(guān)心的是,xLua的性能如何?帶著這些問題,InfoQ對其作者進行了采訪并將內(nèi)容整理成文。
\\嘉賓簡介
\\車雄生,05年畢業(yè),在華為工作了6年,跟著先后在兩游戲創(chuàng)業(yè)公司待了幾年,15年進入騰訊互娛公共組件中心。目前專注于一些游戲公共組件的開發(fā)。
\\技術(shù)背景
\\騰訊自研手游,就我了解的項目來說,大多數(shù)游戲引擎都是Unity3D,少數(shù)用coco2d。
\\xLua這個插件具體用到了哪些游戲中?雖說xLua是2015年3月就完成了第一個版本,但由于當時項目組熱更的意識并沒有很普遍,需求不是很強烈,xLua的開發(fā)資源都調(diào)到更緊急的項目了。直到15年年底正式集成到我們的apollo手游開發(fā)框架,才迎來xLua的第一個項目。到目前為止,我們已知的應用了xLua的項目有十多個,其中不乏一些重量級IP,或者按星級標準打造的產(chǎn)品。
\\在xLua之前,面對iOS無法熱更新的問題,有用ulua的,有用slua的,也有項目用自研的腳本語言,不過當時用人更新的項目也不多。
\\熱更新流程
\\手游的熱更新流程很簡單,只是啟動時檢測下是否有新版本文件,有的話就下載覆蓋老文件,然后啟動。
\\\\下載的文件如果是圖片,模型這些是沒問題的,但如果是Unity原生的代碼邏輯,無論是以前的Mono AOT或者后來的il2cpp,都是編譯成native code,iOS下是跑不了的。
\\解決辦法就一個,別用native code,別用jit,解析執(zhí)行就可以了。包括xLua在內(nèi)的所有熱更新支持方案都是通過“解析執(zhí)行”來實現(xiàn)代碼邏輯熱更新。
\\來自xLua的 Hello world
\\(1)三行代碼跑lua腳本
\\一個完整的例子僅需3行代碼:
\\下載xLua后解壓到Unity工程Assets目錄下,建一個MonoBehaviour拖到場景,在Start里頭加上這么三行:
\\\XLua.LuaEnv luaenv = new XLua.LuaEnv();\luaenv.DoString(\"CS.UnityEngine.Debug.Log('hello world')\");\luaenv.Dispose();\\\運行就可以看到Console打印的hello world。
\\(2)C#調(diào)用lua系統(tǒng)函數(shù)math.max
\\xLua支持把一個Lua函數(shù)綁定到C# delegate。
\\我們先聲明一個delegate,并為它加上CSharpCallLua標簽:
\\\[XLua.CSharpCallLua]\public delegate double LuaMax(double a, double b);\\\然后在上面那例子加上這么兩行(luaenv銷毀前):
\\\var max = luaenv.Global.GetInPath(\"math.max\");\Debug.Log(\"max:\" + max(32, 12));\\\就那么簡單,把lua的math.max綁定到C#的max變量后,調(diào)用就和一個C#函數(shù)調(diào)用差不多了,而且,最最重要的是,執(zhí)行了“XLua/Generate Code”后,max(32, 12)調(diào)用是不產(chǎn)生(C#)gc alloc的,既優(yōu)雅,又高效!(更詳細的可以看XLua\\Doc下的文檔。)
\\xLua全局觀
\\(1)易用性:編輯器下無需生成代碼支持所有特性
\\xLua的易用不僅僅體現(xiàn)在編程,還體現(xiàn)在方方面面的細節(jié)考慮,甚至考慮到團隊配合工作流。
\\xLua僅有兩個菜單選擇,分別是生成代碼和清除生成代碼。在菜單之外,甚至只需要在build手機版本前執(zhí)行一下“Generate Code”即可(這也有API可集成到項目的自動化打包流程)。
\\這就是xLua的特色功能之一:編輯器下無需生成代碼支持所有特性。
\\之所以做這個功能,是因為有的項目反饋,“生成代碼”對于策劃美術(shù)太過遙遠,教了很久還是老忘;還有個大項目反饋說由于代碼很多,每次生成代碼后,Unity3D都要轉(zhuǎn)很久。
\\(2)擴展性:授之以魚,不如授之以漁
\\開發(fā)中我們往往要用到很多東西,比如用PB和后臺交互,解析json格式的配置文件等等。雖說我們都可以在C#那找到相應的庫,然后通過xLua去使用這些庫,但這效率不高,最好能有相應Lua的庫。
\\不少方案是直接集成一些常用的Lua庫,但這帶來些新問題:這些庫不一定用到,卻增大安裝包;集成的庫也不一定符合項目習慣:json解析有人喜歡rapidjson,有人愛用cjson,所謂眾口難調(diào);對于某些項目,這些庫還是不夠,還是得自己去想辦法加;
\\騰訊團隊的設(shè)計原則是授之以魚,不如授之以漁,因此xLua:
\\- 提供了接口、教程,在不修改xLua代碼的情況下,開發(fā)者可以根據(jù)個人喜好加入庫;\
- 通過cmake實現(xiàn)跨平臺編譯,可以選擇伴隨xLua一起編譯,修改一個makefile文件,搞定各平臺編譯。\
- 除了很方便加入第三方Lua插件,xLua的生成引擎支持二次開發(fā),可以編寫生成插件,生成自己所需的一些代碼以及配置。\
(3)性能的保證
\\游戲的性能備受關(guān)注,因此任何模塊的變化都需要盡可能不降低甚至調(diào)優(yōu)游戲整體的性能。xLua設(shè)計原則是在保證運行效率的前提下,盡量的保證開發(fā)效率。
\\對于性能這塊,有幾個至關(guān)重要的版本:
\\第一個版本1.0.0在05年3月份發(fā)布,當時delegate,interface作為最主要的C#訪問Lua的設(shè)定,從接口層面避免了boxing、unboxing、gc alloc,這是一個良好的起點。做一個通用組件的都知道,接口一開始設(shè)計不合理導致的問題很難解決,別人已經(jīng)用了,甚至已經(jīng)養(yǎng)成習慣了,很難糾正。ps:說起這習慣,有的從別的lua插件轉(zhuǎn)為使用xLua的童鞋,一開始習慣用LuaFunction.Call去調(diào)用lua(xLua也保留了這接口,可用于性能要求不高的場合),他們后期就痛苦了,還得一個個地方的改回來。
\\第二個很重要的版本是2.0.0(06年3月發(fā)布),這版本主要目標就性能優(yōu)化,因為當時有個對性能要求極其嚴苛的項目想用lua,嚴苛到什么程度呢?他們覺得C#性能都不放心,戰(zhàn)斗系統(tǒng)打算用C++寫。那版本我們把虛擬機切換到luajit,加入了lazyload技術(shù),逐行語句的優(yōu)化,甚至關(guān)鍵地方不用C#提供的容器,自己寫專用的(比Dictionary實測性能高4倍)。。??梢哉J為我們重做了一個xLua。最終他們的選型測試結(jié)論是選xLua。
\\后來和一些項目的交流發(fā)現(xiàn),項目組很關(guān)注gc alloc這指標,甚至比lua和C#間的互調(diào)性能指標還要看重。于是有了2.1.0版本(06年7月發(fā)布),這版本主要目標是gc優(yōu)化,我們重寫了反射,反射調(diào)用的gc減少到原來的幾分之一,性能提高了3倍左右。我們設(shè)計了一個全新的復雜值類型支持方案,該方案支持的類型更多(只要struct的字段都是值類型即可),包括用戶自定義的struct(別的方案都不支持),也更省內(nèi)存(Vector3為例,內(nèi)存占用只有別的方案的30%)。但也有劣勢的地方,比如你調(diào)用Vector3上的一些方法,會比ulua、slua要差,因為后面兩個把Vector3用lua重新實現(xiàn)了,這類耗時不大的運算相比lua和C#直接的適配成本小太多了,直接在lua做更劃算,不過這差距僅限于那幾個ulua、slua完全重新實現(xiàn)的類。
\\上面只是三個重大節(jié)點,我們覺得性能是一個需要持續(xù)關(guān)注的點:平時想到一個好點子,就會改改,測試下,有提升就加入;建立性能基線,防止某個新功能的加入,某個bug的修改把性能給改壞了。
\\xLua內(nèi)置Lua代碼profiler;支持真機調(diào)試。目前l(fā)ua profiler只是一個小工具,所以沒有做圖形化界面,典型的一個報告如下:
\\\\網(wǎng)上也有類似的工具,我們這個的優(yōu)勢是對C#函數(shù)的支持以及l(fā)uajit下更為準確。
\\真機調(diào)試支持各lua插件都一樣,就是把ZeroBraneStudio調(diào)試需要用到的luasocket庫預先編譯進去而已,沒什么值得介紹的地方。
\\技術(shù)實現(xiàn)的細節(jié)
\\(1) 泛型
\\泛型類型除了運行時動態(tài)實例化之外都支持,而運行時動態(tài)實例化需要jit的支持,iOS下行不通。舉個例子,如果你配了對Dictionary\u0026lt;int, string\u0026gt;生成代碼,那這個類型是可以用的,但如果你新更新的lua代碼,想用一個Dictionary\u0026lt;int, double\u0026gt;,這個類型之前沒生成代碼,而且C#里頭也沒任何地方使用過,這就不支持。靜態(tài)實例化的泛型,其實和非泛型類型處理上沒區(qū)別。
\\(2) 委托事件的封裝
\\委托封裝是根據(jù)委托的接口生成一段操作lua棧的代碼作為委托的實現(xiàn)。舉個例子就很好懂了。比如對于委托:delegate double Add(double a, double b),我們生成如下代碼:
\\\public double SystemDouble(double a, double b)\{\ RealStatePtr L = luaEnv.L;\ int err_func =LuaAPI.load_error_func(L, errorFuncRef);\ \ LuaAPI.lua_getref(L, luaReference);\ \ LuaAPI.lua_pushnumber(L, a);\ LuaAPI.lua_pushnumber(L, b);\ \ int __gen_error = LuaAPI.lua_pcall(L, 2, 1, err_func);\ if (__gen_error != 0)\ luaEnv.ThrowExceptionFromError(err_func - 1);\ \ double __gen_ret = LuaAPI.lua_tonumber(L, err_func + 1);\ LuaAPI.lua_settop(L, err_func - 1);\ return __gen_ret;\}\\\這代碼把調(diào)用轉(zhuǎn)給lua函數(shù),調(diào)用委托就是調(diào)用這函數(shù)。
\\其它方案都有delegate的支持,一般僅用于在lua側(cè)主動傳遞/設(shè)置一個lua函數(shù)到C#,而xLua支持更為完整,比如:
\\- 支持C#主動用delegate來引用一個lua函數(shù)。用delegate代替類似object[] Call(params object[] args)的接口調(diào)用lua最大的好處是可以避免值類型傳遞時的boxing/unboxing,還有參數(shù)數(shù)組,返回值數(shù)組的gc alloc;\
- 支持返回delegate的delegate,可對應到lua的高階函數(shù);\
作為這技術(shù)的一個延伸,xLua支持用一個c# interface引用一個lua table,這個特性和一些IOC框架配合可以實現(xiàn)C#和Lua間無感知(模塊間都通過interface耦合,然后由框架去組裝)。
\\(3) 無縫支持生成代碼及反射
\\生成代碼固然重要,已然是各大主流方案的標配。
\\反射有的方案明確不支持,但從項目的反饋來說,也是至關(guān)重要的:有的項目代碼很多,已經(jīng)接近蘋果的80M Text段的限制,對他們來說,代碼量大小關(guān)乎到能否發(fā)布,反射方式性能不如生成代碼,但對安裝包影響小。
\\這的無縫有兩個含義:
\\對于il2cpp的stripping,xLua也考慮到了,只要你對一個類配置了ReflectionUse,會自動生成Unity的link.xml配置文件,將該類型列為不剪裁。
\\其他Lua插件一覽
\\在xLua之外,還有其他的Lua插件,如 uLua、SLua、C#light等。
\\(1) ulua應用項目是最多的,由于開源得早,名氣也最大,這是它很大的優(yōu)勢。騰訊也有項目用ulua,反饋比較多的問題是它版本的前后兼容問題:
\\- ulua最早是一個叫LuaInterface開源庫的Unity移植,在2015年初換成cs2lua,又在2016年初換成tolua c#,只所以說“換”,是因為這從API角度看可認為三個不同的產(chǎn)品,它們間很難升級,而且是每換一次,之前的版本就徹底不維護了,這給項目帶來很大的困擾。\
- ulua的第一個版本純反射,并不實用,已經(jīng)淡出市場,現(xiàn)存應用用后兩個版本居多。cstolua版本接口比較混亂:它保留了第一版ulua接口之余,搞了一套新接口,這兩套接口之間并不正交,也不是后者完全替代前者,讓人有點無所適從。到了tolua c#版本,這問題解決了,但同時也把反射特性(老接口)給廢了。不過總體來說,ulua在向好的方向走。\
(2) slua代碼質(zhì)量比cstolua好很多(很多人當時選slua的理由),部分支持反射。性能按我們的測試用例整體比tolua c#略低,另外代碼質(zhì)量對比tolua c#已經(jīng)形成不了明顯優(yōu)勢。
\\(3) C#light,個人覺得主要有兩個不足:
\\- 按其實現(xiàn)原理來說,性能不會靠譜,到不了手機上實用的地步;\
- 由于不完整支持C#,本質(zhì)上只是另一種叫C#light的語言(C# like?名字倒很貼切),這兩者代碼配合起來也復雜,甚至它能做到比C#和lua配合更復雜些\
事實也證明了,C# light基本淡出市場,可以忽略不計了。
\\(4) LSharp是C# light作者的后續(xù)作品,倒是可以期盼些,從il層面執(zhí)行,這兩個問題有望改善,可惜后面沒了下文(不維護了)。
\\相比之下,騰訊在設(shè)計xLua時,實現(xiàn)的功能更全,這“全”體現(xiàn)在C#的特性支持得更全些,lua虛擬機版本支持更全;更易用些,比如編輯器下不用生成代碼;另外,性能也不比它們差。
\\說到功能更全,可能有人抱怨并沒有pb,json,sqlite等等功能。其實稍熟悉lua的人都知道,那只是把一些現(xiàn)成lua擴展編譯進去而已,算不上是它做了這些功能。預集成好處是方便,壞處是沒選擇的余地,用不上的東西會占空間,用得上的東西也不一定是你喜歡的庫。
\\xLua的lua庫基于cmake編譯,要加這些庫門檻很低,有教程,改一個Makefile搞定各平臺編譯。在C#測也提供了api來初始化這些庫??偠灾?#xff0c;xLua的原則是授之以漁。
\\xLua的靈感來源
\\xLua立項當初,考察了當時能找到的所有方案,并分析各方案優(yōu)劣,定出第一個版本的特性,大體是基于NLua基礎(chǔ)上加上代碼生成。介紹下NLua,NLua的作者就是LuaInterface的作者,NLua可以認為是LuaInterface的升級版,而前面也說了,第一版uLua是LuaInterface的Unity移植版本,也不能算原創(chuàng)。
\\因為是“站在”生成代碼當時有看過cstolua的實現(xiàn)(那時還沒掛ulua的牌),覺得它通過硬編碼字符串拼接的方式維護性不太好,就用模版來做。感覺這步是走對了,后續(xù)生成代碼調(diào)整起來比較簡單,這對性能調(diào)優(yōu)很有好處。
\\經(jīng)過十多個版本的迭代,優(yōu)化,現(xiàn)在NLua的影子比較淡了(NLua僅支持反射,而xLua的反射在2.1.0版本已經(jīng)完全重寫),就剩下C#引用類型對象在lua的表達的思路沒變。
\\此外,遇到需要調(diào)整較大的bug,我們也會先看同類插件是不是已經(jīng)解決了,對比他們的修改方案和我們的,選更適合的。
\\xLua背后的研發(fā)與團隊
\\xLua目前迭代了十多個版本,從第一個項目開始,平均一個月一個版本。
\\研發(fā)團隊人員目前有一個全職開發(fā)。測試使用的是騰訊互娛的公有資源,很規(guī)范:有一套不斷補充的功能自動化用例,性能測試也建立了基線,確保不會因為功能迭代而影響性能。騰訊互娛有專門的客戶端兼容性測試實驗室,至少中版本號以上的變動我們會提交給他們針對top 100的機型進行兼容性測試。
\\至于lua,luajit的更新跟進,先說luajit吧,luajit變動不大,我第一次用luajit是11年,那時支持到lua5.1,現(xiàn)在也還是lua5.1,中間只是一些bug的修復,性能優(yōu)化,或者新平臺支持等,我們要做事情不多。而lua中版本間差別還是蠻大的,但中版本變動并不頻繁,從5.1到5.2用了6年,從5.2到5.3用了3年,5.3是2015年初發(fā)布的,我個人覺得到下一次中版本變動會很久,不亞于甚至大于5.1到5.2的時間跨度(5.2個人認為只是一個過渡版本)。
\\小版本一般改改bug,等穩(wěn)定后直接升級就可以了,不需要做很多事情,目前xLua的lua版本用的是lua的最新版本5.3.3。
\\聊聊C#,談談Lua
\\C#在開發(fā)效率和運行效率平衡得很好,語言特性也比較全,個人覺得是很優(yōu)秀的一門語言。在Unity3D上的缺憾主要是其mono版本太低,一些很古老的bug,比如著名的foreach性能問題很多個版本都沒解決,新的特性,比如await又不支持。
\\另外在手機平臺iOS不允許應用下載native code運行,jit,剛好把mono應用的熱更新給堵死了,要是mono虛擬機能夠做到像luajit那樣,jit走不通就用interpret模式,其實就沒lua或者其它熱更新方案什么事了。
\\而lua被稱為游戲腳本之王,在游戲領(lǐng)域應用比較廣泛,它設(shè)計之初就考慮到嵌入式領(lǐng)域,比如相對它提供的特性來說,它體積非常小,啟動一個vm占資源也不多,性能也是腳本里頭的佼佼者。
\\lua相對C#而言,首先是它支持解析執(zhí)行,進而支持熱更新。而免編譯對開發(fā)效率提升也是蠻大的,特別是較大的項目。
\\lua的動態(tài)類型有利有弊,好的是沒有編譯期的類型檢查,快速開發(fā)比較有優(yōu)勢,特別在需求三天兩頭就變的游戲領(lǐng)域。缺點是要做出健壯的軟件得有大量的測試來保證,還有由于要做運行期檢查,性能會比靜態(tài)類型語言低。
\\lua的一大特色是語言級的協(xié)程(coroutine)的支持,比Unity3D基于generator模擬的協(xié)程要好很多,對于復雜異步業(yè)務邏輯編寫很有幫助,xLua的配套例子有范例(ps一下,Unity3D的mono版本升級到支持await的話,是更理想的異步方案)。
\\至于C#和lua間如何配合,可能每個人都有不同的看法,但至少有一點是確定的:需求變更大,預計很可能需要熱更的地方,用lua。當然,也可以嘗試最新的開發(fā)模式,全C#開發(fā),lua fix bug。
\\寫在最后
\\xLua應該還有不足,我們會在發(fā)現(xiàn)的第一時間去修改。騰訊xLua團隊極度歡迎大家在發(fā)現(xiàn)不足之后提出反饋。
\\總結(jié)
以上是生活随笔為你收集整理的腾讯开源手游热更新方案,Unity3D下的Lua编程的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 仿真软件都在这里了!20+国内外自动驾驶
- 下一篇: [剑指offer][JAVA]面试题第[