找回密码
 立即注册
查看: 1182|回复: 20

[笔记] 如何评价腾讯在Unity下的xLua(开源)热更方案?

[复制链接]
发表于 2021-4-7 08:59 | 显示全部楼层 |阅读模式
如何评价腾讯在Unity下的xLua(开源)热更方案?
发表于 2021-4-7 09:00 | 显示全部楼层
有人邀请,我来谈谈我得看法,利益相关,slua作者,同时也是才去没多久的tencent员工。

之前没看过xlua代码,首先来分析xlua如何做到“平时用c#开发,需要热更新时用lua代码做热更新”,我粗略思考至少有2种方法。

1)代码生成cs stub函数,为每个hotfix标签的类,导出一个stub,形式类似这样:
  1. namespace xlua.UnityEngine
  2. {
  3. public class GameObject {
  4. UnityEngine.GameObject self;
  5. GameObject() {
  6. self = UnityEngine.GameObject()
  7. }
  8. static public GameObject Find(string s) {
  9. if(hasHotFixFor("GameObject.Find"))
  10. return runHotFix("GameObject.Find",s);
  11. return UnityEngine.GameObject.Find(s)
  12. }
  13. public public string SomeFoo(params object[] args) {
  14. if(hasHotFixFor("GameObject.SomeFoo"))
  15. return runHotFix("GameObject.SomeFoo",self,args)
  16. return self.SomeFoo(args);  
  17. }
  18. }
  19. }
复制代码
需要在il2cpp,或者别的什么地方,能切换 上层代码的 import UnityEngine为 import xlua.UnityEngine.
2)il2cpp 的cpp代码生成stub,这样仅需要stun function,不需要包装一个class,代码形式和上面类似,只不过是cpp的il2cpp调用写法。

下面谈谈我对lua的理解和xlua的看法
我认为xlua的概念很好,很多人用lua就是为了热更新,如果没有热更新的需求,大多数人是不喜欢lua,或者所谓的脚本开发的,xlua很好的解决了这部分人得需求。
但我有一点其他看法,我04年毕业在网易工作的时候,网易的游戏都是基于脚本的,不管是客户端,还是服务器端,那个时候不是lua,就是python,还有一种是类似c语法的脚本(我忘记名字了),这个是云风主导的,当时选择脚本作为逻辑开发语言的核心想法**不是为了热更新**,而是解决
1)划分引擎层和业务层,svn管理好权限,让新来的同学,接触不到核心引擎的代码权限,他们只能在脚本层做业务,等你对业务足够熟悉,对引擎足够了解,对公司足够忠诚后,才开放引擎层代码,这么做早年是为了解决私服问题,很多同学拿着全部源代码去架设私服,这多可怕,所以做业务的程序员只能拿到一个编译后的app和一份脚本接口文档,而编译出来的app会检查线上ip,报告非法服务器地址等,协助打击私服。
2)避免书写不好的c、cpp代码崩溃整个进程,脚本代码出错了,最多影响局部逻辑,还可以上报脚本错误,方便后续解决问题,现在unity里也一样,如果c#代码书写不好,就直接闪退了,不如用lua做一个安全的调用层。
3)快速修改代码,快速跑起来,早年cpp代码编译速度比较慢,修改一行代码调试运行等半天,脚本代码方便修改,方便跑起来,不用等,放到今天也一样,同时iOS还有text size大小的限制,太多的stub function会撑大text size,而lua脚本再多的代码也不会有这个问题,不用再为了text size取舍代码怎么写,功能去留的问题。
4)反外挂,对,你没看错,反外挂,早年PE各种脱壳、反编译工具,使得一个exe几乎没有秘密,外挂作者很容易做外挂,而用脚本后,几乎所有逻辑都是中间代码,这部分中间代码可以通过修改opcode,加密,一边run,一边解密等技术,保证在进程空间内基本没有完成代码存在,对外挂作者是个很大挑战,所以网易的游戏反外挂历来做的都是最好的;
5)最后才是所谓的“热更新”, 当年也不是现在这种热更新,就是每次客户端启动的时候,有一个launcher去服务器下载一个update,然后应用这个update而已。
说了这么多,我想说啥呢,我想说,其实用脚本开发是另外一种架构设计的方法,不是简单为了热更新,当你习惯了用脚本开发作为第一语言开发时,你会自然爱上lua这种设计精巧的语言,无论从设计范式,语言和数据无缝整合,到lua语言强大的描述能力等等,你都自然不自然的把身边所有东西都lua化,而不是为了热更新,即便lua跑起来不如native code快。
最后还想说一个,lua架构的优势
6)远程调试,你可以在获得玩家授权后,随时连线调试线上玩家的运行状态,方便最直接的定位问题和分析现场,比打log更直接,更快速。

