xLua热更新
学习目标
导入xLua框架
C#调用Lua
Lua调用C#
xLua热补丁
C#调用lua(相对较少)
准备工作
导入xLua
导入AssetBundleBrower
导入AB包管理器ABMgr
导入BaseManager
LuaEnv
Lua解析器能够让我们在Unity中执行Lua
一般情况下,保持它的唯一性
1 LuaEnv env = new LuaEnv();
使用 DoString
直接执行Lua语言
1 env.DoString("print('你好世界')" );
执行一个Lua脚本 Lua知识点 :多脚本执行 require
注意:默认寻找脚本的路径是在 Resources
下,并且因为在这里,可能是通过 Resources.Load
去加载Lua脚本, 它只能加载txt bytes
等等后缀的文件无法加载.lua
后缀文件。所以Lua脚本后缀要加一个.txt
1 env.DoString("require('Main')" );
帮助我们清除Lua中我们没有手动释放的对象 (垃圾回收)
帧更新中定时执行或者切场景时执行
销毁Lua解析器
文件重定向
由于直接使用DoString,默认是从Resources
下加载Lua文件,当我们需要在其他文件目录加载,LuaEnv提供了一个AddLoader 方法,可以让我们自定义文件加载目录。
AddLoader
方法参数是一个委托类型:delegate byte[] CustomLoader(ref strin filepath)
具体使用添加自定义加载器
1 2 3 4 5 6 7 8 9 10 11 12 13 14 public byte [] MyAddLoader (ref string path ){ string filepath = Application.dataPath + "/Lua/" + path + ".lua" ; if (File.Exists(filepath)) { return File.ReadAllBytes(filepath); } else { Debug.LogError("env加载lua文件 文件重定向失败,文件名为:" +path); } return null ; }
注册加载器
1 2 LuaEnv env = new LuaEnv(); env.AddLoader(MyAddLoader);
LuaEnv解析器在执行require("filename")
的执行流程是:
首先从加载器委托中依次从自定义加载器中获取文件内容的byte[],所有自定义加载器都返回null
,则默认从Resources文件夹下加载。
如果自定义加载器返回不为空,则后面的自定义加载器和默认加载不执行
注:因为自定义加载器使用字节流加载lua文件,所以不需要在lua文件后添加.txt
后缀
局限 :
本节实现的加载器只能从一个目录当中加载lua文件,不是从AB包加载文件,下一小节将实现一个从AB包当中加载lua文件的自定义加载器。
Lua解析器管理器
从AB包加载文件的方法是,主要是用LoadAsset
方法从AB包中加载成对应的TextAsset
资源,再返回byte[]
,因为LoadAsset
方法和Resources.Load
一样不支持.lua
文件,所以需要添加.txt
后缀
一般来说,只有在最后打包时,才会将文件后缀改为.txt
,平时只需要正常加载就好了。后面会实现一键将某个目录下的lua文件,全部添加.txt
后缀,并设置AssetBundle后打包
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 using System.Collections;using System.Collections.Generic;using System.IO;using UnityEngine;using XLua;public class LuaMgr : BaseManager <LuaMgr >{ private LuaEnv luaEnv; public LuaTable Global { get { return luaEnv.Global; } } public void Init () { if (luaEnv != null ) return ; luaEnv = new LuaEnv(); luaEnv.AddLoader(MyCustomLoader); luaEnv.AddLoader(MyCustomABLoader); } private byte [] MyCustomLoader (ref string filePath ) { string path = Application.dataPath + "/Lua/" + filePath + ".lua" ; if (File.Exists(path)) { return File.ReadAllBytes(path); } else { Debug.Log("MyCustomLoader重定向失败,文件名为" + filePath); } return null ; } private byte [] MyCustomABLoader (ref string filePath ) { TextAsset lua = ABMgr.GetInstance().LoadRes<TextAsset>("lua" , filePath + ".lua" ); if (lua != null ) return lua.bytes; else Debug.Log("MyCustomABLoader重定向失败,文件名为:" + filePath); return null ; } public void DoLuaFile (string fileName ) { string str = string .Format("require('{0}')" , fileName); DoString(str); } public void DoString (string str ) { if (luaEnv == null ) { Debug.Log("解析器为初始化" ); return ; } luaEnv.DoString(str); } public void Tick () { if (luaEnv == null ) { Debug.Log("解析器为初始化" ); return ; } luaEnv.Tick(); } public void Dispose () { if (luaEnv == null ) { Debug.Log("解析器为初始化" ); return ; } luaEnv.Dispose(); luaEnv = null ; } }
全局变量的获取和设置
LuaEnv的Global属性是LuaTable,对应lua的_G
表,他有Get和Set方法用来对lua的全局变量获取和设置,无法获取局部变量
Test.lua
1 2 3 4 testNum = 1 testBool = true testFloat = 0.1 testString = "123"
Unity C#
1 2 3 4 5 6 7 8 9 LuaEnv luaEnv = new LuaEnv(); print(luaEnv.Global.Get<int >("testNum" )); print(luaEnv.Global.Get<bool >("testBool" )); print(luaEnv.Global.Get<string >("testString" )); luaEnv.Global.Set<string ,int >("testNum" , 100 ); luaEnv.Global.Set("testNum" , 10 );
全局函数的获取
与全局变量的获取类似,也是通过LuaEnv.Global.Get()获取,泛型T是一种委托用来存储获取的函数
Lua全局函数类型分四种
Test.lua
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 testFun = function () print ("无参无返回" ) end testFun2 = function (a) print ("有参有返回" ) return a+1 end testFun3 = function (a) print ("多返回值" ) return 1 , 2 , false , "123" , a end testFun = function (a, ...) print ("变长参数" ) arg = {...} print (a) for k,v in pairs (arg ) do print (k,v) end end
无返回值
可以自己声明委托,也可使用现成的委托如Unity的UnityAction
、C#的Action
,还有xLua提供的LuaFunction
接收
自定义委托声明
1 public delegate void CustomCall () ;
接受函数
1 2 3 4 5 6 7 8 9 10 11 CustomCall call = LuaMgr.GetInstance().Global.Get<CustomCall>("testFun" ); call(); UnityAction ua = LuaMgr.GetInstance().Global.Get<UnityAction>("testFun" ); ua(); Action ac = LuaMgr.GetInstance().Global.Get<Action>("testFun" ); ac(); LuaFunction lf = LuaMgr.GetInstance().Global.Get<LuaFunction>("testFun" ); lf.Call();
有参有返回
与无返回值的委托不同,xLua默认是可以识别无返回值的委托的,所以另外三种类型的委托,如果需要自定义的话需要加上[CSharpCallLua]
特性,同时需要重新生成Wrap代码
自定义委托声明:
1 2 [CSharpCallLua ] public delegate int CustomCall2 (int a ) ;
接收函数:
1 2 3 4 5 6 7 8 9 CustomCall2 call2 = LuaMgr.GetInstance().Global.Get<CustomCall2>("testFun2" ); Debug.Log("有参有返回:" + call2(10 )); Func<int , int > sFun = LuaMgr.GetInstance().Global.Get<Func<int , int >>("testFun2" ); Debug.Log("有参有返回:" + sFun(20 )); LuaFunction lf2 = LuaMgr.GetInstance().Global.Get<LuaFunction>("testFun2" ); Debug.Log("有参有返回:" + lf2.Call(30 )[0 ]);
多返回值
多返回值,因为在C#中函数只能返回一个值,所以多返回可以通过ref或out参数或者变长参数来接收,将参数作为返回值。
自定义委托
1 2 3 4 [CSharpCallLua ] public delegate int CustomCall3 (int a, out int b, out bool c, out string d, out int e ) ;[CSharpCallLua ] public delegate int CustomCall4 (int a, ref int b, ref bool c, ref string d, ref int e ) ;
函数接收:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 CustomCall3 call3 = LuaMgr.GetInstance().Global.Get<CustomCall3>("testFun3" ); int b;bool c;string d;int e;Debug.Log("第一个返回值:" + call3(100 , out b, out c, out d, out e)); Debug.Log(b + "_" + c + "_" + d + "_" + e); CustomCall4 call4 = LuaMgr.GetInstance().Global.Get<CustomCall4>("testFun3" ); int b1 = 0 ;bool c1 = true ;string d1 = "" ;int e1 = 0 ;Debug.Log("第一个返回值:" + call4(200 , ref b1, ref c1, ref d1, ref e1)); Debug.Log(b1 + "_" + c1 + "_" + d1 + "_" + e1); LuaFunction lf3 = LuaMgr.GetInstance().Global.Get<LuaFunction>("testFun3" ); object [] objs = lf3.Call(1000 );for ( int i = 0 ; i < objs.Length; ++i ){ Debug.Log("第" + i + "个返回值是:" + objs[i]); }
变长参数
自定义委托
1 2 [CSharpCallLua ] public delegate void CustomCall5 (string a, params int [] args ) ;
函数接收:
1 2 3 4 5 CustomCall5 call5 = LuaMgr.GetInstance().Global.Get<CustomCall5>("testFun4" ); call5("123" , 1 , 2 , 3 , 4 , 5 , 566 , 7 , 7 , 8 , 9 , 99 ); LuaFunction lf4 = LuaMgr.GetInstance().Global.Get<LuaFunction>("testFun4" ); lf4.Call("456" , 6 , 7 , 8 , 99 , 1 );
总结:
C#用委托来接收lua函数,根据函数声明结构可以使用自定义委托、Unity的UnityAction、C#的Action、Func或者xLua的LuaFunction,虽然这几种函数都可以用LuaFunciton接收,但是LuaFunciton定义的返回值是byte[],所以有可能产生垃圾,官方也不推荐。
自定义委托需要注意除无返回值的函数都需要加上[CSharpCallLua]
特性,并生成Wrap代码
List和Dictionary映射table
List
获取指定类型
1 2 3 4 5 testList = {1 ,2 ,3 ,4 ,5 ,6 } List<int > list = LuaMgr.GetInstance().Global.Get<List<int >>("testList" );
获取不同类型
使用object
1 2 3 4 5 testList2 = {"123" , "123" , true , 1 , 1.2 } List<object > list3 = LuaMgr.GetInstance().Global.Get<List<object >>("testList2" );
Dictionary
获取指定类型
1 2 3 4 5 6 7 8 9 10 testDic = { ["1" ] = 1 , ["2" ] = 2 , ["3" ] = 3 , ["4" ] = 4 } Dictionary<string , int > dic = LuaMgr.GetInstance().Global.Get<Dictionary<string , int >>("testDic" );
获取不同类型
使用object
1 2 3 4 5 6 7 8 9 10 testDic2 = { ["1" ] = 1 , [true ] = 1 , [false ] = true , ["123" ] = false } Dictionary<object , object > dic3 = LuaMgr.GetInstance().Global.Get<Dictionary<object , object >>("testDic2" );
类映射table
lua文件
1 2 3 4 5 6 7 8 9 10 11 12 testClas = { testInt = 2 , testBool = true , testFloat = 1.2 , testString = "123" , testFun = function () print ("123123123" ) end testInClass = { testInInt = 10 ; } }
在C#声明一个类,成员变量的名字和类型一定要和lua方一致
要映射的只能是public,private和protected无法赋值
如果变量比lua中的少,就会忽略它
如果变量比lua中的多,不会赋值,也会忽略
类成员,和上述要求一致就会赋值
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 public class CallLuaInClass { public int testInInt; } public class CallLuaClass { public int testInt; public bool testBool; public float testString; public UnityAction testFun; public CallLuaInClass testInClass; public int i; public void Test () { Debug.Log(testInt); } }
映射接收table
1 CallLuaClass obj = LuaMgr.GetInstance().Global.Get<CallLuaClass>("testClas" );
可以看出接受的table有嵌套table
接口映射table
与类映射table类似只有以下需要注意的地方:
但是需要使用属性 与table的元素对应,而不是成员变量
需要在接口声明上添加[CSharpCallLua]
特性并生成Wrap代码,并且用来接收的接口对象修改属性值可以改变lua table 中的值
接口声明:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 [CSharpCallLua ] public interface ICSharpCallInterface { int testInt { get ; set ; } bool testBool { get ; set ; } string testString { get ; set ; } UnityAction testFun { get ; set ; } float testFloat222 { get ; set ; } }
用接口接收table:
1 2 3 4 5 6 ICSharpCallInterface obj = LuaMgr.GetInstance().Global.Get<ICSharpCallInterface>("testClas" ); Debug.Log(obj.testInt); obj.testInt = 10000 ; ICSharpCallInterface obj2 = LuaMgr.GetInstance().Global.Get<ICSharpCallInterface>("testClas" ); Debug.Log(obj2.testInt);
LuaTable映射table
LuaEnv.Global是LuaTble类型,也可以用LuaTable来接收table,那么他的使用就和之前一样使用Get和Set获取和修改table中的值
1 2 3 4 5 6 7 LuaTable table = LuaMgr.GetInstance().Global.Get<LuaTable>("testClas" ); Debug.Log(table.Get<int >("testInt" )); table.Set("testInt" , 55 ); Debug.Log(table.Get<int >("testInt" )); LuaTable table2 = LuaMgr.GetInstance().Global.Get<LuaTable>("testClas" ); Debug.Log(table2.Get<int >("testInt" ));
LuaTable使用完需要释放
注:不建议使用LuaTable和LuaFunction 效率低
总结
总的来说,不管是全局变量、全局函数和table都是使用LuaEnv对象的Global属性的Get()方法来获取的,获取的lua对象选择合适的接收对象,比如全局变量那就是int、string这种基础类型接收,全局函数就是用委托类型对象来接受,table则可以使用类、接口、List、Dictionary、LuaTable来接受。
需要注意的是[CSharpCallLua]
特性可能需要加在自定义委托和接口上,在C#层面的修改在lua层面也被修改,那么只有接口和LuaTable对象可以实现
Lua调用C#
类
lua中使用C#的类非常简单 CS.命名空间.类名
1 2 3 4 GameObject = CS.UnityEngine.GameObject
实例化:
1 2 GameObject = CS.UnityEngine.GameObject local obj = GameObject("游戏物体" )
成员变量和成员方法:
成员方法必须要用: ,成员方法第一个参数为自己的对象
1 2 3 4 5 6 print (obj.transform.position)Vector3 = CS.UnityEngine.Vector3 obj.transform:Translate(Vector3.right)
静态方法:
1 2 local obj = GameObject.Find("游戏物体" )
继承了MonoBehaviour的类:
因为继承了MonoBehaviour的类,不能直接new
,他是在游戏物体初始化的时候实例化的,所以我们需要在游戏物体上挂载该脚本
通过GameObject的 AddComponent添加脚本
xlua提供了一个重要方法 typeof 可以得到类的Type
xlua中不支持无参泛型函数AddComponent<T>()
所以我们要使用另一个重载:
1 obj:AddComponent(typeof(CS.MyMonoCLass))
游戏开始:
因为游戏开始是执行的C#代码,所以我们一般是写一个Main类在游戏开始时执行调用lua文件的代码,之后才可以使用lua来调用C#
枚举
枚举和类的使用一摸一样,只不过不存在实例化,CS.命名空间.枚举名.枚举成员
1 CS.UnityEngine.PermitiveType.Cube
数组、List、字典
C#
1 2 3 4 5 public class Test { public int [] array = new int [5 ]{1 ,2 ,3 ,4 ,5 }; public List<int > list = new List<int >(); public Dictionary<int , string > dic = new Dictonary<int , string >(); }
因为lua调用C#代码,实际上使用的userdata来存储,所以使用方式跟C#一样,例如数组长度使用.Length
获取, 而不是使用#
获取长度
数组:
1 2 3 4 5 6 7 8 9 10 11 12 local array = CS.Test().arrayprint (array.Length)print (array[0 ])for i=0 , array.Length-1 do print (array[i]) end local newArray = CS.System.Array.CreateInstance(System.Int32, 10 )
虽然lua中数组的遍历,索引是从1开始的,但是这里还是按照C#的规则
数组的本质是CS.System.Array
,所以调用他的CreateInstance
静态方法创建
List:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 local list = CS.Test().listprint (list.Count)list:Add(100 ) print (list[0 ])for i=0 , list.Count-1 do print (list[i]) end local list2 = CS.System.Generic["List`[System.Int32]" ]()local List_Int = CS.System.Generic.List(Cs.System.Int32) local list3 = List_Int()
主要注意新老版本的创建方式
Dictionary
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 local dic = CS.Test().dicdic:Add(1 ,"123" ) for k,v in pair(dic) do print (k,v) end print (dic[1 ]) dic.get_Item(1 ) dic.set_Item(1 , "321" ) local _Dic = CS.System.Generic.Dictionary(CS.System.String, CS.UnityEngine.Vector3)local dic2 = _Dic()dic2:Add("111" , CS.UnityEngine.Vector3.right)
字典的获取比较特殊必须,必须使用get_Item
固定方法
字典的创建跟List一样有新老版本
函数–拓展方法
C#代码:
1 2 3 4 5 6 7 8 9 [LuaCallCSharp ] public static class Tool { public static void Move (this User user ) { Debug.Log(obj.name+ "在移动" ); } } public class User { public string name = "达文西" }
Lua使用:
1 2 local user = CS.User()user:Move()
lua中使用拓展方法跟其他成员方法没什么不同,重点是需要在工具类上添加[LuaCallCSharp]
特性,并生成对应Wrap文件
建议所以lua中要使用到的C#类都加上[LuaCallCSharp]
这个特性 ,虽然之前其他类没有加上该特性也可以使用,这是因为没有加上该特性xLua依旧可以通过反射来访问到C#类,但是反射的效率比较低,而使用该特性并生成Wrap文件,相当于访问Wrap文件效率较高
函数–ref和out
之前在C#调用lua中调用lua的多返回值时,是使用了ref或out来接收除第一个返回值的其他返回值,所以这一小节没啥好说的,只是从lua的角度来看,使用利用了lua可以返回多个值来保存C#中的返回值和ref和out修饰的值
1 2 3 4 5 6 7 8 9 10 11 12 13 [LuaCallCSharp ] public class Test { public int RefFunc (int a, ref int b, ref int c, int d ) { b=a+d c=a-d return 100 ; } public int OutFunc (int a, out int b, out int c, int d ) { b=a; c=d; return 200 ; } }
lua使用:
1 2 3 4 5 6 local obj = CS.Test()local a,b,c = obj.RefFunc(1 ,0 ,0 ,1 ) local a,b,c = obj.OutFunc(20 , 30 )
注意: ref修饰的不可以省略,可以使用默认值在参数列表中占位,out修饰的可以省略
函数–重载
lua支持调用C#中的重载函数,但是lua本身不支持重载函数
1 2 3 4 5 6 7 8 9 10 11 12 [LuaCallCSharp ] public class Test { public int Calc () { return 100 ; } public int Calc (int a ) { return a; } public float Calc (float a ) { return a; } }
lua调用C#:
1 2 3 4 local obj = CS.Test()print (obj:Calc()) print (obj:Calc(10 )) print (obj:Calc(10.2 ))
虽然lua支持C#函数的重载,但是因为lua中的数值类型只有Number,所以对C#中的多精度重载函数支持不好,可能会发生意想不到的问题,所以如果要在lua中使用C#类中的重载,尽量避免这种多精度重载。
如果必须要使用多精度重载有办法解决吗?
可以,xLua提供了解决方案(xlua.tofunction
),但是还是尽量别用 ,因为他是使用了反射机制,效率较低
1 2 3 4 5 6 7 8 9 10 local m1=typeof(CS.Test):GetMethod("Calc" ,{typeof(CS.System.Int32)})local m2=typeof(CS.Test):GetMethod("Calc" ,{typeof(CS.System.Single)}) local f1 = xlua.tofunction(m1)local f2 = xlua.tofunction(m2)print (f1(obj, 10 ))print (f1(obj, 10.2 ))
委托和事件
C#代码:
1 2 3 4 5 6 7 8 9 10 public class Test7 { public UnityAction del; public event UnityAction eventAction; public void DoEvent () { if (eventAction != null ) eventAction(); } }
lua代码–委托:
1 2 3 4 5 6 7 8 9 10 11 12 13 local obj = CS.Test7()local fun = function () print ("Lua函数" ) end obj.del = fun obj.del = obj.del + fun obj.del = obj.del - fun obj.del() obj.del = nil obj.del = fun
lua代码–事件:
1 2 3 4 5 6 7 8 9 10 11 12 local fun2 = function () print ("事件加函数" ) end obj:eventAction("+" , fun2) obj:eventAction("+" , fun2) obj:eventAction("-" , fun2) obj:DoEvent()
注意:
事件加减 和 委托非常不一样
事件不能直接清空,只能在C#代码中添加一个清空函数,然后lua中调用该函数
特殊问题–二维数组遍历
C#代码:
1 2 3 public class Test8 { public int [,] array = new int [2 ,3 ]{{1 ,2 ,3 },{4 ,5 ,6 }}; }
lua代码:
1 2 3 4 local obj = CS.Test8()print (obj.array.GetValue(0 ,0 ))print (obj.array.GetValue(0 ,0 ))
二维数组本质上是Array类型, 它有一些像GetValue
一样的方法访问元素,lua脚本不能像C#一样通过array[0,0]
这样访问, 我的理解是这样只是属于C#的语法糖本上还是通过GetValue
调用的
特殊问题–null和nil的比较
nil和null无法映射,不能==
比较,所以一般用obj:Equals(nil)
,前提是对象是一个Object
方案一: 全局函数判断:
1 2 3 4 5 6 function IsNull (obj) if obj == nil or obj:Equals(nil ) then reutrn true end return false end
方案二: C#为Object拓展一个方法
1 2 3 4 5 6 [LuaCallCSharp ] public static class Tool { public static bool IsNull (this Object obj ) { return obj==null ; } }
1 2 3 if obj:IsNull then print ("拓展方法" ) end
特殊问题–系统类型和Lua互相访问
CSharpCallLua 可以让lua调用C#的委托和接口
LuaCallCSharp 可以让Lua调用C#的拓展方法, 建议所有被lua调用的C#类添加
无法为系统类或者第三方类库加上这两种特性:
1 2 3 4 5 6 7 8 9 10 11 public static class Test9 { [CSharpCallLua ] public static List<Type> csharpCallLua = new List<Type>(){ typeof (UnityAction<float >) }; [LuaCallCSharp ] public static List<Type> luaCallCSharp = new List<Type>(){ typeof (UnityAction<float >) }; }
以后需要加特性的第三方类或者系统加到这两个List里面就好了
协程
1 2 3 public class Test10 :MonoBehavior { }
我们不能直接将lua函数传入开启协程中, 需要使用xLua的一个工具表xlua.util
的
cs_generator
方法把他当作协程函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 util = require ("xlua.util" ) GameObject = CS.UnityEngine.GameObject WaitForSeconds = CS.UnityEngine.WaitForSeconds local obj = GameObject("Coroutine" )local mono = obj:AddComponent(typeof(CS.Test10))fun = function () local a=1 while true do coroutine .yield (WaitForSeconds(1 )) print (a) a = a + 1 if a > 10 then mono:StopCoroutine(b) end end end b = mono:StartCoroutine(util.cs_generator(fun))
lua中开启和关闭协程和C#一样, 只是lua函数要被当作协程函数需要使用xlua.util.cs_generator方法
C#的yield return
相当于Lua中 coroutine.yield(返回值)
泛型函数
lua只支持有参数有约束且约束是类的泛型函数