基于MVVM框架的XUUI HelloWorld 新手教程

📅 2026/7/10 6:18:46
基于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)ViewUnity 场景中的 Text、Button 等 uGUI 组件Adapter挂在 uGUI 组件上的 C# 脚本提供Value/OnAction等统一接口Binding EngineLua 侧的绑定引擎负责将 Lua ViewModel 数据推送到 Adapter或将 UI 事件传递回 ViewModelViewModel纯 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字段中填入click2.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.csusingUnityEngine;usingXUUI;publicclassHelloworld:MonoBehaviour{Contextcontextnull;voidStart(){contextnewContext( 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)创建 ViewModelcontext.Attach(gameObject)将当前 GameObject及其子物体绑定到 ViewModel。这一步触发 Binding Engine 扫描适配器、注册数据监听和命令绑定context.Dispose()在对象销毁时清理所有 Lua 引用和事件监听3.2 Lua ViewModel 结构Lua 脚本返回的 table 是 ViewModel 的定义包含三个核心部分data —— 数据模型Modeldata{info{nameJohn,},}这是 MVVM 中的Model层数据被observeable.lua用 metatable 包装成响应式数据当data.info.name的值改变时所有依赖它的 UI 会自动更新computed —— 计算属性computed{messagefunction(data)returnHello ..data.info.name..!end,}这是一个纯函数接收data作为参数返回计算后的值TextAdapter.BindTo message告诉 Binding Engine这个 Text 显示computed.message的结果自动依赖追踪引擎在执行message(data)时会记录它访问了data.info.name并自动注册监听。当name变化时message重新计算UI 自动刷新commands —— 命令commands{clickfunction(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() → 为每个 DataConsumerTextAdapter注册数据监听 │ ├─ 发现 BindTomessage 在 computed 中 │ ├─ 执行 message(data) → 依赖追踪 → 注册 watcher │ └─ 设置 TextAdapter.Value Hello John! ├─ watch_to() → 为每个 DataProducer 绑定UI → data监听 └─ bind_action() → 为每个 EventEmitterButtonAdapter绑定命令 └─ button.OnAction function() commands.click(data) end运行后效果屏幕上显示“Hello John!”点击按钮Console 打印“John”上面的流程图就很好的解释了我们Content参数里的computed 和 Commands是如何被框架执行的主要就是这个Attach的方法跟对应的节点绑定在Attach的时候做了Lua初始化的工作而在初始化的时候加载了xuui.lua.txtlocal 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对象然后处理了ButtonAdaptercollector.collect 在 C# 侧 (Collector.cs:12) 通过 GetComponentsInChildren 找到所有 ButtonAdapter按接口类型分到三个数组里。bindings[3] 是所有实现 EventEmitter 接口的适配器即 ButtonAdapter。5. 绑定关系总结GameObject适配器BindToViewModel 映射方向MessageTextTextAdaptermessagecomputed.message(data)VM → ViewClickButtonButtonAdapterclickcommands.click(data)View → VM6. 动手实验理解了基本流程后可以尝试以下修改来加深理解6.1 修改显示的数据将name John改为name World运行后 Text 变为“Hello World!”6.2 新增一个显示字段在 data 中添加age 25data{info{nameJohn,age25,-- 新增},},添加一个 computedageInfofunction(data)returndata.info.name.. is ..data.info.age.. years oldend,在场景中新建一个 Text挂载 TextAdapterBindTo 设为ageInfo。6.3 修改命令行为把 click 命令改为修改数据clickfunction(data)data.info.nameWorld-- 改数据end,点击按钮后Text 会自动更新为“Hello World!”—— 这就是 MVVM 的威力修改 Model 自动驱动 View 更新。6.4 使用 InputField 实现双向绑定在 Canvas 下创建一个 InputField挂载InputFieldAdapterBindTo 设为info.name。运行时在输入框中打字Text 的文字会随之实时变化。因为InputFieldAdapter 是DataProducerstring— 用户输入触发OnValueChange引擎将输入值写回data.info.name响应式系统检测到name变化触发computed.message重新计算TextAdapter 的Value被更新UI 刷新7. 常见问题Q: BindTo 的值如何对应到 Lua dataBindTo 使用点号分隔路径。例如BindToLua 路径messagecomputed.message先从 computed 查找clickcommands.clickinfo.namedata.info.name直接数据路径module1.clickcommands[module1.click]模块化命令Q: 为什么 TextAdapter 的 BindTo 是 “message” 却显示 computed 的结果Binding Engine 的listen_to函数会先检查BindTo是否在computed表中。如果在就执行计算属性函数而不是直接读取 data。如果在 computed 和 commands 中都找不到则直接从 data 中按路径读取。Q: 适配器只有 Text 和 Button 吗XUUI 还内置了 InputFieldAdapter双向绑定、DropdownAdapter 等。你也可以自己继承AdapterBaseT创建自定义适配器。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.cspublic interface DataProducerT : DataProducer { ActionT OnValueChange { get; set; } // UI 变化时通知谁 } 只有 OnValueChangeUI 变化时触发它把值传出去。 使用例子 — InputFieldAdapter (InputFieldAdapter.cs) public class InputFieldAdapter : AdapterBaseInputField, DataConsumerstring, DataProducerstring { // 作为 DataConsumerVM → View public string Value { set { Target.text value; } } // 作为 DataProducerView → VM public Actionstring OnValueChange { get; set; } void Start() { Target.onValueChanged.AddListener((text) { if (OnValueChange ! null) OnValueChange(text); // 用户输入 → Lua }); } }InputFieldAdapter 同时实现了两个接口所以支持双向绑定。DataConsumer接口消费者DataConsumer —— 消费者VM → View数据从 ViewModel 流向 UI。定义在 DataConsumer.cspublic interface DataConsumerT : DataConsumer { T Value { set; } // 只能设置值推到 UI没有 get } 只有 set 没有 get意味着这是个单向写入通道。 使用例子 — TextAdapter (TextAdapter.cs) public class TextAdapter : AdapterBaseText, DataConsumerstring { public string Value { set { Target.text value; // Lua 算好的值 → 写到 UI } } }Binding 引擎调用 el.Value “Hello John!” → Text 的文字就变了。