关于运行效率
之前在网易,前端后端都是脚本,甚至是更慢的python,网易最新的天下手游引擎,也是基于python的;腾讯这边,服务器几乎清一色cpp,客户端也是这2年才部分lua脚本化的,之前都是native开发,服务器脚本化,我估计是不可能了(其实服务器对coredump的要求更高,脚本化获得的好处更大),究其原因,回答几乎都是担心效率。
以我多年游戏开发经验来说,脚本的运行效率绝对不是游戏运行效率的瓶颈,经过良好的设计的游戏引擎,都是思考过什么应该引擎层做,什么应该脚本化,如何优化脚本效率,如此这样后,即可以获得上面提到的优势,又不会影响运行效率,一般游戏运行效率都是渲染、加载、裁剪等导致的,和脚本半毛钱关系没有,真得需要速度的代码,可以cpp实现了,导出给lua使用,比如寻路,但不熟悉的开发人员,都会把这个锅推给脚本,因为这样他们什么也不用修改,什么也不用优化。

最后来谈谈对xlua提出的概念的看法
xlua提出的概念,是平时开发用c#,需要热更新时用lua来更新对应的函数,这个概念很好,但我对其实际使用中表示疑虑:
1)如果c#代码都是10行左右的小函数,还好,如果是上百行的大函数,需要更新的时候,首先要把上百行的c#代码转换为lua代码,在fix lua代码里的bug,这个过程本来fix c#代码或者已经存在的lua代码可能只需要2分钟,但临时转换上百行lua代码的时间,我估计1天都搞不定,除非再配合一个自动c# -》lua代码的转换器,不过,确实有cs to lua的转换器,也是tencent内部搞出来的,参考这里dreamanlan/Cs2Lua ,目前该项目默认转换为slua 代码。
2)即便存在c#-》lua代码转换器,也不意味着所有c#代码都可以无缝转换为lua代码,有太多c#支持的特性,lua不支持,这势必要在c#代码层面,规定若干规范,否则无法hotfix,但再没试图hotfix前,很难了解该方法是否可以hotfix。
3)一周一更新的高频维护怎么办?貌似使用c#开发,lua hotfix,仅适合周期较长的大版本更新。

综上我认为xlua提出了一个好的概念,但存在一些问题,但瑕不掩瑜,xlua开源也为游戏开发作出了贡献,腾讯在开源领域更进一步,可喜可贺。
发表于 2021-4-7 09:01 | 显示全部楼层
都凌晨了,看见策划同学往群内分享,再看看slua的群里的热切讨论,才知道xLua开源的这一重磅消息。

我是slua的contributor之一,维护其C#服务器独立版本slua-standalone,也算是 @庞巍伟 的未曾见过面的跟班。

当看见xlua的开头介绍,“xLua热补丁技术支持在运行时把一个C#实现(函数,操作符,属性,事件,或者整个类)替换成Lua实现”时,我就心头一颤的:这不就是我为项目做的打补丁热更新方案吗?

--------------

一年前,由于所在项目客户端的所有逻辑功能均使用C#来实现,老大希望这款完成80%的游戏添加代码热更新功能,而这个任务落在我的身上。 起初打算使用Lua脚本进行UI部分代码的改写,但是这对一个将近完成的游戏项目来说,显然是不现实的——洋洋洒洒的几万行代码,这得重构到何年何月?

在几番折腾下,唯一萌生的想法是:能不能往代码里插入一些if代码,让它在需要的时候,才执行Lua代码?
往这这个方向深入,就成型了这样的一套方案:

简书原文:另类Unity热更新大法:代码注入式补丁热更新
(里面具体的实现代码在发布的第二天去掉了)

技术上简单来说,就是使用NRefactory对C#代码进行语法分析出所有函数体,并对其进行if(...)代码的插入
用Java的同学术语来说,使用AOP做代码热修复

