基于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)

具体操作:

  1. 创建空 GameObject,命名为 HelloWorld------这就是入口 GameObject
  2. HelloWorld 下创建 Canvas
  3. 在 Canvas 下创建 Text,命名为 MessageText,调整好位置和字体大小
  4. 在 Canvas 下创建 Button,命名为 ClickButton,修改按钮上的 Text 为 "Click Me"

2.2 挂载适配器组件

XUUI 有两种绑定方式。本教程使用 AdapterBase 方式(MonoBehaviour 组件方式),更直观易懂。

给 MessageText 挂载 TextAdapter
  1. 选中 MessageText GameObject
  2. 在 Inspector 中点击 Add Component
  3. 搜索并添加 XUUI > Text 组件(或直接搜索 TextAdapter
  4. 在 TextAdapter 的 BindTo 字段中填入:message
  5. 确认 TextAdapter 的 Target 已自动指向自身身上的 Text (UnityEngine.UI.Text) 组件
给 ClickButton 挂载 ButtonAdapter
  1. 选中 ClickButton GameObject
  2. 点击 Add ComponentXUUI > Button (或搜索 ButtonAdapter
  3. 在 ButtonAdapter 的 BindTo 字段中填入:click

2.3 挂载 Helloworld 脚本

  1. 选中 HelloWorld(根 GameObject)
  2. 点击 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

csharp 复制代码
using UnityEngine;
using XUUI;

public class Helloworld : MonoBehaviour
{
    Context context = null;

    void Start()
    {
        context = new Context(@"
            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);
    }

    void OnDestroy()
    {
        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)
lua 复制代码
data = {
    info = {
        name = 'John',
    },
}
  • 这是 MVVM 中的 Model
  • 数据被 observeable.lua 用 metatable 包装成响应式数据
  • data.info.name 的值改变时,所有依赖它的 UI 会自动更新
computed ------ 计算属性
lua 复制代码
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 ------ 命令
lua 复制代码
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,按接口类型分到三个数组里。bindings3 是所有实现 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

lua 复制代码
data = {
    info = {
        name = 'John',
        age = 25,      -- 新增
    },
},

添加一个 computed:

lua 复制代码
ageInfo = function(data)
    return data.info.name .. ' is ' .. data.info.age .. ' years old'
end,

在场景中新建一个 Text,挂载 TextAdapter,BindTo 设为 "ageInfo"

6.3 修改命令行为

把 click 命令改为修改数据:

lua 复制代码
click = function(data)
    data.info.name = 'World'  -- 改数据
end,

点击按钮后,Text 会自动更新为 "Hello World!" ------ 这就是 MVVM 的威力,修改 Model 自动驱动 View 更新。

6.4 使用 InputField 实现双向绑定

在 Canvas 下创建一个 InputField,挂载 InputFieldAdapter,BindTo 设为 "info.name"。运行时在输入框中打字,Text 的文字会随之实时变化。因为:

  1. InputFieldAdapter 是 DataProducer<string> --- 用户输入触发 OnValueChange
  2. 引擎将输入值写回 data.info.name
  3. 响应式系统检测到 name 变化,触发 computed.message 重新计算
  4. 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 的文字就变了。

相关推荐
飞天狗1 小时前
长列表滚动卡顿怎么破:虚拟列表选型 + react-virtuoso 实战调优指南
react.js·性能优化
Jazz_z2 小时前
通过 Java 实现 Word 转 TXT
java·c#·word
丁小未2 小时前
基于MVVM框架的XUUI MoreComplex案例
unity·c#
ctrl_v助手2 小时前
C#表达题笔记
开发语言·c#
geovindu4 小时前
CSharp: Dijkstra Algorithms
开发语言·后端·算法·c#
zhangjw345 小时前
第29篇:Java伪共享与对象分配:并发性能优化的关键
java·开发语言·性能优化
公子小六5 小时前
基于.NET的Windows窗体编程之WinForms图像控件
windows·microsoft·c#·.net·winforms
吴可可1235 小时前
C# CAD自定义图元优化切割路径
c#
YigAin6 小时前
Unity Spine 资源出现白边的解决办法
unity·游戏引擎·spine