基于MVVM框架的XUUI HelloWorld 新手教程
概述
本教程将带你从零搭建 XUUI 框架的第一个示例 —— HelloWorld。你将学会:
- 在 Unity 场景中创建 UI 并挂载适配器组件
- 编写 C# 入口脚本创建 XUUI Context
- 理解 Lua ViewModel 的 data / computed / commands 结构
- 理解整个 MVVM 数据绑定流程
1. XUUI 是什么
XUUI 是一个轻量级的 Unity MVVM 框架,使用XLua作为 ViewModel 层,Unity uGUI 作为 View 层。
核心思想:
View (uGUI) ←→ Adapter (C#) ←→ Binding Engine (Lua) ←→ ViewModel (Lua Table)- View:Unity 场景中的 Text、Button 等 uGUI 组件
- Adapter:挂在 uGUI 组件上的 C# 脚本,提供
Value/OnAction等统一接口 - Binding Engine:Lua 侧的绑定引擎,负责将 Lua ViewModel 数据推送到 Adapter,或将 UI 事件传递回 ViewModel
- ViewModel:纯 Lua table,包含
data(数据)、computed(计算属性)、commands(命令)
2. 场景搭建
2.1 创建 GameObject 层级
在 Hierarchy 窗口中创建如下结构:
HelloWorld (GameObject) ├── Canvas │ ├── MessageText (Text) │ └── ClickButton (Button)具体操作:
- 创建空 GameObject,命名为
HelloWorld——这就是入口 GameObject - 在
HelloWorld下创建 Canvas - 在 Canvas 下创建 Text,命名为
MessageText,调整好位置和字体大小 - 在 Canvas 下创建 Button,命名为
ClickButton,修改按钮上的 Text 为 “Click Me”
2.2 挂载适配器组件
XUUI 有两种绑定方式。本教程使用AdapterBase 方式(MonoBehaviour 组件方式),更直观易懂。
给 MessageText 挂载 TextAdapter
- 选中
MessageTextGameObject - 在 Inspector 中点击Add Component
- 搜索并添加XUUI > Text组件(或直接搜索
TextAdapter) - 在 TextAdapter 的
BindTo字段中填入:message - 确认 TextAdapter 的
Target已自动指向自身身上的Text (UnityEngine.UI.Text)组件
给 ClickButton 挂载 ButtonAdapter
- 选中
ClickButtonGameObject - 点击Add Component→XUUI > Button(或搜索
ButtonAdapter) - 在 ButtonAdapter 的
BindTo字段中填入:click
2.3 挂载 Helloworld 脚本
- 选中
HelloWorld(根 GameObject) - 点击Add Component→ 搜索并添加
Helloworld脚本
2.4 场景结构总览
最终 Inspector 结构如下:
HelloWorld (GameObject) └─ Helloworld (Script) Canvas ├── MessageText (GameObject) │ ├── Text (UI.Text) │ └── TextAdapter (Script) │ ├── BindTo: "message" │ └── Target: Text (auto-assigned) └── ClickButton (GameObject) ├── Button (UI.Button) ├── Text (UI.Text) — 按钮文字 └── ButtonAdapter (Script) ├── BindTo: "click" └── Target: Button (auto-assigned)3. 代码解析
3.1 Helloworld.cs
usingUnityEngine;usingXUUI;publicclassHelloworld:MonoBehaviour{Contextcontext=null;voidStart(){context=newContext(@" return { data = { info = { name = 'John', }, }, computed = { message = function(data) return 'Hello ' .. data.info.name .. '!' end }, commands = { click = function(data) print(data.info.name) end, }, } ");context.Attach(gameObject);}voidOnDestroy(){context.Dispose();}}逐行解释
| 行 | 作用 |
|---|---|
Context context = null; | 声明一个 Context 引用,它是 XUUI 的核心管理器 |
context = new Context(...) | 传入一段 Lua 脚本字符串,Context 的构造函数会:1. 初始化 XLua 环境 2. 编译执行脚本返回 Lua table 3. 调用xuui.new(options)创建 ViewModel |
context.Attach(gameObject) | 将当前 GameObject(及其子物体)绑定到 ViewModel。这一步触发 Binding Engine 扫描适配器、注册数据监听和命令绑定 |
context.Dispose() | 在对象销毁时清理所有 Lua 引用和事件监听 |
3.2 Lua ViewModel 结构
Lua 脚本返回的 table 是 ViewModel 的定义,包含三个核心部分:
data —— 数据模型(Model)
data={info={name='John',},}- 这是 MVVM 中的Model层
- 数据被
observeable.lua用 metatable 包装成响应式数据 - 当
data.info.name的值改变时,所有依赖它的 UI 会自动更新
computed —— 计算属性
computed={message=function(data)return'Hello '..data.info.name..'!'end,}- 这是一个纯函数,接收
data作为参数,返回计算后的值 TextAdapter.BindTo = "message"告诉 Binding Engine:这个 Text 显示computed.message的结果- 自动依赖追踪:引擎在执行
message(data)时,会记录它访问了data.info.name,并自动注册监听。当name变化时,message重新计算,UI 自动刷新
commands —— 命令
commands={click=function(data)print(data.info.name)end,}ButtonAdapter.BindTo = "click"告诉引擎:这个按钮触发commands.click- 点击按钮时,Binding Engine 调用
click(data),打印 “John”
4. 运行流程
程序启动时的完整调用链:
Helloworld.Start() └─ new Context(luaScript) ├─ LuaEnv.LoadString(script) → 编译 Lua 返回 table └─ xuui.new(options) → 包装响应式数据,返回 attach 函数 └─ context.Attach(gameObject) └─ binding.bind(data, observe, computed, commands, root) ├─ Collector.Collect(root) → 扫描子物体,收集所有 Adapter ├─ listen_to() → 为每个 DataConsumer(TextAdapter)注册数据监听 │ ├─ 发现 BindTo="message" 在 computed 中 │ ├─ 执行 message(data) → 依赖追踪 → 注册 watcher │ └─ 设置 TextAdapter.Value = "Hello John!" ├─ watch_to() → 为每个 DataProducer 绑定"UI → data"监听 └─ bind_action() → 为每个 EventEmitter(ButtonAdapter)绑定命令 └─ button.OnAction = function() commands.click(data) end运行后效果:
- 屏幕上显示“Hello John!”
- 点击按钮,Console 打印“John”
上面的流程图就很好的解释了,我们Content参数里的computed 和 Commands是如何被框架执行的,主要就是这个Attach的方法,跟对应的节点绑定,在Attach的时候做了Lua初始化的工作,而在初始化的时候加载了xuui.lua.txt
local function new(options) options = options or {} options.data = options.data or {} options.computed = options.computed or {} options.computed[0] = {} options.commands = options.commands or {} options.exports = {} if options.modules then app_init(options) end local observe = observeable.new(options.data) local function attach(el) return binding.bind(options.data, observe, options.computed, options.commands, el) end local function reload(module_name, reload_data) loadmodule(options, options.exports, options.name, module_name, not reload_data) end return attach, reload end里面调用了binding.bind的方法,而_M.bind方法扫描所有的子物体,搜集适配器,其中包含EventEmitters对象然后处理了ButtonAdapter,collector.collect 在 C# 侧 (Collector.cs:12) 通过 GetComponentsInChildren 找到所有 ButtonAdapter,按接口类型分到三个数组里。bindings[3] 是所有实现 EventEmitter 接口的适配器(即 ButtonAdapter)。
5. 绑定关系总结
| GameObject | 适配器 | BindTo | ViewModel 映射 | 方向 |
|---|---|---|---|---|
| MessageText | TextAdapter | "message" | computed.message(data) | VM → View |
| ClickButton | ButtonAdapter | "click" | commands.click(data) | View → VM |
6. 动手实验
理解了基本流程后,可以尝试以下修改来加深理解:
6.1 修改显示的数据
将name = 'John'改为name = 'World',运行后 Text 变为“Hello World!”
6.2 新增一个显示字段
在 data 中添加age = 25:
data={info={name='John',age=25,-- 新增},},添加一个 computed:
ageInfo=function(data)returndata.info.name..' is '..data.info.age..' years old'end,在场景中新建一个 Text,挂载 TextAdapter,BindTo 设为"ageInfo"。
6.3 修改命令行为
把 click 命令改为修改数据:
click=function(data)data.info.name='World'-- 改数据end,点击按钮后,Text 会自动更新为“Hello World!”—— 这就是 MVVM 的威力,修改 Model 自动驱动 View 更新。
6.4 使用 InputField 实现双向绑定
在 Canvas 下创建一个 InputField,挂载InputFieldAdapter,BindTo 设为"info.name"。运行时在输入框中打字,Text 的文字会随之实时变化。因为:
- InputFieldAdapter 是
DataProducer<string>— 用户输入触发OnValueChange - 引擎将输入值写回
data.info.name - 响应式系统检测到
name变化,触发computed.message重新计算 - TextAdapter 的
Value被更新,UI 刷新
7. 常见问题
Q: BindTo 的值如何对应到 Lua data?
BindTo 使用点号分隔路径。例如:
| BindTo | Lua 路径 |
|---|---|
"message" | computed.message(先从 computed 查找) |
"click" | commands.click |
"info.name" | data.info.name(直接数据路径) |
"module1.click" | commands["module1.click"](模块化命令) |
Q: 为什么 TextAdapter 的 BindTo 是 “message” 却显示 computed 的结果?
Binding Engine 的listen_to函数会先检查BindTo是否在computed表中。如果在,就执行计算属性函数而不是直接读取 data。如果在 computed 和 commands 中都找不到,则直接从 data 中按路径读取。
Q: 适配器只有 Text 和 Button 吗?
XUUI 还内置了 InputFieldAdapter(双向绑定)、DropdownAdapter 等。你也可以自己继承AdapterBase<T>创建自定义适配器。
8. 完整文件清单
Assets/ ├── Scenes/ │ └── Helloworld.unity ← 本示例的场景 ├── Scripts/ │ └── Helloworld.cs ← 入口 C# 脚本 ├── XUUI/ │ ├── Scripts/ │ │ ├── ViewModel.cs ← Context 类 │ │ ├── AdapterBase.cs ← 适配器基类 │ │ ├── RawAdapterBase.cs ← 非 MonoBehaviour 适配器基类 │ │ ├── DataConsumer.cs ← VM→View 接口 │ │ ├── DataProducer.cs ← View→VM 接口 │ │ ├── EventEmitter.cs ← 事件接口 │ │ └── UGUIAdapter/ │ │ ├── TextAdapter.cs ← Text 适配器 │ │ ├── ButtonAdapter.cs ← Button 适配器 │ │ ├── InputFieldAdapter.cs ← InputField 适配器 │ │ ├── DropdownAdapter.cs ← Dropdown 适配器 │ │ ├── ViewBinding.cs ← 序列化绑定组件(另一种方式) │ │ └── Collector.cs ← 扫描收集所有适配器 │ └── Resources/ │ ├── xuui.lua.txt ← XUUI 主模块,new() 工厂 │ ├── binding.lua.txt ← 绑定引擎 │ ├── observeable.lua.txt ← 响应式数据系统 │ ├── collector.lua.txt ← Lua 端适配器收集器 │ └── xuui_utils.lua.txt ← 工具函数9.补充
DataProvider接口,生产者
DataProducer —— 生产者,View → VM
数据从 UI 流回 ViewModel。定义在 DataProducer.cs:
public interface DataProducer<T> : DataProducer { Action<T> OnValueChange { get; set; } // UI 变化时通知谁 } 只有 OnValueChange,UI 变化时触发它,把值传出去。 使用例子 — InputFieldAdapter (InputFieldAdapter.cs): public class InputFieldAdapter : AdapterBase<InputField>, DataConsumer<string>, DataProducer<string> { // 作为 DataConsumer:VM → View public string Value { set { Target.text = value; } } // 作为 DataProducer:View → VM public Action<string> OnValueChange { get; set; } void Start() { Target.onValueChanged.AddListener((text) => { if (OnValueChange != null) OnValueChange(text); // 用户输入 → Lua }); } }InputFieldAdapter 同时实现了两个接口,所以支持双向绑定。
DataConsumer接口,消费者
DataConsumer —— 消费者,VM → View
数据从 ViewModel 流向 UI。定义在 DataConsumer.cs:
public interface DataConsumer<T> : DataConsumer { T Value { set; } // 只能设置值推到 UI,没有 get } 只有 set 没有 get,意味着这是个单向写入通道。 使用例子 — TextAdapter (TextAdapter.cs): public class TextAdapter : AdapterBase<Text>, DataConsumer<string> { public string Value { set { Target.text = value; // Lua 算好的值 → 写到 UI } } }Binding 引擎调用 el.Value = “Hello John!” → Text 的文字就变了。