11.xLua实践项目总结

11.总结


11.1 核心要点速览

项目概述

C# 侧 Init + DoLuaFile 起 Lua,AB 提供 UI 与 JSON;Lua 里 InitClass 把 CS.* 拉到全局,BasePanel 按命名规则扫控件,背包 用 ItemGrid + PlayerData + ItemData 联调;编辑器工具把 .lua 批成 .lua.txt 打进 luascripts 包,为热更脚本做准备。

实现顺序建议

  1. 环境:xLua、AB、Object / SplitTools / JsonUtility,C# 能稳定执行 LuaMain
  2. InitClass:全局别名与 Canvas 引用就位,再读表、再挂面板
  3. 资源与表:Excel→JSON、图集、UI 预制打 AB;ItemData、PlayerData:Init()
  4. UI 框架:BasePanel → MainPanel / BagPanel / ItemGrid,LuaMain 里 require 顺序 与下文骨架一致
  5. 热更链路: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,y math.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

赏

×

喜欢就点赞,疼爱就打赏