C#与XLua通信

XLua 是一个用于在 Unity 游戏开发引擎中绑定 Lua 编程语言的框架,它旨在提供快速、高效的 Lua 和 C# 之间的桥梁。绑定过程经过优化,以最小化性能开销,适用于资源密集型的游戏开发。它提供了简单直观的 API,用于将 C# 函数和对象绑定到 Lua,实现两种语言之间的无缝通信。XLua 设计用于在 Unity 支持的各种平台上工作,包括 Windows、macOS、iOS、Android 等,这使得它成为在不同设备上进行游戏开发的多用途选择。这篇文章就介绍一下 Xlua 和 C# 之间是如何通信的。

Lua

Lua 是一种轻量级、高效的脚本语言,由巴西计算机科学家Roberto Ierusalimschy、Waldemar Celes 和 Luiz Henrique de Figueiredo于1993年创建。Lua 的设计目标是成为一个嵌入式脚本语言,通常用于游戏开发、嵌入式系统、脚本扩展等领域。它具有简单的语法、动态类型、自动内存管理等特性,使其易于学习和使用。

关键特点包括:

  • 轻量级:Lua 的设计使其成为一种轻量级语言,适合嵌入到其他应用程序中。

  • 动态类型:Lua 是一种动态类型语言,变量无需声明类型,可以在运行时改变其类型。

  • 自动内存管理:Lua 具有垃圾回收机制,开发者无需手动管理内存。

  • 可扩展性:Lua 具有强大的扩展性,允许通过C语言编写的模块扩展其功能。

CSharp

C#(CSharp) 是由微软开发的一种面向对象的编程语言。它是 .NET 平台的一部分,旨在提供一种现代、安全、高性能的编程语言。C#最初由Anders Hejlsberg和他的团队在Microsoft于2000年发布,它结合了C++的强大性能和Java的简单性。

关键特点包括:

  • 面向对象:C# 是一种面向对象的编程语言,支持封装、继承和多态等面向对象的概念。

  • 类型安全:C# 是一种静态类型语言,编译时会进行类型检查,有助于减少运行时错误。

  • 直观语法:C# 的语法设计旨在简化开发者的工作,提供直观、清晰的语法结构。

  • 强大的标准库:C# 配有丰富的标准库和 .NET 框架,提供了许多现成的类和功能,使开发更加高效。

  • 跨平台:C# 在近年来通过 .NET Core 和 .NET 5 的推动实现了跨平台支持,使得它不仅限于 Windows 平台。

为什么C#和Lua可以通信

C# 和 Lua之间的相互调用通常是通过桥接工具或中间件实现的。这些工具提供了一种机制,使得 C# 和 Lua 两种语言能够在同一项目中进行交互。其中,XLua是一个典型的桥接工具。

  • 绑定框架:工具如 XLua 提供了一个绑定框架,使得 C# 和 Lua 之间的函数调用和数据传递变得更加容易。这些框架允许将 C# 对象和函数绑定到 Lua,以及从 Lua 中调用 C# 的对象和函数。
  • 中间层:这些工具创建了一个中间层,将 C# 和 Lua 之间的交互进行了封装。这个中间层负责处理两种语言之间的数据转换、函数调用和其他细节,从而使得开发者可以更方便地在两种语言之间传递信息。
  • 类型映射:桥接工具通常提供了一种类型映射机制,使得 C# 的数据类型能够与 Lua 的数据类型进行对应。这种映射是关键,因为 C# 和 Lua 在类型系统上有一些差异,比如 C# 是强类型语言,而 Lua 是动态类型语言。
  • Lua 的可扩展性:Lua 本身是一种高度可扩展的语言,可以通过 C 语言编写的模块进行扩展。这使得在 Lua 中调用 C# 函数和对象变得更加容易,因为可以通过 C 编写中间层,实现 C# 和 Lua 之间的交互。
  • Lua 的嵌入性:Lua是一种嵌入式语言,这意味着它可以被轻松地嵌入到其他应用程序中。Lua 解释器是一个相对轻量级、独立的库,可以很容易地集成到 C# 等宿主应用程序中,允许在 C# 代码中调用 Lua 脚本。Lua 提供了用于在 C 类代码中注册函数和数据结构的 API,使得 C# 可以调用通过 Lua 编写的函数。
  • C# 的托管调用:C# 是一种托管语言,运行在.NET运行时中。在 C# 中,可以通过 P/Invoke(Platform Invocation Services) 或使用专门的库(如NLua、LuaInterface等)来调用本地(非托管)代码,这就为 C# 与 C 语言进行交互提供了便利的手段,而 Lua 正是通过这种方式在 C# 中被调用。

