19.toLua基础知识总结

  1. 19.总结
    1. 19.2 核心要点速览
      1. toLua 在本系列中的定位
      2. 前置知识(默认已掌握)
      3. xLua 与 toLua 速查(复习用)
      4. 学习目标怎么排
      5. 与 xLua 对照时的注意点
      6. 环境与工程接入
      7. C# 侧驱动 Lua:解析器、加载与管理器
      8. 全局量、函数与表互操作
      9. toLua 协程(相对标准 coroutine)
      10. Lua 侧使用 C#:命名、对象与集合
    2. 19.3 面试题精选
      1. 基础题
        1. 1. 把 toLua 官方工程并进自建 Unity 工程时,仓库根目录至少要拷贝哪些东西?
          1. 题目
          2. 深入解析
          3. 答题示例
          4. 参考文章
        2. 2. 继承 MonoBehaviour 的类,能在 Lua 里像普通类一样用 类型() 直接实例化吗?
          1. 题目
          2. 深入解析
          3. 答题示例
          4. 参考文章
      2. 进阶题
        1. 1. 改完 customTypeList / customDelegateList 之后,最省事的编辑器操作是什么?不做的后果是什么?
          1. 题目
          2. 深入解析
          3. 答题示例
          4. 参考文章
        2. 2. 自定义 LuaFileUtils 时,为什么要先 new MyLoader() 再 new LuaState()?ReadFile 里通常谁先谁后?
          1. 题目
          2. 深入解析
          3. 答题示例
          4. 参考文章
      3. 深度题
        1. 1. link.xml 在 toLua 工程里主要防什么?和「Lua 调 C#」有什么关系?
          1. 题目
          2. 深入解析
          3. 答题示例
          4. 参考文章
        2. 2. Lua 里已经在用 coroutine.start / coroutine.wait,为什么工程里还要 LuaLooper 和 LuaCoroutine.Register?
          1. 题目
          2. 深入解析
          3. 答题示例
          4. 参考文章
        3. 3. toLua 里 C# 的 event 在 Lua 侧订阅,和 公开 delegate 字段 差在哪?清空怎么做?
          1. 题目
          2. 深入解析
          3. 答题示例
          4. 参考文章

19.总结


19.2 核心要点速览

toLua 在本系列中的定位

  • toLua:Unity 里常用的 C# / Lua 互操作与热更新方案之一;本目录「基础知识」主攻双向调用与日常用法
  • 主线就一句:用 toLua 把 Lua 接进 Unity,并始终清楚 C# 与 Lua 谁主控、谁调谁

前置知识(默认已掌握)

AssetBundleLua 语法xLua 热更xLua 背包实践。后文谈路径、资源与和 xLua 对照时,不再从零讲 AB 或 Lua 语法。

xLua 与 toLua 速查(复习用)

对比点 xLua 常见写法 toLua 常见写法
类型命名 多前缀 CS.命名空间.类型 命名空间.类型CS
C# 读 Lua 表 可映射到类/接口等 多用 LuaTableToArrayToDictTable
Lua 用 C# 枚举 工具较多 tostring / :ToInt() / IntToEnum无顺带手的字符串→枚举
Lua 里延迟/协程 依项目封装 coroutine.start/wait 依赖 LuaLooperUnity StartCoroutine 另需 LuaCoroutine.Register

学习目标怎么排

主线 要落到什么程度
环境 能把 toLua 框架正确接进工程,保证解析器、示例脚本能按课程路径跑起来
C# 调用 Lua 从托管侧起 Lua、读写变量、调用函数、把 Lua 表映射到 C# 容器,以及 toLua 提供的协程衔接
Lua 调用 C# 在脚本里当普通 API 用 C# 类型:类、枚举、集合、扩展方法、ref/out、重载、委托/事件、Lua 侧协程等
热更新 概述里把热更定为概念了解,本系列基础知识不深讲一套可上线热更工程;先分清 toLua 在链路中的位置即可

与 xLua 对照时的注意点

  • 先回忆 xLua 写法再落到 toLua:相同点省时间,不同点才是踩坑区
  • 前缀、Coroutine 两套、C# 读表方式、枚举工具等不要混用;不确定时先扫上一节对照表再写。

