当前位置: 首页 > 工具软件 > xLua > 使用案例 >

初识xLua(一)

田博超
2023-12-01

通过xLua运行Lua程序

导入网上下载的 xLua 包,其中也包涵示例代码和场景。

using XLua;

    private LuaEnv luaenv;

    void Start () {
        
        //构建Lua虚拟机
        luaenv = new LuaEnv();
        
        //通过 Lua 语法(执行一个字符串)输出 Hello world
        //luaenv.DoString("print('Hello world!')"); 
        
        //通过 Lua 调用 C# 语法输出 Hello world
        luaenv.DoString(" CS.UnityEngine.Debug.Log('Hello world') ");

	}

    private void OnDestroy()
    {
        
        luaenv.Dispose();
    }
}


加载运行lua源文件

这里要注意一点 该文件的全名(包括后缀)为:helloworld.lua.txt

        TextAsset ta = Resources.Load<TextAsset>("helloworld.lua"); 

        LuaEnv env = new LuaEnv();
        
        env.DoString(ta.text);

        env.Dispose();

通过内置的 Loader 方法加载,比如,DoString(" require ‘byfile’ ")

        LuaEnv env = new LuaEnv();

        env.DoString("require 'helloworld'");
        
        env.Dispose();

添加自定义的Loader方法

涉及到一个接口:

public delegate byte[] CustomLoader(ref string filepath);
public void LuaEnv.AddLoader(CustomLoader loader)

示例 :

void Start () {
         LuaEnv env = new LuaEnv();

        env.AddLoader(MyLoader);

        env.DoString("require 'test'");

        env.Dispose();
	}
	
    private byte[] MyLoader(ref string filePath)
    {
        string absPath = Application.streamingAssetsPath + "/" + filePath + ".lua.txt";
        return System.Text.Encoding.UTF8.GetBytes(File.ReadAllText(absPath));
    }

C#访问Lua

全局变量

要注意,获取的变量类型一定要对

        LuaEnv luaEnv = new LuaEnv();
        luaEnv.DoString("require 'CSharpCallLua'");
        
        int a = luaEnv.Global.Get<int>("a");//获取到lua里面的全局变量 a
        print(a);
        string str = luaEnv.Global.Get<string>("str");//获取到lua里面的全局变量 str
        print(str);
        bool isDie = luaEnv.Global.Get<bool>("isDie");//获取到lua里面的全局变量 isDie
        print(isDie);

        luaEnv.Dispose();

Table

table 的名字为 person

person = { name="MoGu",age=100 }

把访问到的 table 映射到 class

class Person
    {
        public string name;
        public int age;
    }

        LuaEnv luaEnv = new LuaEnv();
        luaEnv.DoString("require 'CSharpCallLua'");
        
        Person p = luaEnv.Global.Get<Person>("person");
        print(p.name+"-"+ p.age);

        luaEnv.Dispose();

用这种方式访问到 table 后,对类中的值修改,是不会影响原来的 table 的。


把访问到的 table 映射到 interface

person = {
	name="MoGu",age=100 ,
	
	eat=function(self,a,b)
		print(a+b)
	end
}

需要给接口添加一个特性 [CSharpCallLua]

可以通过 interface 的方法访问 Lua 的函数

   [CSharpCallLua]
   interface IPerson
    {
        string name { get; set; }
        int age { get; set; }
        void eat(int a,int b);
    }

        LuaEnv luaEnv = new LuaEnv();
        luaEnv.DoString("require 'CSharpCallLua'");
        
        IPerson p = luaEnv.Global.Get<IPerson>("person");
        print(p.name+"-"+p.age);
        p.eat(12,34);

        luaEnv.Dispose();

用这种方式访问到 table 后,对值修改,是影响原来的 table 的。


把访问到的 table 映射到 Dictionary<>,List<>

给原来的 table 添加几个数据

person = {
	name="MoGu",age=100 ,12,2,2,2,2,true,3.3,
	
	eat=function(self,a,b)
		print(a+b)
	end
}
        LuaEnv luaEnv = new LuaEnv();
        luaEnv.DoString("require 'CSharpCallLua'");
        
        Dictionary<string, object> dict = luaEnv.Global.Get<Dictionary<string, object>>("person");
        foreach(string key in dict.Keys)
        {
           print(key + "-" + dict[key]);
        }
        
        List<int> list = luaEnv.Global.Get<List<int>>("person");
        foreach (object o in list)
        {
           print(o);
        }

        luaEnv.Dispose();

把访问到的 table 映射到 LuaTable

优点是不需要生成代码,缺点是慢

        LuaEnv luaEnv = new LuaEnv();
        luaEnv.DoString("require 'CSharpCallLua'");
        
        LuaTable tab= luaEnv.Global.Get<LuaTable>("person");
        print(tab.Get<string>("name"));
        print(tab.Get<int>("age"));
        print(tab.Length);
        
        luaEnv.Dispose();