嗯,有人问,使用IL进行DLL注入不是更好吗?这时候好基友的作用发挥出来了 (@苏锐v5):

简书:[Unity]基于AOP代码注入的Lua补丁方案

在代码插入后,还需要用到SLua的动态反射功能,来帮助更好的对C#代码进行热修复。那时候,SLua的动态反射能力还远远不满足应用,我对SLua的动态反射部分进行了为数不少的修改,也因此成了slua的Contributor之一。

这套方案,大概从2015年12月开始,就已经正式应用在我们的项目中了,并且一直沿用至今。可以说,80%的BUG都能修复,并且不影响原有C#的开发流程。当然,也躺过不少坑,这也是另一个的话题了。后来呢,为什么没有为这套方案做更多的功能和完善?这个需求解决了,赶紧下一个。

因此,粗看之下,xLua = slua + 代码注入补丁 + 完善工具链 + 腾讯光环。  好不好? 非常好! 这可以节省成千上万的中国人的重复劳动!这可以减少大量的加班时间!这可以造福成千上万的游戏开发者!这可以缩短无数项目的开发周期!我相信xLua的开源,是对整个Unity游戏开发界的深有影响的一件事,它能创造难以估量的经济效益。

当然,这里还得赞叹腾讯公司的气概,从“研发团队人员目前有一个全职开发”可以看出其对该技术的重视,在中小项目,专门让出一个人力去做一个在外行人看起来不知道有什么用的工具,是不可理喻的,“赶紧做完这个功能,给我做别的需求去”在游戏项目中是更常见的情况。如果您不理解您的朋友在游戏公司没日没夜的加班是为什么,工具的贫乏也是其中的原因之一的。

很多时候做游戏是需求导向的,对于新技术优越与否进行鉴别,是否值得深入研究,是否值得推广,这是一件非常不容易的事。xLua开源,短短的时间就积聚了300个Star,这自然跟他的腾讯光环是密切相关的,看它标题“腾讯开源手游热更新方案,Unity3D下的Lua编程”。 轻描淡写的几个字,就能看出公司对技术的鉴别能力,对开发人员的尊重,为广大吃瓜群众建立信任基础。

相比之下,slua, @庞巍伟在创作其时身在西山居(我也是),后来slua在业界范围内也有相当的知名度。除了你正在看的这里,“slua”和“西山居” 好像没有其它地方同时出现吧?

PS:说个题外话,没记错的话,解包过腾讯手游《穿越火线》和《王者荣耀》,里面用的是ulua。
发表于 2021-4-7 09:05 | 显示全部楼层
xLua的作者,关注这帖子一阵子了,一直犹豫作为作者回答这问题会不会有卖瓜之嫌。
回下大家关注的热补丁的overhead。
热补丁的基本原理其实非常简单,了解后任何程序员都很容易分析出开销,比如对于这个类
  1. public class Calc
  2. {
  3.     int Add(int a, int b)
  4.    {
  5.         return a + b
  6.    }
  7. }
复制代码
打了hotfix标签后,xLua会在il层面注入代码,注入之后这个类会类似这样:
  1. public class Calc
  2. {
  3.     static Func<object, int, int, int> hotfix_Add = null;
  4.     int Add(int a, int b)
  5.    {
  6.         if (hotfix_Add != null) return hotfix_Add(this, a, b);
  7.         return a + b
  8.    }
  9. }
复制代码
如果lua中执行了hotfix调用,hotfix_Add会指向一个lua的适配函数。
性能开销:
如果不打补丁,就一个if判断,比注入前多执行两条很轻量级的指令(Ldsfld,Brfalse),我在window下测试,这两指令加起来仅相当于空函数调用开销的十分之一到五分之一。
内存开销:
每个函数会多一个静态delegate引用,注意是静态的,和对象实例个数无关。(Stateful方式会略不同,后面单独分析)。
安装包影响:
一般情况,就一个if和delegate调用。如果泛化函数以及参数含非public类型参数或返回值的函数会多一点:每一个输入参数,每个输出参数,对应一次函数调用,外加三次函数调用。
很难有“增加百分之多少”的数据,因为这和你程序函数的粒度有关。有童鞋在尝试为NGUI全部类型加Hotfix标签来测试,有数据我可以更新上来。

Stateful方式
这个方式的开销和基本和Stateless一样,一个地方除外:内存开销上每个对象实例会增加一个LuaTable的引用(引用而已,不会帮你new一个LuaTable实例,你没在构造函数那返回一个table,这个引用为空)。

