专栏名称: InfoQ
有内容的技术社区媒体。
目录
相关文章推荐
51好读  ›  专栏  ›  InfoQ

腾讯开源手游热更新方案,Unity3D下的Lua编程

InfoQ  · 公众号  · 科技媒体  · 2017-01-04 08:00

正文

作者|车雄生
编辑|木环
腾讯最近在开源方面的动作不断:先是微信跨平台基础组件Mars宣布开源,腾讯手游又于近期开源了Unity3D下Lua编程解决方案——xLua。xLua,何方神圣?有哪些技术细节可以说道说道?
写在前面

xLua是Unity3D下Lua编程解决方案,自2016年初推广以来,已经应用于十多款腾讯自研游戏,因其良好性能、易用性、扩展性而广受好评。现在 腾讯已经将xLua开源到GitHub。

https://github.com/Tencent/xLua

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,Windows,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
三行代码跑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下直接可用);

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全局观
易用性:编辑器下无需生成代码支持所有特性

xLua的易用不仅仅体现在编程,还体现在方方面面的细节考虑,甚至考虑到团队配合工作流。xLua仅有两个菜单选择,分别是生成代码和清除生成代码。在菜单之外,甚至只需要在build手机版本前执行一下“Generate Code”即可(这也有API可集成到项目的自动化打包流程)。

这就是xLua的特色功能之一:编辑器下无需生成代码支持所有特性。之所以做这个功能,是因为有的项目反馈,“生成代码”对于策划美术太过遥远,教了很久还是老忘;还有个大项目反馈说由于代码很多,每次生成代码后,Unity3D都要转很久。

扩展性:授之以鱼,不如授之以渔

开发中我们往往要用到很多东西,比如用PB和后台交互,解析json格式的配置文件等等。虽说我们都可以在C#那找到相应的库,然后通过xLua去使用这些库,但这效率不高,最好能有相应Lua的库。

不少方案是直接集成一些常用的Lua库,但这带来些新问题:这些库不一定用到,却增大安装包;集成的库也不一定符合项目习惯:json解析有人喜欢rapidjson,有人爱用cjson,所谓众口难调;对于某些项目,这些库还是不够,还是得自己去想办法加;

腾讯团队的设计原则是授之以鱼,不如授之以渔,因此xLua:

  • 提供了接口、教程,在不修改xLua代码的情况下,开发者可以根据个人喜好加入库;

  • 通过cmake实现跨平台编译,可以选择伴随xLua一起编译,修改一个makefile文件,搞定各平台编译。

  • 除了很方便加入第三方Lua插件,xLua的生成引擎支持二次开发,可以编写生成插件,生成自己所需的一些代码以及配置。

性能的保证

游戏的性能备受关注,因此任何模块的变化都需要尽可能不降低甚至调优游戏整体的性能。 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库预先编译进去而已,没什么值得介绍的地方。

技术实现的细节
泛型

泛型类型除了运行时动态实例化之外都支持,而运行时动态实例化需要jit的支持,iOS下行不通。举个例子,如果你配了对Dictionary 生成代码,那这个类型是可以用的,但如果你新更新的lua代码,想用一个Dictionary ,这个类型之前没生成代码,而且C#里头也没任何地方使用过,这就不支持。静态实例化的泛型,其实和非泛型类型处理上没区别。

委托事件的封装

委托封装是根据委托的接口生成一段操作lua栈的代码作为委托的实现。举个例子就很好懂了。比如对于委托:delegate double Add(double a, double b),我们生成如下代码:







请到「今天看啥」查看全文