1. 程式人生 > >騰訊開源手遊熱更新方案,Unity3D下的Lua程式設計

騰訊開源手遊熱更新方案,Unity3D下的Lua程式設計

xLua是Unity3D下Lua程式設計解決方案,自2016年初推廣以來,已經應用於十多款騰訊自研遊戲,因其良好效能、易用性、擴充套件性而廣受好評。現在 騰訊已經將xLua開源到GitHub。

2016年12月末,xLua剛剛實現新的突破:全平臺支援用Lua修復C#程式碼bug。

目前Unity下的Lua熱更新方案大多都是要求要熱更新的部分一開始就要用Lua語言實現,不足之處在於:

  1. 接入成本高,有的專案已經用C#寫完了,這時要接入需要把需要熱更的地方用Lua重新實現;
  2. 即使一開始就接入了,也存在同時用兩種語言開發難度較大的問題;
  3. Lua效能不如C#;

xLua熱補丁技術支援在執行時把一個C#實現(函式,操作符,屬性,事件,或者整個類)替換成Lua實現,意味著你可以:

  1. 平時用C#開發;
  2. 執行也是C#,效能秒殺Lua;
  3. 有bug的地方下發個Lua指令碼fix了,下次整體更新時可以把Lua的實現換回正確的C#實現,更新時甚至可以做到不重啟遊戲;

這個新特性iOS,Android,Window,Mac都測試通過了,目前在做一些易用性優化。

那麼,騰訊開源的xLua究竟是怎樣的技術?它是為何如此設計的?更令人關心的是,xLua的效能如何?帶著這些問題,InfoQ對其作者進行了採訪並將內容整理成文。

技術背景

騰訊自研手遊,就我瞭解的專案來說,大多數遊戲引擎都是Unity3D,少數用coco2d。

xLua這個外掛具體用到了哪些遊戲中?雖說xLua是2015年3月就完成了第一個版本,但由於當時專案組熱更的意識並沒有很普遍,需求不是很強烈,xLua的開發資源都調到更緊急的專案了。直到15年年底正式整合到我們的apollo手遊開發框架,才迎來xLua的第一個專案。到目前為止,我們已知的應用了xLua的專案有十多個,其中不乏一些重量級IP,或者按星級標準打造的產品。

在xLua之前,面對iOS無法熱更新的問題,有用ulua的,有用slua的,也有專案用自研的指令碼語言,不過當時用人更新的專案也不多。

熱更新流程

手遊的熱更新流程很簡單,只是啟動時檢測下是否有新版本檔案,有的話就下載覆蓋老檔案,然後啟動。

下載的檔案如果是圖片,模型這些是沒問題的,但如果是Unity原生的程式碼邏輯,無論是以前的Mono AOT或者後來的il2cpp,都是編譯成native code,iOS下是跑不了的。

解決辦法就一個,別用native code,別用jit,解析執行就可以了。包括xLua在內的所有熱更新支援方案都是通過“解析執行”來實現程式碼邏輯熱更新。

