找回密码
 立即注册
查看: 393|回复: 0

浅探 ToLua 框架

[复制链接]
发表于 2022-3-1 17:20 | 显示全部楼层 |阅读模式
简单原理

ToLua 框架主要是通过静态绑定来实现 C# 与 Lua 之间的交互的,基本原理是通过建立一个 Lua 虚拟机来映射 C# 脚本,然后再通过这个虚拟机来运行 Lua 脚本,Lua 脚本在运行时可以通过虚拟机反过来调用 C# 脚本里注册过的物体,虚拟机变相地实现了一个解释器的功能,这种方式的优势在于比起使用反射的 uLua 来说效率更高。
ToLua 框架下可以将实现分成三大部分:普通的 Unity+C# 部分、ToLua 虚拟机部分和 Lua 脚本部分,结构见下图:


下载和安装

Github:GitHub - topameng/tolua: The fastest unity lua binding solution 进入以后点击下载 zip,完成后解压到自己需要的目录,再用 Unity 打开即可。
第一次打开工程时会提示是否需要自动生成注册文件,新手可以选择直接生成,若选择了取消,也可以在编辑器lua 菜单中手动注册。
C# 调用 Lua 代码

创建虚拟机 LuaState state = new LuaState() 和启动虚拟机 state.Start() 之后,就可以调用 Lua 代码。
执行一段 Lua 代码


  • state.DoString(string chunk, string chunkName = "LuaState.cs"):比较少用这种方式加载代码,无法避免代码重复加载覆盖等,需调用者自己保证。第二个参数用于调试信息,或者 error 消息,用于提示出错代码所在文件名称。
  • state.DoFile(string fileName):加载一个 lua 文件,fileName 需要扩展名,可反复执行,后面的变量会覆盖之前的 DoFile 加载的变量。
  • state.Require(string fileName):同 lua require(modname)。
使用 DoFile 和 Require 方法时,要用 state.AddSearchPath(string fullPath) 手动给目标文件添加一个文件搜索位置。
示例:
LuaState state = new LuaState();
state.Start();
string hello =
    @"               
        print('hello tolua#')                                 
    ";
state.DoString(hello, "HelloWorld.cs");

//如果移动了ToLua目录,需要自己手动,这里只是例子就不做配置了
string fullPath = Application.dataPath + "/ToLua/Examples/02_ScriptsFromFile";
state.AddSearchPath(fullPath);  
state.DoFile("ScriptsFromFile.lua");
state.Require("ScriptsFromFile");

// 垃圾回收, 对于被自动 gc 的 LuaFunction, LuaTable, 以及委托减掉的 LuaFunction, 延迟删除的 Object 之类
// 等等需要延迟处理的回收, 都在这里自动执行
state.Collect();
// 检查是否堆栈是否平衡,一般放于 update 中,c# 中任何使用 lua 堆栈操作,
// 都需要调用者自己平衡堆栈(参考 LuaFunction 以及 LuaTable 代码),
// 当 CheckTop 出现警告时其实早已经离开了堆栈操作范围,这时需自行 review 代码
state.CheckTop();
// 释放LuaState 以及其资源
state.Dispose();
state= null;
调用 Lua 变量/函数

state[string]:通过 LuaState [string] 的形式就可以直接获取到,也可以通过这个表达式来直接赋值,取到的是 object 类型。
ToLua table 方法

这里完全可以将 table 看一个 LuaState 来进行操作,ToLua 对 table 的数据结构进行了解析,实现了非常多的方法:

  • state.GetTable(string):从 state 中获取一个 table, 可以串式访问比如 state.GetTable("varTable.map.name") 等于 varTable->map->name(table["map.name"] 不支持串式访问,"map.name" 只是一个key,不是table->map->name)。
  • table.GetMetaTable():可以获取当前 table 的 metatable。
  • table.ToArray():获取表中的数组部分。
  • table.GetTable(key):获取 table[key] 值,类似于 lua_gettable。
  • table.SetTable(key, value):等价于 table[k] = v,类似于lua_settable。
  • table.RawGet(key):获取 table[key]值,类似于 lua_rawget。
  • table.RawSet(key, value):等价于 table[k] = v,类似于lua_rawset。
示例:
private string script =
    @"
        print('Objs2Spawn is: '..Objs2Spawn)
        var2read = 42
        varTable = {1,2,3,4,5}
        varTable.default = 1
        varTable.map = {}
        varTable.map.name = 'map'
        
        meta = {name = 'meta'}
        setmetatable(varTable, meta)
        
        function TestFunc(strs)
            print('get func by variable')
        end
    ";