全局function

先定义一个全局 function

function add(a,b)
	print(a+b)
	return a+b,a,b
end

方式一,映射到delegate
优点,性能好,类型安全;缺点,要生成代码

    [CSharpCallLua]
    delegate int Add(int a, int b,out int resa,out int resb); 

        LuaEnv luaEnv = new LuaEnv();
        luaEnv.DoString("require 'CSharpCallLua'");
        
        Add add = luaEnv.Global.Get<Add>("add");
        int resa = 0; int res b= 0;
        int res = add(34, 78,out resa,out resb);
        print(res);
        print(resa);
        print(resb);
        
        add = null;        
        luaEnv.Dispose();

方式二,映射到 LuaFunction
优缺点和第一种相反

        LuaEnv luaEnv = new LuaEnv();
        luaEnv.DoString("require 'CSharpCallLua'");
        
        LuaFunction func = luaEnv.Global.Get<LuaFunction>("add");
        object[] os= func.Call(1, 2);
        foreach(object o in os)
        {
            print(o);
        }
        
        luaEnv.Dispose();

Lua调用C#常用方式

创建C#对象

在 lua 脚本中

--构造游戏物体,new对象
CS.UnityEngine.GameObject("new by lua")

在 Unity 中调用

        LuaEnv luaEnv = new LuaEnv();
   
        luaEnv.DoString("require 'LuaCallCSharp'");

        luaEnv.Dispose();

访问静态属性和方法

local gameObject = CS.UnityEngine.GameObject

local camera = gameObject.Find("Main Camera")

camera.name = "update by lua"

访问成员属性和方法

注意:
lua 中使用冒号,表示成员方法的调用。它自动完成把当前对象作为一个参数,传入方法。
lua 中使用点,则表示静态属性与方法调用。它需要手工往方法中传递当前对象。

local cameraCom= camera.GetComponent(camera,"Camera")

gameObject.Destroy(cameraCom)

访问父类(子类)属性和方法

定义一个C#父类

namespace XluaPro
{
	public class IsInvoked_Father
	{
        public string FatherClassName = "父类字段";

        public IsInvoked_Father()
        {
            Debug.Log("IsInvoked_Father 父类构造函数");
        }

        public void ShowFatherInfo()
        {
            Debug.Log("IsInvoked_Father.cs 父类的方法 ShowFatherInfo()");
        }
    }
}

再写一个子类

namespace XluaPro
{
	public class IsInvokedClass:IsInvoked_Father
	{
        public string ChildClassName= "子类字段";

       public IsInvokedClass()
        {
            Debug.Log("IsInvokedClass 子类构造函数");
        }

        public void Mehtod1()
        {
            Debug.Log("IsInvokedClass.cs/Mehtod1 方法");
        }
    }
}

在 lua 文件中调用

local IsInvoked = CS.XluaPro.IsInvokedClass
local classObj = IsInvoked()   --自动调用父类与子类的构造函数

--调用普通方法
classObj:Mehtod1()					--ok
--classObj.Mehtod1()			        --语法报错!
classObj.Mehtod1(classObj)		    --语法OK

--调用父类的字段与方法
classObj:ShowFatherInfo();              --调用父类的方法
print(classObj.FatherClassName)         --调用父类的公共字段
print(classObj.ChildClassName)          --调用子类的公共字段


Lua调用C#的方法重载现象

/*  定义方法重载  */

        public void Method2(int num1, int num2)
        {
            Debug.Log(GetType() + "/Method2()/ 重载方法/int浮点型/num1=" + num1 + " num2=" + num2);
        }

        public void Method2(float num1,float num2)
        {
            Debug.Log(GetType()+ "/Method2()/ 重载方法/float浮点型/num1="+ num1+" num2="+num2);
        }

        public void Method2(string str1, string str2)
        {
            Debug.Log(GetType() + "/Method2()/ 重载方法/字符串类型/str1=" + str1 + " str2=" + str2);
        }

lua 中去调用

--测试调用C#方法重载
classObj:Method2(10,20)
classObj:Method2("abc","def")

这里发现 xlua 是支持方法重载的,但为“有限重载”。可以直接通过不同的参数类型进行重载函数的访问,例如:
testobj:TestFunc(100)
testobj:TestFunc(‘hello’)
将分别访问整数参数的 TestFunc 和字符串参数的 TestFunc。

但是,xlua 只一定程度上支持重载函数的调用,因为 lua 的类型远远不如C#丰富,存在一对多的情况,比如C#的int,float,double都对应于 lua 的 number,上面的例子中 TestFunc 如果有这些重载参数,第一行将无法区分开来,只能调用到其中一个(生成代码中排在前面的那个)