环境与工程接入

  • 来源:从 topameng/tolua 拉取工程(ZIP 解压后根目录即带完整 Unity 结构);浏览器里也可用 Download ZIPtolua-master
  • 顶层要带的东西:除了 Assets 整树,还要拷 **Assets 同级目录里的 LuajitLuajit64**(32/64 位 LuaJIT 相关资源),否则 Native 侧对不齐。
  • Assets 里一眼要认识的夹:**Editor(菜单、生成器)、Lua(运行时脚本与基类)、Plugins(各平台 native)、Source(框架 C#,且默认在下面建 Generate 吐 wrap)、ToLua**;根下常有 **link.xml**,给裁剪/IL2CPP 时「别让只给 Lua 用的 C# 被条带优化剃掉」留名单。
  • 生成菜单(顶栏 Lua):分项有 Gen Lua Wrap Files / Gen Lua Delegates / Gen LuaBinder File / Gen LuaWrap + Binder,一键对应 Generate All;重做前可 Clear wrap files 清生成物。同一菜单里还有拷 Lua 到 Resources/Persistent、打 LuaJIT bundle、BaseType、Injection、Profiler 等。日常最常记一条线:CustomSettings → Generate → 看 Source/Generate 是否更新
  • saveDir:默认 Application.dataPath + "/Source/Generate/",下面应有大量 *Wrap.csLuaBinder.csDelegateFactory.cs 等生成物;若目录长期空或明显过期,多半是没生成或生成失败
  • CustomSettingscustomTypeList 管要导出给 Lua 的 类型,**customDelegateList** 管 委托 桥接;列表改过以后必须再走生成,否则 Lua 里根本看不到新类型或委托签名还是旧的。
  • 与热更资源链配套:通过 Package Manager 安装 Asset Bundle Browsercom.unity.assetbundlebrowser,菜单 Window → Asset Bundle Browser,要求 Unity 5.6+),用来配 AB、验包、触发构建——和前面 AssetBundle 前情、后面 Lua 进包流程能接上。
  • 基础框架BaseFrameworkBaseSingletonCSharpBaseSingletonMonoBehaviour 一类单例基类,后面写 Lua 管理器/C# 入口时常从这里延展开。

C# 侧驱动 Lua:解析器、加载与管理器

  • **LuaState**:new 后必须 Start();工程上一个管理器、一套虚拟机,避免多实例带来重复加载与状态分裂。
  • **DoString / DoFile / Require**(默认解析 Assets/Lua 树):
入口 行为要点
DoString 执行字符串;可选第二参数作 chunk 名,报错栈好对回 C#
DoFile 每调必跑文件顶层.lua 可有可无
Require 模块缓存,第二次同名不重复跑顶层不要.lua
  • 收尾:**CheckTopDispose → 引用置 null**。
  • 路径习惯:**AddSearchPath** 只适合编辑器期补磁盘根;**Require/DoFile** 用的逻辑路径仍须在 Lua 子树或已加路径下能解析。真机业务脚本多在 AB / Resources,别只认 Application.dataPath 下那份编辑器路径。
  • 自定义 ReadFile:继承 LuaFileUtilsnew MyLoader(),再 new LuaState()。实现上常:.lua先 AB(路径按 /文件名进包)→ **buffer 仍空再 Resources.Load("Lua/" + fileName)**;菜单拷表会用 .bytes 绕过 **Resources.lua**。
  • LuaManager.Init 推荐顺序(与教程一致,可对照排查缺哪一步):
顺序 动作 备忘
0 (可选)new MyCustomLoader() 发版或真机拉脚本时再开
1 new LuaState + Start()
2 LuaLooperloop.luaState = luaState AddComponent 具体 API 以工程为准
3 LuaCoroutine.Register(luaState, this) 与 Lua 管理器同物体最省事
4 DelegateFactory.Init() 否则 ToDelegate 不稳
5 LuaBinder.Bind(luaState) Lua 才能用已导出的 Unity/自定义类型

业务再封装 DoString / Require / Dispose 即可。

