Unity的XLua热更方案中Lua与CSharp的交互
本文只包含XLua热更方案中客户端开发时用到的相关部分,不讨论服务端具体如何发布热更新、以及用户客户端具体如何载入热更包
Unity的XLua热更方案中Lua与CSharp的交互
一、关于Unity热更
1.1 CSharp热更方案
- CSharp其实可以实现热更新,需使用其反射机制
- CSharp的反射指的是在运行时(通过程序集中的元数据)动态地获取和操作程序集、类型、成员等信息的机制(如实例化对象、调用方法、获取和设置属性等),而无需在编译时明确知道这些类型和成员的具体信息
- 将需频繁更改的部分逻辑做成独立的动态链接库DLL(可包含类,函数等资源)以让其他程序动态加载使用,而主模块代码则不作修改
- 通过反射机制加载这些DLL,用新DLL文件替换旧DLL,主模块加载DLL就实现了热更新
- 某些CSharp热更方案只适用Android热更新,iOS由于其安全机制,不允许在新申请的内存(用于修改后的代码使用)上进行写操作,故目前CSharp的反射基本不被商业项目用作热更新
1.2 Lua热更方案
- 核心是CSharp与Lua代码间的相互调用
- 需为Unity项目导入热更新库,如
XLua或toLua等 - 创建基于Lua解释器的管理器,用Lua解释器来解释执行Lua代码
- XLua等库支持以热补丁形式将已用CSharp实现的逻辑替换为Lua实现
- 需为Unity项目导入热更新库,如
- 商业游戏大体有三种结合Lua热更新的成熟开发方式
- 纯Lua开发:全部逻辑都用Lua实现,好处是灵活性强,坏处是性能较差(Lua毕竟是解释型语言,虽然当代的手机可能不差这点性能)
- 半CSharp半Lua:核心逻辑CSharp业务逻辑Lua,好处是对比纯Lua性能会更好,坏处是灵活性较差(只动例如活动等业务逻辑时可热更,但若动核心逻辑就只能强更了,这一般会导致用户流失)
- CSharp与热补丁:对于一开始就使用纯CSharp实现但中途决定要加Lua热更新的项目,那就只能通过热补丁实现了(这样会使得项目变得不优雅,故最好一开始就定好要不要Lua热更新)
1.3 两类方案对比
- CSharp是静态编译语言
- 代码在编译时被CLR转换成机器码后才能够被计算机执行,故运行时无法修改已编译的代码,且CLR会在运行时对代码进行各种优化和安全检查,这使得热更新困难
- Lua是动态脚本语言
- 代码在运行时被解释器逐行执行而无需静态编译,即运行时动态加载新脚本文件替换已加载代码模块,这使得Lua可在运行时修改已加载代码实现热更新,而无需重新启动整个程序
二、关于XLua方案
- XLua基于Lua虚拟机实现逻辑热更(Lua脚本可作为资源参与热更,但核心代码如渲染管线仍以CSharp实现)
- 通过自动生成CSharp-Lua桥接代码(Wrap文件)实现双向调用
- 利用Unity的
LuaInterface组件实现与引擎API的交互
- 应用场景例子如下
- 频繁变动的配置表(例如以Lua脚本形式储存的配置表数据)
- 大型项目多人协作(例如编写简单UI逻辑时无需修改CSharp核心代码,而是编写Lua脚本,提高效率且降低风险)
- 导入XLua只需将其Github仓库的
Assets目录内的两个文件夹直接拖入你自己项目的对应目录下即可
三、Csharp调用Lua
3.1 管理器设计
3.1.1 Lua解释器生命周期
- 想在Unity的CSharp环境中调用Lua就必须引入Lua解释器(本质上是运行了Lua虚拟机)
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
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
//引用XLua相关命名空间
using XLua;
namespace WZ
{
public class Test : MonoBehaviour
{
private LuaEnv luaEnv;
void Start()
{
//初始化Lua解释器(启动Lua虚拟机)以使得能在Unity中运行Lua脚本
luaEnv = new LuaEnv();
//以字符串形式执行Lua脚本,还可往后多传几个信息参数以便Debug,详见API
luaEnv.DoString("print('Test Call Lua')");
//LUA: Test Call Lua
//UnityEngine.Debug:Log(object)
//XLua.StaticLuaCallbacks:Print(intptr)(at Assets / XLua / Src / StaticLuaCallbacks.cs:629)
//XLua.LuaEnv:DoString(byte[], string, XLua.LuaTable)(at Assets / XLua / Src / LuaEnv.cs:268)
//XLua.LuaEnv:DoString(string, string, XLua.LuaTable)(at Assets / XLua / Src / LuaEnv.cs:288)
//WZ.Test:Start()(at Assets / Scripts / Test.cs:16)
//调用名为TestLua的Lua脚本文件,默认读取的目录为Assets/Resources/下(后续可根据需求自定义)
//但由于Lua脚本文件并非Unity原生资源,其.lua后缀无法被Load等方法识别,故需将.lua后缀改为.lua.txt或其他有效后缀
//该Lua脚本内容为print("Run TestLua.lua.txt Successfully")
luaEnv.DoString("require('TestLua')");
//Unity打印内容为LUA: Run TestLua.lua.txt Successfully
//垃圾回收,会清除Lua中未手动释放的对象,最好不要每帧执行,可定时调用或在跨场景时调用
luaEnv.Tick();
//销毁Lua解释器,若需保持解释器唯一性则不建议销毁
luaEnv.Dispose();
}
}
}
3.1.2 Lua加载路径重定向
- 前文通过CSharp语句
luaEnv.DoString("require('TestLua')");调用Lua脚本时,默认从Assets/Resources/目录下读取TestLua.lua.txt文件,我们可以重定向该加载路径
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
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
//引用文件操作相关命名空间(提供对如File.Exists等方法的支持)
using System.IO;
//引用XLua相关命名空间
using XLua;
namespace WZ
{
public class Test : MonoBehaviour
{
private LuaEnv luaEnv;
void Start()
{
luaEnv = new LuaEnv();
//传入一个定义为public delegate byte[] CustomLoader(ref string filepath);的委托,用于重定向Lua文件的加载路径
//只要被AddLoader自定义添加过的委托中有一个返回了非null的byte[],那么这个byte[]就会被用作加载Lua文件,若全都返回无效byte[],则会继续使用默认加载路径Assets/Resources/
luaEnv.AddLoader(MyCustomLoader1);
luaEnv.AddLoader(MyCustomLoader2);
//该文件TestLua.lua.txt位于默认路径Assets/Resources/下(由输出可见,该脚本名被MyCustomLoader1和MyCustomLoader2接收过,但最终还是由默认加载器成功加载)
luaEnv.DoString("require('TestLua')");
//MyCustomLoader1接收到Lua脚本名TestLua
//MyCustomLoader2重定向Lua文件加载路径失败D:/PROJECTS/FrameworkLuaUI/Assets/MyFolder/TestLua.lua
//LUA: Run TestLua.lua.txt Successfully
//该文件TestLuaRedirect.lua位于自定路径Assets/MyFolder/下(由输出可见,该脚本名被MyCustomLoader1和MyCustomLoader2接收过,最终由MyCustomLoader2成功加载)
//该文件后缀仅为.lua而非.lua.txt是因为我们在MyCustomLoader2中拼接路径时对.lua后缀手动进行了处理,不会出现无法识别的情况
luaEnv.DoString("require('TestLuaRedirect')");
//MyCustomLoader1接收到Lua脚本名TestLuaRedirect
//LUA: Run TestLuaRedirect.lua Successfully
}
//接收的_filepath参数是欲被执行的Lua脚本的文件名
private byte[] MyCustomLoader1(ref string _filepath)
{
Debug.Log("MyCustomLoader1接收到Lua脚本名" + _filepath);
return null;
}
private byte[] MyCustomLoader2(ref string _filepath)
{
//拼接路径,其中Application.dataPath是Assets文件夹的绝对路径,注意添加斜杠
string _path = Application.dataPath + "/MyFolder/" + _filepath + ".lua";
//根据路径查询脚本文件是否存在,存在则读取其字节流返回,不存在则打印错误日志并返回null
if (File.Exists(_path))
return File.ReadAllBytes(_path);
else
{
Debug.LogError("MyCustomLoader2重定向Lua文件加载路径失败" + _path);
return null;
}
}
}
}
3.1.3 Lua管理器核心功能
- 管理器封装如下
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
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System.IO;
using XLua;
namespace WZ
{
//该管理器用于提供唯一的Lua虚拟机(继承自纯CSharp单例类,其上无MonoBehaviour祖先也就无需挂载到GameObject上)
public class LuaManager : SingletonPure<LuaManager>
{
private LuaEnv luaEnv;
#region Interfaces
//获取Lua环境的_G表
public LuaTable Global
{
get
{
return luaEnv.Global;
}
}
//初始化Lua虚拟机
public void Init()
{
//确保唯一Lua虚拟机
if (luaEnv == null)
luaEnv = new LuaEnv();
//重定向Lua文件加载
luaEnv.AddLoader(MyCustomLoaderLua);
//重定向AB包文件加载
luaEnv.AddLoader(MyCustomLoaderAB);
}
//传入Lua脚本无后缀名以执行该脚本
public void RunLuaScript(string _name)
{
luaEnv.DoString(string.Format("require('{0}')", _name));
}
//垃圾回收
public void ReleaseGarbage()
{
luaEnv.Tick();
}
//销毁Lua虚拟机
public void ReleaseLuaEnv()
{
luaEnv.Dispose();
luaEnv = null;
}
#endregion
#region CustomLoader
//实际生产中可通过外部工具脚本进行后缀转换
//该加载器加载AB包中的Lua脚本"_filepath.lua",可在测试Lua脚本时使用(脚本可正常保持".lua"后缀以便调试代码)
private byte[] MyCustomLoaderLua(ref string _filepath)
{
//拼接路径,其中Application.dataPath是Assets文件夹的绝对路径,注意添加斜杠
string _path = Application.dataPath + "/ResourcesAB/LuaScripts/" + _filepath + ".lua";
//根据路径查询脚本文件是否存在,存在则读取其字节流返回,不存在则打印错误日志并返回null
if (File.Exists(_path))
return File.ReadAllBytes(_path);
else
return null;
//若在其它地方调用以下语句,则能成功执行对应Lua脚本
//LuaManager.Instance.Init();
//LuaManager.Instance.RunLuaScript("TestLua");
//LUA: Run Test.lua.txt Successfully
}
//该加载器加载AB包中的Lua脚本"_filepath.lua.txt",只在测试AB包时才使用(需将所有脚本改为".lua.txt"后缀以便被加载)
private byte[] MyCustomLoaderAB(ref string _filepath)
{
//假设在Unity中已将所有Lua脚本打入"lua"标签的AB包,此处加载器目的是从该AB包中加载Lua脚本作为TextAsset类型对象(注意不要异步加载,因为加载结果需被立即返回)
//此处加载"_filepath.lua.txt"文本文件,TextAsset无法直接识别".lua"后缀,故文件需添上".txt"后缀,而原本的".lua"也就被当作文件名的一部分,故此处传入名称时需添上".lua"
TextAsset _luaFile = ABManager.Instance.LoadResSync<TextAsset>("lua", _filepath + ".lua");
//返回该TextAsset的字节流
if (_luaFile != null)
return _luaFile.bytes;
else
return null;
//若在其它地方调用以下语句,则能成功执行对应Lua脚本
//LuaManager.Instance.Init();
//LuaManager.Instance.RunLuaScript("TestLua");
//ABManager: 加载AB包lua
//LUA: Run Test.lua.txt Successfully
}
#endregion
}
}
- 其所继承的单例类定义如下
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
namespace WZ
{
//可继承的单例抽象基类,单例在被第一次访问Instance属性时创建
//此处泛型约束要求继承该类的T需提供公共无参构造,包括未显式声明无参构造的类由编译器提供的默认公共无参构造,非公共的不符合
public abstract class SingletonPure<T> where T : new()
{
private static T instance;
public static T Instance
{
get
{
//若无new()泛型约束则此处创建实例会编译失败
if (instance == null)
instance = new T();
return instance;
}
}
}
}
3.2 访问Lua普通变量
Main.lua脚本内容如下
1
2
3
print("Main.lua")
-- 即便是在Lua脚本内执行其它Lua脚本,也会受到CSharp侧的CustomLoader重定向的路径影响,而不是靠相对路径查找文件
require("TestVariables")
TestVariables.lua脚本内容如下
1
2
3
4
5
6
7
8
print("TestVariables.lua")
-- 用作CSharp调用Lua测试的全局变量,Lua中声明的所有全局变量都会被存到_G表内
testNumber = 666
testFloat = 3.14
testBool = true
testString = "Yui"
-- 测试本地局部变量
local testLocal = 555
- 在CSharp中可通过
_G表访问Lua中的全局变量,无法直接获取Lua中的本地局部变量
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
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
namespace WZ
{
public class Test : MonoBehaviour
{
void Start()
{
LuaManager.Instance.Init();
LuaManager.Instance.RunLuaScript("Main");
//读取对应名称的变量(获取的是值而非引用)
Debug.LogError("testNumber = " + LuaManager.Instance.Global.Get<int>("testNumber").ToString());
Debug.LogError("testFloat = " + LuaManager.Instance.Global.Get<float>("testFloat").ToString());
Debug.LogError("testBool = " + LuaManager.Instance.Global.Get<bool>("testBool").ToString());
Debug.LogError("testString = " + LuaManager.Instance.Global.Get<string>("testString").ToString());
//testNumber = 666
//testFloat = 3.14
//testBool = True
//testString = Yui
//改写对应名称的变量(可以改写为任意类型)
LuaManager.Instance.Global.Set("testNumber", "newValue");
Debug.LogError("testNumber = " + LuaManager.Instance.Global.Get<string>("testNumber").ToString());
//testNumber = newValue
//无法直接读取局部变量
Debug.LogError("testLocal = " + LuaManager.Instance.Global.Get<int>("testLocal").ToString());
//InvalidCastException: can not assign nil to int
//XLua.LuaTable.Get[TKey,TValue] (TKey key, TValue& value) (at Assets/XLua/Src/LuaTable.cs:74)
//XLua.LuaTable.Get[TValue] (System.String key) (at Assets/XLua/Src/LuaTable.cs:345)
//WZ.Test.Start () (at Assets/Scripts/Test.cs:31)
}
}
}
3.3 访问Lua各种函数
- 更新
TestVariables.lua脚本内容如下
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
print("TestVariables.lua")
-- 测试无参无返函数
func1 = function()
print("func1")
end
-- 测试有参有返函数
func2 = function(parm)
print("func2")
return tostring(parm)
end
-- 测试多返回值函数
func3 = function(parm)
print("func3")
return "Yui", 520, true, parm
end
-- 测试变长参数函数
func4 = function(parm, ...)
print("func4")
arg = { ... }
local result = tostring(parm) .. "/"
for _, v in pairs(arg) do
result = result .. tostring(v) .. "/"
end
return result
end
- 在CSharp中可通过多种方式接收到Lua侧的函数,官方建议不要使用
LuaFunction,因为说是会产生一些垃圾
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
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Events;
using XLua;
namespace WZ
{
//自定义形式委托,需标识[CSharpCallLua]特性(在XLua命名空间内)并在Unity中重新生成代码(自动存放在Assets/XLua/Gen/目录下),否则会报错如下
//InvalidCastException: This type must add to CSharpCallLua: WZ.DelegateForFunc2
//XLua.LuaTable.Get[TKey, TValue] (TKey key, TValue& value) (at Assets/XLua/Src/LuaTable.cs:65)
//XLua.LuaTable.Get[TValue] (System.String key) (at Assets/XLua/Src/LuaTable.cs:345)
//WZ.Test.Start() (at Assets/Scripts/Test.cs:37)
[CSharpCallLua]
public delegate string DelegateForFunc2(int _parm);
//由于CSharp没有多返回值委托,故除了第一个返回值(此处为string)外的其余返回值,都需通过传入对应类型的out参数来间接实现多返回值(应正常传入的参数则无需out修饰)
[CSharpCallLua]
public delegate string DelegateForFunc3(int _inParm, out int _returnVal2, out bool _returnVal3, out int _returnVal4);
//该委托接收一个普通参数和一个params变长参数,后者类型取决于具体需要,若确定全为同类型T则直接传T[]数组即可(性能更好),否则只能用object[]数组
[CSharpCallLua]
public delegate string DelegateForFunc4(int _parm, params object[] _parms);
public class Test : MonoBehaviour
{
void Start()
{
LuaManager.Instance.Init();
LuaManager.Instance.RunLuaScript("Main");
#region 无参无返函数
//用Unity提供的UnityAction委托接收,定义为public delegate void UnityAction();(源自using UnityEngine.Events;),等价于用相同定义的自定义委托接收
UnityAction _callFunc1Method1 = LuaManager.Instance.Global.Get<UnityAction>("func1");
if (_callFunc1Method1 != null) { _callFunc1Method1(); }
//LUA: func1
//用CSharp提供的Action委托接收,定义为public delegate void Action();(源自using System;)
Action _callFunc1Method2 = LuaManager.Instance.Global.Get<Action>("func1");
if (_callFunc1Method2 != null) { _callFunc1Method2(); }
//LUA: func1
//用XLua提供的LuaFunction接收,定义为public class LuaFunction : LuaBase(源自using XLua;)
LuaFunction _callFunc1Method3 = LuaManager.Instance.Global.Get<LuaFunction>("func1");
if (_callFunc1Method3 != null) { _callFunc1Method3.Call(); } //Call()调用时参数和返回值都用object[]数组接收,可传入变长参数
//LUA: func1
#endregion
#region 有参有返函数
//使用自定义的委托接收
DelegateForFunc2 _callFunc2Method1 = LuaManager.Instance.Global.Get<DelegateForFunc2>("func2");
if (_callFunc2Method1 != null) { Debug.Log("CSharp _callFunc2Method1 return = " + _callFunc2Method1(999)); }
//LUA: func2
//CSharp _callFunc2Method1 return = 999
//使用CSharp提供的Func委托接收,定义为public delegate TResult Func<in T, out TResult>(T arg);(源自using System;)
Func<int, string> _callFunc2Method2 = LuaManager.Instance.Global.Get<Func<int, string>>("func2");
if (_callFunc2Method2 != null) { Debug.Log("CSharp _callFunc2Method2 return = " + _callFunc2Method2(666)); }
//LUA: func2
//CSharp _callFunc2Method2 return = 666
//用XLua提供的LuaFunction接收
LuaFunction _callFunc2Method3 = LuaManager.Instance.Global.Get<LuaFunction>("func2");
if (_callFunc2Method3 != null) { Debug.Log("CSharp _callFunc2Method3 return = " + _callFunc2Method3.Call(333)[0]); } //此处Call()的返回值只有一个,故取索引0即可
//LUA: func2
//CSharp _callFunc2Method3 return = 333
#endregion
#region 多返回值函数
//使用自定义的委托接收,多返回值通过out参数间接体现
DelegateForFunc3 _callFunc3Method1 = LuaManager.Instance.Global.Get<DelegateForFunc3>("func3");
if (_callFunc3Method1 != null)
{
//其实用ref也可以(需同步修改委托定义也为ref),但out无需初始化作为返回值来说更贴切
int _returnVal2; bool _returnVal3; int _returnVal4;
string _returnVal1 = _callFunc3Method1(1314, out _returnVal2, out _returnVal3, out _returnVal4);
Debug.Log("CSharp _callFunc3Method1 return = " + _returnVal1 + " " + _returnVal2 + " " + _returnVal3 + " " + _returnVal4);
//LUA: func3
//CSharp _callFunc3Method1 return = Yui 520 True 1314
}
//使用XLua提供的LuaFunction接收,多返回值通过Call()返回的的object[]数组体现
LuaFunction _callFunc3Method2 = LuaManager.Instance.Global.Get<LuaFunction>("func3");
if (_callFunc3Method2 != null)
{
object[] _returnVals = _callFunc3Method2.Call("Azusa");
string _returnResult = "CSharp _callFunc3Method2 return = ";
for (int _i = 0; _i < _returnVals.Length; _i++)
_returnResult += _returnVals[_i] + " ";
Debug.Log(_returnResult);
//LUA: func3
//CSharp _callFunc3Method2 return = Yui 520 True Azusa
}
#endregion
#region 变长参数函数
//使用自定义的委托接收
DelegateForFunc4 _callFunc4Method1 = LuaManager.Instance.Global.Get<DelegateForFunc4>("func4");
if (_callFunc4Method1 != null) { Debug.Log("CSharp _callFunc4Method1 return = " + _callFunc4Method1(520, "Mugi", true, 789.0f)); }
//LUA: func4
//CSharp _callFunc4Method1 return = 520/Mugi/true/789.0/
if (_callFunc4Method1 != null) { Debug.Log("CSharp _callFunc4Method1 return = " + _callFunc4Method1(521, 789.0f, "Yui", true, false)); }
//LUA: func4
//CSharp _callFunc4Method1 return = 521/789.0/Yui/true/false/
//使用XLua提供的LuaFunction接收
LuaFunction _callFunc4Method2 = LuaManager.Instance.Global.Get<LuaFunction>("func4");
if (_callFunc4Method2 != null) { Debug.Log("CSharp _callFunc4Method2 return = " + _callFunc4Method2.Call(520, "Mugi", true, 789.0f)[0]); }
//LUA: func4
//CSharp _callFunc4Method2 return = 520/Mugi/true/789.0/
if (_callFunc4Method2 != null) { Debug.Log("CSharp _callFunc4Method2 return = " + _callFunc4Method2.Call(521, 789.0f, "Yui", true, false)[0]); }
//LUA: func4
//CSharp _callFunc4Method2 return = 521/789.0/Yui/true/false/
#endregion
}
}
}
3.4 映射Lua表结构
Lua的
table表结构同样可通过_G表获取,根据table的结构不同可以选择使用不同的接收方式
3.4.1 映射到列表或字典
- 更新
TestVariables.lua脚本内容如下
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
print("TestVariables.lua")
-- 测试映射到CSharp的List
testList1 = { 111, 222, 333, 444, 555 }
testList2 = { true, 1.31, "Yui", 521, false, 520 }
-- 测试映射到CSharp的Dictionary
testDictionary1 = {
["one"] = 111,
["two"] = 222,
["three"] = 333,
["four"] = 444,
["five"] = 555
}
testDictionary2 = {
[true] = "Hi",
["str"] = 100,
[3] = false
}
- 此处使用CSharp的
List和Dictionary分别接收两种对应类型的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
40
41
42
43
44
45
46
47
48
49
50
51
52
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
namespace WZ
{
public class Test : MonoBehaviour
{
void Start()
{
LuaManager.Instance.Init();
LuaManager.Instance.RunLuaScript("Main");
#region List
//已知类型元素
List<int> _list1 = LuaManager.Instance.Global.Get<List<int>>("testList1");
string _strList1 = "testList1 = ";
foreach (var item in _list1)
_strList1 += item.ToString() + " ";
Debug.Log(_strList1);
//testList1 = 111 222 333 444 555
//未知类型元素
List<object> _list2 = LuaManager.Instance.Global.Get<List<object>>("testList2");
string _strList2 = "testList1 = ";
foreach (var item in _list2)
_strList2 += item.ToString() + " ";
Debug.Log(_strList2);
//testList1 = True 1.31 Yui 521 False 520
#endregion
#region Dictionary
//确定类型键值对
Dictionary<string, int> _dic1 = LuaManager.Instance.Global.Get<Dictionary<string, int>>("testDictionary1");
string _strDic1 = "testDictionary1 = ";
foreach (var item in _dic1)
_strDic1 += "(" + item.Key.ToString() + ":" + item.Value.ToString() + ") ";
Debug.Log(_strDic1);
//testDictionary1 = (four:444) (three:333) (two:222) (five:555) (one:111)
//未知类型键值对
Dictionary<object, object> _dic2 = LuaManager.Instance.Global.Get<Dictionary<object, object>>("testDictionary2");
string _strDic2 = "testDictionary2 = ";
foreach (var item in _dic2)
_strDic2 += "(" + item.Key.ToString() + ":" + item.Value.ToString() + ") ";
Debug.Log(_strDic2);
//testDictionary2 = (True:Hi) (3:False) (str:100)
#endregion
}
}
}
3.4.2 映射到类或接口
- 更新
TestVariables.lua脚本内容如下
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
print("TestVariables.lua")
-- 测试映射到CSharp的类(值拷贝)
testClass = {
-- 测试普通成员
memberInt = 520,
memberFloat = 3.14,
memberBool = true,
memberString = "Yui",
memberFunction = function()
print("Run testClass.memberFunction")
end,
-- 测试嵌套类
recursionClass = {
memberInt = 999
}
}
-- 测试映射到CSharp的接口(引用拷贝)
testInterface = {
memberInt = 520,
memberFloat = 3.14,
memberBool = true,
memberString = "Yui",
memberFunction = function()
print("Run testInterface.memberFunction")
end,
}
- 在CSharp中的处理如下,注意此前的其它映射都是值拷贝,唯有映射到接口是引用拷贝
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
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Events;
using XLua;
namespace WZ
{
//测试映射到类
public class MappingRecursionClass
{
public int memberInt;
}
public class MappingTestClass
{
//此处成员名称与类型必须与Lua脚本中对应的表中成员匹配,且需是public,否则无法被访问
//允许缺少成员或存在多余成员(无非是相应的Lua变量无法被接收或类成员只能被初始化为默认值而已)
public int memberInt;
public float memberFloat;
public bool memberBool;
public string memberString;
public UnityAction memberFunction;
public MappingRecursionClass recursionClass;
}
//测试映射到接口,注意接口必须添加[CSharpCallLua]特性,并重新生成XLua代码
[CSharpCallLua]
public interface IMappingTestClass
{
//接口中不允许存在成员变量,但允许存在属性(本质是函数),故可用属性接收Lua表中的变量
//同样需要是名称与类型匹配,且get和set都必须存在且为public(啥也不修饰默认就是public)
int memberInt { get; set; }
float memberFloat { get; set; }
bool memberBool { get; set; }
string memberString { get; set; }
UnityAction memberFunction { get; set; }
}
public class Test : MonoBehaviour
{
void Start()
{
LuaManager.Instance.Init();
LuaManager.Instance.RunLuaScript("Main");
#region Class
//将Lua的表映射到一个类对象上
MappingTestClass _obj = LuaManager.Instance.Global.Get<MappingTestClass>("testClass");
Debug.Log("testClass = " + _obj.memberInt.ToString() + " " + _obj.memberFloat.ToString() + " " + _obj.memberBool.ToString() + " " + _obj.memberString.ToString());
//testClass = 520 3.14 True Yui
_obj.memberFunction();
//LUA: Run testClass.memberFunction
Debug.Log("testClass.recursionClass = " + _obj.recursionClass.memberInt.ToString());
//testClass.recursionClass = 999
//修改类对象成员值不会反映到Lua表中,因其是值拷贝
_obj.memberInt = 1314;
Debug.Log("testClass.memberInt = " + LuaManager.Instance.Global.Get<MappingTestClass>("testClass").memberInt);
//testClass.memberInt = 520
#endregion
#region Interface
//将Lua的表映射到一个接口上,CSharp的接口本不能被实例化,但XLua会自动生成一个类来实现该接口,这也是为何需要使用[CSharpCallLua]修饰并生成代码
IMappingTestClass _iObj = LuaManager.Instance.Global.Get<IMappingTestClass>("testInterface");
Debug.Log("testInterface = " + _iObj.memberInt.ToString() + " " + _iObj.memberFloat.ToString() + " " + _iObj.memberBool.ToString() + " " + _iObj.memberString.ToString());
//testInterface = 520 3.14 True Yui
_iObj.memberFunction();
//LUA: Run testInterface.memberFunction
//修改接口成员值会直接反映到Lua表中,因其是引用拷贝
_iObj.memberInt = 1314;
Debug.Log("testInterface.memberInt = " + LuaManager.Instance.Global.Get<IMappingTestClass>("testInterface").memberInt);
//testInterface.memberInt = 1314
#endregion
}
}
}
3.4.3 映射到LuaTable
- 更新
TestVariables.lua脚本内容如下
1
2
3
4
5
6
7
8
9
10
11
print("TestVariables.lua")
-- 测试映射到LuaTable
testTable = {
memberInt = 520,
memberFloat = 3.14,
memberBool = true,
memberString = "Yui",
memberFunction = function()
print("Run testTable.memberFunction")
end
}
- 在CSharp中的处理如下
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
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Events;
using XLua;
namespace WZ
{
public class Test : MonoBehaviour
{
void Start()
{
LuaManager.Instance.Init();
LuaManager.Instance.RunLuaScript("Main");
//LuaTable类依赖于XLua命名空间,官方不建议使用LuaTable和LuaFunction等类型,因其效率较低且易产生垃圾
LuaTable _tab = LuaManager.Instance.Global.Get<LuaTable>("testTable");
//读取LuaTable内元素的值
Debug.Log("testTable.memberInt = " + _tab.Get<int>("memberInt").ToString());
//testTable.memberInt = 520
Debug.Log("testTable.memberFloat = " + _tab.Get<float>("memberFloat").ToString());
//testTable.memberFloat = 3.14
Debug.Log("testTable.memberBool = " + _tab.Get<bool>("memberBool").ToString());
//testTable.memberBool = True
Debug.Log("testTable.memberString = " + _tab.Get<string>("memberString").ToString());
//testTable.memberString = Yui
_tab.Get<UnityAction>("memberFunction")();
//LUA: Run testTable.memberFunction
//对LuaTable内元素的Set修改会影响Lua侧对应变量的值
_tab.Set("memberInt", 20);
LuaTable _tabRe = LuaManager.Instance.Global.Get<LuaTable>("testTable");
Debug.Log("testTable.memberInt = " + _tabRe.Get<int>("memberInt").ToString());
//testTable.memberInt = 20
//用完就销毁,否则有垃圾
_tab.Dispose();
_tabRe.Dispose();
}
}
}
四、Lua调用Csharp
4.1 Lua初始化入口
- 游戏启动时只能先执行某些CSharp脚本入口,让其调用某些Lua脚本后,Lua才能够访问CSharp(不可能在最开始先调用Lua,其没法直接访问CSharp)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
namespace WZ
{
//Lua调用CSharp前需由此入口初始化Lua框架,否则无法调用
public class LuaEntry : MonoBehaviour
{
void Awake()
{
//初始化Lua环境,往后无需再初始化
LuaManager.Instance.Init();
//执行Lua侧的初始化脚本
LuaManager.Instance.RunLuaScript("Main");
}
}
}
4.2 调用CSharp的类与枚举
- 在Lua中调用已有类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
-- 在Lua中调用CSharp的类直接通过以下方式即可,Unity的GameObject和Tranform等类在UnityEngine命名空间内
-- CS.命名空间.类名
-- 由于Lua中无法使用new,实例化一个对象直接类名括号即可
local obj1 = CS.UnityEngine.GameObject() -- 调用默认的无参构造
obj1.name = "YuiObject"
local obj2 = CS.UnityEngine.GameObject("MugiObject") -- 调用初始化GO名称的构造
-- 为了方便使用,且节约性能,可以使用全局变量存储CSharp中的特定类作为别名使用
GameObject = CS.UnityEngine.GameObject
local obj3 = GameObject("AzusaObject") -- 能够正常使用
-- 类中的静态成员方法可通过.调用,对象的成员也可通过.调用
local cam = GameObject.Find("MainCamera")
CS.UnityEngine.Debug.Log(tostring(cam.transform.position)) -- (0.00, 1.00, -10.00): 239599616
-- 类中的非静态成员方法需通过:调用
cam.transform:Translate(CS.UnityEngine.Vector3.right) -- 向右移动1个单位
CS.UnityEngine.Debug.Log(tostring(cam.transform.position)) -- (1.00, 1.00, -10.00): 835190784
- 在CSharp中声明自定义类
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
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class TestClassOutNamespace
{
public void Print(string _str)
{
Debug.Log(_str + " TestClassOutNamespace");
}
}
namespace WZ
{
//有命名空间,用于测试Lua调用CSharp的类
public class TestClassInNamespace
{
public void Print(string _str)
{
Debug.Log(_str + " TestClassInNamespace");
}
}
public class TestClassMono : MonoBehaviour
{
void Start()
{
Debug.Log("TestClassMono Start");
}
}
}
- 在Lua中调用自定义类
1
2
3
4
5
6
7
8
9
10
11
12
GameObject = CS.UnityEngine.GameObject
-- 调用自定义类TestClassOutNamespace和WZ.TestClassInNamespace(非MonoBehaviour子类,可以直接创建新对象)
local ett1 = CS.TestClassOutNamespace() -- 无命名空间
ett1:Print("ett1") -- ett1 TestClassOutNamespace
local ett2 = CS.WZ.TestClassInNamespace() -- 命名空间WZ
ett2:Print("ett2") -- ett2 TestClassInNamespace
-- 调用自定义类WZ.TestClassMono(MonoBehaviour子类,不能直接new新对象)
local obj = GameObject("NewGameObjectForTest")
-- 需将脚本AddComponent到新建出来的GameObject上,注意此处typeof是XLua提供的重要方法
obj:AddComponent(typeof(CS.WZ.TestClassMono)) -- TestClassMono Start
- 在CSharp中声明枚举
1
2
3
4
5
6
7
8
9
10
namespace WZ
{
//用于测试Lua调用CSharp的枚举
public enum TestEnumInNamespace
{
Type1,
Type2,
Type3
}
}
- 在Lua中调用枚举
1
2
3
4
5
6
7
8
9
10
11
12
-- 调用Unity中的枚举,跟调用类的方法类似,此处调用PrimitiveType的Cube枚举项
local obj = CS.UnityEngine.GameObject.CreatePrimitive(CS.UnityEngine.PrimitiveType.Cube)
-- 注意命名空间
MyEnum = CS.WZ.TestEnumInNamespace
print(MyEnum.Type1) -- LUA: Type1: 0
-- 数值类型转换为枚举,使用固定方法__CastFrom(number)
print(MyEnum.__CastFrom(1)) -- LUA: Type2: 1
-- 字符串类型转换为枚举,使用固定方法__CastFrom(string)
print(MyEnum.__CastFrom("Type3")) -- LUA: Type3: 2
4.3 调用CSharp的数据集合
- 在CSharp中声明数组、列表、字典
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
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
namespace WZ
{
//用于测试Lua调用CSharp的枚举
public class TestContainer
{
//一维数组
public int[] arr = new int[5] { 11, 22, 33, 44, 55 };
//二维数组
public int[,] map = new int[4,3] {
{ 11, 12, 13 },
{ 21, 22, 23 },
{ 31, 32, 33 },
{ 41, 42, 43 }
};
//列表
public List<int> list = new List<int>() { 111, 222, 333, 444, 555 };
//字典
public Dictionary<string, int> dict = new Dictionary<string, int>()
{
{ "one", 1111 },
{ "two", 2222 },
{ "three", 3333 },
{ "four", 4444 },
{ "five", 5555 },
};
}
}
- 在Lua中调用数组、列表、字典
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
local obj = CS.WZ.TestContainer()
---------------------------------------------------------------------------------------------------------------------------</一维数组
-- CSharp中怎么调用数组,此处就怎么调用,而不是像Lua的table那样调用
-- 不能通过#运算符获取长度
print("arr length = " .. tostring(obj.arr.Length)) -- LUA: arr length = 5
-- 索引从0开始而非从1开始
print("arr[0] = " .. tostring(obj.arr[0])) -- LUA: arr[0] = 11
-- 可通过对应类的静态创建方法绕过new关键字创建数据集合实例,注意命名空间是System
local arrNew = CS.System.Array.CreateInstance(typeof(CS.System.Int32), 3)
print(tostring(arrNew)) -- LUA: System.Int32[]: 1773503944
-- 可以直接像CSharp中一样对元素进行赋值
arrNew[0] = 100
arrNew[1] = 200
arrNew[2] = 300
-- 数组和列表都不能用pairs遍历,因XLua并未像对字典一样对pairs进行特殊处理
-- for k, v in pairs(arrNew) do
-- print("arrNew[" .. k .. "]" .. tostring(v))
-- end
for i = 0, arrNew.Length - 1 do
print("arrNew[" .. i .. "] = " .. tostring(arrNew[i]))
end
-- LUA: arrNew[0] = 100
-- LUA: arrNew[1] = 200
-- LUA: arrNew[2] = 300
---------------------------------------------------------------------------------------------------------------------------/>
---------------------------------------------------------------------------------------------------------------------------</二维数组
-- 通过GetLength方法获取二维数组的长宽,0获取行数,1获取列数
print("map row = " .. tostring(obj.map:GetLength(0))) -- LUA: map row = 4
print("map col = " .. tostring(obj.map:GetLength(1))) -- LUA: map col = 3
-- Lua中不支持以[]获取按行列索引获取元素,需以成员方法获取
-- print("map[0,1] = " .. tostring(obj.map[0,1])) -- 这是CSharp中调用二维数组的方式,此处会语法报错
-- print("map[0,1] = " .. tostring(obj.map[0][1])) -- 报错LuaException: c# exception in ArrayIndexer:System.ArgumentException: Only single dimensional arrays are supported for the requested action
print("map[0,1] = " .. tostring(obj.map:GetValue(0, 1))) -- LUA: map[0,1] = 12
---------------------------------------------------------------------------------------------------------------------------/>
---------------------------------------------------------------------------------------------------------------------------</列表
-- 调用方法添加元素,注意冒号调用
obj.list:Add(777)
-- 对于List<T>同样需注意索引从0开始
for i = 0, obj.list.Count - 1 do
print("list[" .. i .. "] = " .. tostring(obj.list[i]))
end
-- LUA: list[0] = 111
-- LUA: list[1] = 222
-- LUA: list[2] = 333
-- LUA: list[3] = 444
-- LUA: list[4] = 555
-- LUA: list[5] = 777
-- 在Lua中创建新的泛型List容器有新旧两种方法
-- 旧版本方法,较繁琐,注意此处System.String是CSharp的规则,故无需CS.前缀
local listOldMethod = CS.System.Collections.Generic["List`1[System.String]"]()
print(tostring(listOldMethod)) -- LUA: System.Collections.Generic.List`1[System.String]: 1418522420
-- 新版本方法,更简洁
local listNewMethod = CS.System.Collections.Generic.List(CS.System.String)()
print(tostring(listNewMethod)) -- LUA: System.Collections.Generic.List`1[System.String]: -922061158
---------------------------------------------------------------------------------------------------------------------------/>
---------------------------------------------------------------------------------------------------------------------------</字典
-- 新增键值对元素,无法直接通过obj.dict["newkey"] = newvalue的方式添加
obj.dict:Add("six", 6666)
-- 修改键值对元素,无法直接通过obj.dict["key"] = newvalue的方式修改
obj.dict:set_Item("one", nil) -- 在Lua中赋的nil会被当作CSharp中对应类型的默认值
-- 对于Dictionary<Key,Value>直接用pairs遍历更方便
for k, v in pairs(obj.dict) do
print("dict[" .. k .. "] = " .. tostring(v))
end
-- LUA: dict[one] = 0
-- LUA: dict[two] = 2222
-- LUA: dict[three] = 3333
-- LUA: dict[four] = 4444
-- LUA: dict[five] = 5555
-- LUA: dict[six] = 6666
-- 在Lua中创建新的泛型Dictionary容器同样有新旧两种方法
-- 旧版本方法过于繁琐不写了,新版本方法如下
local dicNewMethod = CS.System.Collections.Generic.Dictionary(CS.System.String, CS.System.Int32)()
print(tostring(dicNewMethod)) -- LUA: System.Collections.Generic.Dictionary`2[System.String,System.Int32]: -395368172
-- 对于新创建的字典,无法通过["key"]方式访问,而需通过固定方法获取键对应的值
dicNewMethod:Add("key", 114514)
print("dicNewMethod[\"key\"] = " .. tostring(dicNewMethod["key"])) -- LUA: dicNewMethod["key"] = nil
print("dicNewMethod[\"key\"] = " .. tostring(dicNewMethod:get_Item("key"))) -- LUA: dicNewMethod["key"] = 114514
print("dicNewMethod[\"key\"] = " .. tostring(dicNewMethod:TryGetValue("key"))) -- LUA: dicNewMethod["key"] = true
-- CSharp中的声明为bool TryGetValue(TKey key, out TValue value)导致上下两打印结果不同,因为CSharp无多返回值,故用out传参模拟
print(dicNewMethod:TryGetValue("key")) -- LUA: true 114514
---------------------------------------------------------------------------------------------------------------------------/>
4.4 调用CSharp的拓展方法
- 在CSharp中声明
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
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using XLua;
namespace WZ
{
//一个普通的类,拥有一些自己的方法
public class TestOrigin
{
public void Speak(string _words)
{
Debug.LogError("TestOrigin speaks " + _words + " by TestOrigin");
}
}
//扩展方法必须是在静态类中的静态方法,且若想在XLua中调用则必须以[LuaCallCSharp]修饰(添加修饰后需重新生成XLua)
//语法上允许不同类的拓展方法写在同一静态类中,但实践中最好按照类型或功能进行分类
[LuaCallCSharp]
public static class TestExtend
{
//为已有的TestOrigin类添加新的拓展方法Eat,而无需修改原类的代码(避免重新编译)或创建其子类(避免冗余继承结构)
//第一个参数前的this关键字表示这是一个扩展方法,此处表示扩展的目标类型是TestOrigin,该方法可直接通过目标类实例调用
public static void Eat(this TestOrigin _obj)
{
Debug.LogError("TestOrigin eats " + _obj.ToString() + " by TestExtend");
}
}
}
- 在Lua中调用
1
2
3
4
5
6
7
local obj = CS.WZ.TestOrigin()
-- 测试TestOrigin类原有的方法
obj:Speak("Yui") -- TestOrigin speaks Yui by TestOrigin
-- 测试TestOrigin类在静态类TestExtend中的拓展方法,后者须以[LuaCallCSharp]修饰并重新生成XLua,否则报错LuaException: Main:4: attempt to call a nil value (method 'Eat')
obj:Eat() -- TestOrigin eats WZ.TestOrigin by TestExtend
4.5 调用CSharp的各类函数
4.5.1 ref/out参数
- 在CSharp中声明
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
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
namespace WZ
{
public class TestMethodsWithRefOrOut
{
public string FuncRef(int _a, ref int _b, int _c, ref int _d)
{
_b = _a;
_d = _c;
return "FuncRef Over";
}
public void FuncOut(int _a, out int _b, int _c, out int _d)
{
_b = _a * 10;
_d = _c / 10;
Debug.LogError("FuncOut Over");
}
public void FuncRefOut(int _a, ref int _b, int _c, out int _d)
{
_b = 999;
_d = 111;
Debug.LogError("FuncRefOut Over");
}
}
}
- 在Lua中调用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
local obj = CS.WZ.TestMethodsWithRefOrOut()
-- 所有ref类型的参数都需传入一个占位的值,即不能空着
-- ref类型参数以多返回值形式给到Lua侧,若函数有返则体现到首个返回值上,往后依次是经调用后的ref传入参数的值
-- public string FuncRef(int _a, ref int _b, int _c, ref int _d)
local return1, b1, d1 = obj:FuncRef(10, 20, 30, 40)
print("return=" .. tostring(return1) .. " b=" .. tostring(b1) .. " d=" .. tostring(d1))
-- LUA: return=FuncRef Over b=10 d=30
-- 所有out类型的参数不需要传值,空着当他不存在就行
-- out类型参数同样以多返回值形式给到Lua侧,此处测试函数无返回值
-- public void FuncOut(int _a, out int _b, int _c, out int _d)
local b2, d2 = obj:FuncOut(50, 60) -- 此处相当于:50对应_a、60对应_c
print("b=" .. tostring(b2) .. " d=" .. tostring(d2))
-- FuncOut Over
-- LUA: b=500 d=6
-- 综合使用,注意ref不能空而out需空即可
-- public void FuncRefOut(int _a, ref int _b, int _c, out int _d)
local b3, d3 = obj:FuncRefOut(50, 0, 60) -- 此处相当于:50对应_a、0对应_b、60对应_c
print("b=" .. tostring(b3) .. " d=" .. tostring(d3))
-- FuncRefOut Over
-- LUA: b=999 d=111
4.5.2 调用重载函数
- 在CSharp中声明
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
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
namespace WZ
{
public class TestReloadedMethods
{
public int TestFunc1()
{
return 100;
}
public int TestFunc1(int _a, int _b)
{
return _a + _b;
}
public int TestFunc2(int _a)
{
return _a;
}
public float TestFunc2(float _a)
{
return _a;
}
}
}
- 在Lua中调用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
local obj = CS.WZ.TestReloadedMethods()
-- 即便Lua本身语法不支持函数重载,但此处可调用CSharp中的重载
print(tostring(obj:TestFunc1())) -- LUA: 100
print(tostring(obj:TestFunc1(111, 222))) -- LUA: 333
-- 试图调用两个以int/float区分的重载函数,由于Lua只有number而不区分整型和浮点型,此处会不符预期
print(tostring(obj:TestFunc2(10))) -- LUA: 10
print(tostring(obj:TestFunc2(10.2))) -- LUA: 0
-- 所以最好需要尽量这种情况,比如统一全部使用float就得了,别搞这样的多精度重载
-- 但是你硬要写也有性能较低的办法,此处使用反射获取函数信息,传入函数名和参数类型列表(此处只有一个参数)
local infox = typeof(CS.WZ.TestReloadedMethods):GetMethod("TestFunc2", { typeof(CS.System.Int32) }) -- CSharp的int对应System.Int32
local infoy = typeof(CS.WZ.TestReloadedMethods):GetMethod("TestFunc2", { typeof(CS.System.Single) }) -- CSharp的float对应System.Single
-- 然后通过XLua提供的方法将上述信息转化为Lua侧的方法
local funcx = xlua.tofunction(infox)
local funcy = xlua.tofunction(infoy)
-- 注意成员方法的首个参数传对象,而静态方法则省略
print(tostring(funcx(obj, 10))) -- LUA: 10
print(tostring(funcy(obj, 10.2))) -- LUA: 10.199999809265
4.5.3 调用泛型函数
- 在CSharp中声明
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
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
namespace WZ
{
public class TestGenericMethods
{
public class TestFather { }
public class TestChild : TestFather { }
public interface ITestInterface { }
public class TestInterfaceImpl : ITestInterface { }
//泛型方法,有参数无约束
public void TestFunc1<T>(T _a)
{
Debug.Log("测试有参数、无约束的泛型方法 " + _a.ToString());
}
//泛型方法,有参数有约束(T须为TestFather或其子类)
public void TestFunc2<T>(T _a) where T : TestFather
{
Debug.Log("测试有参数、有子类约束的泛型方法 " + _a.ToString());
}
//泛型方法,无参数有约束(T须为TestFather或其子类)
public void TestFunc3<T>() where T : TestFather
{
Debug.Log("测试无参数、有子类约束的泛型方法");
}
//泛型方法,有参数有约束(但约束为接口约束)
public void TestFunc4<T>(T _a) where T : ITestInterface
{
Debug.Log("测试有参数、有接口约束的泛型方法 " + _a.ToString());
}
}
}
- 在Lua中一般认为只支持调用有
class泛型约束且接收泛型类型参数的泛型方法,除非通过新版本XLua提供的xlua.get_generic_method方法,但不建议使用,因其对Unity打包时存在限制,即对于Building Setting->Player Settings->Other Settings->Configuration->Scripting Backend项的Mono和IL2CPP两个配置值- 若选
Mono则xlua.get_generic_method支持对所有形式泛型方法的调用 - 若选
IL2CPP则xlua.get_generic_method要求泛型参数必须为引用类型,若为值类型则该值类型必须已经在CSharp侧被使用过,才能被支持在Lua侧作为泛型函数的泛型类型使用
- 若选
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
local obj = CS.WZ.TestGenericMethods()
local objFather = CS.WZ.TestGenericMethods.TestFather()
local objChild = CS.WZ.TestGenericMethods.TestChild()
local objImpl = CS.WZ.TestGenericMethods.TestInterfaceImpl()
-- 不支持调用:无泛型约束的泛型方法(除非通过xlua.get_generic_method获取,并设置泛型类型)
-- obj:TestFunc1(999) -- LuaException: invalid arguments to TestFunc1
local testFunc1Generic = xlua.get_generic_method(CS.WZ.TestGenericMethods, "TestFunc1")
local testFunc1Integer = testFunc1Generic(CS.System.Int32) -- 例如指定泛型类型T为int
-- 通过xlua.get_generic_method中设置的该成员泛型函数所属的class的对象来调用(如果是静态方法则不用在第一个参数传类对象)
testFunc1Integer(obj, 999) -- 测试有参数、无约束的泛型方法 999
-- 支持调用:有子类泛型约束且有参数的泛型方法
obj:TestFunc2(objFather) -- 测试有参数、有子类约束的泛型方法 WZ.TestGenericMethods+TestFather
obj:TestFunc2(objChild) -- 测试有参数、有子类约束的泛型方法 WZ.TestGenericMethods+TestChild
-- 不支持调用:有泛型约束但无参数的泛型方法
-- obj:TestFunc3() -- LuaException: invalid arguments to TestFunc3
-- 不支持调用:非class泛型约束的泛型方法(此处以接口约束为例)
-- obj:TestFunc4(objImpl) -- LuaException: invalid arguments to TestFunc4
4.5.4 调用协程函数
- 在CSharp中声明
1
2
3
4
5
6
7
8
9
10
11
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
namespace WZ
{
//注意需是继承自MonoBehaviour的类,才能调用StartCoroutine以启动协程
public class Test : MonoBehaviour
{
}
}
- 在Lua中调用
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
GameObject = CS.UnityEngine.GameObject
WaitForSeconds = CS.UnityEngine.WaitForSeconds -- 用于在协程内等待指定秒数
-- CSharp中需借助MonoBehaviour的类的StartCoroutine成员方法来启动协程,故需借助在GameObject上挂载的脚本组件来调用
local obj = GameObject("CoroutineHelper")
local scp = obj:AddComponent(typeof(CS.WZ.Test)) -- 挂载一个内有名为Test的MonoBehaviour子类的脚本
-- 定义协程函数
local coFunc = function()
local count = 9
while count > 0 do
print("coFunc round " .. tostring(count))
-- 此处无法使用CSharp的yield return xxx语法,但可以使用Lua的coroutine.yield(xxx)语法
-- coroutine.yield(nil) -- 等待1帧
coroutine.yield(WaitForSeconds(1)) -- 等待1秒
count = count - 1
end
end
-- 试图将coFunc作为协程函数直接启动,发现报错,因为不能将Lua侧的函数传给CSharp侧的StartCoroutine
-- scp:StartCoroutine(coFunc) -- LuaException: invalid arguments to UnityEngine.MonoBehaviour.StartCoroutine!
-- 引入XLua提供的工具模块,通过其cs_generator方法包裹Lua侧函数传给StartCoroutine
util = require("xlua.util")
scp:StartCoroutine(
util.cs_generator(
function()
-- 想写啥写啥
coFunc()
print("coFunc finished")
end
)
)
-- 运行结果如下,每行均间隔1秒被打印出来
-- LUA: coFunc round 9
-- LUA: coFunc round 8
-- LUA: coFunc round 7
-- LUA: coFunc round 6
-- LUA: coFunc round 5
-- LUA: coFunc round 4
-- LUA: coFunc round 3
-- LUA: coFunc round 2
-- LUA: coFunc round 1
-- LUA: coFunc finished
4.6 调用CSharp的委托与事件
- 在CSharp中声明
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
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Events;
namespace WZ
{
public class TestDelAndEvent
{
//接收一个string参数的委托/事件
public UnityAction<string> testDel;
public event UnityAction<string> testEvent;
//由于事件只能在类的内部调用,故此处提供一个方法供Lua侧调用
public void DoTestEvent(string _str)
{
if (testEvent != null)
testEvent(_str);
}
//由于事件对外不提供清空方法,故此处提供一个
public void ClearTestEvent()
{
testEvent = null;
}
}
}
- 在Lua中调用
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
local obj = CS.WZ.TestDelAndEvent()
-- 用于注册的函数
local testFunc = function(parm)
print("testFunc = " .. tostring(parm))
end
-- 用CSharp中的委托来装Lua侧的函数,注意此处声明的函数必须与CSharp对应委托的签名保持一致
-- 初始化委托时由于是nil而无法使用加号注册,需先用等号赋值
obj.testDel = testFunc
-- 然后才可往该委托内多播更多的同签名函数(但最好不要用Lambda,因为不好从委托中取消注册,此处只是为了方便罢了)
obj.testDel = obj.testDel + function(parm)
print("lambda = " .. tostring(parm))
end
-- 测试调用该委托,因为不是成员函数所以无需使用:调用
obj.testDel("call testDel")
-- LUA: testFunc = call testDel
-- LUA: lambda = call testDel
obj.testDel = obj.testDel - testFunc -- 取消注册
obj.testDel("call testDel")
-- LUA: lambda = call testDel
obj.testDel = nil -- 清空委托,此时调用会报错
-- obj.testDel("call testDel") -- LuaException: Main:23: attempt to call a nil value (field 'testDel')
-- 事件加减函数和委托不一样,需像成员函数一样通过以下方式加减
obj:testEvent("+", testFunc)
obj:testEvent("+", function(parm)
print("lambda = " .. tostring(parm))
end)
obj:DoTestEvent("call testEvent")
-- LUA: testFunc = call testEvent
-- LUA: lambda = call testEvent
obj:testEvent("-", testFunc) -- 取消注册
obj:DoTestEvent("call testEvent")
-- LUA: lambda = call testEvent
obj:ClearTestEvent() -- 清空事件
obj:DoTestEvent("call testEvent") -- 调用后无输出
4.7 重要注意事项
4.7.1 如何比较null与nil
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
-- 若一个物体身上没有某个组件则添加该组件,若有则无需添加
local go = CS.UnityEngine.GameObject("TestGO")
-- 正常思路就是先尝试获取该物体上的对应组件,若空则执行添加逻辑
local rb = go:GetComponent(typeof(CS.UnityEngine.Rigidbody))
print(tostring(rb)) -- LUA: null: 0
if rb == nil then
print("Target component is empty") -- 这个根本没打印出来,说明执行时没进入此处if内
rb = go:AddComponent(typeof(CS.UnityEngine.Rigidbody))
end
print(tostring(rb)) -- LUA: null: 0
-- 坑点在于上述rb是CSharp中的null类型,是不能与Lua侧的nil使用==判等的
-- 可对null使用Equals方法与nil进行比较,但若万一传入的就是nil则会导致报错,所以一般会写一个通用的全局判空方法如下
function IsNull(obj)
if obj == nil or obj:Equals(nil) then
return true
else
return false
end
end
-- 然后再用上述方法来书写逻辑
if IsNull(rb) then
print("Target component is empty")
rb = go:AddComponent(typeof(CS.UnityEngine.Rigidbody))
end
-- LUA: Target component is empty
print(tostring(rb)) -- LUA: TestGO (UnityEngine.Rigidbody): -32252
-- 还有一种解决方法,是在CSharp侧为Object类拓展一个判空方法专供Lua侧使用
-- [LuaCallCSharp]
-- public static class ExtendLuaCheckNull
-- {
-- public static boll IsNull(this Object _obj)
-- {
-- return _obj == null;
-- }
-- }
-- 然后在Lua中如下使用即可
-- if rb:IsNull() then
-- end
4.7.2 以XLua修饰系统类型
- 前文提到过Lua调用CSharp侧的拓展方法时需添加
[LuaCallCSharp]修饰,虽然对于其它不承载拓展方法的类,即便不修饰[LuaCallCSharp]在Lua中被调用也不会报错,但对于需要被Lua侧访问的类,建议都加上该修饰以提升性能- 若无该修饰,Lua会默认在运行时通过反射查找被调用的CSharp中对应的类和方法等,效率较低(动态)
- 若有该修饰,重新生成XLua后会为被标记的类生成某些对应的代码而避免反射,以提高性能(静态)
- 但是对于自定义类我们可以直接手动修饰,但对于系统已有类或是第三方库的类,我们却不能直接去修改代码加上修饰,对于
[CSharpCallLua]同样存在该问题,如下所示
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
GameObject = CS.UnityEngine.GameObject
UI = CS.UnityEngine.UI
-- 假设当前场景上存在一个名为TestSlider的拥有Slider组件的游戏对象(UGUI)
-- 我们想在Lua侧找到该对象,然后为其Value变化的事件新增一个事件函数注册
-- 需先获取该对象身上的Slider组件脚本
local slider = GameObject.Find("Canvas/TestSlider"):GetComponent("Slider")
print(tostring(slider)) -- LUA: TestSlider (UnityEngine.UI.Slider): -32468
-- 然后通过Unity提供的方法AddListener将新委托注册到成员onValueChanged上,接收的参数即变化的值,此处采取默认设置即以0~1范围内的浮点数表示拖动进度
slider.onValueChanged:AddListener(function(f)
print("slider.onValueChanged = " .. tostring(f))
end)
-- 此时运行会报错LuaException: c# exception:This type must add to CSharpCallLua: UnityEngine.Events.UnityAction<float>,stack: at XLua.ObjectTranslator.CreateDelegateBridge
-- 上述报错提醒需为UnityEngine.Events.UnityAction<float>添加[CSharpCallLua]修饰,因为该类型就是前文onValueChanged委托的类型,但UnityAction是Unity系统提供的泛型委托,我们没办法直接修饰
- XLua提供了解决上述问题的办法,即如下所示在CSharp侧写一个静态类,然后声明一个静态列表将所需添加修饰的类型都写进去,然后重新生成XLua我们就会发现原来报错的功能恢复了,滑动
TestSlider时能正常打印信息LUA: slider.onValueChanged = 0.40952387452126等
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
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Events;
using XLua;
namespace WZ
{
public static class TestCall
{
//注意此处是固定写法,列表内存入想要添加[CSharpCallLua]修饰的系统类型,然后重新生成XLua
[CSharpCallLua]
public static List<System.Type> csharpCallLuaList = new List<System.Type>() {
typeof(UnityAction<float>)
//还可填入更多所需要的类型,此处只写一个来测试
};
//对于[LuaCallCSharp]同理
[LuaCallCSharp]
public static List<System.Type> luaCallCSharpList = new List<System.Type>() {
typeof(GameObject),
typeof(Rigidbody)
//可填入更多其它需在Lua使用的系统类型
};
}
}
本文由作者按照 CC BY-NC-SA 4.0 进行授权