关于开源
当然,这决策来自领导,但我个人也有点小想法:
xLua在内部推广使用的过程中,深感项目的挑剔、指责和各种场景下的折腾才是一个通用组件快速进步和完善的动力。开源,无疑是把这放大化,也更加有利于xLua的健康发展。
发表于 2021-4-7 09:09 | 显示全部楼层
昨天群里面讨论的蛮多,我也关注并看了下。
xlua 描述了一下几个优点(我觉得比较有特色的)

1)C# 代码的“HotFix”
  1. // 要fix的C#类
  2. [Hotfix]
  3. public class HotfixCalc
  4. {
  5.     public int Add(int a, int b)
  6.     {
  7.         return a - b;
  8.     }
  9.     public Vector3 Add(Vector3 a, Vector3 b)
  10.     {
  11.         return a - b;
  12.     }
复制代码
使用这种方式“HotFix”替换C#中的代码,支持用lua函数替换C#的构造函数,函数,属性,事件的替换。
很明显看出需要额外标记C#代码,这可能需要提前规划可能需要热更的C#代码,或者将尽可能多的C#代码打上标记(肯定会有额外性能开销)。这对实际工程的项目肯定会有影响,感觉也有些奇怪,还需要xlua的作者或者实际使用过的人详解一下。

2)编辑器下无需代码生成(tolua, slua 均需要手动点击菜单)
xlua 在编辑器模式下使用TypeBuilder,IlEmit 动态生成接口的C#代码,避免了每次新注册或者改变导入类时,都需要生成代码并加载。在实际项目中,确实经常由于未运行代码生成菜单,导致一些编译或运行错误。如果项目逻辑主要使用lua开发,并不设计到频繁的互操作的话,此问题有所缓解。

3)“No GC”的优化。
这一点也是tolua,slua优化的重点。
"设计了一个全新的复杂值类型支持方案,该方案支持的类型更多(只要struct的字段都是值类型即可),包括用户自定义的struct(别的方案都不支持)"
做到这一点,需要分析C#类型信息,看的出来xlua还是下了些功夫的,但是tolua, slua 均提供了常用的值类型例如Vector3的lua实现,用户在C#层自定义值类型需要导出到lua层使用,这种情况其实并不多见(甚至也不该这么做)。

我在工作中实际使用过静态语言,C/C++/Java/C#,  也使用过动态语言Python/Js/Lua。Lua/Js 相对 C/C++ 确实有明显的效率提高。但是和2000年之后的静态语言“C#/Scala/Go/Swift ” 相比却不尽然。TypeScript 的出现并流行,说明类型系统在实际复杂的工程实施中是非常有益处的。

其实我们想要的是在Unity下使用C#高效的开发,并可以热更新。
这里就不得不推荐我写的Bridge.lua编译器了。
yanghuan/bridge.lua
Bridge.lua可以将C#代码编译成Lua,生成的对应代码依然拥有良好的可读性,其已经在项目中得到验证并使用,欢迎需要的尝试并反馈。
发表于 2021-4-7 09:14 | 显示全部楼层
真羡慕全职做开源的同学
发表于 2021-4-7 09:14 | 显示全部楼层
应该是代码注入 改了csharp生成的il代码,使所有类都有了切换调用csharp函数和 lua函数的能力,性能不知道怎么样,这样普通的csharp类本身也要负担起lua的存储 和状态检测的一些开销
发表于 2021-4-7 09:22 | 显示全部楼层
无意中看到这个问题,也说说看法吧。
这几年手游中挂lua的方案比较流行,究其根本,主要目的都是为了热更,个人认为这个方向有点走的太邪了,你要是cocos用脚本我还比较好理解,毕竟c++写起来难一点,尤其是现在相当多的手游开发人员都没有什么端游开发的底子,代码质量比较堪忧,unity下挂lua,实在是必要性不大。

先说说xlua的技术方案,示例代码看了一下,不知道腾讯内部团队使用时候什么流程做得,只能说,开发用c#,补丁临时改lua,这个梦太美了一点,先不说你根本不知道bug会发生在哪里,其实就是所有代码都要打上hotfix标签,这个也能接受,但是所有例子的代码用的基本上都是内置类型,没有涉及复杂的游戏逻辑,c#下复杂的类和结构在代码中传来传去的情况,多线程使用下数据传递的情况,lua那边数据层还不是要跟c#预先对接,这种方案做下来,跟你所有逻辑开始就用Lua写有什么区别。更何况你在c#层写的东西,是不是一定能在lua实现,语言特性还有很大差别,比如lambda之类的玩意。

