Unity3D游戏xlua轻量级热修复框架
Posted SChivas
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Unity3D游戏xlua轻量级热修复框架相关的知识,希望对你有一定的参考价值。
一 这是什么东西
前阵子刚刚集成xlua到项目,目的只有一个:对线上游戏C#逻辑有Bug的地方执行修复,通过考察xlua和tolua,最终选择了xlua,很大部分原因是因为项目已经到了后期,线上版本迭代了好几次,所以引入Lua的目的不是为了开发新版本模块。xlua在我们的这种情况下很是适用,如xlua作者所说,用C#开发,用lua热更,xlua这套框架为我们提供了诸多便利,至少我可以说,在面临同样的情况下,你用tolua去做同样的事情是很费心的。但是如果你是想用xlua做整套客户端游戏逻辑的,这篇文对你可能就没什么借鉴意义了。其实纯lua写逻辑,使用xlua还是tolua并不是那么重要,因为与c#交互会少很多,而且一般都是耗性能的地方才放c#,即使网上有各种lua框架性能的评测,其实我感觉意义都不太大,如果真要频繁调用,那不管xlua还是tolua你都要考虑方案去优化的。
当时在做完这个xlua热更框架,本打算写篇博文分享一下。后来,由于工作一直比较忙,这个事情就被搁浅了下来,另外,集成xlua时自己写的代码少得可伶,感觉也没什么太多要分享的地方。毕竟热修复,本质上来说就是一个轻量级的东西。除非你是新开的项目,一开始就遵循xlua热更的各种规范。而如果你是后期引入的xlua,那么,xlua热修复代码的复杂度,很大程度上取决于你框架原先c#代码的写法,比如说委托的使用,在c#侧经常作为回调去使用,xlua的demo里对委托的热修复示例是这样的:
1 public Action<string> TestDelegate = (param) => 2 { 3 Debug.Log("TestDelegate in c#:" + param); 4 }; 5 6 public void TestFunction(Action<string> callback) 7 { 8 //do something 9 callback("this is a test string"); 10 //do something 11 } 12 13 public void TestCall() 14 { 15 TestFunction(TestDelegate); 16 }
这里相当于把委托定义为了成员变量,那么你在lua侧,如果要热修复TestCall函数,要将这个委托作为回调传递给TestFunction,只需要使用self.TestDelegate就能访问,很简单。而问题就在于,我们项目之前对委托的使用方式是这样的:
1 public void TestDelegate(String param) 2 { 3 Debug.Log("TestDelegate in c#:" + param); 4 } 5 6 public void TestFunction(Action<string> callback) 7 { 8 //do something 9 callback("this is a test string"); 10 //do something 11 } 12 13 public void TestCall() 14 { 15 TestFunction(TestDelegate); 16 }
那么问题就来了,这个TestDelegate是一个函数,在调用的时候才自动创建了一个临时委托,那么Lua侧,你就没办法简单地去热更了,怎么办?这里我要说的就是类似这样的一些问题,因为一开始没有考虑过进行xlua热更,所以导致没有明确匹配xlua热更规则的相关代码规范,从而修复困难。
这个例子可能举得不是太好,你可以暴力修改项目中所有这样写法的地方(只要你乐意- -),另外,下面的这种写法有GC问题,这个问题是项目历史遗留下来的。
二 现行xlua分享的弊端
当初在集成xlua到项目时,发现现行网络上对xlua的大多分享,没有直接命中我所面临的问题,有实际借鉴意义的项目不多,对很多分享来说:
1)体积太重:集成了各种资源热更新、场景管理、音乐管理、定时器管理等等边缘模块,xlua内容反而显得太轻。
2)避重就轻:简单集成xlua,然后自己用NGUI或者UGUI写了个小demo,完事。
三 轻量级xlua热修复框架
其实说是xlua的一个扩展更加贴切,对xlua没有提供的一些外围功能进行了扩展。xlua的设计还是挺不错的,相比tolua的代码读起来还是要清爽多了。
3.1 框架工程结构
我假设你已经清楚了xlua做热修复的基本流程,因为下面不会对xlua本身的热更操作做太多说明。先一张本工程的截图:
xlua热修复框架工程结构
1)Scripts/xlua/XLuaManager:xlua热修复环境,包括luaState管理,自定义loader。
2)Resources/xlua/Main.lua:xlua热修复入口
3)Resources/xlua/Common:提供给lua代码使用的一些工具方法,提供lua逻辑代码到C#调用的一层封装
4)Scripts/xlua/Util:为xlua的lua脚本提供的C#侧代码支持,被Resources/xlua/Common所使用
5)Scripts/test/HotfixTest:需要热修复的c#脚本
6)Resources/xlua/HotFix:热修复脚本
需要说明的一点是,这里所有的热修复示例我都没有单独去做demo演示了,其实如果你真的需要,自己去写测试也没多大问题,所有Lua热更对应的C#逻辑都在,好进行对比。本文主要说的方向有这么几点:
1)消息系统:打通cs和lua侧的消息系统,其中的关键问题是泛型委托
2)对象创建:怎么样在lua侧创建cs对象,特别是泛型对象
3)迭代器:cs侧列表、字典之类的数据类型,怎样在lua侧泛型迭代
4)协程:cs侧协程怎么热更,怎么在lua侧创建协程
5)委托作为回调:cs侧函数用作委托回调,当作函数调用的形参时,怎样在lua侧传递委托形参
3.2 lua侧cs泛型对象创建
对象创建xlua给的例子很简单,直接new CS.XXX就好,但是如果你要创建一个泛型List对象,比如List<string>,要怎么弄?你可以为List<sting>在c#侧定义一个静态辅助类,提供类似叫CreateListString的函数去创建,但是你不可能为所有的类型都定义这样一层包装吧。所以,问题的核心是,我们怎么样在Lua侧只知道类型信息,就能让cs代劳给我们创建出对象:
1 --common.helper.lua 2 -- new泛型array 3 local function new_array(item_type, item_count) 4 return CS.XLuaHelper.CreateArrayInstance(item_type, item_count) 5 end 6 7 -- new泛型list 8 local function new_list(item_type) 9 return CS.XLuaHelper.CreateListInstance(item_type) 10 end 11 12 -- new泛型字典 13 local function new_dictionary(key_type, value_type) 14 return CS.XLuaHelper.CreateDictionaryInstance(key_type, value_type) 15 end
这是Resources/xlua/Common下的helper脚本其中的一部分,接下来的脚本我都会在开头写上模块名,不再做说明。这个目录下的代码为lua逻辑层代码提过对cs代码访问的桥接,这样做有两个好处:第一个是隐藏实现细节,第二个是容易更改实现。这里的三个接口都使用到了Scripts/xlua/Util下的XLuaHelper来做真实的事情。这两个目录下的脚本大概的职责都是这样的,Resources/xlua/Common封装lua调用,如果能用lua脚本实现,那就实现,不能实现,那在Resources/xlua/Common写cs脚本提供支持。下面是cs侧相关代码:
1 // CS.XLuaHelper 2 // 说明:扩展CreateInstance方法 3 public static Array CreateArrayInstance(Type itemType, int itemCount) 4 { 5 return Array.CreateInstance(itemType, itemCount); 6 } 7 8 public static IList CreateListInstance(Type itemType) 9 { 10 return (IList)Activator.CreateInstance(MakeGenericListType(itemType)); 11 } 12 13 public static IDictionary CreateDictionaryInstance(Type keyType, Type valueType) 14 { 15 return (IDictionary)Activator.CreateInstance(MakeGenericDictionaryType(keyType, valueType)); 16 }
3.3 lua侧cs迭代器访问
xlua作者在demo中给出了示例,只是个人觉得用起来麻烦,所以包装了一层语法糖,lua代码如下:
1 -- common.helper.lua 2 -- cs列表迭代器:含包括Array、ArrayList、泛型List在内的所有列表 3 local function list_iter(cs_ilist, index) 4 index = index + 1 5 if index < cs_ilist.Count then 6 return index, cs_ilist[index] 7 end 8 end 9 10 local function list_ipairs(cs_ilist) 11 return list_iter, cs_ilist, -1 12 end 13 14 -- cs字典迭代器 15 local function dictionary_iter(cs_enumerator) 16 if cs_enumerator:MoveNext() then 17 local current = cs_enumerator.Current 18 return current.Key, current.Value 19 end 20 end 21 22 local function dictionary_ipairs(cs_idictionary) 23 local cs_enumerator = cs_idictionary:GetEnumerator() 24 return dictionary_iter, cs_enumerator 25 end
这部分代码不需要额外的cs脚本提供支持,只是实现了lua的泛型迭代,能够用在lua的for循环中,使用代码如下(只给出列表示例,对字典是类似的):
1 -- common.helper.lua 2 -- Lua创建和遍历泛型列表示例 3 local helper = require \'common.helper\' 4 local testList = helper.new_list(typeof(CS.System.String)) 5 testList:Add(\'111\') 6 testList:Add(\'222\') 7 testList:Add(\'333\') 8 print(\'testList\', testList, testList.Count, testList[testList.Count - 1]) 9 10 -- 注意:循环区间为闭区间[0,testList.Count - 1] 11 -- 适用于列表子集(子区间)遍历 12 for i = 0, testList.Count - 1 do 13 print(\'testList\', i, testList[i]) 14 end 15 16 -- 说明:工作方式与上述遍历一样,使用方式上雷同lua库的ipairs,类比于cs的foreach 17 -- 适用于列表全集(整区间)遍历,推荐,很方便 18 -- 注意:同cs的foreach,遍历函数体不能修改i,v,否则结果不可预料 19 for i, v in helper.list_ipairs(testList) do 20 print(\'testList\', i, v) 21 end
要看懂这部分的代码,需要知道lua中的泛型for循环是怎么样工作的:
1 for var_1, ..., var_n in explist do 2 block 3 end
对于如上泛型for循环通用结构,其代码等价于:
1 do 2 local _f, _s, _var = explist 3 while true do 4 local var_1, ... , var_n = _f(_s, _var) 5 _var = var_1 6 if _var == nil then break end 7 block 8 end 9 end
泛型for循环的执行过程如下:
首先,初始化,计算 in 后面表达式的值,表达式应该返回范性 for 需要的三个值:迭代函数_f,状态常量_s和控制变量_var;与多值赋值一样,如果表达式返回的结果个数不足三个会自动用 nil 补足,多出部分会被忽略。
第二,将状态常量_s和控制变量_var作为参数调用迭代函数_f(注意:对于 for 结构来说,状态常量_s没有用处,仅仅在初始化时获取他的值并传递给迭代函数_f)。
第三,将迭代函数_f返回的值赋给变量列表。
第四,如果返回的第一个值为 nil 循环结束,否则执行循环体。
第五,回到第二步再次调用迭代函数。
如果控制变量的初始值是 a0,那么控制变量将循环:a1=_f(_s,a0)、a2=_f(_s,a1)、……,直到 ai=nil。对于如上列表类型的迭代,其中explist = list_ipairs(cs_ilist),根据第一点,可以得到_f = list_iter,_s = cs_ilist, _var = -1,然后进入while死循环,此处每次循环拿_s = cs_ilist, _var = -1作为参数调用_f = list_iter,_f = list_iter内部对_var执行自增,所以这里的_var就是一个计数变量,也是list的index下标,返回值index、cs_ilist[index]赋值给for循环中的i、v,当遍历到列表末尾时,两个值都被赋值为nil,循环结束。这个机制和cs侧的foreach使用迭代器的工作机制是有点雷同的,如果你清楚这个机制,那么这里的原理就不难理解。
3.4 lua侧cs协程热更
先看cs侧协程的用法:
1 // cs.UIRankMain 2 public override void Open(object param, UIPathData pathData) 3 { 4 // 其它代码省略 5 StartCoroutine(TestCorotine(3)); 6 } 7 8 IEnumerator TestCorotine(int sec) 9 { 10 yield return new WaitForSeconds(sec); 11 Logger.Log(string.Format("This message appears after {0} seconds in cs!", sec)); 12 yield break; 13 }
很普通的一种协程写法,下面对这个协程的调用函数Open,协程函数体TestCorotine执行热修复:
1 -- HotFix.UIRankMainTest.lua 2 -- 模拟Lua侧的异步回调 3 local function lua_async_test(seconds, coroutine_break) 4 print(\'lua_async_test \'..seconds..\' seconds!\') 5 -- TODO:这里还是用Unity的协程相关API模拟异步,有需要的话再考虑在Lua侧实现一个独立的协程系统 6 yield_return(CS.UnityEngine.WaitForSeconds(seconds)) 7 coroutine_break(true, seconds) 8 end 9 10 -- lua侧新建协程:本质上是在Lua侧建立协程,然后用异步回调驱动, 11 local corotineTest = function(self, seconds) 12 print(\'NewCoroutine: lua corotineTest\', self) 13 14 local s = os.time() 15 print(\'coroutine start1 : \', s) 16 -- 使用Unity的协程相关API:实际上也是CS侧协程结束时调用回调,驱动Lua侧协程继续往下跑 17 -- 注意:这里会在CS.CorotineRunner新建一个协程用来等待3秒,这个协程是和self没有任何关系的 18 yield_return(CS.UnityEngine.WaitForSeconds(seconds)) 19 print(\'coroutine end1 : \', os.time()) 20 print(\'This message1 appears after \'..os.time() - s..\' seconds in lua!\') 21 22 local s = os.time() 23 print(\'coroutine start2 : \', s) 24 -- 使用异步回调转同步调用模拟yield return 25 -- 这里使用cs侧的函数也是可以的,规则一致:最后一个参数必须是一个回调,回调被调用时表示异步操作结束 26 -- 注意: 27 -- 1、如果使用cs侧函数,必须将最后一个参数的回调(cs侧定义为委托)导出到[CSharpCallLua] 28 -- 2、用cs侧函数时,返回值也同样通过回调(cs侧定义为委托)参数传回 29 local boolRetValue, secondsRetValue = util.async_to_sync(lua_async_test)(seconds) 30 print(\'coroutine end2 : \', os.time()) 31 print(\'This message2 appears after \'..os.time() - s..\' seconds in lua!\') 32 -- 返回值测试 33 print(\'boolRetValue:\', boolRetValue, \'secondsRetValue:\', secondsRetValue) 34 end 35 36 -- 协程热更示例 37 xlua.hotfix(CS.UIRankMain, \'Open\', function(self, param, pathData) 38 print(\'HOTFIX:Open \', self) 39 -- 省略其它代码 40 -- 方式一:新建Lua协程,优点:可新增协程;缺点:使用起来麻烦 41 print(\'----------async call----------\') 42 util.coroutine_call(corotineTest)(self, 4)--相当于CS的StartCorotine,启动一个协程并立即返回 43 print(\'----------async call end----------\') 44 45 -- 方式二:沿用CS协程,优点:使用方便,可直接热更协程代码逻辑,缺点:不可以新增协程 46 self:StartCoroutine(self:TestCorotine(3)) 47 end) 48 49 -- cs侧协程热更 50 xlua.hotfix(CS.UIRankMain, \'TestCorotine\', function(self, seconds) 51 print(\'HOTFIX:TestCorotine \', self, seconds) 52 --注意:这里定义的匿名函数是无参的,全部参数以闭包方式传入 53 return util.cs_generator(function() 54 local s = os.time() 55 print(\'coroutine start3 : \', s) 56 --注意:这里直接使用coroutine.yield,跑在self这个MonoBehaviour脚本中 57 coroutine.yield(CS.UnityEngine.WaitForSeconds(seconds)) 58 print(\'coroutine end3 : \', os.time()) 59 print(\'This message3 appears after \'..os.time() - s..\' seconds in lua!\') 60 end) 61 end)
代码看起来有点复杂,但是实际上要说的点都在代码注释中了。xlua作者已经对协程做了比较好的支持,不需要我们另外去操心太多。
3.5 lua侧创建cs委托回调
这里回归的是篇头所阐述的问题,当cs侧某个函数的参数是一个委托,而调用方在cs侧直接给了个函数,在lua侧怎么去热更的问题,先给cs代码:
1 // cs.UIArena 2 private void UpdateDailyAwardItem(List<BagItemData> itemList) 3 { 4 if (itemList == null) 5 { 6 return; 7 } 8 9 for (int i = 0; i < itemList.Count; i++) 10 { 11 UIGameObjectPool.instance.GetGameObject(ResourceMgr.RESTYPE.UI, TheGameIds.UI_BAG_ITEM_ICON, new GameObjectPool.CallbackInfo(onBagItemLoad, itemList[i], Vector3.zero, Vector3.one * 0.65f, m_awardGrid.gameObject)); 12 } 13 m_awardGrid.Reposition(); 14 }
这是UI上面普通的一段异步加载背包Item的Icon资源问题,资源层异步加载完毕以后回调到当前脚本的onBagItemLoa函数对UI资源执行展示。现在就这段代码执行一下热修复:
1 -- HotFix.UIArenaTese.lua 2 -- 回调热更示例(消息系统的回调除外) 3 -- 1、缓存委托 4 -- 2、Lua绑定(实际上是创建LuaFunction再cast到delegate),需要在委托类型上打[CSharpCallLua]标签--推荐 5 -- 3、使用反射再执行Lua绑定 6 xlua.hotfix(CS.UIArena, \'UpdateDailyAwardItem\', function(self, itemList) 7 print(\'HOTFIX:UpdateDailyAwardItem \', self, itemList) 8 9 if itemList == nil then 10 do return end 11 end 12 13 for i, item in helper.list_ipairs(itemList) do 14 -- 方式一:使用CS侧缓存委托 15 local callback1 = self.onBagItemLoad 16 -- 方式二:Lua绑定 17 local callback2 = util.bind(function(self, gameObject, object) 18 self:OnBagItemLoad(gameObject, object) 19 end, self) 20 -- 方式三: 21 -- 1、使用反射创建委托---这里没法直接使用,返回的是Callback<,>类型,没法隐式转换到CS.GameObjectPool.GetGameObjectDelegate类型 22 -- 2、再执行Lua绑定--需要在委托类型上打[CSharpCallLua]标签 23 -- 注意: 24 -- 1、使用反射创建的委托可以直接在Lua中调用,但作为参数时,必须要求参数类型一致,或者参数类型为Delegate--参考Lua侧消息系统实现 25 -- 2、正因为存在类型转换问题,而CS侧的委托类型在Lua中没法拿到,所以在Lua侧执行类型转换成为了不可能,上面才使用了Lua绑定 26 -- 3、对于Lua侧没法执行类型转换的问题,可以在CS侧去做,这就是[CSharpCallLua]标签的作用,xlua底层已经为我们做好这一步 27 -- 4、所以,这里相当于方式二多包装了一层委托,从这里可以知道,委托做好全部打[CSharpCallLua]标签,否则更新起来很受限 28 -- 5、对于Callback和Action类型的委托(包括泛型)都在CS.XLuaHelper实现了反射类型创建,所以不需要依赖Lua绑定,可以任意使用 29 -- 静态函数测试 30 local delegate = helper.new_callback(typeof(CS.UIArena), \'OnBagItemLoad2\', typeof(CS.UnityEngine.GameObject), typeof(CS.System.Object)) 31 delegate(self.gameObject, nil) 32 -- 成员函数测试 33 local delegate = helper.new_callback(self, \'OnBagItemLoad\', typeof(CS.UnityEngine.GameObject), typeof(CS.System.Object)) 34 local callback3 = util.bind(function(self, gameObject, object) 35 delegate(gameObject, object) 36 end, self) 37 38 -- 其它测试:使用Lua绑定添加委托:必须[CSharpCallLua]导出委托类型,否则不可用 39 callback5 = callback1 + util.bind(function(self, gameObject, object) 40 print(\'callback4 in lua\', self, gameObject, object) 41 end, self) 42 43 local callbackInfo = CS.GameObjectPool.CallbackInfo(callback3, item, Vector3.zero, Vector3.one * 0.65, self.m_awardGrid.gameObject) 44 CS.UIGameObjectPool.instance:GetGameObject(CS.ResourceMgr.RESTYPE.UI, CS.TheGameIds.UI_BAG_ITEM_ICON, callbackInfo) 45 end 46 self.m_awardGrid:Reposition() 47 end)
有三种可行的热修复方式:
1)缓存委托:就是在cs侧不要直接用函数名来作为委托参数传递(会临时创建一个委托),而是在cs侧用一个成员变量缓存委托,并使用函数初始化它,使用时直接self.xxx访问。
2)Lua绑定:创建一个闭包,需要在cs侧的委托类型上打上[CSharpCallLua]标签,实际上xlua作者建议将工程中所有的委托类型打上这个标签。
3)使用反射再执行lua绑定:这种方式使用起来很受限,这里不再做说明,要了解的朋友自己参考源代码。
3.6 打通lua和cs的消息系统
cs侧消息系统使用的是这个:http://wiki.unity3d.com/index.php/Advanced_CSharp_Messenger。里面使用了泛型编程的思想,xlua作者在demo中针对泛型接口的热修复给出的建议是实现扩展函数,但是扩展函数需要对一个类型去做一个接口,这里的消息系统类型完全是可以任意的,显然这种方案显得捉襟见肘。核心的问题只有一个,怎么根据参数类型信息去动态创建委托类型。
委托类型其实是一个数据结构,它引用静态方法或引用类实例及该类的实例方法。在我们定义一个委托类型时,C#会创建一个类,有点类似C++函数对象的概念,但是它们还是相差很远,由于时间和篇幅关系,这里不再做太多说明。总之这个数据结构在lua侧是无法用类似CS.XXX去访问到的,正因为如此,所以才为什么所有的委托类型都需要打上[CSharpCallLua]标签去做一个映射表。lua不能访问到cs委托类型,没关系,我们可以在cs侧创建出来就行了。而Delegate 类是委托类型的基类,所有的泛型委托类型都可通过它进行函数调用的参数传递,解决泛型委托的传参问题。先看下lua怎么用这个消息系统:
1 -- HotFix.UIArenaTest.lua 2 -- Lua消息响应 3 local TestLuaCallback = function(self, param) 4 print(\'LuaDelegateTest: \', self, param, param and param.rank) 5 end 6 7 local TestLuaCallback2 = function(self, param) 8 print以上是关于Unity3D游戏xlua轻量级热修复框架的主要内容,如果未能解决你的问题,请参考以下文章