【ArchSummit架构师峰会】探讨数据与人工智能相互驱动的关系>>> 了解详情
写点什么

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

  • 2017-01-02
  • 本文字数:7134 字

    阅读完需:约 23 分钟

写在前面

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 对其作者进行了采访并将内容整理成文。

嘉宾简介

车雄生,05 年毕业,在华为工作了 6 年,跟着先后在两游戏创业公司待了几年,15 年进入腾讯互娱公共组件中心。目前专注于一些游戏公共组件的开发。

技术背景

腾讯自研手游,就我了解的项目来说,大多数游戏引擎都是 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<luamax>("math.max");
Debug.Log("max:" + max(32, 12));
</luamax>

就那么简单,把 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 团队极度欢迎大家在发现不足之后提出反馈。

2017-01-02 18:0019857
用户头像

发布了 58 篇内容, 共 42.6 次阅读, 收获喜欢 35 次。

关注

评论

发布
暂无评论
发现更多内容

web前端培训-通过JS 可以读取电脑上所有数据

@零度

JavaScript 前端开发

TDengine 助力智慧燃气,支撑数百万智能终端的接入管理

TDengine

“既要性能,也要安全”,这样的Rust,谁不喜欢!

非凸科技

rust 编程语言 软件开发 招聘

行业分析| 音视频呼叫邀请适用于多领域

anyRTC开发者

音视频 WebRTC 语音通话 视频通话 呼叫邀请

为什么要做等保二级,有什么好处?

行云管家

网络安全 等保 等保2.0

提高企业产品交付效率系列(1)—— 企业应用一键安装和升级

北京好雨科技有限公司

Kubernetes PaaS rainbond

NetCore性能排查

神农写代码

提效24.3%!看OA预算管理系统的低代码开发实践

鲸品堂

低代码开发

Java篇|忘记格式化代码,把女朋友鸽了

Jianmu

Java 自动化 持续集成 建木CI 格式化代码

中科柏诚:用数字技术纾困解难,助力中小企业恢复成长活力

联营汇聚

教育行业可以用云管平台吗?有案例介绍吗?

行云管家

云计算 企业上云 云管平台 云管理

TDengine 在智慧矿山系统中的应用

TDengine

FabEdge V0.5.0 新特性:支持跨集群服务访问

BoCloud博云

开源 边缘计算 cncf

【CI/CD研讨会报名,截止最后一天】全程参会,还有惊喜奖品等你拿!

龙智—DevSecOps解决方案

cicd 持续集成 jenkins CI/CD 持续发布

看完微信抢红包算法你就明白,为啥你不是手气最佳

华为云开发者联盟

算法 微信红包 手气最佳 剩余金额随机法 割线法

OpenHarmony标准设备应用开发(一)——HelloWorld

OpenHarmony开发者

Hello World ! OpenHarmony 标准设备

还搞不明白,一次性给你总结好网络层概念

华为云开发者联盟

网络协议 IP 网络层 组网

Apache APISIX 2.13.0 发布

API7.ai 技术团队

开源 API网关 API Gateway Apache APISIX

菜鸟不菜,职场小白大变身

龙智—DevSecOps解决方案

Jira Jira插件 工作流扩展 并行审批 jira并行审批

华为云GaussDB专家走进课堂,跟莘莘学子聊聊数据库

华为云开发者联盟

数据库 人才培养 华为云 GaussDB 华为云数据库

四大功能!带你初识 Fabric | 容器网络系列第2期

BoCloud博云

云原生 容器网络方案

从0到100:基于微信小程序的羽毛球馆预约系统的开发笔记

CC同学

围绕用户体验持续进化 英特尔Evo平台打造开放、多元创新优势

科技新消息

java培训-Redis 原理与知识总结分享 不愁面试

@零度

redis JAVA开发

在 Rainbond 中一键安装高可用 Nacos 集群

北京好雨科技有限公司

开源 Kubernetes nacos PaaS rainbond

安利一个小众但实用的导航网站(推荐收藏)

小炮

导航网站

给工厂做开发,竟然喝着咖啡听“交响”?

阿里云云效

云计算 阿里云 DevOps 研发 研发提效

OceanBase 在证券行业基金资管场景落地实践与解决方案

OceanBase 数据库

证券 oceanbase

2022年中国音频行业产品洞察分析

易观分析

音频体验 在线音频

通过IPv6隧道实现天翼云云主机IPv4和IPv6双栈接入

天翼云开发者社区

网络

20万字《网易智企技术合辑》重磅发布!

网易云信

人工智能 大数据 大前端 即时通讯IM 音视频技术

腾讯开源手游热更新方案,Unity3D下的Lua编程_.NET_木环_InfoQ精选文章