从工程层面来说,大部分做通用组件的程序,不大搞得清楚或者不愿意亲身接触实际开发中的复杂情况和需求变更的情况,这些通用组件在实际情况中使用和示例代码根本就是两个维度。就像上面有人回答过的一样,实际游戏逻辑,一个上百行的函数出了BUG,你改到LUA得多长时间,关键是,不是你改完就完了,还得测,多分支的关键函数,你要是C#开发的,你就改了某个情况两行代码,那测试可以就测那两行一个分支,你在lua上重写一遍,那就是所有跟这个函数相关的用例都要测一遍,这时间效率成本,何况你紧急更新草草测这两下,还不能保证质量,就怕搞出其他BUG。

说到底,为热更搞这个东西,根本就是穷折腾。现在项目的实际情况就是,开发的时候负责人对游戏根本没谱,东抄一点西抄一点,他也说不清可能更新些啥,或者后头还有更不靠谱的运营,他也不想替运营抗事,就跟程序说什么都可能热更,就在这种不靠谱的需求上出技术方案。严重复杂化整体的工程技术方案。

实际游戏运营,那些重要的功能更新,哪个不是开发测试老长时间才上,这种情况下,用不用lua有什么区别,何况重要功能往往伴随大量资源更新,你自己更资源还要自己弄cdn分发,自己做断点续传,成本和复杂性一下就上去了,苹果现成的分发机制不用,简直就是脑子烧着了,没几个国内用户用手机网络更新资源,wifi下10mb还是50mb其实没那么大区别。

至于说修复BUG,发布前测试干嘛去了?费这么大劲引入的复杂方案,我看造成BUG的几率少说翻一番,lua这种运行时才会出问题的语言,更严重依赖测试的完整性。

总而言之,这种方案,只适合家大业大的地方,测试人多不怕麻烦,事分的细可以相互甩锅,钱多不怕周期成本高。
发表于 2021-4-7 09:28 | 显示全部楼层
1.个人感觉更适合大公司大型项目,并且开发代码权限明确,开发人员职能分配更清楚游戏,要求软件发布时已经有比较高的质量的完善的功能。

2.个人感觉更适合更新周期比较长的一些海外代理项目等,更新则“运营人员”维护,而非“开发人员”。一个版本出来n个月不更新海外新版本,运营可以一直HotFix到代理需要有大版本更新。

2.个人感觉与其说是热更新,不如说是热修复,对象是“本期已发布更新”,而非“下期预发布更新”,下一版开发周期长,本期将一直使用HotFix维护。这样可以使当前版本的维护和开发脱离,维护不需要知道开发的代码,职责可以分配给其它部门,有助于代码的分级保密和防止运营向开发团队的甩锅。

3.个人感觉如果A不考虑性能,B更新频繁,C更强调“下一版本功能和数据更新”,而非“本版本bug修正数据调整”,D或者使用纯Lua的话,或许ulua和其它是一个更不错的选择。

4.如果开发部门总爱收到其他团队的旧版甩锅影响开发进度,那就。。把xLua甩给他们把。

5.只是个人理解,欢迎批评指正。
发表于 2021-4-7 09:28 | 显示全部楼层
歪个楼 大概2010年的时候 华为搞了一套一样的东西 运行时热更一个实现到目标 不过当时的目标语言是C 当时在组里深感牛逼 第一时间掌握了这个技术 然后乐此不疲的给各个部门的daily build打补丁玩儿

所以从NE5000E看到现在的华为终端真是 相当佩服华为这股努力走在前列的心气

然后现在或许可以加上一个腾讯^^
懒得打字嘛,点击右侧快捷回复 【右侧内容,后台自定义】
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

小黑屋|手机版|Unity开发者联盟 ( 粤ICP备20003399号 )

GMT+8, 2024-5-18 15:57 , Processed in 0.099060 second(s), 27 queries .

Powered by Discuz! X3.5 Licensed

© 2001-2024 Discuz! Team.

快速回复 返回顶部 返回列表