Lua调用C#中带参方法

C#中可变参数方法,Parmms关键字

//定义带有返回数值,有参数的方法,且有params 关键字
        public int Method3(int num1, int num2, params string[] strArray)
        {
            Debug.Log(GetType() + "/Method3()/ 带有params关键字的方法/");
            foreach (string item in strArray)
            {
                Debug.Log("输入的字符串内容:"+item);
            }
            return num1 + num2;
        }

在 lua 中

--测试C#中带有params 关键字的方法
local intResult=classObj:Method3(20,70,"Hello ","World","EveryOne")
print("调用parmas关键字的方法,返回数值= "..intResult)

C#结构体参数

lua 使用一个表,来映射C#的结构体

//定义结构体(建议结构体成员为小写)
    public struct MyStruct
    {
        public string x;
        public string y;
    }

//带有结构体参数的方法
        public void Method4(MyStruct p)
        {
            Debug.Log("测试lua调用结构体方法");
            Debug.Log("p.x="+ p.x);
            Debug.Log("p.y="+ p.y);
        }

在 lua 中

--测试lua调用C#中带有结构体参数的方法
--定义一个表
myStructTable={x="C#语言",y="lua语言"}
classObj:Method4(myStructTable)

C#接口参数

注意: 接口需要加入标记: [CSharpCallLua]
lua 使用一个表,来映射C#的接口

//定义接口
    [XLua.CSharpCallLua]
    public interface MyInterface
    {
        int x { get; set; }
        int y { get; set; }
        void Speak();
    }


        //方法具有接口为参数的
        public void Method5(MyInterface p)
        {
            Debug.Log("测试lua调用具有接口为参数的方法");
            Debug.Log("p.x=" + p.x);
            Debug.Log("p.y=" + p.y);
            p.Speak();
        }

在 lua 中

--测试lua调用C#中带有接口参数的方法
--定义一个表
myInterfaceTable=
{
	x=1000,
	y=300,

	Speak=function()
		print("lua中 Speak 方法被调用!")
	end
}
classObj:Method5(myInterfaceTable)

C#委托参数

委托需要加入标记: [CSharpCallLua]
lua 使用一个函数,来映射C#的委托

//定义委托
    [XLua.CSharpCallLua]
    public delegate void MyDelegate(int num);

        //方法具有委托为参数
        public void Method6(MyDelegate p)
        {
            Debug.Log(GetType()+"/Method6()/委托参数:");
            //调用
            p.Invoke(88);
        }

在 lua 中

--定义lua调用C#中带有委托参数的方法
--定义函数
myDelegate=function(num)
	print("lua 中对应委托方法。参数num="..num)
end

classObj:Method6(myDelegate)

__

Lua接收C#方法返回的多个结果数值

基本规则: 参数的输入输出属性(out,ref)

A: C#的普通参数算一个输入形参,ref修饰的算一个输入形参,out不算, 然后从左往右对应lua 调用的实参列表。
B: Lua调用返回值处理规则:C#函数的返回值(如果有的话)算一个返回值,out算一个返回值,ref算一个返回值,然后从左往右对应lua的多返回值。

        //定义一个具有多返回数值的方法
        public int Method7(int num1,out int num2,ref int num3)
        {
            Debug.Log(GetType()+"/Method7()/测试lua接收C#的多返回数值");
            num2 = 3000;
            num3 = 999;
            return num1 + 100;
        }

在 lua 中

--接收C#多返回数值
local num1=10
local num2=20
local res1,res2,res3=classObj:Method7(num1,num2)
print("res1="..res1)  --输出结果: 110
print("res2="..res2)  --输出结果: 3000
print("res3="..res3)  --输出结果: 999

Lua如何调用C#泛型方法

基本规则:lua 不直接支持 C# 的泛型方法,但可以通过扩展方法功能进行封装后调用。

使用 Extension methods (扩展方法)技术就是 C# 中在不改变原始类的基础上,使用一种机制可以无限扩展这个类功能的机制。

原始类为: A
扩展类为: Ext_A

注意: Ext_A 必须是一个静态类,且扩展方法也必须是静态的。方法的参数中必须要有被扩展类作为其中一个参数,此参数前面必须有this 关键字修饰。

        //定义一个具有泛型方法为参数的。
        public void Method8(List<string> strArray)
        {
            Debug.Log(GetType() + "/Method8()/这是一个具有泛型方法为参数的方法");
            foreach (string item in strArray)
            {
                Debug.Log("泛型集合中的内容="+item);
            }
        }

在 lua 中

--lua中可以直接调用具有泛型为参数的方法
myTable8={"lua语言","C#语言","C++语言"}
classObj:Method8(myTable8);  

