接上一篇的C#调用Lua部分,我们一般做纯Lua逻辑的游戏主要用的就是Lua调用C#的API,这篇笔记就来简单的介绍具体是如何操作的。
调用Class
在开始学习Lua脚本中调用C#中的Class之前,需要新建一个C#脚本并进行lua虚拟机初始化和载入lua中的main脚本,因为Lua无法直接访问C#,一定是先从C#调用Lua脚本后,才能把核心逻辑交给Lua来编写。
完成上述步骤,就开始在Lua脚本中调用Class吧。
--获取类是固定套路:CS.命名空间.类名
--例如:CS.UnityEngine.GameObject
-- 获取Unity中的类并实例化对象
-- 通过C#中的类实例化对象,因为lua中没有new实例化,所以直接括号类名就是实例化对象,默认调用的就相当于无参构造
-- 相当于C#中,GameObject obj1 = new GameObject();
local obj1 = CS.UnityEngine.GameObject()
local obj2 = CS.UnityEngine.GameObject('parent')
--为了方便使用并节约性能,定义全局变量储存C#中的类,相当于取了一个别名
GameObject = CS.UnityEngine.GameObject
Debug = CS.UnityEngine.Debug
local obj3 = GameObject('newGo2')
--使用对象中的成员变量,用'.'即可
Debug.Log(obj1.transform.position)
--使用对象中的成员方法,一定要用':'
obj1.transform:SetParent(obj2.transform)
obj3.transform:SetParent(obj2.transform)
--获取自定义类并实例化对象
local t1 = CS.Test1()
t1:Hello("test1")
local t2 = CS.Test.Test2()
t2:Hello("test2")
--获取继承了mono的类并增加组件
--一般在c#中需要用addcomponent来添加脚本,而不能直接new实例化
--因为xlua中不支持无参泛型函数,所以需要使用另一个重载(type)
--xlua提供了typeof方法,可以得到类的Type
local obj4 = GameObject('AddComponentTest')
obj4:AddComponent(typeof(CS.LuaCallCSharp))
下面是自定义类和继承mono的代码:
using UnityEngine;
//自定义类
public class Test1
{
public void Hello(string str)
{
Debug.Log("Hello1:" + str);
}
}
namespace Test
{
public class Test2
{
public void Hello(string str)
{
Debug.Log("Hello2:" + str);
}
}
}
public class LuaCallCSharp : MonoBehaviour
{
private void Start()
{
Debug.Log("Start:LuaCallCSharp");
}
}
调用Enum
调用枚举和类的调用规则是了类似的,为CS.命名空间.枚举名.枚举成员
,同时也支持取别名,具体Lua代码如下
--调用Unity中自带的枚举
PrimitiveType = CS.UnityEngine.PrimitiveType
local obj = GameObject.CreatePrimitive(PrimitiveType.Cube)
--自定义枚举
E_PlayaerAction = CS.E_PlayaerAction
--数值转枚举
local a = E_PlayaerAction.__CastFrom(1)
Debug.Log(a)
--字符串转枚举
local b = E_PlayaerAction.__CastFrom("Attack")
Debug.Log(b)
C#中的自定义枚举:
public enum E_PlayaerAction
{
Idle,
Move,
Attack
}
调用数组、List、Dictionary
下面展示了Lua中如何调用C#中的数组、List、Dictionary等数据结构,下面详细介绍了如何访问、遍历、在Lua中创建、修改等操作。
Lua部分的代码:
obj = CS.CallArray()
--数组
--使用C#中的方法获取长度,不能使用Lua中的#
print(obj.array.Length)
--访问元素
print(obj.array[0])
--遍历数组
--虽然Lua遍历是从1开始的,但是数组是C#的规则所以还是应该从0开始遍历,因此Length要-1
for i=0,obj.array.Length-1 do
print(obj.array[i])
end
--Lua中创建C#的数组,使用Array类中的静态方法
array2 = CS.System.Array.CreateInstance(typeof(CS.System.Int32), 10)
print(array2.Length)
--List
--增加元素,注意调用对象成员方法要用":"
obj.list:Add(1)
obj.list:Add(3)
obj.list:Add(5)
--长度
print(obj.list.Count)
--遍历
for i=0,obj.list.Count-1 do
print(obj.list[i])
end
--Lua中创建C#的List
--老版本的Xlua,比较麻烦
list2 = CS.System.Collections.Generic["List`1[System.String]"]()
list2:Add("test")
print(list2.Count)
--新版本 >v2.1.12
List_String = CS.System.Collections.Generic.List(CS.System.String)
list3 = List_String()
list2:Add("test2")
print(list2.Count)
--Dictionary
obj.dic:Add(1,"12")
obj.dic:Add(2,"12")
obj.dic:Add(3,"13")
--长度
print(obj.dic.Count)
--遍历
for k,v in pairs(obj.dic) do
print(k,v)
end
--Lua中创建C#的Dictionary
Dic_String_Vector3 = CS.System.Collections.Generic.Dictionary(CS.System.String, CS.UnityEngine.Vector3)
dic2 = Dic_String_Vector3()
dic2:Add("test1", CS.UnityEngine.Vector3.right)
--Lua中创建字典,直接用dicName["key"]得到是nil,所以如果要通过键来取值,需要使用一个固定方法
print(dic2:get_Item("test1"))
--通过键来设置值同理
dic2:set_Item("test1", nil)
print(dic2:get_Item("test1"))
C#部分的代码:
public class CallArray
{
public int[] array = new int[5] { 1, 2, 3, 4, 5 };
public List<int> list = new List<int>();
public Dictionary<int, string> dic = new Dictionary<int, string>();
}
调用拓展方法
Lua部分的代码:
CallFunction = CS.CallFunction
--静态方法用.
CallFunction.Eat()
--成员方法用:
obj = CallFunction()
obj:Speak("helloworld")
--拓展方法和成员方法一致
obj:Move()
C#部分的代码:
public class CallFunction
{
public string name = "xiaoming";
public void Speak(string str)
{
Debug.Log(str);
}
public static void Eat()
{
Debug.Log("eat");
}
}
[LuaCallCSharp]
public static class Tools
{
public static void Move(this CallFunction obj)
{
Debug.Log(obj.name + "move");
}
}
总结:
要使用扩展方法和使用成员方法一致,要调用C#中某个类的扩展方法就一定要在扩展方法的静态类上加入[LuaCallCSharp]特征。
虽然出了拓展方法外的其他类都不会报错,但是建议要在Lua中使用的C#类都可以加上[LuaCallCSharp]的特性,这样预先将代码生成,可以提高Lua访问C#类的性能。
调用参数包含ref和out的函数
Lua部分的代码:
CallRefOutFunction = CS.CallRefOutFunction
obj = CallRefOutFunction()
--ref除了第一个返回值为正常的返回值,其他ref参数会以多返回值的形式返回给lua
--参数数量如果少了,会默认使用默认值来补位
a,b,c = obj:RefFun(1,0,0,1)
print(a,b,c)
--out参数除了第一个返回值为正常的返回值,其他out参数还是以多返回值的形式接收
--out参数不需要传递值,否则会用默认值占用原本非out参数的变量
a,b,c = obj:OutFun(1,1)
print(a,b,c)
-- ref out同时存在,符合上面说的规则
a,b,c = obj:RefOutFun(1,1,1)
print(a,b,c)
C#部分的代码:
public class CallRefOutFunction
{
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;
}
}
总的来说:
- 从返回值上看,ref和out都会以多返回值的形式返回,原来如果有返回值的话原来的返回值是多返回值中的第一个
- 从参数看,ref参数需要传递来占位,out参数不需要传递来占位
调用重载函数
Lua部分的代码:
obj = CS.CallOverloadFunction()
print(obj:Fun())
print(obj:Fun(1,2))
print(obj:Fun(3.3))--0
print(obj:Fun(3))--3.0
--输出很明显不正确
--通过反射的方式来加载函数
--得到指定函数的相关信息
m1 = typeof(CS.CallOverloadFunction):GetMethod("Fun",{typeof(CS.System.Int32)})
m2 = typeof(CS.CallOverloadFunction):GetMethod("Fun",{typeof(CS.System.Single)})
m3 = typeof(CS.CallOverloadFunction):GetMethod("Fun",{typeof(CS.System.Int32),typeof(CS.System.Int32)})
--转为lua函数
f1 = xlua.tofunction(m1)
f2 = xlua.tofunction(m2)
f3 = xlua.tofunction(m3)
--如果是成员方法,第一个参数传对象(obj)
--如果是静态方法,直接传参数
print(f1(obj, 3))
print(f2(obj, 3.3))
print(f3(obj, 1, 2))
C#部分的代码:
public class CallOverloadFunction
{
public int Fun()
{
return 100;
}
public int Fun(int a, int b)
{
return a + b;
}
public float Fun(int a)
{
return a;
}
public float Fun(float a)
{
return a;
}
}
总结:
虽然lua支持调用C#的重载函数,但是由于Lua的数值类型只有Number,因此对多精度的重载函数支持不好,虽然xlua通过反射的机制提供了解决上面问题的方案,但是性能比较差,不推荐使用。
调用委托和事件
这一节主要关注Lua中调用C#的委托和事件的不同,能够复习到委托和事件的不同点,比如事件无法在类的外部被赋值或者调用,这里可以看看这篇文章复习下。
Lua部分的代码:
obj = CS.CallDel()
--委托是函数的容器,我们想要使用C#的委托来装lua中的函数
fun = function()
print("LuaFun")
end
--Lua中没有复合运算符,不能+=fun
--第一次要=,因为del的初始值为nil
obj.del = fun
--委托中可以添加临时的匿名函数,但是最好不要这么写,因为找不到对应的匿名函数造成不好减,只能直接清空
obj.del = obj.del + function()
print("临时函数")
end
obj.del()
--清空委托
del = nil
--因为事件不能在外部调用,所以事件和委托的使用方法不一致
--使用冒号添加和删除函数,第一个参数传入加号或者减号字符串,表示添加还是修改函数
--事件也可以添加匿名函数,但是最好不要这么写,因为找不到对应的匿名函数造成不好减,只能直接清空
obj:eventAction("+",fun)
obj:eventAction("+",fun)
--事件不能直接调用,必须在C#中提供调用事件的方法
obj:DoEvent()
obj:eventAction("-",fun)
obj:DoEvent()
--同样地,事件不能直接清空,需要在C#中提供对应地方法
obj:ClearEvent()
C#部分的代码:
public class CallDel
{
public UnityAction del;
public event UnityAction eventAction;
public void DoEvent()
{
if (eventAction != null)
{
eventAction();
}
}
public void ClearEvent()
{
eventAction = null;
}
}
调用二维数组
这里主要关注C#中调用二维数组元素和Lua中调用C#的二维数组元素的不同点。
obj = CS.Array2()
print(obj.array:GetLength(0))
print(obj.array:GetLength(1))
--获取元素
--不能通过[x,y][x][y]等方法访问元素,而是通过array里的成员方法来获取
print(obj.array:GetValue(0,0))
--遍历元素
for i=0,obj.array:GetLength(0)-1 do
for j=0,obj.array:GetLength(1)-1 do
print(obj.array:GetValue(i,j))
end
end
C#部分的代码:
public class Array2
{
public int[,] array = new int[2, 3] { { 1, 2, 3 }, { 4, 5, 6 } };
}
nil和null的比较
需求:往场景对象上添加一个Rigidbody组件,如果已经存在组件就不加,如果还不存在就添加组件
Lua部分的代码:
-- 需求:往场景对象上添加一个Rigidbody组件,如果已经存在组件就不加,如果还不存在就添加组件
GameObject = CS.UnityEngine.GameObject
Rigidbody = CS.UnityEngine.Rigidbody
local obj = GameObject("nil&null")
local rigidbody = obj:GetComponent(typeof(Rigidbody))
--因为是新对象肯定获取不到,所以rigidbody是null的
print(rigidbody)
--因为nil和null并不相同,在lua中不能使用==进行判空,一定要使用Equals方法进行判断是否为null
--这里如果rigidbody为nil可能报错,所以可以自己提供一个判空函数同时判断nil和null来进行判空
--注意下面这个IsNull全局函数最好定义在lua脚本启动的主函数Main中
function IsNull(obj)
if obj == nil or obj:Equals(nil) then
return true
end
return false
end
--使用Lua中的自定义的判空函数进行判断
if IsNull(rigidbody) then
rigidbody = obj:AddComponent(typeof(Rigidbody))
end
print(rigidbody)
--使用C#中的拓展方法来进行判断
if rigidbody:IsNull() then
rigidbody = obj:AddComponent(typeof(Rigidbody))
end
print(rigidbody)
C#部分的代码:
下面这个为Object判空的代码就是专门给lua调用的,用于判空的函数,因为lua无法直接比较null和nil
[LuaCallCSharp]
public static class IsNullClass
{
public static bool IsNull(this UnityEngine.Object obj)
{
return obj == null;
}
}
Lua和系统类或委托相互使用
对于自定义的类型,可以添加[CSharpCallLua]和[LuaCallCSharp]这两个特性使Lua和自定义类型能相互访问。
比如要在C#的委托来映射Lua中的函数,用C#的接口映射Lua中的Table就会用到[CSharpCallLua];比如要在Lua中使用C#的拓展方法时,需要在拓展方法所在的类加入[LuaCallCSharp]特性,同时也推荐所有被Lua访问的类都加入这个特性。
但是对于系统类或第三方代码库,我们并不能修改他们的代码,去增加这两种特性,具体的解决方法见下面的C#代码。
public static class AddXluaFeature
{
//实现为系统类添加[CSharpCallLua]和[LuaCallCSharp]特性
[CSharpCallLua]
public static List<Type> csharpCallLuaList = new List<Type>()
{
//将需要添加特性的类放入list中,再手动生成Xlua代码即可
typeof(UnityAction<float>),
};
[LuaCallCSharp]
public static List<Type> luaCallCsharpList = new List<Type>()
{
//将需要添加特性的类放入list中,再手动生成Xlua代码即可
typeof(GameObject),
typeof(Rigidbody),
};
}
调用协程
XLua中调用unity的协程和在C#中使用方法不太一样,需要用到xlua提供的工具表才行,具体详见下面的代码:
--要使用协程就必须要使用xlua提供的工具表xlua.util
util = require("xlua.util")
GameObject = CS.UnityEngine.GameObject
WaitForSeconds = CS.UnityEngine.WaitForSeconds
obj = GameObject("Coroutine")
--增加一个继承mono的脚本给刚刚实例化的GameObjeect
mono = obj:AddComponent(typeof(CS.LuaCallCSharp))
--想要被开启协程的函数
fun = function ()
local a = 1
while true do
-- lua不能直接使用C#中的yield return,所以使用Lua协程返回
coroutine.yield(WaitForSeconds(1))
print(a)
a = a + 1
if a > 10 then
--停止协程
mono:StopCoroutine(cor)
end
end
end
--我们不能直接将lua函数放到StartCoroutine的参数中
--必须使用util.cs_generator(fun)的返回值来作为开启协程的参数才行
cor = mono:StartCoroutine(util.cs_generator(fun))
调用泛型函数
之前提到过Lua对泛型的函数支持不好,下面测试一下Lua究竟支持哪些泛型函数,如何用XLua来适配不支持的泛型函数。
Lua部分的代码:
obj = CS.TestType()
child = CS.TestType.TestChild()
father = CS.TestType.TestFather()
--Lua支持有约束有参数的泛型函数
obj:TestFun1(child,father)
obj:TestFun1(father,child)
--Lua不支持没有约束的泛型函数,下面这个会报错
--Lua不支持有约束但没有参数的泛型函数
--Lua不支持非Class约束的泛型函数
--obj:TestFun2(child)
--obj:TestFun3()
--obj:TestFun4(child)
--xlua适配了上面不支持的泛型函数
--1.得到泛型函数,xlua提供了得到泛型函数的方法get_generic_method(类名,函数名)
fun = xlua.get_generic_method(CS.TestType,"TestFun2")
--2.指定泛型类型
fun_r = fun(CS.System.Int32)
--3.调用泛型方法
--如果是成员方法:第一个参数是调用函数的对象,后面的参数为泛型函数的参数
--如果是静态方法就不需要传调用函数的对象(也没有)
fun_r(obj,2)
C#部分的代码(测试用的泛型函数):
public class TestType
{
public interface ITest { }
public class TestFather { }
public class TestChild : TestFather,ITest { }
public void TestFun1<T>(T a,T b) where T:TestFather
{
Debug.Log("有参数有约束的泛型函数");
}
public void TestFun2<T>(T a, T b)
{
Debug.Log("有参数无约束的泛型函数");
}
public void TestFun3<T>() where T : TestFather
{
Debug.Log("无参数有约束的泛型函数");
}
public void TestFun4<T>(T a, T b) where T : ITest
{
Debug.Log("有参数有约束但是约束是接口的的泛型函数");
}
}
使用限制:
- 打包时如果使用mono打包,这种方式可以正常使用
- 如果使用il2cpp打包,泛型参数需要是引用类型或者是在C#中已经调用过同类型的泛型函数的值类型。