C#和Lua交互方式

  • C#调用Lua:

    • 一般方式:在通常的情况下,C# 调用 Lua 是通过调用 Lua 解释器的底层 DLL 库来实现的。这种方式涉及到与 Lua 解释器的交互,通常使用 Lua 的 CLR package 或其他插件提供的机制。
    • XLua方式:在 XLua 中,是使用 C# 调用 XLua 库的接口来实现 C# 与 Lua 的交互,而不是通过 DLL 方式。
    • 原理:在 C# 中调用 Lua 的过程涉及到虚拟机的概念,其中 Lua 使用了 Lua 虚拟机来执行 Lua 脚本。虚拟机提供了一个虚拟栈,C# 代码可以通过虚拟栈与 Lua 脚本进行交互。这个栈有两种计数方式,栈底是1,往上递增;栈顶是-1,往下递减;C# Call Lua 时,C# 把请求或数据放在栈顶,然后 lua 从栈顶取出数据,在 lua 中做出相应处理(查询,改变),然后把处理结果放回栈顶,最后 C# 再从栈顶取出 lua 处理完的数据,完成数据交互。
  • Lua调用C#:

    • 一般方式:在通常的情况下,Lua 调用 C# 需要使用 Lua 的 CLR package 或其他 Lua 插件提供的机制。这包括将 C# 对象注册到 Lua 环境中,以便 Lua 可以直接调用 C# 对象的方法。
    • XLua 方式:在 XLua 中,有两种主要的方式来实现 Lua 调用 C#:
      • Wrap 方式:在 XLua 中,通过生成 Wrap 文件,该文件包含对 C# 类和方法的封装。Lua 文件调用 Wrap 文件,然后 Wrap 文件再调用 C# 文件。这种方式需要在运行前生成 Wrap 文件,但它提供了更高的性能和类型安全。
      • 反射方式:当处理系统 API、DLL 或第三方库等情况时,可能无法提前生成静态映射关系。在这种情况下,可以通过动态反射调用 C# 方法的方式来实现 Lua 对 C# 的调用。这种方式灵活,但执行效率相对较低。

XLua简介

xlua github下载地址
XLua 是一个用于在 C# 和 Lua 之间进行交互的库,它提供了一种强大而灵活的方式,使 C# 和 Lua 能够无缝地通信。它提供了一种高效的方式,让开发者能够使用 Lua 脚本来扩展和定制 Unity 应用程序。以下是 XLua 的一些主要特点和功能:

  • 高性能:XLua 采用了一系列优化策略,包括 IL 运行时代码生成,JIT 编译等,以保证在运行时的性能表现。这使得 XLua 适用于对性能要求较高的游戏开发场景。

  • 自动化 Wrap 生成:XLua 能够自动生成 C# 和 Lua 之间的 Wrap 代码,无需手动编写繁琐的映射代码。通过使用自动生成的 Wrap 文件,XLua 能够实现对 C# 类和方法的直接调用。

  • 无 GC 垃圾回收:XLua 的设计考虑了垃圾回收的性能问题,并尽量避免了不必要的 GC 开销。这有助于在运行时减少垃圾回收的频率,提高性能。

  • 支持 Unity 热更新:XLua 能够实现对 Lua 脚本的热更新,这意味着你可以在不重新编译和重新打包应用的情况下,动态更新游戏逻辑。这对于快速迭代和修复问题非常有用。

  • 支持 Unity 事件和协程:XLua 能够直接处理 Unity 的事件系统(比如 MonoBehaviour 的 Awake、Update等方法)和协程,使得 Lua 脚本能够更方便地与 Unity 引擎进行交互。

  • 支持异步调用:XLua 支持异步调用,能够更好地处理异步操作,比如 Unity 的异步加载场景、异步下载等。

  • 丰富的扩展功能:XLua 提供了一系列的扩展功能,包括导出自定义类、委托、泛型支持等,使得XLua更加灵活和易用。

总体来说,XLua 为 Unity 开发者提供了一种灵活、高性能的 Lua 脚本集成方式,让开发者能够在 Unity 中更加方便地利用 Lua 进行快速开发和定制。

XLua关键类

LuaEnv

LuaEnv 是 XLua 中的一个核心类,用于管理 Lua 环境和 Lua 执行引擎,负责管理整个 Lua 环境的创建、执行和资源释放。它充当了 Lua 解释器和执行环境的角色。在 Unity 中,它通常在游戏启动时被创建,用于初始化 Lua 环境,执行 Lua 启动脚本,管理 Lua 全局变量、以及处理 Lua 脚本的热更新等操作。

  • 创建 Lua 环境:在使用 XLua 时,通常需要创建一个 LuaEnv 的实例。这个实例负责管理整个 Lua 环境,包括加载和执行 Lua 脚本。

    1
    2
    3
    using XLua;

    LuaEnv luaEnv = new LuaEnv();
  • 执行 Lua 代码:LuaEnv 可以执行 Lua 代码字符串或 Lua 文件。

    1
    luaEnv.DoString("print('Hello from Lua!')");
  • 注册 C# 对象到 Lua 环境:通过 Global 属性可以注册 C# 对象到 Lua 环境中,使得 Lua 脚本可以直接访问和调用这些 C# 对象。

    1
    luaEnv.Global.Set("myCSharpObject", new MyCSharpClass());
  • 设置全局变量:

    1
    luaEnv.Global:Set("myGlobalVar", 42)
  • 获取全局变量:

    1
    local value = luaEnv.Global:Get<number>("myGlobalVar")
  • 执行 Lua 函数:LuaEnv 通过 DoString 或 DoFile 执行 Lua 代码,也可以通过 Get 方法获取 Lua 函数并执行。

    1
    2
    LuaFunction myLuaFunction = luaEnv.Global.Get<LuaFunction>("myLuaFunction");
    myLuaFunction.Call();
  • 资源释放:在使用完 LuaEnv 后,需要确保及时释放资源,避免内存泄漏。

    1
    luaEnv.Dispose();
  • 自定义加载器:AddLoader 允许注册自定义的 Lua 脚本加载器,这样就可以在 Lua 脚本中使用自定义的加载逻辑。这对于管理和加载不同文件夹中的 Lua 文件或使用特殊加载逻辑的场景非常有用。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    // 创建 LuaEnv 实例
    LuaEnv luaEnv = new LuaEnv();

    // 添加自定义加载器
    luaEnv.AddLoader((ref string filename) =>
    {
    // 自定义加载逻辑,返回 Lua 文件内容的字节数组
    byte[] luaContent = // ... your loading logic here ...
    return luaContent;
    });

    // 执行 Lua 脚本
    luaEnv.DoString("require 'myCustomScript'");

    // 释放 LuaEnv 资源
    luaEnv.Dispose();

    在这个例子中,AddLoader 接受一个函数,该函数用于定义自定义加载逻辑。可以在这个函数中根据传入的文件名加载相应的 Lua 文件内容,然后返回该内容的字节数组。

  • 创建一个新的 Lua 用户数据:NewUserData 方法用于在 Lua 环境中创建一个新的 Lua 用户数据。用户数据是一种特殊类型,允许将 C# 对象传递给 Lua,并在 Lua 中使用它。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    // 创建 LuaEnv 实例
    LuaEnv luaEnv = new LuaEnv();

    // 创建一个新的 Lua 用户数据
    object myUserData = luaEnv.NewUserData(new MyClass());

    // 在 Lua 脚本中使用该用户数据
    luaEnv.DoString("print(myUserData.myProperty)");

    // 释放 LuaEnv 资源
    luaEnv.Dispose();

    在这个例子中,NewUserData 创建了一个包装了 MyClass 对象的 Lua 用户数据。在 Lua 脚本中,可以通过 myUserData 访问该对象的属性和方法。