void Start ()
{
    LuaState state = new LuaState();
    state.Start();
    state["Objs2Spawn"] = 5;
    state.DoString(script);

    //通过LuaState访问
    Debugger.Log("Read var from lua: {0}", state["var2read"]);
    Debugger.Log("Read table var from lua: {0}", state["varTable.default"]);  //LuaState 拆串式table

    //cache成LuaTable进行访问
    LuaTable table = state.GetTable("varTable");
    Debugger.Log("Read varTable from lua, default: {0} name: {1}", table["default"], table["map.name"]);
    table["map.name"] = "new";  //table 字符串只能是key
    Debugger.Log("Modify varTable name: {0}", table["map.name"]);

    table.AddTable("newmap");
    LuaTable table1 = (LuaTable)table["newmap"];
    table1["name"] = "table1";
    Debugger.Log("varTable.newmap name: {0}", table1["name"]);
    table1.Dispose();

    table1 = table.GetMetaTable();
    if (table1 != null)
    {
        Debugger.Log("varTable metatable name: {0}", table1["name"]);
    }

    object[] list = table.ToArray();
    for (int i = 0; i < list.Length; i++)
    {
        Debugger.Log("varTable[{0}], is {1}", i, list);
    }

    table.Dispose();                        
    state.CheckTop();
    state.Dispose();
}
调用 Lua coroutine

待续。
调用 Lua 的函数


  • state.Invoke:临时调用一个 lua function 并返回一个值,这个操作并不缓存 lua function,适合频率非常低的函数调用,可带参数。
  • state.Call:看起来和 Invoke 类似,只是无返回值。
  • state.GetFunction(string): 获取并缓存一个 lua 函数,string 支持串式操作。
  • LuaFunction.Call:不需要返回值的函数调用操作。
  • LuaFunction.Invoke:有一个返回值的函数调用操作。
  • LuaFunction.BeginPCall():开始函数调用。
  • LuaFunction.Push:压入函数调用需要的参数。
  • LuaFunction.PCall():调用 lua 函数。
  • LuaFunction.CheckNumber():提取函数返回值, 并检查返回值为 lua number 类型。
  • LuaFunction.EndPCall():结束 lua 函数调用,清楚函数调用造成的堆栈变化。
  • luaFunc.ToDelegate:创建一个委托,后续直接调用委托即可。
  • LuaFunction.Dispose():释放 LuaFunction,递减引用计数,如果引用计数为 0,则从 _R 表删除该函数。
示例:
private string script =
@"  function luaFunc(num)                        
        return num + 1
    end
";

void Start ()
{
    LuaState state = new LuaState();
    state.Start();
    DelegateFactory.Init();
    state.DoString(script);

    state.Call<int>("luaFunc", 123456, false);
    Debugger.Log("state.Call");

    int num = state.Invoke<int, int>("luaFunc", 123456, false);
    Debugger.Log("state.Invoke: {0}", num);

    LuaFunction luaFunc = state.GetFunction("luaFunc");
    if (luaFunc != null)
    {
        luaFunc.Call<int>(123456);
        Debugger.Log("LuaFunction.Call");

        num = luaFunc.Invoke<int, int>(123456);
        Debugger.Log("LuaFunction.Invoke: {0}", num);

        luaFunc.BeginPCall();               
        luaFunc.Push(123456);
        luaFunc.PCall();
        num = (int)luaFunc.CheckNumber();                    
        luaFunc.EndPCall();
        Debugger.Log("LuaFunction.PCall: {0}", num);
        
        Func<int, int> func = luaFunc.ToDelegate<Func<int, int>>();
        num = func(123456);
        Debugger.Log("LuaFunction.ToDelegate: {0}", num);
    }
            
    state.CheckTop();
    luaFunc.Dispose();
    luaFunc = null;
    state.Dispose();
    state = null;
}
Lua 调用 C# 方法/变量

从 C# 或者其他语言中来调用 Lua 都不算太困难,只需要提前约定特定方法名然后载入脚本即可,但 C# 是需要提前编译的,怎么通过一段解释器来调用 C# 中的对象就是主要的难点了,ToLua实现的就是这两方面的功能。
大多数人会想到的最直接的实现思路大概都是通过反射来实现,uLua 也是通过反射来实现的,但是反射的效率非常低,虽然确实可以实现,但问题还是非常明显。
ToLua 是通过方法名绑定的方式来实现这个映射的,首先构造一个 Lua 虚拟机,在虚拟机启动后对所需的方法进行绑定,在虚拟机运行时可以在 Lua 中调用特定方法,虚拟机变相地实现了一个解释器的功能,在 Lua 调用特定方法和对象时,虚拟机会在已绑定的方法中找到对应的 C# 方法和对象进行操作,并且 ToLua 已经自动实现了一些绑定的方法。
LuaBinder.Bind()

LuaBinder 由 GenLuaBinder() 自动生成,主要分为以下几个部分:

  • LuaState.BeginModule/EndModule():用于绑定命名空间,可以逐层嵌套。
  • wrap.Register():注册相应的 wrap 文件,每个 wrap 文件都是对一个 c# 类的包装,实现了将变量/方法绑定进入虚拟机中。在 lua 中,通过对 wrap 类中的函数调用,间接的对 c# 实例进行操作。
  • LuaState.BeginPreLoad/AddPreLoad/EndPreLoad():预加载相关(未深入了解)。
wrap 文件

参考 & 引用

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

×
懒得打字嘛,点击右侧快捷回复 【右侧内容,后台自定义】
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

小黑屋|手机版|Unity开发者联盟 ( 粤ICP备20003399号 )

GMT+8, 2025-5-11 05:08 , Processed in 0.136108 second(s), 26 queries .

Powered by Discuz! X3.5 Licensed

© 2001-2025 Discuz! Team.

快速回复 返回顶部 返回列表