热更新解决方案--tolua学习笔记
一.tolua使用准备工作:从GitHub上下载tolua(说明:这篇笔记使用的Unity版本是2019.4.18f1c1,使用的tolua是2021年4月9日从GitHub上Clone的tolua工程文件,没有下载release版本,使用的ide为vscode)
1.在GitHub上搜索tolua,点击下图搜索结果中选中的项目进入:
2.进入后Clone项目或者下载右边的release发布出来的tolua,这里我选择的是Clone代码:
3.导入tolua,将下载的tolua工程中的相关文件复制到自己的工程中。
将Luajit和Luajit64这两个目录复制。注意:这两个目录复制前和Assets目录同级,复制后也应该和Assets目录同级。
将Assets目录下的所有文件夹复制到自己的工程文件中Assets目录下。
打开Unity,会出现一个选择框,选择左边按钮。
接下来出现自动生成的选择框,点击确定。
成功导入后上方菜单栏出现Lua菜单,如果刚才不小心选择了取消,这里也可以选择Generate All重新生成。
4.其他功能脚本导入,主要是AB包工具(在Unity中通过PackageManager导入AB包工具,导入方法详见:热更新基础--AssetBundle学习笔记)和AB包管理器(为了方便加载AB包,我们可以制作一个AB包的管理器脚本,脚本详见:热更新基础--AssetBundle学习笔记)
二.C#调用lua
1.在C#中使用lua代码,和xlua类似,tolua也有一个解析器类用于执行lua代码
void Start() { //初始化tolua解析器 LuaState state = new LuaState(); //启动解析器 state.Start(); //执行lua代码 state.DoString("print('欢迎来到tolua')"); //执行代码时可以自定义代码出处,方便查看代码问题 state.DoString("print('hello tolua')","CSharpCallLua.cs"); //执行lua文件,执行文件时后缀可加可不加 state.DoFile("Main"); //执行lua文件,不添加文件后缀 state.Require("Main"); //检查解析器栈顶是否为空 state.CheckTop(); //销毁解析器 state.Dispose(); //置空 state = null; }
和xlua对比,解析器的使用方法类似。tolua解析器对象需要Start方法启动后使用,xlua直接使用解析器对象;tolua提供了DoString方法执行单句lua代码、DoFile方法执行lua文件(后缀可选)、Require方法执行lua文件(不加后缀),而xlua只提供了DoString方法执行单句lua代码,要执行lua文件需要使用DoString方法执行lua中的require语句;tolua销毁解析器时必须先执行CheckTop方法再Dispose方法销毁,xlua直接就可以销毁;其次xlua还提供了Tick方法进行垃圾回收。总体来看,xlua和tolua解析器使用是基本相同,只是名称不同而已。
2.tolua解析器自定义路径
void Start() { LuaState state = new LuaState(); state.Start(); //默认执行lua文件路径是Lua文件夹下的文件,如果是lua文件夹下的子文件,通过Lua文件夹下相对目录执行 //state.Require("CSharpCallLua/LoaderTest"); //如果是其他路径,可以通过AddSearchPath方法添加搜索路径,添加的是绝对路径(这里Lua文件夹下的目录也可以) state.AddSearchPath(Application.dataPath + "/Lua/CSharpCallLua"); state.DoFile("LoaderTest.lua"); //有添加路径的方法也有移除路径的方法,但是实际应用中基本没有移除路径的需求 state.RemoveSeachPath(Application.dataPath + "/Lua/CSharpCallLua"); }
和xlua对比,xlua中并没有提供自定义添加解析路径的方法,tolua提供了这个方法。这种添加解析路径的方法在实际使用中有局限性,主要是无法从AB包中解析,但是实际应用时自己写的lua代码都需要打到AB包中。xlua中直接添加解析lua文件的方法,我们称为重定向,这个重定向的方法根据从外部传入的文件名(注意这个参数是ref的,最好不要修改以方便后续的委托方法执行)读取文件最后返回byte数组,一旦得到一个不为空的返回值即终止后续重定向方法的执行;而tolua中也可以实现自定义解析方法(就是xlua中的重定向),这样就可以解决如何从AB包中读取lua文件的问题。
3.自定义解析方式(重定向)
首先我们看到tolua中的LuaFileUtils类,这个类是单例模式的,在其中有一个可以被子类重写的方法ReadFile,这就是根据lua文件名解析lua文件的核心方法(系统自定义的lua解析路径和我们添加的lua解析路径都会存储到一个list集合中,ReadFile方法会调用FindFile方法去解析路径,FindFile方法核心代码块是遍历lua解析路径的list,然后看list中是否有相应的文件),所以可以通过重写这个方法实现自定义解析方式
1)创建一个类继承LuaFileUtils,重写其中的ReadFile方法,在这个方法中自定义自己的解析方式。
/// <summary> /// 继承LuaFileUtils,tolua会调用这个类中的ReadFile方法读取lua文件,而且这个方法是virtual的,支持重写 /// </summary> public class CustomToluaLoader : LuaFileUtils { /// <summary> /// 重写ReadFile方法自定义解析lua文件的方式 /// </summary> /// <param name="fileName"></param> /// <returns></returns> public override byte[] ReadFile(string fileName) { //可以先调用父类解析器加载 //byte[] bytes = base.ReadFile(fileName); //校验父类解析器是否解析到了,如果父类方法没有找到,再使用自定义的解析 //if(bytes.Length != 0) // return bytes; //保证后缀必须是.lua if(!fileName.EndsWith(".lua")) fileName += ".lua"; //从AB包中加载 //拆分路径,得到文件名 string[] strs = fileName.Split('/'); //加载AB包文件 TextAsset luaFile = AssetBundleManager.Instance.LoadRes<TextAsset>("lua",strs[strs.Length-1]); //校验是否加载到文件 if(luaFile != null) return luaFile.bytes; //从Resources文件下加载,tolua可以一键将文件拷贝到Resources目录中的Lua文件夹下 string path = "Lua/" + fileName; TextAsset text = Resources.Load<TextAsset>(path); //加载资源 Resources.UnloadAsset(text); //校验是否加载到文件 if(text != null) return text.bytes; return null; } }
2)为了使子类重写的方法得到调用,由于父类是单例模式实现的,而且上图中还可以看到父类单例的set方法是protexted的,所以在执行lua文件前我们需要new一下子类,目的是让父类的instance先存储一个子类对象,这样调用时实际就是调用的子类重写的方法。
void Start() { LuaState state = new LuaState(); state.Start(); //加载lua文件前new以下自定义的CustomToluaLoader类,使其父类LuaFileUtils的instance单例保存的是子类对象,这样才能执行子类自定义的方法 new CustomToluaLoader(); //如果从Resources目录下加载,在执行文件前记得要将文件复制到Resources目录下,所有文件会保存在Resources目录下的Lua文件夹中,填写这个文件在Lua文件夹中的相对路径 state.Require("CSharpCallLua/LoaderTest"); }
3)执行结果:没有报错
总结:和xlua对比,明显tolua的重定向方式更加麻烦,xlua的加载方式可以分开,可以添加多个自定义的加载方法,每种方式一个方法,但是tolua的所有加载方式都必须在同一个方法(重写的ReadFile方法)中实现。我们这里自定义了两种加载方式,一种是从Resources目录下加载(一般用于加载tolua的框架中的lua代码,系统自动加载的),一种是从AB包中加载(用于加载自己写的lua代码)。在使用这两种方式加载lua文件时,有一些细节问题还需要注意:
1)从Resources目录下加载时,tolua定义了编辑器可以实现一键将所有lua文件迁移到Resources目录下,迁移后的lua文件都保存在Resources下的Lua目录下,如下图:
2)从AB包中加载lua文件时,AB包管理器打包会和tolua的生成代码有冲突,因此需要先清除tolua生成代码再打AB包,打包后重新生成代码,清除AB包时会弹出自动生成的选择框,记得选择取消(不然又自动生成了代码,清楚了个寂寞),下面分别是清除代码、弹出自动生成框和重新生成代码的选择:
4.tolua解析器管理器:和xlua类似,我们可以提供一个tolua的解析器管理器,封装一下xlua解析器,实现更方便调用xlua。
/// <summary> /// 管理唯一的tolua解析器 /// </summary> public class LuaManager : MonoBehaviour { //持有的全局唯一的解析器 private LuaState luaState; //提供给外部访问的解析器 public LuaState LuaState{ get{ return luaState; } } //单例模块,需要继承MonoBehaviour的单例,自动创建空物体并挂载自身 private static LuaManager instance; public static LuaManager Instance { get { //如果没有单例,自动创建一个空物体并挂载脚本,设置过场景不移除 if (instance == null) { GameObject obj = new GameObject("LuaManager"); DontDestroyOnLoad(obj); instance = obj.AddComponent<LuaManager>(); } return instance; } } //在Awake中初始化解析器 private void Awake() { Init(); } /// <summary> /// 初始化解析器方法,为解析器赋值 /// </summary> private void Init(){ //自定义解析路径,建议开发时注释掉这段代码,打包时取消注释 //new CustomToluaLoader(); luaState = new LuaState(); luaState.Start(); } /// <summary> /// 提供给外部执行单句lua代码 /// </summary> /// <param name="luaCode">lua代码</param> /// <param name="chunkName">lua代码出处</param> public void DoString(string luaCode,string chunkName = "LuaManager.cs"){ //判空 if(luaState == null) Init(); luaState.DoString(luaCode,chunkName); } /// <summary> /// 提供给外部执行lua文件的方法 /// 只封装require,不提供dofile加载(require加载不会重复执行lua代码) /// </summary> /// <param name="fileName"></param> public void Require(string fileName){ //判空 if(luaState == null) Init(); luaState.Require(fileName); } public void Dispose(){ //校验是否为空,解析器为空就不用再执行了 if(luaState == null) return; luaState.CheckTop(); luaState.Dispose(); //需要置空,不置空还会在栈内存储引用 luaState = null; } }
和xlua解析器管理器相比,tolua的管理器更加复杂一些。tolua解析器是继承mono的单例模式,xlua解析器是普通单例模式。这个解析器并不完善,后续学习过程中还需要继续完善。
5.使用lua解析器管理器调用lua代码。这是通过lua解析器调用lua地路径,和xlua地方式相同,之后测试都使用这种方法调用就不再赘述。之后粘贴的lua代码都是Test.lua中的代码。
1)在Unity中挂载脚本,在脚本中执行Main.lua脚本。
void Start() { LuaManager.Instance.Require("Main"); }
2)将Main.lua作为所有lua脚本地主入口,在这个脚本中在调用各种其他lua脚本执行lua代码。
print("do Main.lua succeed") --启动测试脚本 require("CSharpCallLua/Test")
3)被Main.lua启动地测试脚本。
print("do Test.lua succeed")
4)执行结果,可以看到两个脚本都成功执行
6.C#中获取lua的变量
print("do Test.lua succeed") --全局变量 testNumber = 1 testBool = true testFloat = 4.5 testString = "movin" --局部变量 local testLocal = 57
void Start() { LuaManager.Instance.Require("Main"); //获取全局变量 //lua解析器提供了索引器直接访问全局变量,这里的LuaState不是类名,是LuaManager中的luaState解析器对象对应的属性 Debug.Log(LuaManager.Instance.LuaState["testNumber"]); Debug.Log(LuaManager.Instance.LuaState["testFloat"]); Debug.Log(LuaManager.Instance.LuaState["testBool"]); Debug.Log(LuaManager.Instance.LuaState["testString"]); //得到的全局变量存储为object类型,使用Convert类中的静态方法转换类型 //值拷贝,无法通过修改转存的变量值修改lua中变量值,但是索引器提供了set方法修改 int value = Convert.ToInt32(LuaManager.Instance.LuaState["testNumber"]); value = 100; Debug.Log(LuaManager.Instance.LuaState["testNumber"]); LuaManager.Instance.LuaState["testNumber"] = 101; Debug.Log(LuaManager.Instance.LuaState["testNumber"]); //还可以使用索引器为lua新加全局变量 LuaManager.Instance.LuaState["newNumber"] = 56; Debug.Log(LuaManager.Instance.LuaState["newNumber"]); //本地变量无法获取 Debug.Log(LuaManager.Instance.LuaState["testLocal"]); }
与xlua对比,在xlua中通过获取_G表对象然后通过get和set方法读取lua中的变量,而tolua直接通过索引值访问,其实相当于将_G表进行了封装,显然tolua使用更为方便,但是tolua也存在缺陷,xlua可以通过泛型指定类型,tolua获取的值是object类型,还需要转换类型。
7.C#使用lua中的函数(无参无返回)
print("do Test.lua succeed") --定义函数 --无参无返回 function testFun() print("无参无返回") end
void Start() { LuaManager.Instance.Require("Main"); //方法一:通过GetFunction方法获取 LuaFunction function = LuaManager.Instance.LuaState.GetFunction("testFun"); //使用Call方法执行 function.Call(); //使用完成后需要销毁 function.Dispose(); //方法二:通过索引器获取函数,需要转型 function = LuaManager.Instance.LuaState["testFun"] as LuaFunction; //执行方法相同 function.Call(); function.Dispose(); //使用改进:转化为委托使用,其实就是将得到的LuaFunction对象转换为委托 function = LuaManager.Instance.LuaState["testFun"] as LuaFunction; //使用ToDelegate方法将LuaFunction对象转换为委托 UnityAction action = function.ToDelegate<UnityAction>(); action(); function.Dispose(); action = null; }
注意:要想在C#中使用委托转存LuaFunction对象,需要初始化tolua中的委托工厂,否则委托无法使用。在Lua解析器管理器类LuaManager中的Init方法中添加初始化委托工厂的代码:
private void Init(){ //自定义解析路径,建议开发时注释掉这段代码,打包时取消注释 //new CustomToluaLoader(); luaState = new LuaState(); luaState.Start(); //初始化委托工厂,没有初始化无法使用委托 DelegateFactory.Init(); }
8.C#使用lua中的函数(有参数,有返回)
print("do Test.lua succeed") --定义函数 --有参有返回 function testFun2(a) print("有参有返回") return a + 10 end
void Start() { LuaManager.Instance.Require("Main"); //方法一:通过luaFunction的Call方法执行 LuaFunction function = LuaManager.Instance.LuaState["testFun2"] as LuaFunction; //开始调用 function.BeginPCall(); //传递参数 function.Push(234); //得到返回值 function.PCall(); //得到返回值 int result = (int)function.CheckNumber(); Debug.Log(result); //执行结束 function.EndPCall(); //方法二:通过luaFunction的Invoke方法执行 //最后一个泛型为返回值类型,前面的泛型指定参数类型 result = function.Invoke<int,int>(78); Debug.Log(result); //方法三:使用委托转存,执行委托 Func<int,int> func = function.ToDelegate<Func<int,int>>(); result = func(645); Debug.Log(result); function.Dispose(); //方法四:直接执行 //使用LuaState的Invoke成员方法执行 Debug.Log(LuaManager.Instance.LuaState.Invoke<int,int>("testFun2",400,true)); }
9.C#使用lua中的函数(多返回值)
print("do Test.lua succeed") --定义函数 --多返回值 function testFun3(a) print("多返回值") return a-10,a,a+10,a>0 end
public delegate int CustomDelegate(int a,out int a2,out int a3,out bool b1); public class CallFunction : MonoBehaviour { void Start() { LuaManager.Instance.Require("Main"); //方法一:通过Call调用 LuaFunction function = LuaManager.Instance.LuaState["testFun3"] as LuaFunction; //开启使用 function.BeginPCall(); //传递参数 function.Push(3); //执行函数 function.PCall(); //得到多返回值 int a1 = (int)function.CheckNumber(); int a2 = (int)function.CheckNumber(); int a3 = (int)function.CheckNumber(); bool b1 = (bool)function.CheckBoolean(); //结束使用 function.EndPCall(); Debug.Log(a1 + "_" + a2 + "_" + a3 + "_" + b1); //方法二:通过out或者ref类型的委托接收,这里测试了out,ref是一样可以使用的 CustomDelegate customDelegate = function.ToDelegate<CustomDelegate>(); a1 = customDelegate(100,out a2,out a3,out b1); Debug.Log(a1 + "_" + a2 + "_" + a3 + "_" + b1); } }
注意:在tolua中使用自定义的委托时,除了要初始化委托工厂类,还需要将自定义的委托注册到tolua中。
1)打开Editor目录下的CustomSetting类
2)找到customDelegateList变量,在这个数组中添加自定义的委托类型(下图中选取的位置就是刚才代码中使用的对返回值委托类型)
3)在Unity中生成相应的委托代码,点击Gen Lua Delegates或者Generate All都可以
10.C#使用lua函数(变长参数)
print("do Test.lua succeed") --定义函数 --变长参数 function testFun4(a,...) print("变长参数") print(a) arg = {...} for k,v in pairs(arg) do print(k,v) end end
public delegate void CustomDelegate(int a,params object[] objs); public class CallFunction : MonoBehaviour { void Start() { LuaManager.Instance.Require("Main"); //方法一:通过自定义委托执行变长参数 LuaFunction function = LuaManager.Instance.LuaState.GetFunction("testFun4"); CustomDelegate customDelegate = function.ToDelegate<CustomDelegate>(); customDelegate(100,true,false,"movin",12,3.5); //方法二:通过LuaFunction中的Call方法执行,没有返回值可以使用这种方式,使用泛型指定参数类型 function.Call<int,bool,bool,string,int,float>(100,true,false,"movin",12,3.5f); } }
总结:
1)和xlua相比,tolua中C#使用lua函数的方法更多;
2)和刚才使用变量的思路类似,在xlua中,提供了LuaTable类和LuaFunction类对应lua中的表和函数,而lua中的所有全局变量都存储在_G表中,因此xlua提供了一个特殊的可以直接访问的_G表对象(LuaTable类对象),LuaTable类中提供了Get方法(可以指定泛型)和Set方法用于获取和设置参数的值,所以我们可以通过_G表对象和其中的Get、Set成员方法访问到lua中的变量;而tolua也可以理解为有类似的机制,但是又对_G表对象进行了进一步的封装(我们看不到tolua中的_G表),从刚才的获取变量和获取各种函数来看,使用了CheckXXX系列方法来封装变量的获取,使用GetFunction方法来封装函数的获取,除了方法封装还提供了索引器的getset方法封装,所以获取方式很多样;
3)xlua中对于通过Get方法从表中获得的不同类型的变量就可以使用C#中不同的类型接收,如果是lua中的number、boolean等基础数据类型就使用对应的数据类型接收,如果是函数类型就使用委托接收,当然像lua中table类型可以使用数组、集合等接收(根据实际需求和表特点确定);tolua获取到的函数类型是LuaFunction类型的,但是tolua和xlua中的LuaFunction类并不相同,它们有相同点(如使用后都需要dispose)也有不同点(比如tolua中的执行函数的方式更多),tolua获取到的变量类型通过CheckXXX系列方法获取,然后进行类型转换后使用(得到的原始类型时object类型的);
4)获取函数时,xlua不推荐使用LuaFunction类,推荐直接使用委托接收获取的lua函数;tolua不论是通过GetFunction还是索引器获取函数,都需要先存储为LuaFunction类型,可以使用LuaFUnction直接执行函数(Invoke方法执行无返回值函数,Call方法执行无参无返回值函数,PCall系列方法各种函数都可以执行),也可以通过ToDelegate方法得到lua函数转化的委托再执行委托;
5)不论时tolua还是xlua,自定义的委托类型或者其他类型都需要让框架生成相应的代码后使用。xlua使用[CSharpCallLua]和[LuaCallCSharp]两个特性指定需要生成代码的委托等C#类型,然后使用框架定义好的编辑器生成代码;tolua则不是使用特性,而是将需要生成代码的C#类型在CustomSettings类中指定,然后在使用前通过相应工厂的Init静态方法来初始化(如使用委托前需要调用DelegateFactory类的静态方法Init来初始化),最后再使用框架定义好的编辑器生成代码。
11.C#使用lua中的table表现的list和dictionary
print("do Test.lua succeed") --list和dictionary --table表现的list testList = {1,3,5,7,8,9} testList2 = {"movin","加油",true,5,89.3} --table表现的dictionary testDic = { ["a"] = 1, ["b"] = 2, ["c"] = 4, ["s"] = 87, } testDic2 = { ["movin"] = 34, [true] = "hehehe", ["2"] = false, }
void Start() { LuaManager.Instance.Require("Main"); //通过LuaTable来获取 //获取table表现的list LuaTable table = LuaManager.Instance.LuaState.GetTable("testList"); Debug.Log(table[1]); Debug.Log(table[2]); Debug.Log(table[3]); Debug.Log(table[4]); Debug.Log(table[5]); LuaTable table2 = LuaManager.Instance.LuaState.GetTable("testList2"); Debug.Log(table2[1]); Debug.Log(table2[2]); Debug.Log(table2[3]); Debug.Log(table2[4]); Debug.Log(table2[5]); //遍历,先将luatable转换为object类型的数组,再遍历 object[] objs = table.ToArray(); foreach (var item in objs) { Debug.Log("遍历出来的" + item); } //引用拷贝,luatable中的值,lua中的值也会改变 table[1] = 99; Debug.Log(LuaManager.Instance.LuaState.GetTable("testList")[1]); //获取table表现的dictionary LuaTable dic = LuaManager.Instance.LuaState.GetTable("testDic"); Debug.Log(dic["a"]); Debug.Log(dic["b"]); Debug.Log(dic["c"]); Debug.Log(dic["s"]); //LuaTable对象通过中括号得到值的方式中括号中的键只支持int和string //对于像bool类型的键,使用ToDicTable方法将LuaTable进行转换后才能获取值,通过泛型指定键值类型 LuaTable dic2 = LuaManager.Instance.LuaState.GetTable("testDic2"); LuaDictTable<object,object> luaDic2 = dic2.ToDictTable<object,object>(); Debug.Log(luaDic2["movin"]); Debug.Log(luaDic2[true]); Debug.Log(luaDic2["2"]); //LuaDictTable对象使用迭代器遍历 IEnumerator<LuaDictEntry<object,object>> ie = luaDic2.GetEnumerator(); while(ie.MoveNext()){ Debug.Log(ie.Current.Key + "_" + ie.Current.Value); } //引用拷贝 dic["a"] = 8848; Debug.Log(LuaManager.Instance.LuaState.GetTable("testDic")["a"]); }
12.C#使用lua中的table
print("do Test.lua succeed") --自定义table testClass = { testInt = 2, testBool = false, testFloat = 1.5, testString = "movin", testFun = function() print("class中的函数") end }
void Start() { LuaManager.Instance.Require("Main"); //通过GetTable方法获取 LuaTable table = LuaManager.Instance.LuaState.GetTable("testClass"); //访问table中的变量 Debug.Log(table["testInt"]); Debug.Log(table["testBool"]); Debug.Log(table["testFloat"]); //获取其中的函数 LuaFunction function = table.GetLuaFunction("testFun"); function.Call(); }
和xlua对比,tolua在表的使用上更加通用。xlua通过LuaTable类对象调用Get方法获取table中的各种类型,调用Set方法设置参数的值,在lua解析器中提供了一个特殊的LuaTable类对象对应lua中的_G表,然后通过_G表对象获取全局变量。Get方法可以通过泛型指定将lua中的全局变量映射到C#中的接收对象类型(类对象、接口、集合、函数及字符串、整型、浮点型等),对这些类型还需要使用特性生成代码后使用。tolua将_G表封装了起来,提供了GetTable、GetFunction等方法获取lua中的变量,在C#端也提供了对应Lua端的类型对象,使用这些类型对象接收获取到的lua变量,同时,提供了直接使用变量的方法和将变量转化为C#类型的方法。最后,对lua中表的引用在tolua和xlua中都是地址引用,也就是在C#中将得到的类型对象值改变,lua的值一并改变。
13.C#使用lua中的协程
print("do Test.lua succeed") --定义协程,这些内容是tolua提供的协程 local coDelay = nil --开启协程 StartDelay = function() coDelay = coroutine.start(Delay) end Delay = function() local c = 1 while true do --等待1s,执行一次 coroutine.wait(1) print("Count:"..c) c = c + 1 if c > 8 then StopDelay() break end end end --关闭协程 StopDelay = function() coroutine.stop(coDelay) end
void Start() { LuaManager.Instance.Require("Main"); //直接在lua中调用start函数就可以开启协程 LuaFunction function = LuaManager.Instance.LuaState.GetFunction("StartDelay"); function.Call(); function.Dispose(); }
注意:C#使用lua中的协程时,tolua为我们提供了一种协程的书写格式,这个知识点重点在lua端如何定义协程,C#端只需要开启一个方法的调用。更重要的是,这样直接调用并不能使协程跑起来,还需要在Unity中添加脚本,并将脚本和lua解析器绑定,这段代码定义在LuaManager中的Init函数中:
//使用协程时需要添加一个脚本 LuaLooper loop = this.gameObject.AddComponent<LuaLooper>(); //将解析器和lualooper绑定 loop.luaState = luaState;
运行结果:
14.在C#调用lua代码中常用的API类图对比(tolua和xlua对比,前3张图片为tolua,最后一张图片为xlua,只是梳理了学习过程中用到的API,还可以继续完善):
15.对比tolua和xlua在C#使用lua时的异同
1)在xlua中,我们可以简单粗暴地在C#端获取lua端地变量和方法等。xlua地lua运行环境类提供了一个LuaTable类型的Global属性,这个属性只提供了get方法,返回的就是_G表对象,对应了lua中的_G表。使用LuaTable类型的_G表对象的Get方法就可以获取各种lua中的全局变量,Get方法使用泛型指定获取的值类型,参数传递变量名称字符串。可以说,我们可以使用这个单一的方法获取各种全局变量,只是根据获取到的对象的不同需要作一些不同的处理(一些自定义的对象需要生成代码才能使用);一般情况下,有Get就应当有Set,使用Set方法在C#端也可以简单粗暴地把值、方法、表等设置到lua中。
2)在tolua中,C#端使用lua端的变量和函数等的方式就要复杂一些,因为tolua将_G表封装了起来,提供了固定的获取各种类型参数的方法。LuaState运行环境类提供了索引器获取各种变量,获取到的类型是object类型的,需要转换类型后使用;提供了GetFunction和GetTable方法获取全局的函数和表。tolua中使用LuaTable和LuaFunction类对应lua中的表和函数(xlua中同样有这两个类,但是除了LuaTable地Get和Set方法很少使用),通过GetFunction和GetTable得到的函数和表也存储为LuaFunction和LuaTable类型。LuaFunction类中提供了三种执行不同类型函数的方法,也提供了将自身转化为C#的委托的方法;LuaTable类中提供了根据索引或者键名访问和修改表中值的索引器,也提供了将对象自身转化为C#中数组或者字典的方法,还提供了遍历的方式。总的来说,tolua在LuaState类中提供了将全局变量取出的方法(索引器或者GetTable、GetFunction等),如果取出的是表或者函数,存储为LuaTable或者LuaFunction类型,这两种类型对象可以直接使用也可以转化为C#中对应的对象使用;如果取出的是string或者bool等基本数据类型的变量,直接强转使用。
3)不论是tulua还是xlua,得到的表都是引用类型。
4)不论是tolua还是xlua,委托、类等自定义的类型要想使用都需要生成代码,只是两者生成代码的方式不同。xlua通过特性指定需要生成代码的位置,tolua通过添加配置表指定需要生成代码的位置。
5)xlua在lua运行环境类中提供了方便的AddLoader方法进行lua代码重定向。tolua在lua运行环境类中提供了AddSearchPath类方便地添加读取lua代码地路径,但是实际使用中往往要从AB包中读取lua代码,这种方式并不使用,但是tolua进行代码重定向麻烦一些,通过继承重写的方式进行重定向。
三.lua调用C#
1.tolua调用C#的类,使用方法和xlua基本相同
print("do CallClass.lua succeed") --在tolua中访问C#和xlua非常相似 --xlua使用CS.命名控件.类名,tolua使用命名空间.类名,相当于不写CS local obj1 = UnityEngine.GameObject() local obj2 = UnityEngine.GameObject("movin") --可以定义全局变量存储常用的C#类(取别名)以方便使用、节约性能 GameObject = UnityEngine.GameObject local obj3 = GameObject("movin2") --类中的静态对象直接使用.调用,成员属性使用.调用,成员方法使用:调用 local position = GameObject.Find("movin").transform.position print(position.x) local rigidbody = GameObject.Find("movin2"):AddComponent(typeof(UnityEngine.Rigidbody)) print(rigidbody) --如果发现使用过程中lua不认识这个类,检查这个类是否添加到了自定义设置文件中并生成代码 Debug = UnityEngine.Debug Debug.Log(position.y)
注意:与xlua类似的,在使用过程中如果出现不识别的类,tolua需要将类添加到配置文件中(xlua是添加特性),配置文件所在位置和添加位置如下图:
上图中鼠标选中的部分分别是配置文件和自己添加的类(Unity的Debug类需要自己添加),添加完成后不要忘记生成代码。tolua麻烦的地方还有在使用Unity的类之前需要绑定lua解析器(LuaState对象),在LuaManager类中的Init方法中需要添加如下代码:
//lua使用Unity中的类需要绑定 LuaBinder.Bind(luaState);
2.tolua调用C#的枚举
/// <summary> /// 自定义枚举 /// </summary> public enum E_MyEnum{ Idle, Move, Atk, }
print("do CallEnum.lua succeed") --枚举调用规则和类的调用规则相同 --调用Unity自带的枚举 PrimitiveType = UnityEngine.PrimitiveType GameObject = UnityEngine.GameObject local obj = GameObject.CreatePrimitive(PrimitiveType.Cube) --调用自定义枚举 local c = E_MyEnum.Idle print(c) --枚举转字符串 print(tostring(c)) --枚举转数字 print(c:ToInt()) print(E_MyEnum.Move:ToInt()) --数字转枚举 print(E_MyEnum.IntToEnum(2))
和xlua相比,使用方式基本相同,但是需要注意以下几点:1)如果lua不能识别C#中的类或枚举等,xlua是添加特性并生成代码,tolua是在配置文件中添加相应的类或枚举类型并生成代码;2)在枚举使用过程中枚举对应的数字和枚举的相互转换两者的处理方法并不相同;3)xlua提供了字符串转枚举的方法,tolua没有提供;4)xlua直接print打印获取到的枚举类型打印出的是字符串,而tolua打印出的是userdata类型(tolua在存储获取到的枚举是没有转换类型,存储的还是C#中的枚举)
3.tolua调用C#的数组
/// <summary> /// 自定义类中定义了一个数组 /// </summary> public class CustomClass{ public int[] array = new int[]{1,23,4,6,4,5}; }
print("do CallArray.lua succeed") --获取类 local customClass = CustomClass() --获取类中的数组,按照C#的规则使用 print(customClass.array.Length) print(customClass.array[1]) --查找元素 print(customClass.array:IndexOf(3)) --遍历 for i = 0,customClass.array.Length - 1 do print("position "..i.." in array is "..customClass.array[i]) end --tolua比xlua多了一些遍历方式 --迭代器遍历 local iter = customClass.array:GetEnumerator() while iter:MoveNext() do print("iter:"..iter.Current) end --转成table遍历 local t = customClass.array:ToTable() for i=1,#t do print("table:"..t[i]) end --创建数组 local array2 = System.Array.CreateInstance(typeof(System.Int32),7) print(array2.Length) print(array2[0]) array2[0] = 99 print(array2[0])
4.tolua使用C#的list和dictionary
/// 自定义类中定义了一个数组 /// </summary> public class CustomClass{ public int[] array = new int[]{1,23,4,6,4,5}; public List<int> list = new List<int>(); public Dictionary<int,string> dic = new Dictionary<int, string>(); }
print("do CallListDic.lua succeed") local customClass = CustomClass() --使用list --向list中添加元素 customClass.list:Add(2) customClass.list:Add(45) customClass.list:Add(87) --获取元素 print(customClass.list[1]) --长度 print(customClass.list.Count) --遍历 for i = 0,customClass.list.Count - 1 do print(customClass.list[i]) end --创建list --tolua对泛型支持不好,需要自己在配置文件中添加对应的泛型类型生成后才能使用 --如List<string>、List<int>等等需要一一添加 local list2 = System.Collections.Generic.List_string() list2:Add("movin") print(list2[0]) --使用dictionary customClass.dic:Add(1,"movin") customClass.dic:Add(2,"干饭人") customClass.dic:Add(3,"干饭魂") --获取值 print(customClass.dic[2]) --遍历 --tolua中不支持使用lua的pairs遍历方式进行遍历,需要使用迭代器 local iter = customClass.dic:GetEnumerator() while iter:MoveNext() do local v = iter.Current print(v.Key,v.Value) end local keyIter = customClass.dic.Keys:GetEnumerator() while keyIter:MoveNext() do print(keyIter.Current,customClass.dic[keyIter.Current]) end local valueIter = customClass.dic.Values:GetEnumerator() while valueIter:MoveNext() do print(valueIter.Current) end --创建dictionary local dic2 = System.Collections.Generic.Dictionary_int_string() dic2:Add(4,"movin") print(dic2[4]) --如果键是字符串,tolua不能通过索引器访问值,可以使用TryGetValue方法 local dic3 = System.Collections.Generic.Dictionary_string_int() dic3:Add("movin",455) local b,v = dic3:TryGetValue("movin",nil) print(v)
和xlua对比,使用上基本一致,都是调用C#的相关方法使用就可以了,区别主要在于:1)xlua和tolua的遍历方式不同,不论是数组、列表还是字典,tolua一般使用迭代器遍历,而xlua使用lua的pairs方式遍历;2)创建list或者dictionary时,tolua的写法和xlua的写法不一样,但是两者都遵循各自的固定写法;3)对于泛型的支持tolua非常差,如果要使用泛型定义list或者dictionary,需要在配置文件中配置自己使用的泛型类型,并生成代码。
5.拓展方法
public static class Tools{ public static void Move(this CallFunctions cfs){ Debug.Log(CallFunctions.name + " is moving"); } } public class CallFunctions{ public static string name = "movin"; public void Speak(string str){ Debug.Log(str); } public static void Eat(){ Debug.Log(name + " is eating"); } }
print("do CallFunction.lua succeed") --静态方法使用.执行 CallFunctions.Eat(); --成员方法使用冒号执行 local callFunctions = CallFunctions() callFunctions:Speak("movin move") --如果使用拓展方法,需要在配置文件中配置 callFunctions:Move()
注意:使用拓展方法时,在C#中的配置文件中进行的配置不太一样,如下图:
与xlua对比,xlua中使用拓展方法加上特性生成代码即可,和其他特性的添加相同,而tolua则需要添加不太一样的代码配置。使用方法上都是把拓展方法当作成员方法使用即可。
6.tolua使用C#的ref和out参数方法
public int RefFun(int a,ref int b,ref int c,int d){ b = a + d; c = a - d; return 100; } public int OutFun(int a,out int b,out int c,int d){ b = a + d; c = a - d; return 200; } public int RefOutFun(int a,out int b,ref int c,int d){ b = a + d; c = a - d; return 300; }
print("do CallFunction.lua succeed") --通过多返回值的方式使用ref和out --第一个返回值为默认返回值,之后返回值是ref和out的返回值 --out参数使用任意值占位,ref参数需要传递 local obj = CallFunctions() print(obj:RefFun(12,32,42,1)) print(obj:OutFun(12,0,0,5)) print(obj:RefOutFun(12,0,43,5))
和xlua对比,使用基本相似,区别在于xlua中out参数省略,而tolua中out参数任意值占位(nil都可以),但是不能省略。此外,如果出现使用ref或out都可以的情况,推荐在tolua中使用out(官方没有讲到ref的使用,只讲到了out的使用)。
7.tolua使用C#中的重载函数
public class CallFunctions{ public int Calc(){ return 100; } public int Calc(int a){ return a; } public string Calc(string a){ return a; } public int Calc(int a,int b){ return a + b; } public int Calc(int a,out int b){ b = 10; return a + b; } }
print("do CallFunction.lua succeed") --使用重载方法 local obj = CallFunctions() --lua中只有Number一种数值类型,所以xlua和tolua都对C#中的整型、浮点型等重载支持不好 print(obj:Calc()) print(obj:Calc(1)) print(obj:Calc(1.4)) print(obj:Calc("123")) --对于同样类型参数的两个重载函数,一个参数有out,一个参数没有out --根据参数的值确定调用的函数,out参数使用nil占位,非out参数不使用nil print(obj:Calc(13,24)) print(obj:Calc(13,nil)) --官方不推荐使用ref参数,这里如果是ref参数和没有ref参数的重载不能分清是一个重要原因
和xlua对比,tolua中重载函数并不需要特别声明,像其他函数一样传递参数调用就好,只是应该调用哪个重载是需要tolua去分辨的,由于lua和C#的差异性,导致重载函数使用过程中产生了两个问题:一是lua中只有Number一种数值类型,而C#中有浮点型、整型等总共超过十种数值类型,C#中数值类型的重载函数该调用哪一个lua是分不清楚的;二是ref类型参数和没有ref类型参数的问题,对于out类型和非out类型,可以通过传递nil值来区分,但是ref类型参数本来就需要初始化,和非ref类型参数的重载无法分辨。在xlua中重载函数的调用非常相似,只是out参数不用传递值,但是这两个问题同样存在,不过xlua提供了通过反射让lua分清楚数值类型重载的方式,效率低就是了。
8.tolua使用C#中委托和事件
public class CallDelegates{ public UnityAction actions; public event UnityAction events; public void DoAction(){ if(actions != null) actions(); } public void DoEvent(){ if(events != null) events(); } public void ClearEvent(){ events = null; } }
print("do CallDelegate.lua succeed") --定义函数存储到委托中 local obj = CallDelegates() local fun = function() print("lua function fun") end --第一次添加委托需要使用等号,之后使用+=(在lua中不支持+=,需要写全) obj.actions = fun obj.actions = obj.actions + fun obj.actions = obj.actions + function() print("the third function in actions") end --tolua中无法直接执行委托,需要在C#中提供执行委托的方法 obj:DoAction() --添加函数使用+=,移除函数自然使用-= obj.actions = obj.actions - fun obj.actions = obj.actions - fun obj:DoAction() obj.actions = nil obj:DoAction() --tolua中事件的使用和委托基本相同,区别只在事件只能+=,不能= obj.events = obj.events + fun obj.events = obj.events + fun obj.events = obj.events + function() print("the third function in events") end obj:DoEvent() --移除事件和移除委托相同 obj.events = obj.events - fun obj.events = obj.events - fun obj:DoEvent() --清空事件,在C#中必须提供清空事件的方法 obj:ClearEvent() obj:DoEvent()
和xlua对比,委托的使用基本相同,只是tolua不能直接执行委托,需要在C#端提供执行委托的封装方法,而xlua可以直接执行委托;事件的使用上,xlua和tolua添加和移除事件的方式不同,执行事件的方式基本相同(都要在C#中提供方法封装,执行这个方法间接执行事件)。
9.tolua使用C#中的协程
print("do CallCoroutine.lua succeed") --记录协程 local coDelay = nil --tolua提供了一些方便开启协程的方法 StartDelay = function() --使用StartCoroutine方法开启协程(tolua提供) coDelay = StartCoroutine(Delay) end --协程函数 Delay = function() local c = 1 while true do --使用WaitForSeconds方法等待(tolua提供) WaitForSeconds(1) --tolua还提供了其他的协程方法 --Yield(0) --WaitForFixedUpdate() --WaitForEndOfFrame() --Yield(返回值) print("Count:"..c) c = c + 1 if c > 6 then StopDelay() break end end end --停止协程函数 StopDelay = function() StopCoroutine(coDelay) coDelay = nil end --开始调用协程 StartDelay()
注意:要想在tolua中使用其定义的StartCoroutine等协程函数,需要注册协程。在LuaManager中的Init函数中添加注册代码,添加后的Init函数如下:
private void Init(){ //自定义解析路径,建议开发时注释掉这段代码,打包时取消注释 //new CustomToluaLoader(); luaState = new LuaState(); luaState.Start(); //初始化委托工厂,没有初始化无法使用委托 DelegateFactory.Init(); //使用协程时需要添加一个脚本 LuaLooper loop = this.gameObject.AddComponent<LuaLooper>(); //将解析器和lualooper绑定 loop.luaState = luaState; //使用tolua提供的协程方法,需要进行lua协程注册 LuaCoroutine.Register(luaState,this); //lua使用Unity中的类需要绑定 LuaBinder.Bind(luaState); }
和xlua对比,tolua使用协程相对来说更加简单,它为我们提供了一些协程函数可以直接调用,只要注册协程后就可以使用,而xlua没有为我们提供协程函数,在定义协程时只能使用lua提供的协程函数。
四.最后将最终版的LuaManager类粘贴到这里(和前面的版本相比,主要在Init方法中增添了一些代码)
/// <summary> /// 管理唯一的tolua解析器 /// </summary> public class LuaManager : MonoBehaviour { //持有的全局唯一的解析器 private LuaState luaState; //提供给外部访问的解析器 public LuaState LuaState{ get{ return luaState; } } //单例模块,需要继承MonoBehaviour的单例,自动创建空物体并挂载自身 private static LuaManager instance; public static LuaManager Instance { get { //如果没有单例,自动创建一个空物体并挂载脚本,设置过场景不移除 if (instance == null) { GameObject obj = new GameObject("LuaManager"); DontDestroyOnLoad(obj); instance = obj.AddComponent<LuaManager>(); } return instance; } } //在Awake中初始化解析器 private void Awake() { Init(); } /// <summary> /// 初始化解析器方法,为解析器赋值 /// </summary> private void Init(){ //自定义解析路径,建议开发时注释掉这段代码,打包时取消注释 //new CustomToluaLoader(); luaState = new LuaState(); luaState.Start(); //初始化委托工厂,没有初始化无法使用委托 DelegateFactory.Init(); //使用协程时需要添加一个脚本 LuaLooper loop = this.gameObject.AddComponent<LuaLooper>(); //将解析器和lualooper绑定 loop.luaState = luaState; //使用tolua提供的协程方法,需要进行lua协程注册 LuaCoroutine.Register(luaState,this); //lua使用Unity中的类需要绑定 LuaBinder.Bind(luaState); } /// <summary> /// 提供给外部执行单句lua代码 /// </summary> /// <param name="luaCode">lua代码</param> /// <param name="chunkName">lua代码出处</param> public void DoString(string luaCode,string chunkName = "LuaManager.cs"){ //判空 if(luaState == null) Init(); luaState.DoString(luaCode,chunkName); } /// <summary> /// 提供给外部执行lua文件的方法 /// 只封装require,不提供dofile加载(require加载不会重复执行lua代码) /// </summary> /// <param name="fileName"></param> public void Require(string fileName){ //判空 if(luaState == null) Init(); luaState.Require(fileName); } public void Dispose(){ //校验是否为空,解析器为空就不用再执行了 if(luaState == null) return; luaState.CheckTop(); luaState.Dispose(); //需要置空,不置空还会在栈内存储引用 luaState = null; } }
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步