XLua
XLua的来源以及源码
来源
xLua是由腾讯维护的一个开源项目。该项目自2016年初开始推广,已经应用于十多款腾讯自研游戏,因其良好性能、易用性、扩展性而广受好评。
源码
https://github.com/Tencent/xLua
概念
-
xLua定义:
xLua为Unity、.Net、Mono等C#环境增加了Lua脚本编程的能力。借助xLua,Lua代码可以方便地与C#相互调用,实现了C#与Lua的无缝集成。 -
模块与require:
在Lua中,模块可以视为一个程序库,通过require函数加载。require函数会返回一个由模块常量或函数组成的table,并定义一个包含该table的全局变量。这种机制使得Lua代码可以方便地组织和管理,同时也支持了热更新功能。通过自定义loader,xLua可以实现在运行时加载和替换Lua代码,从而实现热更新。 -
热更新:
热更新是指在应用程序运行时,无需重新编译或重启应用程序,即可更新应用程序的部分功能或数据。在xLua中,热更新功能是通过Lua脚本实现的。开发者可以将需要更新的功能或数据编写成Lua脚本,并在运行时通过xLua加载和执行这些脚本。由于Lua脚本的轻量级和灵活性,热更新功能可以非常高效地实现。
综上所述,xLua是一个功能强大、性能出色的开源项目,它为C#环境增加了Lua脚本编程的能力,并提供了热更新等高级功能。这些特性使得xLua在游戏开发、应用程序更新等领域具有广泛的应用前景。
XLua的官方教程解析
hello World
一个快速入门的例子,告诉你如何使用基本的XLua的对象以及输出。
代码
using XLua;
public class HelloWorld : MonoBehaviour
{
public void Start()
{
LuaEnv luaenv = new LuaEnv();
luaenv.DoString("CS.UnityEngine.Debug.Log('hello world')");
luaenv.Dispose();
}
}
解释
-
CS
在XLua中,CS是一个特殊的命名空间前缀,它代表“CSharp”的缩写,即C#。XLua是一个用于在Lua脚本中调用C#代码的库,它允许你在Unity等使用C#作为主要开发语言的平台上,通过Lua脚本来扩展或修改游戏逻辑。这种机制为游戏开发者提供了一种灵活的方式来动态地调整游戏行为,而无需重新编译C#代码。当你在XLua脚本中看到CS.UnityEngine这样的代码时,它的含义是:
CS:表示接下来的命名空间或类是在C#中定义的。
UnityEngine:这是Unity游戏引擎的核心命名空间之一,包含了大量的类和方法,用于游戏开发中的图形渲染、物理模拟、声音播放、输入处理等功能。
因此,CS.UnityEngine整体上是告诉XLua,你想要访问Unity引擎中UnityEngine命名空间下的某个类或方法。这种访问方式使得Lua脚本能够直接使用Unity引擎提供的丰富功能,从而实现更加灵活和动态的游戏开发流程。例如,如果你想要在Lua脚本中创建一个Unity的GameObject,你可以这样做:
local gameObject = CS.UnityEngine.GameObject('MyGameObject')
这里,CS.UnityEngine.GameObject引用了C#中UnityEngine命名空间下的GameObject类,并通过调用其构造函数创建了一个新的GameObject实例。
总之,CS前缀是XLua提供的一种机制,用于在Lua脚本中方便地引用和使用C#中的代码和资源。
-
LuaEnv
负责创建和管理 Lua 虚拟机(Lua State)以及 C# 和 Lua 之间的交互。XLua 是一个高性能的 Lua 脚本引擎,它允许在 C# 项目中嵌入并执行 Lua 脚本。LuaEnv 是 XLua 中的一个核心类,它负责创建和管理 Lua 虚拟机(Lua State)以及 C# 和 Lua 之间的交互。
LuaEnv 类概述
LuaEnv 类封装了 Lua 虚拟机(Lua State),并提供了一系列方法用于执行 Lua 脚本、调用 Lua 函数、访问 Lua 变量等。以下是 LuaEnv 类的一些关键特性和功能:
创建 Lua 虚拟机:LuaEnv 实例在创建时会初始化一个新的 Lua 虚拟机。
全局变量管理:可以通过 LuaEnv 访问和设置 Lua 全局变量。
执行 Lua 脚本:可以执行字符串形式的 Lua 脚本。
C# 与 Lua 交互:提供机制将 C# 对象暴露给 Lua,以及从 Lua 调用 C# 方法。
垃圾回收:管理 Lua 虚拟机中的垃圾回收,确保资源得到正确释放。
创建 LuaEnv 对象
创建 LuaEnv 对象通常是通过调用 XLua.LuaEnv.NewInstance() 方法来完成的。这个方法会返回一个 LuaEnv 实例,你可以使用这个实例来执行 Lua 脚本和管理 Lua 虚拟机。
以下是一个简单的示例,展示了如何创建 LuaEnv 对象并执行 Lua 脚本:
csharp using XLua; using System; class Program { static void Main(string[] args) { // 创建 LuaEnv 对象 LuaEnv luaEnv = new LuaEnv(); // 执行 Lua 脚本 luaEnv.DoString(@" print('Hello from Lua!') "); // 释放 LuaEnv 对象 luaEnv.Dispose(); } }
在这个示例中,我们首先创建了一个 LuaEnv 对象,然后使用 DoString 方法执行了一段简单的 Lua 脚本,该脚本在控制台打印了一条消息。最后,我们调用了 Dispose 方法来释放 LuaEnv 对象占用的资源。
LuaEnv 对象的特点
-
生命周期管理:LuaEnv 对象应该在使用完毕后被正确释放,以避免内存泄漏。这通常是通过调用 Dispose 方法来实现的。
-
线程安全:LuaEnv 对象不是线程安全的,因此不应该在多个线程之间共享同一个 LuaEnv 实例。
-
C# 与 Lua 交互:通过 LuaEnv 对象,你可以将 C# 对象传递给 Lua 脚本,并在 Lua 脚本中调用这些对象的方法。同样,你也可以从 Lua 脚本中调用 C# 方法。
注意事项
-
资源管理:确保在不再需要 LuaEnv 对象时调用 Dispose 方法,以释放资源。
-
线程安全:避免在多个线程之间共享 LuaEnv 实例。
-
异常处理:在执行 Lua 脚本时,可能会遇到各种异常(如语法错误、运行时错误等),因此应该做好异常处理。
总之,LuaEnv 是 XLua 中用于管理 Lua 虚拟机和实现 C# 与 Lua 交互的核心类。通过创建和使用 LuaEnv 对象,你可以在 C# 项目中嵌入和执行 Lua 脚本,并实现两者之间的无缝交互。
-
-
DoString()
DoString 方法允许你直接运行一段 Lua 字符串代码。下面是对 LuaEnv.DoString 方法的详细解析。DoString 方法的基本签名如下:
public void DoString(string chunk, string chunkName = "chunk", LuaTable env = null);
参数:
- chunk:要执行的 Lua 代码字符串。
- chunkName(可选):用于错误报告和调试的代码块名称,默认值为 “chunk”。chunkName 参数对于调试非常有用,因为它可以帮助你定位出错代码的位置。当你遇到 Lua 代码执行错误时,XLua 会抛出异常,并且异常信息中会包含 chunkName 和 Lua 堆栈跟踪,帮助你快速定位问题。
- env(可选):指定代码执行时的环境表(LuaTable),默认为 null,表示使用全局环境。
// 创建一个新的 LuaEnv 实例 using (LuaEnv luaEnv = new LuaEnv()) { // 使用 DoString 方法执行 Lua 代码 luaEnv.DoString(@" print('Hello from Lua!') function add(a, b) return a + b end "); // 获取并执行 Lua 中的函数 LuaFunction addFunc = luaEnv.Global.GetInPath<LuaFunction>("add"); int result = addFunc.Call<int>(3, 4); Console.WriteLine("Result of add(3, 4): " + result); }
- 创建了一个 LuaEnv 实例。
- 使用 DoString 方法执行了一段 Lua 代码,这段代码定义了一个名为 add 的函数。
- 通过 luaEnv.Global.GetInPath(“add”) 获取了 add 函数,并调用它来计算 3 + 4。
注意事项
- 资源管理:LuaEnv 是一个实现了 IDisposable 接口的类,因此在使用完 LuaEnv 后,应该调用 Dispose 方法来释放资源。在上面的示例中,我们使用了 using 语句来自动管理资源。
- 错误处理:DoString 方法在 Lua 代码执行出错时会抛出异常。你可以使用 try-catch 块来捕获和处理这些异常。
- 性能:虽然 DoString 方法很方便,但如果你需要频繁执行相同的 Lua 代码,考虑将代码编译成 Lua 函数并缓存起来,这样可以提高性能。
U3DScripting
展示怎么用lua来写MonoBehaviour。并且示例搭建一个读取lua代码文件的环境。
代码
/*
* Tencent is pleased to support the open source community by making xLua available.
* Copyright (C) 2016 THL A29 Limited, a Tencent company. All rights reserved.
* Licensed under the MIT License (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at
* http://opensource.org/licenses/MIT
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
*/
using UnityEngine;
using System.Collections;
using System.Collections.Generic;
using XLua;
using System;
namespace XLuaTest
{
[System.Serializable]
public class Injection
{
public string name;
public GameObject value;
}
[LuaCallCSharp]
public class LuaBehaviour : MonoBehaviour
{
public TextAsset luaScript;
public Injection[] injections;
internal static LuaEnv luaEnv = new LuaEnv(); //all lua behaviour shared one luaenv only!
internal static float lastGCTime = 0;
internal const float GCInterval = 1;//1 second
private Action luaStart;
private Action luaUpdate;
private Action luaOnDestroy;
private LuaTable scriptScopeTable;
void Awake()
{
// 为每个脚本设置一个独立的脚本域,可一定程度上防止脚本间全局变量、函数冲突
scriptScopeTable = luaEnv.NewTable();
// 设置其元表的 __index, 使其能够访问全局变量
using (LuaTable meta = luaEnv.NewTable())
{
meta.Set("__index", luaEnv.Global);
scriptScopeTable.SetMetaTable(meta);
}
// 将所需值注入到 Lua 脚本域中
scriptScopeTable.Set("self", this);
foreach (var injection in injections)
{
scriptScopeTable.Set(injection.name, injection.value);
}
// 如果你希望在脚本内能够设置全局变量, 也可以直接将全局脚本域注入到当前脚本的脚本域中
// 这样, 你就可以在 Lua 脚本中通过 Global.XXX 来访问全局变量
// scriptScopeTable.Set("Global", luaEnv.Global);
// 执行脚本
luaEnv.DoString(luaScript.text, luaScript.name, scriptScopeTable);
// 从 Lua 脚本域中获取定义的函数
Action luaAwake = scriptScopeTable.Get<Action>("awake");
scriptScopeTable.Get("start", out luaStart);
scriptScopeTable.Get("update", out luaUpdate);
scriptScopeTable.Get("ondestroy", out luaOnDestroy);
if (luaAwake != null)
{
luaAwake();
}
}
// Use this for initialization
void Start()
{
if (luaStart != null)
{
luaStart();
}
}
// Update is called once per frame
void Update()
{
if (luaUpdate != null)
{
luaUpdate();
}
if (Time.time - LuaBehaviour.lastGCTime > GCInterval)
{
luaEnv.Tick();
LuaBehaviour.lastGCTime = Time.time;
}
}
void OnDestroy()
{
if (luaOnDestroy != null)
{
luaOnDestroy();
}
scriptScopeTable.Dispose();
luaOnDestroy = null;
luaUpdate = null;
luaStart = null;
injections = null;
}
}
}
local speed = 10
local lightCpnt = nil
function start()
print("lua start...")
print("injected object", lightObject)
lightCpnt= lightObject:GetComponent(typeof(CS.UnityEngine.Light))
end
function update()
local r = CS.UnityEngine.Vector3.up * CS.UnityEngine.Time.deltaTime * speed
self.transform:Rotate(r)
lightCpnt.color = CS.UnityEngine.Color(CS.UnityEngine.Mathf.Sin(CS.UnityEngine.Time.time) / 2 + 0.5, 0, 0, 1)
end
function ondestroy()
print("lua destroy")
end
解释
这段C#脚本展示了如何搭建一个lua解释器的monobehaviour组件。
-
LuaTable
在XLua中,LuaTable是一个非常重要的类,代表了Lua中的Table表,这是lua中唯一的数据结构,用于表示数组,字典,对象等等。Lua 表是通过键值对来存储数据的,其中键和值都可以是任意类型的 Lua 值(nil 除外,nil 不能作为键)。
在 XLua 中,LuaTable 类封装了 Lua 表的行为,使得 C# 代码可以方便地操作 Lua 表。LuaTable 提供了多种方法来访问和修改表中的元素,包括通过索引访问数组元素,通过键访问字典元素,以及设置和获取字段值等。
LuaTable 的使用
- 创建 LuaTable
在 XLua 中,你可以通过
LuaEnv
的NewTable
方法来创建一个新的 LuaTable:LuaEnv luaEnv = new LuaEnv(); LuaTable table = luaEnv.NewTable();
- 访问和修改 LuaTable
- 通过索引访问数组元素:
table.SetInPath<int>(1, 10); // 设置 table[1] = 10 int value = table.GetInPath<int>(1); // 获取 table[1] 的值
- 通过键访问字典元素:
table.Set<string, int>("key", 100); // 设置 table["key"] = 100 int dictValue = table.Get<string, int>("key"); // 获取 table["key"] 的值
- 设置和获取字段值:
table["fieldName"] = "value"; // 设置字段 fieldName 的值为 "value" string fieldValue = table["fieldName"].AsString(); // 获取字段 fieldName 的值
转换LuaTable和C#对象
XLua 提供了将 LuaTable 转换为 C# 对象,以及将 C# 对象转换为 LuaTable 的功能。这通常通过自定义的转换器或标记接口来实现。
// 假设有一个 C# 类 public class MyClass { public int Value; } // 将 LuaTable 转换为 MyClass 对象 LuaTable luaTable = ...; // 假设已经有一个 LuaTable 实例 MyClass myObject = luaTable.CastTo<MyClass>(); // 将 MyClass 对象转换为 LuaTable LuaTable newTable = luaEnv.NewTable(); newTable.Set("Value", 123); MyClass anotherObject = newTable.CastTo<MyClass>();
注意
- 在使用 LuaTable 时,确保 LuaEnv 实例是有效的,并且在不再需要时正确关闭它,以避免内存泄漏。
- 访问 LuaTable 中的元素时,要注意类型匹配,否则可能会引发异常。
- 当 LuaTable 表示一个 Lua 函数时,调用该函数时传入的参数和期望的返回类型需要与 Lua 脚本中的定义相匹配。
-
meta.Set("__index", luaEnv.Global);
元表以及元方法这里简单复习一下,就明白这句话上下文的含义。
- 元表:每个 Lua 表都可以有一个关联的元表。当 Lua 试图对一个表执行某些操作(如访问不存在的字段、调用表作为函数等)时,它会检查该表的元表是否定义了相应的元方法(metamethod)来处理这些操作。
- __index 元方法:__index 元方法用于处理对表中不存在的键的访问。当 Lua 尝试访问一个表中不存在的键时,如果表的元表中定义了 __index 元方法,Lua 就会调用这个元方法,而不是简单地返回 nil。
解析
- 创建元表:首先,通过
luaEnv.NewTable()
创建一个新的 Lua 表 meta,这个表将用作元表。 - 设置
__index
元方法:然后,使用meta.Set("__index", luaEnv.Global);
将__index
元方法设置为luaEnv.Global
。luaEnv.Global
代表 Lua 的全局环境表,它包含了所有全局变量和函数。 - 应用元表:最后,通过
scriptScopeTable.SetMetaTable(meta);
将这个元表 meta 应用到 scriptScopeTable 上。
含义
- 当 scriptScopeTable 被访问一个不存在的键时,Lua 会查找 scriptScopeTable 的元表 meta 中的 __index 元方法。
- 由于 __index 元方法被设置为 luaEnv.Global,Lua 会尝试在全局环境中查找该键。
- 这意味着,如果 scriptScopeTable 中没有某个键,Lua 会在全局环境中查找这个键,如果找到了,就返回它的值。
注意
这种机制允许你创建一个具有“回退”行为的表,即当表中缺少某个键时,可以自动从全局环境中获取该键的值。这在组织 Lua 代码和脚本时非常有用,特别是当你想要将一组变量或函数限制在一个局部作用域内,但仍希望它们能够访问全局环境中的某些内容时。
然而,需要注意的是,过度使用或不当使用元表和 __index 元方法可能会导致代码难以理解和维护,因为它们引入了额外的间接层。因此,在使用这些特性时应该谨慎考虑其必要性和潜在的影响。
-
scriptScopeTable.Set("self", this);
在XLua中,scriptScopeTable.Set(“self”, this); 这行代码的作用是在XLua的脚本作用域表中设置一个名为 “self” 的键,并将其值设置为当前C#对象的实例(即 this 指向的对象)。这允许在XLua脚本中通过 self 关键字访问和操作C#代码中的对象。下面是对这行代码的详细解释:
- scriptScopeTable 是一个在XLua中用于存储脚本作用域变量的表(或字典)。在XLua中,每个脚本执行时都有一个与之关联的作用域,这个作用域中包含了脚本可以访问的所有变量和函数。
- 通过操作这个作用域表,C#代码可以向Lua脚本传递数据,或者从Lua脚本中接收数据。
Set方法
- Set 方法是XLua提供的一个用于向作用域表中添加或更新键值对的方法。
- 它的第一个参数是键(在这个例子中是字符串 “self”),第二个参数是与该键关联的值(在这个例子中是 this,即当前C#对象的实例)。
使用
- 假设你有一个C#对象,它包含了一些需要在Lua脚本中使用的数据或方法。
- 通过 scriptScopeTable.Set(“self”, this);,你可以将这个C#对象传递给Lua脚本,并在脚本中通过 self 访问它。
- 这使得Lua脚本能够调用C#对象的方法、访问其属性,甚至修改它们。
简单示例使用C#对象及方法
假设你有一个C#类 MyClass,它有一个方法 PrintMessage:
public class MyClass
{
public void PrintMessage(string message)
{
Debug.Log(message);
}
}
你可以在C#代码中创建一个 MyClass 的实例,并通过XLua将其传递给Lua脚本:
MyClass myObject = new MyClass();
luaEnv.Global.Set("scriptScopeTable", new LuaTable());
luaEnv.Global.Get<LuaTable>("scriptScopeTable").Set("self", myObject);
luaEnv.DoString(@"self:PrintMessage('Hello from Lua!')
");
在这个例子中,Lua脚本通过 self 访问了C#对象 myObject,并调用了它的 PrintMessage
lua调用方法的区别
在Lua中,冒号(:
)和点(.
)都用于访问对象的成员(属性或方法),但它们之间有一个重要的区别:冒号调用会自动将对象本身作为第一个参数传递给方法,而点调用则不会。这个特性与许多其他面向对象编程语言中的方法调用类似,特别是在那些方法中第一个参数通常是对象自身的语言中。
冒号调用(:
)
当你使用冒号调用一个方法时,Lua会自动在方法调用前插入对象本身作为第一个参数。这通常用于对象的方法,其中第一个参数是隐式的self
,代表调用该方法的对象实例。
例如:
self:PrintMessage('Hello from Lua!')
这实际上是Lua语法糖,它会被解释为:
PrintMessage(self, 'Hello from Lua!')
但这里有个前提,即PrintMessage
方法是在与self
相关联的表的元表中,或者直接在该表中定义的,并且它期望第一个参数是调用它的对象。然而,在XLua的上下文中,当你通过scriptScopeTable.Set("self", this);
将C#对象设置为Lua中的self
时,你实际上是在期望Lua能够调用C#对象的方法。XLua处理这种调用,使得当你使用冒号语法时,它能够正确地将self
作为第一个参数传递给C#方法。
点调用(.
)
点调用则不会自动插入对象本身作为第一个参数。它仅仅是从表中检索一个值,这个值可以是一个函数、一个数字、一个字符串等。如果你尝试使用点调用来调用一个期望有self
参数的方法,你会得到一个错误,除非你手动传递self
作为第一个参数。
例如:
self.PrintMessage('Hello from Lua!') -- 这会失败,除非PrintMessage是一个不接受self作为第一个参数的函数
在XLua中,如果你尝试这样调用C#对象的方法,它不会工作,因为C#方法期望有一个this
指针(在Lua中对应self
),而点调用不会提供这个指针。
结论
因此,在XLua中使用self:PrintMessage()
而不是self.PrintMessage()
的原因是,冒号调用会自动将self
作为第一个参数传递给方法,这是C#方法所期望的。而点调用则不会,所以它不适用于调用期望有this
(或self
)参数的C#方法。
注意全局变量的使用区别
//第一种方法
scriptScopeTable.Set("Global", luaEnv.Global);
//第二种方法
using (LuaTable meta = luaEnv.NewTable())
{
meta.Set("__index", luaEnv.Global);
scriptScopeTable.SetMetaTable(meta);
}
这两段代码在 Lua 集成环境中的含义和效果有显著的区别。让我们逐一分析它们:
第一段代码做了以下事情:
- 设置一个键值对:在 scriptScopeTable 这个 Lua 表中,它创建或更新了一个键为 “Global” 的条目。
- 值是全局环境:这个条目的值被设置为 luaEnv.Global,即 Lua 的全局环境表。
效果:
在 scriptScopeTable 的作用域内,你可以通过 scriptScopeTable[“Global”] 或 scriptScopeTable.Get(“Global”)(取决于你使用的 Lua 绑定库的接口)来访问 Lua 的全局环境。
这并不改变 scriptScopeTable 的元表(metatable),也不会影响 scriptScopeTable 中其他键的查找行为。
第二段代码做了以下事情:
- 创建一个新的元表:通过 luaEnv.NewTable() 创建了一个新的 Lua 表 meta。
- 设置 __index 元方法:在 meta 表中,它设置了一个键为 “__index” 的条目,其值为 luaEnv.Global。
- 应用元表:将 meta 表设置为 scriptScopeTable 的元表。
效果:
当 scriptScopeTable 被访问一个不存在的键时,Lua 会查找 scriptScopeTable 的元表 meta。
在 meta 中,它找到了 “__index” 元方法,其值为 luaEnv.Global。
因此,Lua 会在 luaEnv.Global(即全局环境表)中查找该键。
如果在全局环境表中找到了该键,Lua 会返回其值;否则,返回 nil。
关键区别:
- 访问方式:
第一段代码通过直接在 scriptScopeTable 中设置一个键来访问全局环境,而第二段代码通过修改 scriptScopeTable 的元表来实现对全局环境的间接访问。 - 查找行为:
在第一段代码中,访问全局环境是显式的,你需要知道键 “Global” 的存在。在第二段代码中,访问全局环境是隐式的,当你尝试访问 scriptScopeTable 中不存在的键时,它会自动回退到全局环境。 - 性能考虑:
虽然这种差异在大多数情况下可能不会对性能产生显著影响,但在某些极端情况下(例如,大量不存在的键被频繁访问),使用元表可能会引入一些额外的查找开销。
总的来说,这两段代码提供了不同的机制来在 Lua 集成环境中访问全局环境,选择哪种机制取决于你的具体需求和设计考虑。