來自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。

  1. 第一和第三行分別LuaEnv的建立以及銷燬,所謂LuaEnv可以理解為lua虛擬機器,往往整個工程一個虛擬機器即可:
  2. DoString裡頭可以是任意合法的lua程式碼,例子中呼叫了UnityEngine.Debug.Log介面列印了一個log(C#的靜態函式在CS下直接可用);

(2)C#呼叫lua系統函式math.max

xLua支援把一個Lua函式繫結到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變數後,呼叫就和一個C#函式呼叫差不多了,而且,最最重要的是,執行了“XLua/Generate Code”後,max(32, 12)呼叫是不產生(C#)gc alloc的,既優雅,又高效!(更詳細的可以看XLua\Doc下的文件。)

xLua全域性觀

(1)易用性:編輯器下無需生成程式碼支援所有特性

xLua的易用不僅僅體現在程式設計,還體現在方方面面的細節考慮,甚至考慮到團隊配合工作流。

xLua僅有兩個選單選擇,分別是生成程式碼和清除生成程式碼。在選單之外,甚至只需要在build手機版本前執行一下“Generate Code”即可(這也有API可整合到專案的自動化打包流程)。

這就是xLua的特色功能之一:編輯器下無需生成程式碼支援所有特性。

之所以做這個功能,是因為有的專案反饋,“生成程式碼”對於策劃美術太過遙遠,教了很久還是老忘;還有個大專案反饋說由於程式碼很多,每次生成程式碼後,Unity3D都要轉很久。

(2)擴充套件性:授之以魚,不如授之以漁

開發中我們往往要用到很多東西,比如用PB和後臺互動,解析json格式的配置檔案等等。雖說我們都可以在C#那找到相應的庫,然後通過xLua去使用這些庫,但這效率不高,最好能有相應Lua的庫。

不少方案是直接整合一些常用的Lua庫,但這帶來些新問題:這些庫不一定用到,卻增大安裝包;整合的庫也不一定符合專案習慣:json解析有人喜歡rapidjson,有人愛用cjson,所謂眾口難調;對於某些專案,這些庫還是不夠,還是得自己去想辦法加;

騰訊團隊的設計原則是授之以魚,不如授之以漁,因此xLua:

  • 提供了介面、教程,在不修改xLua程式碼的情況下,開發者可以根據個人喜好加入庫;
  • 通過cmake實現跨平臺編譯,可以選擇伴隨xLua一起編譯,修改一個makefile檔案,搞定各平臺編譯。
  • 除了很方便加入第三方Lua外掛,xLua的生成引擎支援二次開發,可以編寫生成外掛,生成自己所需的一些程式碼以及配置。

(3)效能的保證

遊戲的效能備受關注,因此任何模組的變化都需要儘可能不降低甚至調優遊戲整體的效能。xLua設計原則是在保證執行效率的前提下,儘量的保證開發效率。

對於效能這塊,有幾個至關重要的版本:

第一個版本1.0.0在05年3月份釋出,當時delegate,interface作為最主要的C#訪問Lua的設定,從介面層面避免了boxing、unboxing、gc alloc,這是一個良好的起點。做一個通用元件的都知道,介面一開始設計不合理導致的問題很難解決,別人已經用了,甚至已經養成習慣了,很難糾正。ps:說起這習慣,有的從別的lua外掛轉為使用xLua的童鞋,一開始習慣用LuaFunction.Call去呼叫lua(xLua也保留了這介面,可用於效能要求不高的場合),他們後期就痛苦了,還得一個個地方的改回來。

第二個很重要的版本是2.0.0(06年3月釋出),這版本主要目標就效能優化,因為當時有個對效能要求極其嚴苛的專案想用lua,嚴苛到什麼程度呢?他們覺得C#效能都不放心,戰鬥系統打算用C++寫。那版本我們把虛擬機器切換到luajit,加入了lazyload技術,逐行語句的優化,甚至關鍵地方不用C#提供的容器,自己寫專用的(比Dictionary實測效能高4倍)。。。可以認為我們重做了一個xLua。最終他們的選型測試結論是選xLua。

後來和一些專案的交流發現,專案組很關注gc alloc這指標,甚至比lua和C#間的互調效能指標還要看重。於是有了2.1.0版本(06年7月釋出),這版本主要目標是gc優化,我們重寫了反射,反射呼叫的gc減少到原來的幾分之一,效能提高了3倍左右。我們設計了一個全新的複雜值型別支援方案,該方案支援的型別更多(只要struct的欄位都是值型別即可),包括使用者自定義的struct(別的方案都不支援),也更省記憶體(Vector3為例,記憶體佔用只有別的方案的30%)。但也有劣勢的地方,比如你呼叫Vector3上的一些方法,會比ulua、slua要差,因為後面兩個把Vector3用lua重新實現了,這類耗時不大的運算相比lua和C#直接的適配成本小太多了,直接在lua做更划算,不過這差距僅限於那幾個ulua、slua完全重新實現的類。

上面只是三個重大節點,我們覺得效能是一個需要持續關注的點:平時想到一個好點子,就會改改,測試下,有提升就加入;建立效能基線,防止某個新功能的加入,某個bug的修改把效能給改壞了。

xLua內建Lua程式碼profiler;支援真機除錯。目前lua profiler只是一個小工具,所以沒有做圖形化介面,典型的一個報告如下:

網上也有類似的工具,我們這個的優勢是對C#函式的支援以及luajit下更為準確。

真機除錯支援各lua外掛都一樣,就是把ZeroBraneStudio除錯需要用到的luasocket庫預先編譯進去而已,沒什麼值得介紹的地方。

技術實現的細節

(1) 泛型

泛型型別除了執行時動態例項化之外都支援,而執行時動態例項化需要jit的支援,iOS下行不通。舉個例子,如果你配了對Dictionary<int, string>生成程式碼,那這個型別是可以用的,但如果你新更新的lua程式碼,想用一個Dictionary<int, double>,這個型別之前沒生成程式碼,而且C#裡頭也沒任何地方使用過,這就不支援。靜態例項化的泛型,其實和非泛型型別處理上沒區別。

(2) 委託事件的封裝

委託封裝是根據委託的介面生成一段操作lua棧的程式碼作為委託的實現。舉個例子就很好懂了。比如對於委託: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;
}

這程式碼把呼叫轉給lua函式,呼叫委託就是呼叫這函式。

