19.总结
19.2 核心要点速览
toLua 在本系列中的定位
- toLua:Unity 里常用的 C# / Lua 互操作与热更新方案之一;本目录「基础知识」主攻双向调用与日常用法。
- 主线就一句:用 toLua 把 Lua 接进 Unity,并始终清楚 C# 与 Lua 谁主控、谁调谁。
前置知识(默认已掌握)
AssetBundle、Lua 语法、xLua 热更、xLua 背包实践。后文谈路径、资源与和 xLua 对照时,不再从零讲 AB 或 Lua 语法。
xLua 与 toLua 速查(复习用)
| 对比点 | xLua 常见写法 | toLua 常见写法 |
|---|---|---|
| 类型命名 | 多前缀 CS.命名空间.类型 |
命名空间.类型,无 CS |
| C# 读 Lua 表 | 可映射到类/接口等 | 多用 LuaTable、ToArray、ToDictTable |
| Lua 用 C# 枚举 | 工具较多 | tostring / :ToInt() / IntToEnum;无顺带手的字符串→枚举 |
| Lua 里延迟/协程 | 依项目封装 | coroutine.start/wait 依赖 LuaLooper;Unity 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 ZIP 拿tolua-master。 - 顶层要带的东西:除了
Assets整树,还要拷 **Assets同级目录里的Luajit、Luajit64**(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.cs、LuaBinder.cs、DelegateFactory.cs等生成物;若目录长期空或明显过期,多半是没生成或生成失败。CustomSettings:customTypeList管要导出给 Lua 的 类型,**customDelegateList** 管 委托 桥接;列表改过以后必须再走生成,否则 Lua 里根本看不到新类型或委托签名还是旧的。- 与热更资源链配套:通过 Package Manager 安装 Asset Bundle Browser(
com.unity.assetbundlebrowser,菜单 Window → Asset Bundle Browser,要求 Unity 5.6+),用来配 AB、验包、触发构建——和前面 AssetBundle 前情、后面 Lua 进包流程能接上。 - 基础框架:
BaseFramework里BaseSingletonCSharp、BaseSingletonMonoBehaviour一类单例基类,后面写 Lua 管理器/C# 入口时常从这里延展开。
C# 侧驱动 Lua:解析器、加载与管理器
- **
LuaState**:new后必须Start();工程上一个管理器、一套虚拟机,避免多实例带来重复加载与状态分裂。 - **
DoString/DoFile/Require**(默认解析Assets/Lua树):
| 入口 | 行为要点 |
|---|---|
DoString |
执行字符串;可选第二参数作 chunk 名,报错栈好对回 C# |
DoFile |
每调必跑文件顶层;.lua 可有可无 |
Require |
模块缓存,第二次同名不重复跑顶层;不要带 .lua |
- 收尾:**
CheckTop→Dispose→ 引用置null**。 - 路径习惯:**
AddSearchPath** 只适合编辑器期补磁盘根;**Require/DoFile** 用的逻辑路径仍须在Lua子树或已加路径下能解析。真机业务脚本多在 AB / Resources,别只认Application.dataPath下那份编辑器路径。 - 自定义
ReadFile:继承LuaFileUtils;先new MyLoader(),再new LuaState()。实现上常:补.lua→ 先 AB(路径按/取文件名进包)→ **buffer仍空再Resources.Load("Lua/" + fileName)**;菜单拷表会用.bytes绕过 **Resources禁.lua**。 LuaManager.Init推荐顺序(与教程一致,可对照排查缺哪一步):
| 顺序 | 动作 | 备忘 |
|---|---|---|
| 0 | (可选)new MyCustomLoader() |
发版或真机拉脚本时再开 |
| 1 | new LuaState + Start() |
|
| 2 | 挂 LuaLooper,loop.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"] = v。local不在全局环境,C# 按名取多为 **null**。 - Lua 函数(C# 侧最短记忆):
var f = luaState.GetFunction("foo");
f.Call(); // 无返回值
f.Invoke<int, int>(1); // 最后一泛型为返回值,前序为实参类型
f.Dispose();
- 手写栈:**
BeginPCall→Push→PCall→CheckNumber等 →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。 - 委托与
event:UnityAction等委托在 Lua 里+/-挂函数,空委托不要nil + fn,先=第一个 再+追加。不能在 Lua 里myDelegate()直接调,要靠DoDelegate这类 C# 封装。**event** 禁止 **myEvent = fn**,只能 **+/-**;清空多回 C# 的ClearEvent。匿名函数 不便单独−掉。 - Unity 协程(
StartCoroutine、WaitForSeconds等):逻辑写在 Lua,驱动宿主是同一个MonoBehaviour(常和LuaManager同物体)。- 必须先 **
LuaCoroutine.Register(luaState, 该宿主)**,Lua 里才能稳定 **StartCoroutine/StopCoroutine**。 - 若 Lua 侧要有 **
Update**,C# 每帧GetFunction("Update"):Call()转发即可。
- 必须先 **
19.3 面试题精选
基础题
1. 把 toLua 官方工程并进自建 Unity 工程时,仓库根目录至少要拷贝哪些东西?
题目
除了 Assets 下的全部内容,还要不要动与 Assets 同级的目录?如果要,一般是哪几个?
深入解析
- toLua 仓库根目录典型包含
Assets、Luajit、Luajit64、ProjectSettings等;合并到自建工程时,至少要把Assets整树与 同级的Luajit、Luajit64一并拷入;只拷Assets不拷双 Luajit 目录时,运行时或 native 路径容易对不上。 ProjectSettings是否整包合并取决于你是否要让示例工程设置覆盖新建工程;本条操作重点是 Assets + 双 Luajit 目录。
答题示例
要把
Assets整棵目录拷过去,同时带上 **Assets同级的Luajit、Luajit64**,用来对应不同位宽的 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。先要有GameObject,AddComponent(typeof(你的脚本))挂上,再GetComponent取组件实例调方法。
参考文章
- 11.Lua调用CSharp-类
进阶题
1. 改完 customTypeList / customDelegateList 之后,最省事的编辑器操作是什么?不做的后果是什么?
题目
在 CustomSettings 里增删了要导出的类型或委托后,接下来在 Unity 里要点什么?若忘记这一步,Lua 侧通常会出现什么现象?
深入解析
- 绑定代码是 **生成出来的 C#**(各
*Wrap.cs、LuaBinder.cs、DelegateFactory.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文件加载重定向
深度题
1. link.xml 在 toLua 工程里主要防什么?和「Lua 调 C#」有什么关系?
题目
解压后的 Assets 里常带 link.xml,它解决的是哪类构建问题?为什么纯 C# 工程里没脚本能「看见」的调用,到了 IL2CPP/裁剪仍可能把类型裁掉?
深入解析
- Unity 托管裁剪会依据 静态分析 + 少量保留规则 删掉「看似未使用」的程序集成员;Lua 通过反射/wrap 间接调用的 C#,静态分析往往 追踪不到,容易被裁掉。
link.xml用 linker 保留规则 显式点名 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,为什么工程里还要 LuaLooper 和 LuaCoroutine.Register?
题目
toLua 扩展的协程 API 和 Lua 标准库里的 coroutine.create 等不是同一套;没有 LuaLooper 时,通常会出现什么现象?LuaManager.Init 里为什么要给 LuaLooper.luaState 赋值并调用 LuaCoroutine.Register?
深入解析
coroutine.wait本质是「等一段时间再继续」,要靠引擎Update驱动;**LuaLooper** 挂在 Unity 侧,每帧推进 toLua 的协程调度,否则wait不会走完。LuaCoroutine.Register把当前 MonoBehaviour 和LuaState绑进协程桥,让start/stop能找对驱动目标;缺这一步常见现象是 Lua 里协程起不来或停不干净。
答题示例
coroutine.start/wait/stop是 toLua 提供的一层封装,依赖 Unity 帧循环;**LuaLooper** 负责在每帧里驱动这些 wait。
LuaCoroutine.Register把 Lua 状态和 C# 宿主(一般是挂管理器的 MonoBehaviour) 接上,**Init里loop.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 里可先= nil或ClearDelegate;event一般回 **C# 里ClearEvent**,由声明类自己把委托链置空,避免 Lua 直接动 事件存储槽 触发异常。
答题示例
event在 Lua 里用+/-增删订阅,不要指望myEvent = 某个函数整块替换。
delegate公开字段可以= nil或走ClearDelegate;event一般要 C# 里ClearEvent卸干净。
参考文章
- 17.Lua调用CSharp-委托和事件
转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至 785293209@qq.com