另外一种就是

自定义泛型类

namespace XluaPro
{
    [XLua.LuaCallCSharp]
	public class MyGengerric
	{
        public T GetMax<T>(T num1, T num2) where T : IComparable
        {
            if (num1.CompareTo(num2)<0)
            {
                return num2;
            }
            else {
                return num1;
            }
        }		
	}
}
//C#方法中,调用我们自定义的泛型方法
        public void Method_InvokeGenger()
        {
            //int maxNum = 0;
            //int num1 = 100;
            //int num2 = 200;

            //MyGengerric obj = new MyGengerric();
            //maxNum = obj.GetMax<int>(num1, num2);
            //Debug.Log("C#中比较两个数字大小: "+maxNum);

            //测试字符串的比较
            string maxStr = string.Empty;
            string str1 = "xd";
            string str2 = "kb";

            MyGengerric obj = new MyGengerric();
            maxStr = obj.GetMax<string>(str1, str2);
            Debug.Log("C#中比较两个字符串大小: " + maxStr);


        }

在 lua 中,只能让上述方法跑起来

--让C#方法运行起来。
classObj:Method_InvokeGenger()

如果要这样,去调用那个自定义泛型方法,会有错误

--lua中直接调用C#中定义的泛型方法
local maxNum=CS.XluaPro.MyGengerric:GetMax<int>(20,30)  --报语法错误
print("maxNum="..maxNum)

那么怎么办,就有了扩展方法

这个类是一个“扩展方法”,本类的功能是扩展原有“MyGengerric”类的功能。
注意:
扩展方法有两大注意事项:
A: 扩展方法类,必须是静态类。
B: 定义的扩展方法的参数,第一个参数必须是this ,然后跟需要扩展的类名称全称。

namespace XluaPro
{
    [XLua.LuaCallCSharp]
    public static class Extension_MyGengerric
	{
        /// <summary>
        /// 定义扩展方法
        /// </summary>
        /// <param name="gen"></param>
        /// <param name="num1"></param>
        /// <param name="num2"></param>
        /// <returns></returns>
        public static int ExtGetMax(this XluaPro.MyGengerric gen, int num1, int num2)
        {
            if (num1<num2)
            {
                return num2;
            }
            else {
                return num1;
            }
        }	
	}
}

在 C# 中调用这个扩展方法

        //在C#中学习调用C#的扩展方法
        public void Test8_InvokeExtensionMethod()
        {
            int maxNum = 0;
            int num1 = 800;
            int num2 = 200;

            MyGengerric obj = new MyGengerric();
            maxNum=obj.ExtGetMax(num1, num2);
            Debug.Log("[应用扩展方法] C#中得到最大数值="+maxNum);
        }

那如果是在 lua 中调用这个扩展方法

--在lua中通过调用"扩展方法",来间接完成对C#“泛型方法”功能的实现。
local maxNum=CS.XluaPro.MyGengerric():ExtGetMax(888,66)
print("[在lua中扩展方法调用] maxNum="..maxNum)

其他知识点

参数带默认值的方法
与C#调用有默认值参数的函数一样,如果所给的实参少于形参,则会用默认值补上。

枚举类型
枚举值就像枚举类型下的静态属性一样。

testobj:EnumTestFunc(CS.Tutorial.TestEnum.E1)

上面的 EnumTestFunc 函数参数是 Tutorial.TestEnum 类型的。

委托与事件
delegate 属性可以用一个 luaFunction 来赋值
比如 testobj 里头有个事件定义是这样:public event Action TestEvent;

增加事件回调
testobj:TestEvent(’+’, lua_event_callback)

移除事件回调
testobj:TestEvent(’-’, lua_event_callback)


Lua调用C#经验小结

1。lua 调用C#,需要在 Xlua 中生成“适配代码”,并在这个类打入一个 [LuaCallCSharp] 的标签。

2。如果lua调用C#的系统API ,则无法拿到源代码,无法打入标签。则使用“静态列表”方式解决。

public static List<Type> mymodule_LuaCallCS_List = new List<Type>()
  {
          typeof(GameObject),
          typeof(Dictionary<string,int>),
  };
 

然后把以上代码放入一个静态类中即可。

3。实际开发过程中,lua 调用 C# 用的比较多。xlua 的优点体现在没有必要每次修改的时候,都要生成代码。主要原理是依赖于编译器环境下,利用反射来动态生成代码接口。

4。在标有 “[XLua.LuaCallCSharp]” 的 C# 类中,添加新的方法后,如果是生成了代码类,则必须重新生成或者删除,否则 Xlua 还是用以前生成的代码进行注册查询,会出现 lua 异常:“试图访问一个nil 的方法”。

 类似资料: