接上一篇的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;
    }
}

总的来说:

  1. 从返回值上看,ref和out都会以多返回值的形式返回,原来如果有返回值的话原来的返回值是多返回值中的第一个
  2. 从参数看,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("有参数有约束但是约束是接口的的泛型函数");
    }
}

使用限制:

  1. 打包时如果使用mono打包,这种方式可以正常使用
  2. 如果使用il2cpp打包,泛型参数需要是引用类型或者是在C#中已经调用过同类型的泛型函数的值类型。