全局量、函数与表互操作

  • 全局变量:**luaState["name"]** 读写;先 读成 C# 值类型再改本地变量 不会回写 Lua,要动 Lua 全局须 luaState["k"] = vlocal 不在全局环境,C# 按名取多为 **null**。
  • Lua 函数(C# 侧最短记忆)
var f = luaState.GetFunction("foo");
f.Call();                 // 无返回值
f.Invoke<int, int>(1);    // 最后一泛型为返回值,前序为实参类型
f.Dispose();
  • 手写栈:**BeginPCallPushPCallCheckNumber 等 → EndPCall**,与 Invoke 二选一即可,别混在同一轮里漏配对。
  • ToDelegate<T>:先把 DelegateFactory.Init 跑通(在 Init,不是单靠“构造函数”);多返回值要声明匹配签名的自定义 delegate,写进 CustomSettings.customDelegateList重新生成。变长参常见 params object[] 的 delegate,再 **ToDelegate**。
  • **LuaTable**:toLua 不像 xLua 那样把表映射成 C# 类/接口,而是 GetTable / 中括号强转拿到表句柄;字段仍用 table["k"]改 C# 侧的表就是对 Lua 里同一张表的引用。嵌套子表用 **GetTable<LuaTable>(key)**,成员函数用 **GetLuaFunction**。
  • 数组形 Lua 表(C# 用 LuaTable 读 Lua 侧):按下标读;**ToArray()** 便于在 C# 里遍历;下标以 Lua 规则为准(系列示例里常从 1 起)。映射成字典形时:键全是字符串可继续用 LuaTable 索引;含 bool 等键则 ToDictTable<...> 更稳。

toLua 协程(相对标准 coroutine

API 角色
coroutine.start / stop toLua 封的启停
coroutine.wait 按 Unity 时间片等,依赖帧驱动,不是裸 Lua yield 全套
  • LuaLooper + LuaCoroutine.Register 缺任一,wait 往往推不动。C# 侧 GetFunction("入口函数").Call() 只负责把 Lua 逻辑拉起来。

Lua 侧使用 C#:命名、对象与集合

  • 命名:**命名空间.类型CS.。未导出、未 LuaBinder.Bind 的类型在 Lua 里会直接报错或 nil**。
  • 构造(无 new 关键字,括号即构造):
local go = UnityEngine.GameObject("name")
GameObject = UnityEngine.GameObject   -- 常做全局别名省字
local go2 = GameObject("other")
  • 调用约定静态 类.成员实例字段 对象.字段实例方法 对象:方法(…)(冒号带 self)。Debug也须 导出 + 生成。**MonoBehaviour 不能 new**,用 go:AddComponent(typeof(脚本))GetComponent(typeof(脚本))AddComponent<T>() 无参泛型在 toLua 里弱,示例统一 typeof 重载
  • 枚举:**类型.枚举项tostring** 得名字,**:ToInt()** 得底层整数,**类型.IntToEnum(n)** 从整数还原;相对 xLua 没有现成的「字符串一键转枚举」。打印常看到 userdata,是绑定层包了一层。
  • 托管数组 / List / Dictionary(Lua 里当 C# 用,不按 Lua 下标习惯):
类型 长度 / 索引 遍历 常见坑
数组 **Length**,元素 [0..Length-1] for + 下标,或 GetEnumerator,或 ToTable 再按 Lua 扫 别用 # 量 C# 数组
List **Count**,读 [i] 同上 整体 list[i] = … 赋值 易报错
Dictionary [key] 读写 **:GetEnumerator()**,或 .Keys/.Values 迭代器 别当 Lua table 用 pairs
  • 新建 List<string>Dictionary<int,string> 等:Lua 侧常见 List_string()Dictionary_int_string()生成别名,前提是 CustomSettings 已导出并 Generate
  • 扩展方法:**CustomSettings** 里 **_GT(typeof(被扩展类)).AddExtendType(typeof(静态扩展类))**,生成后 Lua 才能 **实例:扩展方法()**。
  • ref / out → Lua 多返回值(占位实参仍要传,out 常用 **0/nil**):
C# 侧 Lua 收到的顺序(第一个起)
return,带 ref/out 函数返回值,再按形参从左到右 ref/out 的最终值
  • 重载:靠实参类型匹配;out 位可传 nil 帮忙命中重载。**OverloadFunction(10)** 走 int 版时 返回 10,不是无参版的 100。重载 + ref 容易选错签名,正文更倾向 多写 out、少用 ref
  • 委托与 eventUnityAction 等委托在 Lua 里 +/- 挂函数,空委托不要 nil + fn,先 = 第一个+ 追加不能在 Lua 里 myDelegate() 直接调,要靠 DoDelegate 这类 C# 封装。**event** 禁止 **myEvent = fn**,只能 **+/-**;清空多回 C# 的 ClearEvent匿名函数 不便单独 掉。
  • Unity 协程StartCoroutineWaitForSeconds 等):逻辑写在 Lua,驱动宿主是同一个 MonoBehaviour(常和 LuaManager 同物体)。
    • 必须先 **LuaCoroutine.Register(luaState, 该宿主)**,Lua 里才能稳定 **StartCoroutine / StopCoroutine**。
    • 若 Lua 侧要有 **Update**,C# 每帧 GetFunction("Update"):Call() 转发即可。

19.3 面试题精选

基础题

1. 把 toLua 官方工程并进自建 Unity 工程时,仓库根目录至少要拷贝哪些东西?

题目

除了 Assets 下的全部内容,还要不要动与 Assets 同级的目录?如果要,一般是哪几个?

深入解析
  • toLua 仓库根目录典型包含 AssetsLuajitLuajit64ProjectSettings 等;合并到自建工程时,至少要把 Assets 整树同级的 LuajitLuajit64 一并拷入;只拷 Assets 不拷双 Luajit 目录时,运行时或 native 路径容易对不上。
  • ProjectSettings 是否整包合并取决于你是否要让示例工程设置覆盖新建工程;本条操作重点是 Assets + 双 Luajit 目录
答题示例

要把 Assets 整棵目录拷过去,同时带上 **Assets 同级的 LuajitLuajit64**,用来对应不同位宽的 LuaJIT;不要只复制 Assets 而丢掉这两个顶层文件夹。

参考文章
  • 2.环境准备

2. 继承 MonoBehaviour 的类,能在 Lua 里像普通类一样用 类型() 直接实例化吗?

题目

自定义 MonoBehaviour 在 Lua 侧推荐怎么挂到物体上?为什么示例用 **typeof + AddComponent**,而不是泛型 **AddComponent<T>**?

深入解析
  • Unity 不允许MonoBehaviour 子类当普通 C# 对象 **new**;必须挂在 GameObject 上,由引擎跑 Awake/Start/生命周期
  • toLua 对 AddComponent<T>() 这类无参泛型成员支持弱,教程里用 AddComponent(typeof(某脚本))非泛型重载;取回组件同理 **GetComponent(typeof(...))**,再 : 调实例方法
答题示例

不能当普通类 new。先要有 GameObjectAddComponent(typeof(你的脚本)) 挂上,再 GetComponent 取组件实例调方法。

参考文章
  • 11.Lua调用CSharp-类

进阶题

1. 改完 customTypeList / customDelegateList 之后,最省事的编辑器操作是什么?不做的后果是什么?

题目

CustomSettings 里增删了要导出的类型或委托后,接下来在 Unity 里要点什么?若忘记这一步,Lua 侧通常会出现什么现象?

深入解析
  • 绑定代码是 **生成出来的 C#**(各 *Wrap.csLuaBinder.csDelegateFactory.cs 等),不是改完列表自动增量同步;标准流程是走 Lua → Generate All(或分项生成 Wrap / Delegate / Binder)。
  • 不生成:Lua 里 找不到新类型、调用报 nil / 无此类注册,或委托 签名与 C# 不一致 而在压栈/回调时炸;底层表现就是 Lua 状态机里没有对应注册项
答题示例

改列表后在菜单里执行 Generate All(或等价的 Wrap/Delegate/Binder 生成流程),把 Source/Generate 刷一遍。

若没生成,Lua 侧往往表现为类型未注册、成员为 nil,或委托绑定还是旧代码,运行时才暴露。

参考文章
  • 2.环境准备

2. 自定义 LuaFileUtils 时,为什么要先 new MyLoader()new LuaState()ReadFile 里通常谁先谁后?

题目

继承并重写 ReadFile 时,实例化顺序反了会怎样?真机资源从 AB 与 Resources 两路走时,教程里典型的尝试顺序是什么?

深入解析
  • LuaFileUtils 在框架里是单例语义:先 new 自定义子类,才会在后续 LuaState 创建、走默认加载时走到你的 ReadFile;顺序反了,虚拟机已经按旧 loader 配好,热更路径逻辑挂不上。
  • 工程习惯:上层业务 Lua 多在 AB;框架自带脚本或拷贝进 Resources.bytes 走第二条。实现上常 AB 命中则返回buffer 仍空再拼 Lua/ + 名Resources.Load
答题示例

new 自定义 **LuaFileUtils**,再建 **LuaState**,这样全局加载入口已经换成你的 **ReadFile**。

典型顺序是 AB 先、Resources 兜底;避免已发布包体仍去拼 DataPath 下只在编辑器存在的路径。

参考文章
  • 4.CSharp调用Lua-Lua文件加载重定向

深度题

题目

解压后的 Assets 里常带 link.xml,它解决的是哪类构建问题?为什么纯 C# 工程里没脚本能「看见」的调用,到了 IL2CPP/裁剪仍可能把类型裁掉?

深入解析
  • Unity 托管裁剪会依据 静态分析 + 少量保留规则 删掉「看似未使用」的程序集成员;Lua 通过反射/wrap 间接调用的 C#,静态分析往往 追踪不到,容易被裁掉。
  • link.xmllinker 保留规则 显式点名 assembly / type / method,让 只被 Lua 使用的 API 仍打进包体;缺它时常见症状是 IL2CPP 包一运行就 MissingMethod / TypeLoad,而 Editor 下正常。
答题示例

link.xml 是给 Managed Stripping / IL2CPP 看的保留清单,避免 只从 Lua(wrap/反射路径)调用 的 C# 类型或方法被条带优化删掉。

Editor 不裁那么狠时问题不露馅,一发 裁剪后包 就出现类型或成员缺失,所以 toLua 示例工程会默认带一份。

参考文章
  • 2.环境准备

2. Lua 里已经在用 coroutine.start / coroutine.wait,为什么工程里还要 LuaLooperLuaCoroutine.Register

题目

toLua 扩展的协程 API 和 Lua 标准库里的 coroutine.create 等不是同一套;没有 LuaLooper 时,通常会出现什么现象?LuaManager.Init 里为什么要给 LuaLooper.luaState 赋值并调用 LuaCoroutine.Register

深入解析
  • coroutine.wait 本质是「等一段时间再继续」,要靠引擎 Update 驱动;**LuaLooper** 挂在 Unity 侧,每帧推进 toLua 的协程调度,否则 wait 不会走完
  • LuaCoroutine.Register 把当前 MonoBehaviourLuaState 绑进协程桥,让 start/stop 能找对驱动目标;缺这一步常见现象是 Lua 里协程起不来或停不干净
答题示例

coroutine.start / wait / stop 是 toLua 提供的一层封装,依赖 Unity 帧循环;**LuaLooper** 负责在每帧里驱动这些 wait。

LuaCoroutine.RegisterLua 状态C# 宿主(一般是挂管理器的 MonoBehaviour) 接上,**Initloop.luaState = luaState** 则是让 looper 知道要推进哪一套虚拟机。

参考文章
  • 5.CSharp调用Lua-Lua管理器
  • 10.CSharp调用Lua-toLua提供的协同程序

3. toLua 里 C# 的 event 在 Lua 侧订阅,和 公开 delegate 字段 差在哪?清空怎么做?

题目

event 能不能在 Lua 里写 myEvent = luaFunction?能不能像公开的 UnityAction 字段那样 myDelegate = nil 清空?事件要彻底卸订阅时更稳的做法?

深入解析
  • **event 对外只能 +=/-=**(Lua 里对应 +/-),不能在外面 = 整块替换;绑定层往往会挡住 **myEvent = fn**,这和 C# 事件不是普通字段一致。
  • 公开 delegate 字段在 Lua 里可先 = nilClearDelegateevent 一般回 **C# 里 ClearEvent**,由声明类自己把委托链置空,避免 Lua 直接动 事件存储槽 触发异常。
答题示例

event 在 Lua 里用 +/- 增删订阅,不要指望 myEvent = 某个函数 整块替换。

delegate 公开字段可以 = nil 或走 ClearDelegateevent 一般要 C# 里 ClearEvent 卸干净。

参考文章
  • 17.Lua调用CSharp-委托和事件

转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至 785293209@qq.com

×

喜欢就点赞,疼爱就打赏