xLua学习笔记(三) Lua调用C#代码
将下列代码挂载到任何一个GameObject上,这样就能在Unity中加载并执行Resources/Lua文件夹下的csharp_call.lua.txt文件中Lua代码了
public class CSharpRun : MonoBehaviour
{
private LuaEnv luaenv;
void Start ()
{
luaenv = new LuaEnv();
luaenv.AddLoader(LuaLoader);
luaenv.DoString
(
@"
require 'csharp_call'
"
);
}
void Update ()
{
if (luaenv != null)
{
luaenv.Tick();
}
}
void Destroy()
{
if (luaenv != null)
{
luaenv.Dispose();
}
}
private byte[] LuaLoader(ref string filename)
{
TextAsset text = Resources.Load("Lua/" + filename + ".lua") as TextAsset;
return text.bytes;
}
}
访问C#方法、属性
使用下列Lua代码来创建两个GameObject对象,访问对象的name属性并调用其SetActive方法
--创建C#对象
local go1 = CS.UnityEngine.GameObject("Cube")
local go2 = CS.UnityEngine.GameObject("Sphere")
--访问对象属性
print("GameObject1:", go1.name, "GameObject2:", go2.name)
--修改对象属性
go1.name = "Cube--"
go2.name = "Sphere--"
print("GameObject1:", go1.name, "GameObject2:", go2.name)
--调用对象方法(调用对象方法时注意使用":"语法糖)
go1:SetActive(false) --go1.SetActive(go1, false) 也可以这样调用
go2:SetActive(false) --go2.SetActive(go2, false) 也可以这样调用
运行得到结果,GameObject的名字被成功地修改
游戏物体Cube和Sphere被创建到了Hierarchy下,同时被设置Enable状态
访问C#静态方法、静态属性
下面的Lua代码:
- 获取Time类的引用
- 读取Time类的静态属性deltaTime
- 修改了Time类的静态属性timeScale
- 获取GameObject类的引用
- 调用GameObject类的静态方法Find,找到场景中的主相机Main Camera
--读取静态属性
local Time = CS.UnityEngine.Time
print("Time.deltaTime:", Time.deltaTime)
--修改静态属性
print("Time.timeScale Before:", Time.timeScale)
Time.timeScale = 0.5
print("Time.timeScale After:", Time.timeScale)
--调用静态方法(调用静态方法时可以直接使用".")
local GameObject = CS.UnityEngine.GameObject
local mainCamera = GameObject.Find("Main Camera")
print("mainCamera:", mainCamera.name)
运行得到结果
访问自定义的C#类
如果要通过Lua访问自己定义的类的话,需要给被Lua代码访问的类加上一个Attribute:[LuaCallCSharp],用来生成Lua的适配代码
下面对[LuaCallCSharp]的解释参考xLua的Github主页的FAQ
XLua.LuaCallCSharp
一个C#类型加了这个配置,xLua会生成这个类型的适配代码(包括构造该类型实例,访问其成员属性、方法,静态属性、方法),否则将会尝试用性能较低的反射方式来访问。
一个类型的扩展方法(Extension Methods)加了这配置,也会生成适配代码并追加到被扩展类型的成员方法上。
xLua只会生成加了该配置的类型,不会自动生成其父类的适配代码,当访问子类对象的父类方法,如果该父类加了LuaCallCSharp配置,则执行父类的适配代码,否则会尝试用反射来访问。
反射访问除了性能不佳之外,在il2cpp下还有可能因为代码剪裁而导致无法访问,后者可以通过下面介绍的ReflectionUse标签来避免。
XLua.ReflectionUse
一个C#类型类型加了这个配置,xLua会生成link.xml阻止il2cpp的代码剪裁。
对于扩展方法,必须加上LuaCallCSharp或者ReflectionUse才可以被访问到。
建议所有要在Lua访问的类型,要么加LuaCallCSharp,要么加上ReflectionUse,这才能够保证在各平台都能正常运行。
下面定义BaseClass和DeriveClass,其中BaseClass是DeriveClass的基类
[LuaCallCSharp]
class BaseClass
{
public string BaseField = "BaseField";
public string _BaseProperty = "BaseProperty";
public string BaseProperty
{
set { _BaseProperty = value; }
get { return _BaseProperty; }
}
public static string BaseStatic = "BaseStatic";
public static string _BaseStaticProperty = "BaseStaticProperty";
public static string BaseStaticProperty
{
set { _BaseStaticProperty = value; }
get { return _BaseStaticProperty; }
}
public void BasePrint()
{
Debug.Log("BasePrint() Called.");
}
public static void BaseStaticPrint()
{
Debug.Log("BaseStaticPrint() Called.");
}
}
[LuaCallCSharp]
class DeriveClass : BaseClass
{
public string DeriveField = "DeriveField";
public string _DeriveProperty = "BaseDeriveProperty";
public string DeriveProperty
{
set { _DeriveProperty = value; }
get { return _DeriveProperty; }
}
public static string DeriveStatic = "DeriveStatic";
public static string _DeriveStaticProperty = "DeriveStaticProperty";
public static string DeriveStaticProperty
{
set { _DeriveStaticProperty = value; }
get { return _DeriveStaticProperty; }
}
public void DerivePrint()
{
Debug.Log("DerivePrint() Called.");
}
public static void DeriveStaticPrint()
{
Debug.Log("DeriveStaticPrint() Called.");
}
}
访问的方式和之前访问Unity中类的方式相同,另外xLua中除了能够访问和修改类自身的乘以外,还支持在派生类中访问和修改基类的成员,访问的规则和C#中相同
另外,访问权限不足时,Lua中相应的变量会被赋值为nil
local BaseClass = CS.BaseClass
local baseClass = CS.BaseClass()
local DeriveClass = CS.DeriveClass
local deriveClass = CS.DeriveClass()
print('----------Base----------')
print(baseClass.BaseField)
print(baseClass.BaseProperty)
baseClass:BasePrint()
print(BaseClass.BaseStatic)
print(BaseClass.BaseStaticProperty)
BaseClass.BaseStaticPrint()
print('---------Derive---------')
print(deriveClass.DeriveField)
print(deriveClass.DeriveProperty)
deriveClass:DerivePrint()
print(DeriveClass.DeriveStatic)
print(DeriveClass.DeriveStaticProperty)
DeriveClass.DeriveStaticPrint()
print('------Derive->Base------')
print(deriveClass.BaseField)
print(deriveClass.BaseProperty)
deriveClass:BasePrint()
print(DeriveClass.BaseStatic)
print(DeriveClass.BaseStaticProperty)
DeriveClass.BaseStaticPrint()
访问C#复杂函数
- 对于参数,Lua会从左到右取C#函数中的普通参数或者ref参数依次作为自己的参数
- 对于返回值,Lua会从左到右取C#函数中的返回值、ref参数或者out参数依次作为自己的返回值
例如,对于下面的函数ComplexFunction
[LuaCallCSharp]
class ComplexClass
{
public string ComplexFunction(int arg0, ref int arg1, string arg2, out string arg3, Parameter param)
{
Debug.Log("=========C#=========");
Debug.Log("arg0:" + arg0);
Debug.Log("arg1:" + arg1);
Debug.Log("arg2:" + arg2);
Debug.Log("arg3:");
Debug.Log("Parameter:" + param);
arg1++;
arg3 = "3(string)";
return "ComplexFunction return.";
}
}
[LuaCallCSharp]
class Parameter
{
public int param1;
public string param2;
public override string ToString()
{
return string.Format("param1:{0} param2:{1}", param1, param2);
}
}
按照上面的原则,首先从左到右检索函数ComplexFunction的普通参数或ref参数
结果为:arg0,arg1,arg2,param
所以在Lua中调用时需要传入以上四个参数
接下来从左到右检索函数的返回值、ref参数或者out参数
结果为:ComplexFunction函数的返回值,arg1,arg3
所以在Lua中将返回上述4个值
local complexClass = CS.ComplexClass()
local ret, arg1, arg3 = complexClass:ComplexFunction(
0,
1,
"2(string)",
{param1 = 3, param2 = "4(string)"}
)
print("=========Lua========")
print("ret:", ret)
print("arg1:", arg1)
print("arg3:", arg3)
运行得到结果
操作符重载和函数重载
C#中定义的操作符重载和函数重载在Lua中基本上能够使用,不过需要注意的是由于Lua中表示数值的类型只有一种(number),所以C#中对于数值类型之间的重载是不能够正确的识别的,通常只会调用类型符合的重载函数列表中先定义的函数
假设有下面两个C#类,在Vector类中重载了操作符"+",在Overload类中对函数Add进行了重载,类型分别是int,float和string
[LuaCallCSharp]
class Vector
{
public int x;
public int y;
public Vector(int x, int y)
{
this.x = x;
this.y = y;
}
public static Vector operator +(Vector vec1, Vector vec2)
{
return new Vector(vec1.x + vec2.x, vec1.y + vec2.y);
}
}
[LuaCallCSharp]
class Overload
{
public int Add(int num1, int num2)
{
Debug.Log("Add-int");
return num1 + num2;
}
public float Add(float num1, float num2)
{
Debug.Log("Add-float");
return num1 + num2;
}
public string Add(string num1, string num2)
{
Debug.Log("Add-string");
return num1 + num2;
}
}
接下来使用Lua来访问进行验证,首先定义了两个Vector并相加,接着尝试向Overload的Add函数中传入整数,浮点数和字符串
local vec1 = CS.Vector(1,1)
local vec2 = CS.Vector(2,3)
local vec3 = vec1 + vec2
print("x:", vec3.x, "y:", vec3.y)
local overload = CS.Overload()
overload:Add(1, 1)
overload:Add(2.0, 2.0)
overload:Add("3", "3")
通过运行可以看到结果如下
由于在Lua中只有一种数值类型(number),所以参数为int和float类型的Add函数都满足要求,这个时候会调用先定义的重载函数,也就是重载为int类型的Add
当先定义参数float类型后定义int类型的Add函数时,Lua代码调用就是参数类型为float的Add函数了
[LuaCallCSharp]
class Overload
{
public float Add(float num1, float num2)
{
Debug.Log("Add-float");
return num1 + num2;
}
public int Add(int num1, int num2)
{
Debug.Log("Add-int");
return num1 + num2;
}
public string Add(string num1, string num2)
{
Debug.Log("Add-string");
return num1 + num2;
}
}
更换顺序后结果发生了变化
可变参数与默认参数
定义SpecialParam类,包含一个有默认参数的函数和一个有可变参数的函数
[LuaCallCSharp]
class SpecialParam
{
public void DefaultParam(int arg0, string arg1 = "1", int arg2 = 2)
{
Debug.Log("arg0:" + arg0);
Debug.Log("arg1:" + arg1);
Debug.Log("arg2:" + arg2);
}
public void VariableParam(int arg0, params string[] args)
{
Debug.Log("arg0:" + arg0);
Debug.Log("args:");
foreach (string arg in args)
{
Debug.Log(arg + " ");
}
}
}
在Lua中调用它们的时候,参数的规则与C#中相同
local specialParam = CS.SpecialParam()
specialParam:DefaultParam(1, "3")
print("================================")
specialParam:VariableParam(0, "1", "2", "3", "4")
输出结果
访问C#枚举
定义枚举Language和EnumParam类,EnumParam类中的PrintEnum函数会根据传入枚举的类型输出不同的日志
[LuaCallCSharp]
public enum Language
{
C_PLUS_PLUS,
C_SHARP
}
[LuaCallCSharp]
class EnumParam
{
public void PrintEnum(Language language)
{
switch (language)
{
case Language.C_PLUS_PLUS:
Debug.Log("C++");
break;
case Language.C_SHARP:
Debug.Log("C#");
break;
}
}
}
在Lua中有以下五种方法可以访问到枚举变量:
- 当作普通的静态属性访问
- 使用__CastFrom函数,从枚举值对应的数值做类型转换
- 使用__CastFrom函数,从枚举值对应的字符串做类型转换
- 直接传入枚举值对应的数值
- 直接传入枚举值对应的字符串
官方文档上只提到了前三种方法,并且方法2和方法3需要生成代码才能使用,不过经过实验发现,不生成代码时上述五种方法都能够使用
local enumParam = CS.EnumParam()
local Language = CS.Language
enumParam:PrintEnum(Language.C_PLUS_PLUS)
enumParam:PrintEnum(Language.__CastFrom(0))
enumParam:PrintEnum(Language.__CastFrom("C_PLUS_PLUS"))
enumParam:PrintEnum(0)
enumParam:PrintEnum("C_PLUS_PLUS")
enumParam:PrintEnum(Language.C_SHARP)
enumParam:PrintEnum(Language.__CastFrom(1))
enumParam:PrintEnum(Language.__CastFrom("C_SHARP"))
enumParam:PrintEnum(1)
enumParam:PrintEnum("C_SHARP")
输出结果
访问C#委托
下面的DelegateClass类定义了一个接受string类型参数无返回值的委托类型,3个委托变量action,actionString1和actionString2
[LuaCallCSharp]
class DelegateClass
{
[CSharpCallLua]
public delegate void ActionString(string arg);
public ActionString action = (arg) =>
{
Debug.Log("action:" + arg);
};
public ActionString actionString1 = (arg) =>
{
Debug.Log("actionString1:" + arg);
};
public ActionString actionString2 = (arg) =>
{
Debug.Log("actionString2:" + arg);
};
}
在使用Lua代码访问C#委托时需要注意,访问委托类型的方式与访问静态变量的方式相同,访问(静态/非静态)委托的变量的方式与访问(静态/非静态)成员变量的方式相同
由于在Lua中没有"+="和"-="操作符,在增加委托链的时候只能使用"+"和"-"操作符
local delegateClass = CS.DelegateClass()
--使用DelegateClass类的对象访问委托变量action
local action1 = delegateClass.action
action1("hi-1")
action1 = action1 + delegateClass.actionString1 + delegateClass.actionString2
action1("hi-2")
action1 = action1 - delegateClass.actionString2
action1("hi-3")
--使用DelegateClass类访问委托类型ActionString,定义一个ActionString类型的委托变量action2
--此时action2的值为nil
local action2 = CS.DelegateClass.ActionString
action2 = delegateClass.actionString1
action2("hi-4")
action2 = action2 + delegateClass.actionString2
action2("hi-5")
action2 = action2 - delegateClass.actionString2
action2("hi-6")
输出结果
在增减委托链的时候除了可以使用C#委托变量外,还可以加减Lua函数
function lua_action(arg)
print("lua_action:", arg)
end
action2 = action2 + lua_action
访问C#事件
下面的EventClass类定义了一个无参数无返回值的委托类型EventAction和基于委托类型EventAction的事件Events,同时提供了两个定义好的委托类型action1和action2,和一个触发事件的函数TriggerEvent
[LuaCallCSharp]
class EventClass
{
[CSharpCallLua]
public delegate void EventAction();
public event EventAction Events;
public EventAction action1 = () =>
{
Debug.Log("action1");
};
public EventAction action2 = () =>
{
Debug.Log("action2");
};
public void TriggerEvent()
{
Events();
}
}
在访问C#事件的时候需要生成代码,所以必须要为事件的委托类型加上一个Attribute:[CSharpCallLua],关于为什么这里需要加[CSharpCallLua]而不是[LuaCallCSharp],在xLua的github主页的FAQ上作者是这么解释的:
LuaCallCSharp以及CSharpCallLua两种生成各在什么场景下用?
看调用者和被调用者,比如要在lua调用C#的GameObject.Find函数,或者调用gameobject的实例方法,属性等,GameObject类要加LuaCallSharp,而想把一个lua函数挂到UI回调,这是调用者是C#,被调用的是一个lua函数,所以回调声明的delegate要加CSharpCallLua。
有时会比较迷惑人,比如List.Find(Predicate match)的调用,List当然是加LuaCallSharp,而Predicate却要加CSharpCallLua,因为match的调用者在C#,被调用的是一个lua函数。
更无脑一点的方式是看到“This delegate/interface must add to CSharpCallLua : XXX”,就把XXX加到CSharpCallLua即可。
在添加事件的时候,既可以使用C#中的委托变量,也可以使用Lua中的函数
同时在添加和移除事件的时候应该使用以下的方式
object:event("+", delegate)
object:event("-", delegate)
function lua_action()
print("lua_action:")
end
local eventClass = CS.EventClass()
eventClass:Events("+", lua_action)
eventClass:Events("+", eventClass.action1)
eventClass:Events("+", eventClass.action2)
eventClass:TriggerEvent()
运行结果
注意在Lua中不能通过以下方式来触发事件
eventClass:Events()
因为Events此时只是一个记录事件委托链的Table,并不是一个函数
类型信息与泛型方法
一个很简单的需求就是我们想要给新创建的GameObject添加某一个组件
在C#中一般的做法是使用AddComponent函数
gameObject.AddComponent<Rigidbody>();
gameObject.AddComponent("Rigidbody");
gameObject.AddComponent(typeof(Rigidbody));
AddComponent函数有三种重载形式,可以通过泛型、类名字符串和类的类型信息Type对象三种方式来为一个GameObject对象添加一个组件
而xLua不支持泛型,如果想要调用只能通过定义扩展方法,然后在Lua中通过调用扩展方法的方式来进行间接地调用
[LuaCallCSharp]
static class ExtendedMethod
{
public static Rigidbody AddComponentRigidbody(this GameObject gameobject)
{
return gameobject.AddComponent<Rigidbody>();
}
[LuaCallCSharp]
public static List<Type> luaCallCSharpList = new List<Type>()
{
typeof(GameObject),
};
}
由于GameObject是Unity的API,不能修改其源代码,所以不能通过在其上添加** [LuaCallCSharp]**,所以这里使用了另外一种方法
静态列表
有时我们无法直接给一个类型打标签,比如系统api,没源码的库,或者实例化的泛化类型,这时你可以在一个静态类里声明一个静态字段,该字段的类型除BlackList和AdditionalProperties之外只要实现了IEnumerable<Type>就可以了(这两个例外后面具体会说),然后为这字段加上标签:
[LuaCallCSharp]
public static List<Type> mymodule_lua_call_cs_list = new List<Type>()
{
typeof(GameObject),
typeof(Dictionary<string, int>),
};
这样就能通过Lua代码间接调用AddComponent的泛型重载形式来添加Rigidbody组件
local go = CS.UnityEngine.GameObject("SuperCube")
go:AddComponentRigidbody()
AddComponent的字符串重载形式则可以在Lua代码中直接调用
local go = CS.UnityEngine.GameObject("SuperCube")
go:AddComponent("Animator")
对于传入Type的第三种重载形式,xLua在Lua API中为我们提供了一个和C#中typeof函数一样的函数,在Lua代码中也可以通过typeof得到类的类型信息
local go = CS.UnityEngine.GameObject("SuperCube")
go:AddComponent(typeof(CS.UnityEngine.Rigidbody))
类型转换
很多第三方库对外只暴露接口,而但我们通过Lua来调用的时候,这些没有对外暴露类只能够通过反射的方式来进行访问,如果这个接口被频繁地调用,势必会影响性能
为了提高运行效率,可以使用之前提到的静态列表的方式,将第三方库对外暴露的接口加入到代码生成列表中,生成Lua适配代码,这样然后在Lua中把具体的实现类转换为接口,然后通过接口来调用C#代码
下面举一个列子,假设某个第三方库是这样的
[LuaCallCSharp]
interface IFuckable
{
void Fuck();
}
[LuaCallCSharp]
class Lisa : IFuckable
{
public int id = 100;
public void Fuck()
{
Debug.Log("Lisa[" + id + "] Can Fuck!");
}
}
/*IFuckable的实现类很多*/
[LuaCallCSharp]
class WhoreHouse
{
public IFuckable GetWhore()
{
return new Lisa();
}
}
其中IFuckable和WhoreHouse是该库对外暴露的接口,外部通过调用WhoreHouse的GetWhore方法来得到不同的实现了IFuckable接口的对象
在Lua代码中,我们通过GetWhore拿到了一个woman对象,但是由于不知道woman到底是哪一个具体的实现类,所以直接调用的时候xLua会通过反射的方式来访问该实现类
为了通过IFuckable接口来进行调用,需要在Lua中将得到的woman对象转换为IFuckable接口类型
xLua为我们提供了一个类型转换函数cast,该函数有两个参数:
- 需要进行类型转换的对象
- 转换类型的Type对象(使用之前提到的typeof函数得到Type对象)
local whorehouse = CS.WhoreHouse()
local woman = whorehouse:GetWhore()
woman:Fuck()
cast(woman, typeof(CS.IFuckable))
woman:Fuck()
从输出的结果中可以看出,转换后实现类独有的字段仍然id能够正确的输出