11.总结
11.1 核心要点速览
项目概述
C# 侧 Init + DoLuaFile 起 Lua,AB 提供 UI 与 JSON;Lua 里 InitClass 把 CS.* 拉到全局,BasePanel 按命名规则扫控件,背包 用 ItemGrid + PlayerData + ItemData 联调;编辑器工具把 .lua 批成 .lua.txt 打进 luascripts 包,为热更脚本做准备。
实现顺序建议
- 环境:xLua、AB、Object / SplitTools / JsonUtility,C# 能稳定执行 LuaMain
- InitClass:全局别名与 Canvas 引用就位,再读表、再挂面板
- 资源与表:Excel→JSON、图集、UI 预制打 AB;ItemData、
PlayerData:Init() - UI 框架:BasePanel → MainPanel / BagPanel / ItemGrid,LuaMain 里 require 顺序 与下文骨架一致
- 热更链路:Lua→txt、AssetImporter 批设包名、整包验证
环境准备与脚本栈
工程拆三块:基础框架、xLua 与 AB、Lua 工具脚本。工具链重点三件事:面向对象底座、JSON、字符串拆分;运行入口是 C# 初始化 Lua 管理器后再拉起 Lua 主文件。
| 模块 | 作用 | 要点 |
|---|---|---|
Object.lua |
极简 OOP:new/subClass |
new:实例元表指到类表,类表上挂 __index 指回自己,实例找不到成员就走到类方法。subClass:在 _G 上注册子类表,__index 指子类自身,base 指父类,setmetatable(子类, 父类) 继承父类成员 |
JsonUtility.lua |
dkjson(模块 + 可选全局 json) |
Json.decode 等与 InitClass 里 Json = require("JsonUtility") 对齐;encode 对 function 等类型有跳过逻辑,别把任意 Lua 函数写进 JSON |
SplitTools.lua |
string.split |
空分隔符返回 false;用 plain 模式的 string.find 循环切分,尾部剩余一段最后 insert |
启动链(正文示例):
void Start()
{
LuaManager.Instance.Init();
Debug.Log("C#主脚本准备就绪");
LuaManager.Instance.DoLuaFile("LuaMain");
}
print("Lua主脚本准备就绪")
C# 侧保证 Init 完成再 DoLuaFile,避免虚拟机或路径没就绪就去 require/执行主脚本;Lua 主脚本里再逐步挂 UI 与业务。
UGUI 面板拼装
- 导入 UI → Canvas → 拼 主面板 / 背包 / 格子 预制;导图小节当步骤 checklist,层级一次摆对、命名一次对齐,后面
GetComponentsInChildren才省事。
InitClass 与全局别名
- 聚合 require(
Object、SplitTools、JsonUtility)并把常用 CS.* 缩写到全局:GameObject、Resources、RectTransform、UI下的Image/Text/Button/Toggle/ScrollRect、Vector2/3、SpriteAtlas、AssetBundleManager等。 Canvas = GameObject.Find("Canvas").transform在 require 时执行一次,全系列共用;场景里必须存在该名字的 Canvas。- 依赖顺序:先有
InitClass,再ItemData、面板脚本;否则typeof(TextAsset)、Json、AssetBundleManager等未就绪会直接报错。
配置表、AB 与运行时数据
| 步骤 | 产出 | 脚本/注意 |
|---|---|---|
| 配置 | Excel→JSON 数组 | 字段示例:id、name、icon、type、tips |
| 资源 | icon 图集、UI 预制 | 打 AB;xLua 报生成缺失则 补生成再打包 |
| 运行时表 | ItemData[id] 字典 |
json 包加载 TextAsset,Json.decode 后转存 |
| 玩家背包 | equips / items / gems |
元素 {id,num};Init 当前为写死测试数据,可换本地存档或服务器 |
BasePanel
- 成员:
panelObj、controls、isInitEvent。 - Init:AB 加载预制,
SetParent(Canvas,false);GetComponentsInChildren(UIBehaviour),仅当 name 含btn、tog、img、sv、txt才入库。 - controls:
controls[节点名][C# 类型名] = 组件,同一物体多UIBehaviour互不争抢引用。 - GetControl(name, typeName);ShowMe 先 Init 再 **SetActive(true)**。
MainPanel
BasePanel:subClass("MainPanel");Init 中self.base.Init(self, name)后用isInitEvent只挂一次监听。- btnRole 的 Button 回调里 **BagPanel:ShowMe(“BagPanel”)**。
ItemGrid
- Init:拉 ItemGrid 预制,设父与 localPosition,Find
imgIcon、Text。 - InitData:
ItemData[id];icon按_拆分 图集名与精灵名;SpriteAtlas:GetSprite;Text 写数量。 - Destroy:GameObject.Destroy 后 obj = nil。
BagPanel
- ChangeType
nowType == type直接 return,避免重复刷同一页签- 先 Destroy 旧 ItemGrid,清空
items - 按 type 取 PlayerData.equips / items / gems,循环
ItemGrid:new()→ Init → InitData - 网格:x
(i-1)%4*175,ymath.floor((i-1)/4)*175 - 末尾
nowType = type,页签状态才与数据一致
- Init:svBag 的 ScrollRect 下 Viewport/Content 手动物理路径 Find;btnClose;三个 Toggle 对应 装备 / 道具 / 宝石
- ShowMe:
base.ShowMe后若nowType == -1,首次ChangeType(1)保底装备页
Lua 启动顺序骨架
require("InitClass")
require("ItemData")
require("PlayerData")
PlayerData:Init()
require("BasePanel")
require("MainPanel")
require("BagPanel")
require("ItemGrid")
MainPanel:ShowMe("MainPanel")
Lua 批量改后缀与 AB
- 菜单 XLua/自动生成txt后缀的Lua:
LuaScripts/*.lua→LuaScriptsTxt/*.lua.txt;可先清空目标目录旧 *.txt。 - 用
Path.GetFileName,避免 Windows 下用/截取路径失败。 AssetDatabase.Refresh后以Assets/...调 GetAtPath,assetBundleName = "luascripts"统一进热更包。
11.2 面试题精选
基础题
1. Lua 里的 typeof 和 InitClass 有什么关系
题目
ItemData.lua 里 LoadAssetBundleResource(..., typeof(TextAsset)) 中的 typeof 是什么?和 Lua 自带的 type() 有何区别?不用 InitClass 行不行?
深入解析
- xLua 把 C# 的
typeof暴露到 Lua,用来取 CLR 的 System.Type,交给LoadAssetBundleResource这类 C# API 做类型判别或重载选择。 type()是 Lua 值类型标签(table、string 等),与typeof完全不是一层概念,面试时别混答。- InitClass 里若未把
TextAsset绑到CS.UnityEngine.TextAsset(或未 require 到可用符号),Lua 侧typeof(TextAsset)会报未定义或类型不对;所以 别名脚本要先于 ItemData。
答题示例
typeof是走 xLua 调 C# 的,拿的是 C# 类型对象,给加载接口用。Lua 的
type()看的是 Lua 值种类,和typeof无关。InitClass 先把
TextAsset等指好,再跑 ItemData,否则typeof(TextAsset)根本就不可用。
参考文章
- 4.常用类别名
- 5.数据相关
进阶题
1. Object 里 new 和 subClass 各自改了哪些表、元表关系是什么
题目
Object.lua 里 Object:new() 产出的实例,和 Object:subClass("Foo") 产出的类表,在元表与 __index 上分别是什么关系?子类实例调方法时,Lua 按什么顺序查找?
深入解析
new:local obj = {}后setmetatable(obj, self),当前self是类表;类表里用self.__index = self,实例缺键时通过元表的__index找到类表本身,从而拿到类上的方法。subClass:子类表obj设obj.__index = obj、obj.base = self、setmetatable(obj, self)。子类 类表 的未定义键会到 父类表;之后对子类做子类:new()时,产出的 实例 元表指向子类表,查找实例方法先命中子类,再沿继承链向上。- 易混点:不要误以为
subClass里该反复改父类的__index;应让 子类表自己的__index指向子类,继承靠 子类表的 metatable = 父类表。
答题示例
new出来的是实例,元表指向当前类,类上__index回指类,所以实例能调到类方法。
subClass注册的是「类表」:子类__index指自己,setmetatable(子类, 父类)管继承;实例再用子类去new,查找会从子类类表开始。调用实例方法时,先在实例表找,没有就看元表类的
__index,再沿父类链。
参考文章
- 2.环境准备
2. BasePanel 的 controls 为什么做成「控件节点名 → 类型名 → 组件」
题目
BasePanel 扫 UIBehaviour 时,为什么用 controls[controlName][typeName] 两层表,而不是 controls[controlName] = 单个组件?
深入解析
- 同一 GameObject 上可能同时挂 Button + Image 等多个 UIBehaviour,若字典值只存一个引用,后写的会 覆盖 先写的,GetControl 时拿不到另一类型。
- typeName 用
GetType().Name,与GetControl第二个参数传入的字符串 对齐,调用侧明确自己要 Button 还是 Image。 - 命名前缀(btn/tog/img…)把大量无关 Transform 过滤掉,减少字典噪音与误绑定。
答题示例
一层表只能放一个组件,同一物体多个 UI 脚本会互相覆盖。
外层用节点名、内层用 C# 类型名,和
GetControl(name, "Button")这种取法一一对应。前缀规则还能在拼面板阶段就挡住没命名的杂控件。
参考文章
- 6.面板基类
深度题
1. 为何由 C# Start 里 Init 再 DoLuaFile,而不是反过来或直接 Awake 里全做
题目
示例把 LuaManager.Instance.Init() 和 DoLuaFile("LuaMain") 放在 Start 里,这段顺序如果颠倒或挪到 Awake,在你做 xLua 工程时可能踩哪些坑?
深入解析
- Init 在前:一般要完成 loader、路径、
LuaEnv构造、自定义 require 搜索路径等;未完成就去DoLuaFile,常见是 找不到脚本、Globals 未绑好 或 初始化只执行了一半。 - 与 Unity 生命周期的关系:同物体上若还有别的脚本在
Awake里依赖 Lua 已跑起来,会把启动点提到更晚或显式分阶段;反之若 Lua 里立刻要读UnityEngine对象,需保证 C# 侧该绑的已经绑完。 - 工程取舍:实践里常把「虚拟机/搜索路径」与「首包 Lua 入口」拆成两步,便于打日志和排错;真要提前到
Awake,需核对LuaManager实现是否允许且 Execution Order 是否与其它依赖一致。
答题示例
先
Init再DoLuaFile,是保证 Lua 环境、loader、路径就绪后再执行主脚本;顺序反了容易加载失败或半初始化。
Awake与Start取决于项目里还有谁依赖 Lua:若别的Awake要等 Lua,就得调执行顺序或把 Lua 启动再提前;否则Start晚一帧往往能避开部分依赖顺序问题。具体仍要以你项目
LuaManager的实现为准,关键是 环境就绪 与 依赖脚本 两件事不要竞态。
参考文章
- 2.环境准备
2. 为什么要把 .lua 复制成 .lua.txt 再打 AB、并用编辑器批量设包名
题目
工具把 LuaScripts/*.lua 拷到 LuaScriptsTxt/*.lua.txt,刷新资源后把路径统一标成 luascripts AB。这样设计在玩 xLua + AssetBundle 时主要在解决什么问题?
深入解析
- 源文件是
*.lua,导入工程参与打包的是*.lua.txt,走 TextAsset 导入与 AssetBundle 赋值,和「热更包里是一段文本再由 xLua DoString/loader 执行」的路径对齐;仓库/编辑仍可用常规.lua扩展名写逻辑。 - 整批复制 + 清空目标目录 避免删源 Lua 后目标目录仍残留旧文件进包。
AssetDatabase.Refresh后再改 importer,否则 GetAtPath 可能仍指向旧缓存;统一 assetBundleName 便于运行时 单包拉齐 多份脚本,减少散包管理成本。- Path.GetFileName 规避 Windows 反斜杠路径 下用
/手动截取文件名的坑。
答题示例
后缀改成
.lua.txt是按 TextAsset 进包,再用统一 AB 名整批热更,运行时读文本交给 xLua。先 Refresh 再改 AB 名,否则 Importer 还是旧的;统一 luascripts 便于一条包加载全部脚本。
用
Path.GetFileName是为了在 Windows 上也能稳定取文件名。
参考文章
- 10.批量转换Lua脚本后缀工具
转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至 785293209@qq.com