其它方案都有delegate的支援,一般僅用於在lua側主動傳遞/設定一個lua函式到C#,而xLua支援更為完整,比如:

  • 支援C#主動用delegate來引用一個lua函式。用delegate代替類似object[] Call(params object[] args)的介面呼叫lua最大的好處是可以避免值型別傳遞時的boxing/unboxing,還有引數陣列,返回值陣列的gc alloc;
  • 支援返回delegate的delegate,可對應到lua的高階函式;

作為這技術的一個延伸,xLua支援用一個c# interface引用一個lua table,這個特性和一些IOC框架配合可以實現C#和Lua間無感知(模組間都通過interface耦合,然後由框架去組裝)。

(3) 無縫支援生成程式碼及反射

生成程式碼固然重要,已然是各大主流方案的標配。

反射有的方案明確不支援,但從專案的反饋來說,也是至關重要的:有的專案程式碼很多,已經接近蘋果的80M Text段的限制,對他們來說,程式碼量大小關乎到能否釋出,反射方式效能不如生成程式碼,但對安裝包影響小。

這的無縫有兩個含義:

  1. 兩者在支援的特性以及特性的使用方式都是一致的,兩者方式間切換,業務邏輯程式碼不用修改,改改配置就可以了;
  2. 兩者無縫配合,比如一個繼承鏈上,任意一個類都可以選擇生成程式碼或者反射,比如子類選擇生成程式碼,父類由於不常用選擇了反射,還是可以在子類物件上呼叫父類的方法;

對於il2cpp的stripping,xLua也考慮到了,只要你對一個類配置了ReflectionUse,會自動生成Unity的link.xml配置檔案,將該型別列為不剪裁。

其他Lua外掛一覽

在xLua之外,還有其他的Lua外掛,如 uLua、SLua、C#light等。

(1) ulua應用專案是最多的,由於開源得早,名氣也最大,這是它很大的優勢。騰訊也有專案用ulua,反饋比較多的問題是它版本的前後相容問題:

  • ulua最早是一個叫LuaInterface開源庫的Unity移植,在2015年初換成cs2lua,又在2016年初換成tolua c#,只所以說“換”,是因為這從API角度看可認為三個不同的產品,它們間很難升級,而且是每換一次,之前的版本就徹底不維護了,這給專案帶來很大的困擾。
  • ulua的第一個版本純反射,並不實用,已經淡出市場,現存應用用後兩個版本居多。cstolua版本介面比較混亂:它保留了第一版ulua介面之餘,搞了一套新介面,這兩套介面之間並不正交,也不是後者完全替代前者,讓人有點無所適從。到了tolua c#版本,這問題解決了,但同時也把反射特性(老介面)給廢了。不過總體來說,ulua在向好的方向走。

(2) slua程式碼質量比cstolua好很多(很多人當時選slua的理由),部分支援反射。效能按我們的測試用例整體比tolua c#略低,另外程式碼質量對比tolua c#已經形成不了明顯優勢。

(3) C#light,個人覺得主要有兩個不足:

  • 按其實現原理來說,效能不會靠譜,到不了手機上實用的地步;
  • 由於不完整支援C#,本質上只是另一種叫C#light的語言(C# like?名字倒很貼切),這兩者程式碼配合起來也複雜,甚至它能做到比C#和lua配合更復雜些

事實也證明了,C# light基本淡出市場,可以忽略不計了。

(4) LSharp是C# light作者的後續作品,倒是可以期盼些,從il層面執行,這兩個問題有望改善,可惜後面沒了下文(不維護了)。

相比之下,騰訊在設計xLua時,實現的功能更全,這“全”體現在C#的特性支援得更全些,lua虛擬機器版本支援更全;更易用些,比如編輯器下不用生成程式碼;另外,效能也不比它們差。

說到功能更全,可能有人抱怨並沒有pb,json,sqlite等等功能。其實稍熟悉lua的人都知道,那只是把一些現成lua擴充套件編譯進去而已,算不上是它做了這些功能。預整合好處是方便,壞處是沒選擇的餘地,用不上的東西會佔空間,用得上的東西也不一定是你喜歡的庫。

xLua的lua庫基於cmake編譯,要加這些庫門檻很低,有教程,改一個Makefile搞定各平臺編譯。在C#測也提供了api來初始化這些庫。總而言之,xLua的原則是授之以漁。

xLua的靈感來源

xLua立項當初,考察了當時能找到的所有方案,並分析各方案優劣,定出第一個版本的特性,大體是基於NLua基礎上加上程式碼生成。介紹下NLua,NLua的作者就是LuaInterface的作者,NLua可以認為是LuaInterface的升級版,而前面也說了,第一版uLua是LuaInterface的Unity移植版本,也不能算原創。