LuaFunction

LuaFunction 是 XLua 中用于表示 Lua 函数的类型。通过 LuaFunction,可以在 C# 中调用 Lua 中定义的函数。

  • 获取 LuaFunction 对象:可以通过 LuaEnv 或者从 LuaTable 中获取 Lua 函数。通常,这个对象会被保存在 C# 中以便后续调用。

    1
    2
    3
    4
    5
    6
    7
    luaEnv.DoString(@"
    function myLuaFunction(message)
    print('Lua method called with message: ' .. message)
    end
    ");
    // 获取名为 "myLuaFunction" 的 Lua 函数
    LuaFunction myLuaFunction = luaEnv.Global.Get<LuaFunction>("myLuaFunction");
  • 调用 Lua 函数: 一旦获取了 LuaFunction 对象,你可以使用 Call 方法来调用 Lua 函数,也可以传递参数给 Lua 函数。

    1
    2
    3
    4
    5
    // 调用 Lua 函数
    myLuaFunction.Call();

    // 传递参数给 Lua 函数
    myLuaFunction.Call("Hello"); //Hello
  • 获取返回值:如果 Lua 函数有返回值,可以通过 Call 的返回值来获取。

    1
    2
    // 调用 Lua 函数并获取返回值
    var result = myLuaFunction.Call();
  • 错误处理:调用 Lua 函数时,应该注意处理可能发生的错误。可以通过检查 LuaEnv 的 LastException 属性来获取最后一次执行时产生的异常信息。

    1
    2
    3
    4
    5
    6
    7
    8
    try
    {
    myLuaFunction.Call();
    }
    catch (Exception e)
    {
    Debug.LogError($"Lua function call error: {e.Message}");
    }
  • 释放资源:在不再需要 LuaFunction 时,应该及时释放资源,以防止内存泄漏。

    1
    myLuaFunction.Dispose();

总体来说,LuaFunction 提供了在 C# 中调用 Lua 函数的接口。在与 Lua 交互的过程中,可能会频繁使用这个类型来执行 Lua 中的函数,并根据需要处理返回值和错误。

LuaTable

LuaTable 是在 XLua 中用于在 C# 中表示 Lua 表(table)的类型。Lua 表是一种类似于数组或字典的数据结构,可在 Lua 中用于存储和组织数据。在 C# 中,LuaTable 类型允许在 C# 代码中操作 Lua 表。

  • 创建新表:NewTable 方法用于在 Lua 环境中创建一个新的 Lua 表。这是一种存储数据的数据结构,类似于数组或字典。

    1
    2
    3
    4
    5
    // 创建 LuaEnv 实例
    LuaEnv luaEnv = new LuaEnv();

    // 创建一个新的 Lua 表
    LuaTable myTable = luaEnv.NewTable();
  • 向 Lua 表中添加元素:使用 Set 方法向表中添加元素。这里的键可以是字符串或整数,值可以是任意类型。

    1
    2
    3
    4
    // 向表中添加数据
    myTable.Set("key1", "value1");
    myTable.Set("key2", 42);
    myTable.Set(1, "item1");
  • 将 Lua 表注册到 Lua 环境中:在默认情况下,DoString 方法执行的 Lua 代码与 C# 中的变量并不共享。要在 Lua 脚本中使用 C# 中的变量,需要将这些变量传递到 Lua 环境中。

    1
    2
    3
    4
    5
    6
    7
    // 将 Lua 表注册到 Lua 环境中
    luaEnv.Global.Set("myTable", myTable);
    // 在 Lua 脚本中使用该表
    luaEnv.DoString(@"
    print(myTable.key1)
    print(myTable.key2)
    ");
  • 从 Lua 表中获取元素:使用 Get 方法从表中获取元素。根据键的类型,可以返回不同类型的值。

    1
    2
    3
    4
    string value1 = myTable.Get<string>("key1");
    int value2 = myTable.Get<int>("key2");
    string item1 = myTable.Get<string>(1);
    Debug.Log(value1); //输出value1
  • 迭代LuaTable:使用 ForEach 方法可以迭代 Lua 表中的元素。注意在 XLua 中,LuaTable.ForEach() 方法的类型参数不是可以自动推断的,因此在使用时需要显式指定类型参数,只会迭代 key 为当前指定类型的表元素。

    1
    2
    3
    4
    5
    6
    myTable.ForEach<string, object>((key, value) => {
    Debug.Log($"string Key: {key}, Value: {value}"); // value1 42
    });
    myTable.ForEach<int, object>((key, value) => {
    Debug.Log($"int Key: {key}, Value: {value}"); // item1
    });

C# 和 Lua 相互调用

在 Lua 和 C# 之间进行调用通常有两个主要的方向:Lua 调用 C#(Lua Call C#)和 C# 调用 Lua(C# Call Lua)。

C# 调用 Lua 脚本的三种方法

  • 直接运行代码:

    1
    2
    LuaEnv luaEnv = new LuaEnv();
    luaEnv.DoString("print('Hello from Lua!')");
  • 运行Resources目录下的 lua 脚本

    1
    2
    3
    4
    // xlua默认的脚本寻找路径是在Resources文件夹中,但是由于unity并不能识别Lua文件,所以直接加载时会报错的。解决方案是将lua后缀多加一个txt,将其转换为文本文件
    LuaEnv luaEnv = new LuaEnv();
    // 加载 Resources 目录下的 Lua 脚本文本
    luaEnv.DoString("require('LuaTest')");
  • 运行任意目录下的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
    void Start()
    {
    LuaEnv luaEnv = new LuaEnv();
    //加载脚本loader
    luaEnv.AddLoader(MyCustomLoader);
    // 相对路径或绝对路径,这里示例使用相对路径
    string luaScriptPath = "Assets/Scripts/LuaTest";

    try
    {
    // 执行 Lua 脚本
    luaEnv.DoString(string.Format("require '{0}'", luaScriptPath));
    }
    catch (System.Exception e)
    {
    Debug.LogError("Lua Error: " + e.Message);
    }
    }

    // 自定义 Lua 文件加载器
    byte[] MyCustomLoader(ref string filepath)
    {
    // 构建完整路径
    string fullFilePath = Path.Combine(Application.dataPath, filepath + ".lua");

    // 读取 Lua 脚本内容
    if (File.Exists(fullFilePath))
    {
    return File.ReadAllBytes(fullFilePath);
    }

    return null;
    }

C# 调用 Lua 示例

C#访问lua基本数据类型

1
2
3
4
5
-- Lua 脚本
testNumber = 1
testBool = true
testFloat = 1.3
testString = "13"
1
2
3
4
5
6
// C#脚本
int i = luaEnv.Global.Get<int>("testNumber");
bool b = luaEnv.Global.Get<bool>("testBool");
float f = luaEnv.Global.Get<float>("testFloat");
string s = luaEnv.Global.Get<string>("testString");
Debug.Log(i + " " + b + " " + f + " " + s); // 1 true 1.3 13

这些简单类型是值拷贝,即使给新的变量赋值,比如 i = 13,也不会影响到 lua 文件里的变量;除非用 set 方法,才会改变 lua 文件里的简单类型变量,例如:

1
luaEnv.Global.Set("testNumber", 13);

C#访问lua function

映射到delegate(推荐方法)

在 XLua 中,将 Lua 函数映射到 C# 的委托类型(delegate)是一种性能较好、类型安全的方式。缺点是需要生成代码(如果没生成代码会抛 InvalidCastException 异常)。

  • delegate 怎样声明:对于 function 的每个参数就声明一个输入类型的参数。
  • 多返回值要怎么处理:从左往右映射到 c# 的输出参数,输出参数包括返回值,out 参数,ref 参数。
  • 参数、返回值支持哪些类型:支持各种复杂类型,out,ref 修饰的,甚至可以返回另外一个delegate。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
-- lua表方法
function Func()
print("hello world!")
end

function FuncWithSingleParam(value)
print(value)
end

function FuncWithMulParam(age, name, isWoman)
print(age.." "..name.." "..tostring(isWoman))
end

function FuncWithSingleReturn()
return 0
end

function FuncWithMulReturn()
return 0, "value1", true
end

function FuncWithMulReturnAndParams(age, name, isWoman)
return age, name, isWoman
end
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
// 映射到C#委托
[CSharpCallLua]
public delegate void FuncDelegate();

[CSharpCallLua]
public delegate void FuncWithSingleParamDelegate(string value);

[CSharpCallLua]
public delegate void FuncWithMulParamDelegate(int age, string name, bool isWoman);

[CSharpCallLua]
public delegate int FuncWithSingleReturnDelegate();

[CSharpCallLua]
public delegate int FuncWithMulReturnDelegate(out string result1, out bool result2);

[CSharpCallLua]
public delegate int FuncWithMulReturnAndParamsDelegate(int age, string name, bool isWoman, out string result1, out bool result2);

void Start()
{
……
FuncDelegate func = luaEnv.Global.Get<FuncDelegate>("Func");
func(); //hello world!

FuncWithSingleParamDelegate funcWithSingleParam = luaEnv.Global.Get<FuncWithSingleParamDelegate>("FuncWithSingleParam");
funcWithSingleParam("Hello"); //Hello

FuncWithMulParamDelegate funcWithMulParam = luaEnv.Global.Get<FuncWithMulParamDelegate>("FuncWithMulParam");
funcWithMulParam(25, "Alice", true); //25 Alice true

FuncWithSingleReturnDelegate funcWithSingleReturn = luaEnv.Global.Get<FuncWithSingleReturnDelegate>("FuncWithSingleReturn");
int singleReturnValue = funcWithSingleReturn();
Debug.Log($"FuncWithSingleReturn Result: {singleReturnValue}"); //FuncWithSingleReturn Result: 0

FuncWithMulReturnDelegate funcWithMulReturn = luaEnv.Global.Get<FuncWithMulReturnDelegate>("FuncWithMulReturn");
string result1;
bool result2;
int result0 = funcWithMulReturn(out result1, out result2);
Debug.Log($"FuncWithMulReturn Result: {result0}, {result1}, {result2}"); //FuncWithMulReturn Result: 0, value1, True

FuncWithMulReturnAndParamsDelegate funcWithMulReturnAndParams = luaEnv.Global.Get<FuncWithMulReturnAndParamsDelegate>("FuncWithMulReturnAndParams");
result0 = funcWithMulReturnAndParams(23, "Anne", true, out result1, out result2);
Debug.Log($"FuncWithMulReturnAndParams Result: {result0}, {result1}, {result2}"); //FuncWithMulReturnAndParams Result: 23, Anne, True
}
映射到LuaFunction

这种方式的优缺点刚好和第一种相反。使用起来很简单,LuaFunction 有一个变参的 Call 函数,可以传任意类型,任意个数的参数,返回值是 object 的数组,对应于 lua 的多返回值。

1
2
3
LuaFunction luaFunction = luaEnv.Global.Get<LuaFunction>("FuncWithMulReturnAndParams");
object[] objects = luaFunction.Call("Bob", 22, false);
Debug.Log($"FuncWithMulReturnAndParams Result: {objects[0]}, {objects[1]}, {objects[2]}"); //FuncWithMulReturnAndParams Result: Bob, 22, False

C#访问table

映射到class或者struct

table 的属性可以多于或者少于 class 的属性,可以嵌套其它复杂类型,过程是值拷贝(深拷贝),改变一端的值不会改变另外一端。直接将 Lua 表中的函数映射到 C# 类的字段或属性上是有限制的。通常,这种映射的能力比较有限,而且 XLua 不直接支持在 C# 中定义 Lua 函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
-- 定义lua全局表
GlobalTable = {
Name = "Lua",
Age = 3,
TableFunc = function(name)
return "Name: "..name
end,
SubTable = {
SubName = "SubLua",
PrintInfo = function()
print(GlobalTable.SubTable.SubName)
end,
ReturnInfo = function()
return GlobalTable.SubTable.SubName
end
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// C#映射类
public class MyCSharpClass
{
public string Name;
public int Age;
public Func<string, string> TableFunc;
public MySubCSharpClass SubTable;
}

public class MySubCSharpClass
{
public string SubName;
public UnityAction PrintInfo;
public Func<string> ReturnInfo;
}
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
//C#执行脚本
public class CSharpCallLuaRunner : MonoBehaviour
{
void Start()
{
LuaEnv luaEnv = new LuaEnv();
//加载脚本loader
luaEnv.AddLoader(MyCustomLoader);
// 相对路径或绝对路径,这里示例使用相对路径
string luaScriptPath = "Assets/Scripts/MyLuaTable";

try
{
// 执行 Lua 脚本
luaEnv.DoString(string.Format("require '{0}'", luaScriptPath));
}
catch (System.Exception e)
{
Debug.LogError("Lua Error: " + e.Message);
}

//注意用这种方式还是深拷贝,改变这里的内容,不会改变lua表里的内容
MyCSharpClass myClass = luaEnv.Global.Get<MyCSharpClass>("GlobalTable");
// 直接访问 Lua 表中的变量
Debug.Log($"Name: {myClass.Name}, Age: {myClass.Age}"); // Name: Lua, Age: 3
// 直接调用 Lua 表中的方法
string result = myClass.TableFunc(myClass.Name);
Debug.Log($"Result of TableFunc: {result}"); // Result of TableFunc: Name: Lua
// 访问子表中的变量
Debug.Log($"SubName: {myClass.SubTable.SubName}"); // SubName: SubLua
// 访问子表中的方法
myClass.SubTable.PrintInfo(); //LUA: SubLua
string resultSub = myClass.SubTable.ReturnInfo();
Debug.Log($"Result of SubTableFunc: {resultSub}"); // Result of SubTableFunc: SubLua
}

// 自定义 Lua 文件加载器
byte[] MyCustomLoader(ref string filepath)
{
// 构建完整路径
string fullFilePath = Path.Combine(Application.dataPath, filepath + ".lua");

// 读取 Lua 脚本内容
if (File.Exists(fullFilePath))
{
return File.ReadAllBytes(fullFilePath);
}

return null;
}
}
映射到一个interface(引用方式)

使用代码生成器生成接口的实例是推荐的方式。这种方式依赖于生成代码(如果没生成代码会抛 InvalidCastException 异常),代码生成器会生成这个 interface 的实例,如果 get 一个属性,生成代码会 get 对应的 table 字段,如果 set 属性也会设置对应的字段。甚至可以通过 interface 的方法访问 lua 的函数。这样的方式可以在一定程度上提高代码的类型安全性,并减少手动编写映射代码的工作。在 XLua 中,可以使用工具来生成代码以实现接口映射。

1
2
3
4
5
6
7
8
9
10
11
-- lua表
Person = {
name = "fex",
age = 22,
eatWithParam = function(self, a, b)
print(a + b)
end,
eatWithReturn = function(self, a, b)
return a + b
end
}
1
2
3
4
5
6
7
8
9
// C#接口,必须打上[CSharpCallLua]标签
[CSharpCallLua]
public interface IPersonInterface
{
string name { get; set; }
int age { get; set; }
void eatWithParam(int a, int b);
int eatWithReturn(int a, int b);
}

注意,定义接口以后需要执行 XLua-Generate Code。

1
2
3
4
5
6
7
8
9
//浅拷贝,引用方式,在C#中可以修改lua表的值    
IPersonInterface p = luaEnv.Global.Get<IPersonInterface>("Person");
Debug.Log(p.name); //fex
Debug.Log(p.age); //22
p.name = "hello fex";
Debug.Log(p.name); //hello fex
p.eatWithParam(1, 2); //3
int result = p.eatWithReturn(2, 3);
Debug.Log($"Result of Func: {result}"); //Result of Func: 5
映射到Dictionary<>,List<>

一种更轻量级的值拷贝方式,一般只用于映射静态数据。对于表中的键值对元素,可以映射到字典,对于表中的列表元素,可以映射到列表。由于映射到字典或列表时,value 的值的类型不一定相同,所以可以使用 object 类型。

1
2
3
4
5
6
7
8
9
10
-- lua表数据
Data = {
name = "fex",
age = 22,
true,
1,
1.5,
"item1",
}
Data[5] = "item2"
1
2
3
4
5
6
7
8
9
10
11
12
13
// C#映射到字典
Dictionary<object, object> dicData = luaEnv.Global.Get<Dictionary<object, object>>("Data");
foreach(var key in dicData.Keys)
{
print("Key:" + key + " value:" + dicData[key]);
}
// Key:1 value:True
// Key:2 value:1
// Key:3 value:1.5
// Key:4 value:item1
// Key:5 value:item2
// Key:name value:fex
// Key:age value:22

如果将字典的键类型设置为某一种数据类型,例如 int,那么只会返回表中键类型为 int 的字典。

1
2
3
4
5
6
7
8
9
10
11
// C#映射到列表
List<object> list = luaEnv.Global.Get<List<object>>("Data");
foreach(var value in list)
{
Debug.Log("value:" + value);
}
// value: True
// value: 1
// value: 1.5
// value: item1
// value: item2
映射到LuaTable类

一种更轻量级的引用方式,映射到 LuaTable 类,一般不推荐使用。这种方式好处是不需要生成代码,但也有一些潜在的问题和限制,比如效率低,比方式2要慢一个数量级,没有类型检查等。而且如果 LuaTable 不用了需要释放 Dispose ,不然会产生垃圾。这种方式更适合用在一些比较复杂且使用频率很低的情况。这种方式是浅拷贝的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
-- lua表数据
GlobalTable = {
Name = "Lua",
Age = 3,
TableFunc = function(name)
return "Name: "..name
end,
SubTable = {
SubName = "SubLua",
PrintInfo = function()
print(GlobalTable.SubTable.SubName)
end,
ReturnInfo = function(name)
return name
end
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 映射到LuaTable
LuaTable t = luaEnv.Global.Get<LuaTable>("GlobalTable");
//访问表元素
string name = t.Get<string>("Name");
Debug.Log(name); // Lua
//访问表方法
LuaFunction tableFunc = t.Get<LuaFunction>("TableFunc");
object[] result = tableFunc.Call("John");
Debug.Log($"TableFunc Result: {result[0]}"); // TableFunc Result: Name: John
//访问子表
LuaTable subt = t.Get<LuaTable>("SubTable");
//设置子表元素
subt.Set("SubName", "Elsa");
//访问子表方法
subt.Get<LuaFunction>("PrintInfo").Call(); // Elsa
object[] subResult = subt.Get<LuaFunction>("ReturnInfo").Call("Amma");
Debug.Log($"SubTableFunc Result: {subResult[0]}"); // SubTableFunc Result: Amma

使用建议

  • 限制全局访问:访问 Lua 全局数据的代价相对较高,尤其是对 table 和 function。建议在初始化时将要调用的 Lua function 获取一次(映射到 delegate),然后保存下来,以后直接调用该 delegate,以减小性能开销。

  • 解耦与 Lua 的关系:如果 Lua 侧的实现部分都以 delegate 和 interface 的方式提供,使用方可以完全与 XLua 解耦。可以有一个专门的模块负责 XLua 的初始化以及 delegate、interface 的映射,然后将这些 delegate 和 interface 设置到需要使用它们的地方。

  • 管理 LuaFunction 的生命周期:如果你使用 LuaFunction,要注意管理它们的生命周期。确保在不再需要时适当地进行清理,以防止内存泄漏。

  • 避免频繁的 Lua 与 C# 交互:避免在短时间内频繁进行 Lua 与 C# 的交互,因为这可能会导致性能问题。尽量将交互的操作集中在一起,减少交互的次数。

Lua 调用 C# 示例

创建和访问 C# 实例

1
2
3
4
5
6
7
8
9
10
// C# 类
public class MyCSharpClass
{
public int MyProperty { get; set; }

public void MyMethod()
{
Debug.Log(MyProperty);
}
}
1
2
3
4
5
6
-- 创建 C# 对象实例
myInstance = CS.MyCSharpClass()
-- 设置属性值
myInstance.MyProperty = 42
-- 调用方法
myInstance:MyMethod() --42

或者

1
local newGameObj = CS.UnityEngine.GameObject("Empty")

需要注意的点是:

  • lua里没有new关键字;

  • C#相关代码都放在CS下;

  • lua支持重载;

访问 C# 静态方法/属性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class MyCSharpClass
{
public int MyProperty { get; set; }
public static int MyStaticProperty { get; set; }

public void MyMethod()
{
Debug.Log(MyProperty);
}

public static void MyStaticMethod()
{
Debug.Log(MyStaticProperty);
}
}

public static class MyStaticClass
{
public static int MyProperty { get; set; }
public static void MyMethod()
{
Debug.Log(MyProperty);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
-- 获取 C# 类型信息
-- 创建C#对象实例
local myInstance = CS.MyCSharpClass()
-- 访问对象的属性和方法
myInstance.MyProperty = 42
myInstance:MyMethod() --42

-- 访问非静态类的静态属性和方法
local myClass = CS.MyCSharpClass
myClass.MyStaticProperty = 50
myClass:MyStaticMethod() --50

-- 访问静态类属性和方法
local myStaticClass = CS.MyStaticClass
myStaticClass.MyProperty = 60
myStaticClass:MyMethod() --60

lua 重载

直接通过不同参数类型进行重载,示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class MyCSharpClass
{
public static void TestFunc(int value)
{
Debug.Log("TestFunc(int): " + value);
}

public static void TestFunc(float value)
{
Debug.Log("TestFunc(float): " + value);
}

public static void TestFunc(string value)
{
Debug.Log("TestFunc(string): " + value);
}
}
1
2
3
4
-- TestFunc 被重载了,xLua 需要明确传递参数类型
CS.MyCSharpClass.TestFunc(42) -- 调用参数为整数的重载
CS.MyCSharpClass.TestFunc(1.5) -- 不会调用参数为float的重载,而是调用参数为整数的重载,并且value变成0
CS.MyCSharpClass.TestFunc("hello") -- 调用参数为字符串的重载

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

可变参数方法

1
2
3
4
5
6
7
8
public class MyCSharpClass
{
public static void VariableParamsFunc(int a, params object[] arg)
{
Debug.Log("Argument_a: " + a);
Debug.Log("Argument_var: " + string.Join(", ", arg));
}
}
1
2
-- lua端调用示例
CS.MyCSharpClass:VariableParamsFunc(5, 'hello', true, 42, 3.14)

使用Extension methods

在 C# 里定义了,lua 里就能直接使用。

注:使用 Extension methods 可以在已有的类型(types)中添加方法(Methods),而无需通过增加一种新的类型或修改已有的类型。比如说,想要给 string 类型增加一个 PrintStrLength() 方法,打印出字符串的长度。使用 Extension methods 可以不需要去修改 string 类型而实现这一方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
[LuaCallCSharp]
public class MyClass
{
}

[LuaCallCSharp]
public static class MyClassExtensions
{
public static void PrintLen(this MyClass myClass, string value)
{
Debug.Log($"length of \"{value}\" is {value.Length}");
}
}
1
2
3
-- lua端调用拓展方法
myClass = CS.MyClass()
myClass:PrintLen("Hello world!") -- length of "Hello world!" is 12

注意:

  • Extension methods 扩展方法必须定义在一个静态类中
  • Extension methods 是一种特殊的静态方法,在被扩展的类型调用时要像实例方法一样调用:"hello".PrintStrLength();
  • 扩展方法的声明:使用 this 关键字,后面跟着要操作的对象类型
  • 扩展方法的使用:使用扩展方法时需先使用 using 导入扩展方法所在的名字空间
  • C#不能扩展静态类(如 System.Convert)

泛化(模版)方法

不直接支持,可以通过 Extension methods 功能进行封装后调用。

  • 使用泛型为参数的方法

    1
    2
    3
    4
    5
    6
    7
    8
    9
    // C# 
    public class MyCSharpClass
    {
    //定义一个具有泛型为参数的方法
    public void Method(List<string> strArray)
    {
    Debug.Log(string.Join(" ", strArray));
    }
    }
    1
    2
    3
    4
    -- lua
    myClass = CS.MyCSharpClass()
    myTable = {"lua", "C#", "C++"}
    myClass:Method(myTable) -- lua C# C++
  • 调用C#中的泛型方法

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    // C#
    [LuaCallCSharp]
    public class MyClass
    {
    public void Print<T>(T value)
    {
    Debug.Log(value);
    }
    }

    [LuaCallCSharp]
    public static class MyClassExtensions
    {
    public static void LuaPrint(this MyClass myClass, int value)
    {
    myClass.Print(value);
    }

    public static void LuaPrint(this MyClass myClass, string value)
    {
    myClass.Print(value);
    }
    }
    1
    2
    3
    4
    -- lua
    myClass = CS.MyClass()
    myClass:LuaPrint(42) --42
    myClass:LuaPrint("Hello") --Hello

枚举类型

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
public enum Language
{
PHP,
Charp,
Python,
C,
Java
}

[LuaCallCSharp]
public class ChooseLanguage
{
public void GetLanguage(Language lang)
{
switch (lang)
{
case Language.PHP:
Debug.Log("选择了PHP语言");
break;
case Language.Charp:
Debug.Log("选择了Charp语言");
break;
case Language.Python:
Debug.Log("选择了Python语言");
break;
case Language.C:
Debug.Log("选择了C语言");
break;
case Language.Java:
Debug.Log("选择了Java语言");
break;
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
-- lua端访问
Language = CS.Language
choose = CS.ChooseLanguage()
choose:GetLanguage(Language.PHP)
choose:GetLanguage(Language.__CastFrom(1)) --索引从0开始
choose:GetLanguage(Language.__CastFrom('Python'))
choose:GetLanguage(3)
choose:GetLanguage('Java')
-- 选择了PHP语言
-- 选择了Charp语言
-- 选择了Python语言
-- 选择了C语言
-- 选择了Java语言

delegate使用(调用,+,-)

使用Lua代码访问 C# 委托时需要注意,访问委托类型的方式与访问静态变量的方式相同,访问(静态/非静态)委托的变量的方式与访问(静态/非静态)成员变量的方式相同。由于在 Lua 中没有 “+=” 和 “-=” 操作符,在改变委托链的时候只能使用”+“和”-“操作符。

+ 操作符:对应 C# 的 + 操作符,把两个调用串成一个调用链,右操作数可以是同类型的 C# delegate 或者是 lua 函数。

- 操作符:对应 C# 的 - 操作符,把一个 delegate 从调用链中移除。

注:delegate 属性可以用一个 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
public class DelegateExample : MonoBehaviour
{
// 定义一个简单的委托类型
public delegate void MyDelegate(string message);

// 一个委托属性
public MyDelegate myDelegateProperty;

// 一个接受委托参数的方法
public void InvokeDelegate(MyDelegate myDelegate, string message)
{
myDelegate?.Invoke(message);
}

// 一个普通成员方法
public void MemberMethod(string message)
{
Debug.Log("MemberMethod called with message: " + message);
}

// 一个静态方法
public static void StaticMethod(string message)
{
Debug.Log("StaticMethod called with message: " + message);
}
}
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
-- lua端调用
local delegateExample = CS.DelegateExample()

-- 创建 Lua 函数
local function LuaFunction(message)
print("Lua function called with message: " .. message)
end

-- 创建 C# 委托实例
local myDelegate = CS.DelegateExample.MyDelegate(LuaFunction)

delegateExample.myDelegateProperty = myDelegate

-- 调用 C# 方法,触发委托链
delegateExample:InvokeDelegate(delegateExample.myDelegateProperty, "Hello from Lua")
-- Lua function called with message: Hello from Lua

--添加一个成员方法
delegateExample.myDelegateProperty = delegateExample.myDelegateProperty + function(message)
delegateExample:MemberMethod(message)
end
delegateExample:InvokeDelegate(delegateExample.myDelegateProperty, "Hello from Lua after add MemberMethod")
-- Lua function called with message: Hello from Lua after add MemberMethod
-- MemberMethod called with message: Hello from Lua after add MemberMethod

--移除Lua方法
delegateExample.myDelegateProperty = delegateExample.myDelegateProperty - myDelegate

--添加静态方法
delegateExample.myDelegateProperty = delegateExample.myDelegateProperty + function(message)
CS.DelegateExample.StaticMethod(message)
end
delegateExample:InvokeDelegate(delegateExample.myDelegateProperty, "Hello from Lua after remove and add")
-- MemberMethod called with message: Hello from Lua after remove and add
-- StaticMethod called with message: Hello from Lua after remove and add

event

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class EventClass 
{
public delegate void TestDelegate();

public event TestDelegate Events;

public TestDelegate action1 = () =>
{
Debug.Log("触发了action1");
};

public TestDelegate action2 = () =>
{
Debug.Log("触发了action2");
};

public void TriggerEvent()
{
Events();
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
local eventClass = CS.EventClass()

local function luaFunction()
print("触发了lua function")
end

eventClass:Events("+", eventClass.action1)
eventClass:Events("+", luaFunction)
--注意在Lua中不能通过以下方式来触发事件eventClass:Events()
eventClass:TriggerEvent()
-- 触发了action1
-- 触发了lua function

eventClass:Events("-", eventClass.action1)
eventClass:Events("+", eventClass.action2)
eventClass:TriggerEvent()
-- 触发了lua function
-- 触发了action2

复杂类型

对于一个有无参构造函数的 C# 复杂类型,在 lua 侧可以直接用一个 table 来代替,该 table 对应复杂类型的 public 字段有相应字段即可,支持函数参数传递,属性赋值等,例如:

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
// C#
[LuaCallCSharp]
public class MyClass
{
// 属性
public int MyProperty { get; set; }

// 成员方法
public void MyMethod()
{
Debug.Log("MyMethod called");
}

// 静态方法
public static void StaticMethod()
{
Debug.Log("StaticMethod called");
}

// 嵌套类
public class NestedClass
{
public void NestedMethod()
{
Debug.Log("NestedMethod called");
}
}
}
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
-- lua
local MyClass = CS.MyClass
-- 创建 C# 类实例
local myObject = MyClass()
-- 将 C# 类映射到 Lua table
local myTable = {
MyProperty = myObject.MyProperty,
MyMethod = function()
myObject:MyMethod()
end,
StaticMethod = function()
CS.MyClass.StaticMethod()
end,
NestedClass = {
NestedMethod = function(self)
local nestedObject = CS.MyClass.NestedClass()
nestedObject:NestedMethod()
end
}
}

-- 使用 Lua table
print("MyProperty value:", myTable.MyProperty)
myTable.MyMethod()
myTable.StaticMethod()
myTable.NestedClass:NestedMethod()