因為是“站在”生成程式碼當時有看過cstolua的實現(那時還沒掛ulua的牌),覺得它通過硬編碼字串拼接的方式維護性不太好,就用模版來做。感覺這步是走對了,後續生成程式碼調整起來比較簡單,這對效能調優很有好處。

經過十多個版本的迭代,優化,現在NLua的影子比較淡了(NLua僅支援反射,而xLua的反射在2.1.0版本已經完全重寫),就剩下C#引用型別物件在lua的表達的思路沒變。

此外,遇到需要調整較大的bug,我們也會先看同類外掛是不是已經解決了,對比他們的修改方案和我們的,選更適合的。

xLua背後的研發與團隊

xLua目前迭代了十多個版本,從第一個專案開始,平均一個月一個版本。

研發團隊人員目前有一個全職開發。測試使用的是騰訊互娛的公有資源,很規範:有一套不斷補充的功能自動化用例,效能測試也建立了基線,確保不會因為功能迭代而影響效能。騰訊互娛有專門的客戶端相容性測試實驗室,至少中版本號以上的變動我們會提交給他們針對top 100的機型進行相容性測試。

至於lua,luajit的更新跟進,先說luajit吧,luajit變動不大,我第一次用luajit是11年,那時支援到lua5.1,現在也還是lua5.1,中間只是一些bug的修復,效能優化,或者新平臺支援等,我們要做事情不多。而lua中版本間差別還是蠻大的,但中版本變動並不頻繁,從5.1到5.2用了6年,從5.2到5.3用了3年,5.3是2015年初發布的,我個人覺得到下一次中版本變動會很久,不亞於甚至大於5.1到5.2的時間跨度(5.2個人認為只是一個過渡版本)。

小版本一般改改bug,等穩定後直接升級就可以了,不需要做很多事情,目前xLua的lua版本用的是lua的最新版本5.3.3。

聊聊C#,談談Lua

C#在開發效率和執行效率平衡得很好,語言特性也比較全,個人覺得是很優秀的一門語言。在Unity3D上的缺憾主要是其mono版本太低,一些很古老的bug,比如著名的foreach效能問題很多個版本都沒解決,新的特性,比如await又不支援。

另外在手機平臺iOS不允許應用下載native code執行,jit,剛好把mono應用的熱更新給堵死了,要是mono虛擬機器能夠做到像luajit那樣,jit走不通就用interpret模式,其實就沒lua或者其它熱更新方案什麼事了。

而lua被稱為遊戲指令碼之王,在遊戲領域應用比較廣泛,它設計之初就考慮到嵌入式領域,比如相對它提供的特性來說,它體積非常小,啟動一個vm佔資源也不多,效能也是腳本里頭的佼佼者。

lua相對C#而言,首先是它支援解析執行,進而支援熱更新。而免編譯對開發效率提升也是蠻大的,特別是較大的專案。

lua的動態型別有利有弊,好的是沒有編譯期的型別檢查,快速開發比較有優勢,特別在需求三天兩頭就變的遊戲領域。缺點是要做出健壯的軟體得有大量的測試來保證,還有由於要做執行期檢查,效能會比靜態型別語言低。

lua的一大特色是語言級的協程(coroutine)的支援,比Unity3D基於generator模擬的協程要好很多,對於複雜非同步業務邏輯編寫很有幫助,xLua的配套例子有範例(ps一下,Unity3D的mono版本升級到支援await的話,是更理想的非同步方案)。

至於C#和lua間如何配合,可能每個人都有不同的看法,但至少有一點是確定的:需求變更大,預計很可能需要熱更的地方,用lua。當然,也可以嘗試最新的開發模式,全C#開發,lua fix bug。

寫在最後

xLua應該還有不足,我們會在發現的第一時間去修改。騰訊xLua團隊極度歡迎大家在發現不足之後提出反饋。

作者簡介

車雄生,05年畢業,在華為工作了6年,跟著先後在兩遊戲創業公司待了幾年,15年進入騰訊互娛公共元件中心。目前專注於一些遊戲公共元件的開發。

【QCon北京2017】【QCon北京2017】Google 為什麼要把 20 億行程式碼放到一個倉庫中?阿里新一代實時計算引擎Blink面臨哪些挑戰?智慧運維裡的時間序列:預測、異常檢測等是怎麼做的?來QCon你就知道了。海量技術乾貨彙集,點亮你的技術之旅。即刻報名,盡享8折特惠

ILRuntime 5小時前by Larlf Wang

ILRuntime在沿著LSharp的方向前進,也是一個值得推薦的選擇,只是現在應用不多,可以重點關注下!