基于MVVM框架的XUUI Tutorial_App — 多模块应用框架教程

发布时间:2026/7/10 1:50:48

基于MVVM框架的XUUI Tutorial_App — 多模块应用框架教程
Tutorial_App — 多模块应用框架与之前教程最大的不同模块化架构Helloworld 和 MoreComplex 都是单个 ViewModel 的简单示例。App 模式引入了真正的模块化架构将 UI 逻辑拆分为多个模块module各自独立每个模块有自己的data/computed/commands互不干扰模块之间通过exports可控交互支持模块热刷新reload不影响其他模块1. 场景搭建1.1 创建 GameObject 层级App (GameObject) ├── Canvas │ ├── Module1InfoText (Text) ← 显示 module1.info │ ├── Module1ClickBtn (Button) ← 触发 module1.click │ ├── Module2InfoText (Text) ← 显示 module2.info │ ├── Module2ClickBtn (Button) ← 触发 module2.click │ ├── SayHelloBtn (Button) ← 触发 module2.say_hello_to_csharp │ ├── ReloadModule1Btn (Button) ← 重载 module1 │ └── ReloadModule2Btn (Button) ← 重载 module21.2 挂载适配器GameObject适配器BindTo说明Module1InfoTextTextAdaptermodule1.info显示 module1 的 computed.infoModule1ClickBtnButtonAdaptermodule1.click触发 module1 的 click 命令Module2InfoTextTextAdaptermodule2.info显示 module2 的 computed.infoModule2ClickBtnButtonAdaptermodule2.click触发 module2 的 click 命令SayHelloBtnButtonAdaptermodule2.say_hello_to_csharp调用 C# 的 HelloCSharpReloadModule1BtnButtonAdapterReloadModule1App.cs 的 Command重载 module1ReloadModule2BtnButtonAdapterReloadModule2App.cs 的 Command重载 module2关键区别BindTo 的格式变为模块名.路径因为 App 模式下有多个模块UI 需要显式指定数据来自哪个模块。2. 代码解析2.1 App.cs —— C# 入口usingUnityEngine;usingXUUI;publicclassApp:MonoBehaviour{Contextcontextnull;voidStart(){contextnewContext( return { name myapp, modules {module1, module2}, } );context.AddCSharpModule(ModuleManager,this);context.Attach(gameObject);}voidOnDestroy(){context.Dispose();}[Export]publicvoidHelloCSharp(Interface2data,intp){Debug.Log(data.selectdata.select, pp);}[Command]publicvoidReloadModule1(){context.ReloadModule(module1);}[Command]publicvoidReloadModule2(){context.ReloadModule(module2);}}逐行解释代码作用name myapp定义应用名称模块加载时会按myapp.module1、myapp.module2的路径 requiremodules {module1, module2}声明两个模块框架自动加载并为每个创建独立沙盒context.AddCSharpModule(ModuleManager, this)将当前 C# 对象注册为名为 “ModuleManager” 的模块其[Export]方法可在 Lua 中调用context.Attach(gameObject)绑定 UI开始数据监听[Export] public void HelloCSharp(...)标记为导出函数Lua 代码可以调用ModuleManager.HelloCSharp(data, 1024)[Command] public void ReloadModule1()标记为命令UI 可通过 BindTo“ReloadModule1” 绑定按钮2.2 module1.lua.txtreturn{data{namehaha,select0,},commands{clickfunction(data)module2.set_select(data.select)-- 跨模块调用调用 module2 的 exportsdata.selectdata.select0and1or0end,},computed{infofunction(data)returnstring.format(i am %s, my select is %d,data.name,data.select)end,},exports{hellofunction(p)print(hello, p ..p)end,},}2.3 module2.lua.txtlocaldata{messagehehe,select1,}return{datadata,commands{clickfunction(data)module1.hello(1)-- 跨模块调用调用 module1 的 exportsdata.selectdata.select0and1or0end,say_hello_to_csharpfunction(data)ModuleManager.HelloCSharp(data,1024)-- 调用 C# 的 Export 方法end,},computed{infofunction(data)returnstring.format(message is %s, select is %d,data.message,data.select)end,},exports{set_selectfunction(p)data.selectpend,},}3. 核心概念详解3.1 模块沙盒与数据隔离每个模块运行在独立的沙盒中沙盒 module1: data { name haha, select 0 } commands.click 只能读写 module1 的 data computed.info 只能读取 module1 的 data 沙盒 module2: data { message hehe, select 1 } commands.click 只能读写 module2 的 data computed.info 只能读取 module2 的 data这意味着即使两个模块定义了同名变量如select也不会冲突command 函数收到的是自己模块的 data不能直接修改其他模块的数据不同开发团队可以独立开发各自模块不用担心全局命名冲突3.2 模块间通信exports模块通过exports暴露接口供其他模块调用module2.commands.click 调用 module1.hello(1) ↓ 调用 module1.exports.hello(p) ↓ 打印 hello, p 1 module1.commands.click 调用 module2.set_select(data.select) ↓ 调用 module2.exports.set_select(p) ↓ 设置 module2 的 data.select p注意module2.exports.set_select使用了闭包引用外部的local datalocaldata{...}-- 闭包变量return{datadata,exports{set_selectfunction(p)data.selectp-- 直接操作闭包引用的 dataend,},}这种方式绕过了 command 的只能看到自己 data的限制——因为set_select不是 command不接收data参数而是通过 Lua 闭包直接访问原始data表。这在需要允许其他模块修改本模块数据时非常有用。3.3 C# 桥接Export 与 AddCSharpModuleApp.cs 中AddCSharpModule(ModuleManager, this)注册后Lua 侧调用: ModuleManager.HelloCSharp(data, 1024) ↓ XLua 桥接: data → XLua 自动包装为 Interface2 代理 ↓ C# 侧: HelloCSharp(Interface2 data, int p) → Debug.Log(data.select data.select , p 1024)这里的Interface2是一个 C# 接口XLua 自动将 Lua table 代理为实现了该接口的对象。所以data.select读取的就是 Lua 模块数据表中的select字段。3.4 模块热刷新Reload[Command]publicvoidReloadModule1(){context.ReloadModule(module1);}调用context.ReloadModule(module1)时重新 requiremyapp.module1加载最新的 Lua 脚本保留旧的data状态数据不丢失用新的commands替换旧的——UI 上已绑定的按钮自动关联新命令重新计算所有computed——UI 自动更新module2完全不受影响这非常适合需要热更新的场景修改 Lua 脚本后点击 Reload 按钮立即生效无需重新运行 Unity。4. 完整数据流初始化流程App.Start() └─ new Context(luaScript) ├─ 解析 namemyapp, modules{module1,module2} ├─ require myapp.module1 → 创建沙盒1包装响应式数据 ├─ require myapp.module2 → 创建沙盒2包装响应式数据 └─ 注册模块间引用module1 中可访问 module2反之亦然 └─ context.AddCSharpModule(ModuleManager, this) └─ 注册 [Export] 方法到 Lua 环境 └─ context.Attach(gameObject) └─ 扫描适配器按 模块名.路径 绑定点击 module1.click 时用户点击 Module1ClickBtn → ButtonAdapter.OnAction() → commands[module1.click](module1.data) → module2.set_select(module1.data.select) -- 跨模块调用 → module1.data.select 0 → module2 的 data 变为 0 → module2 的 UI 自动更新 → module1.data.select 1 -- 修改自己的数据 → module1 的 UI 自动更新点击 SayHelloBtn 时用户点击 SayHelloBtn → ButtonAdapter.OnAction() → commands[module2.say_hello_to_csharp](module2.data) → ModuleManager.HelloCSharp(data, 1024) → XLua 接口代理 → C# HelloCSharp(Interface2 data, int p) → Debug.Log(data.select0, p1024)5. 绑定关系总结GameObject适配器BindToViewModel 映射方向Module1InfoTextTextAdaptermodule1.infomodule1 的 computed.infoVM → ViewModule1ClickBtnButtonAdaptermodule1.clickmodule1 的 commands.clickView → VMModule2InfoTextTextAdaptermodule2.infomodule2 的 computed.infoVM → ViewModule2ClickBtnButtonAdaptermodule2.clickmodule2 的 commands.clickView → VMSayHelloBtnButtonAdaptermodule2.say_hello_to_csharpmodule2 的 commands.say_hello_to_csharpView → VMReloadModule1BtnButtonAdapterReloadModule1App.cs 的 [Command] ReloadModule1View → C#ReloadModule2BtnButtonAdapterReloadModule2App.cs 的 [Command] ReloadModule2View → C#6. 跟之前教程的对比特性HelloworldMoreComplexApp 模式模块数1 个隐含模块1 个隐含模块多个独立模块BindTo 格式infooptionsmodule1.info数据隔离无无各模块沙盒隔离模块通信N/AN/A通过 exportsC# 集成无无AddCSharpModule Export热刷新不支持不支持ReloadModule 支持Lua 文件位置内联在 C# 中内联在 C# 中独立的 .lua.txt 文件7. 动手实验7.1 观察模块隔离点击 module1 的 Click 按钮观察两个 InfoText 的变化。你会看到Module1InfoText 里的select在 0 和 1 之间切换Module2InfoText 里的select可能也被 module1 的跨模块调用改变了7.2 尝试跨模块调用链修改module1.commands.click让它调用module2.hello()如果 module2 也有 exports.hello 的话观察调用链如何串联两个模块。7.3 测试热刷新运行场景观察 UI 显示修改module1.lua.txt中的data.name haha改为data.name xixi点击ReloadModule1按钮观察 Module1InfoText 的显示更新为 “i am xixi, …” 而 Module2InfoText 不受影响7.4 新增一个 module3在Resources/myapp/下创建module3.lua.txt在 App.cs 的 modules 列表中加入module3添加对应的 UI 和适配器绑定8. 完整文件清单Assets/ ├── Scenes/ │ └── App.unity ← 本示例的场景 ├── Scripts/ │ └── App.cs ← C# 入口App 模式启动器 ├── Resources/ │ └── myapp/ │ ├── module1.lua.txt ← 模块1data/commands/computed/exports │ └── module2.lua.txt ← 模块2data/commands/computed/exports C# 桥接 └── XUUI/ ├── Scripts/ │ ├── ViewModel.cs ← Context 类包含 AddCSharpModule / ReloadModule │ ├── AdapterBase.cs │ └── UGUIAdapter/ │ ├── TextAdapter.cs │ ├── ButtonAdapter.cs │ └── ... └── Resources/ ├── xuui.lua.txt ← XUUI 主模块包含 app 模式的初始化逻辑 ├── binding.lua.txt ← 绑定引擎 ├── observeable.lua.txt ← 响应式数据系统 └── ...9. 常见问题Q: module1 和 module2 是全局变量吗是的但它们是沙盒内的全局变量。框架在加载每个模块时会把其他模块名注册到当前沙盒的全局环境中所以module1中可以直接用module2.set_select(...)。这本质上是框架管理的命名空间不是真正的 Lua 全局变量不会污染全局环境。Q: 为什么 exports 里的函数能修改 data 而 command 不行command 的函数签名是function(data)这里的data是框架传入的当前模块的 data 副本引用command 只能读写本模块的 data。而 exports 里的函数通过 Lua闭包捕获了外部的data变量可以自由操作。这是设计上的有意区别command 是模块对外的行为接口exports 是模块间的编程接口。Q: Reload 后 data 会重置吗不会。ReloadModule会保留当前的 data 状态只替换 commands、computed 和 exports。所以你在 Reload 前修改的数据会保持不变。Q: 模块脚本必须放在 Resources/myapp 目录下吗不是必须的。默认按 Lua 的 require 规则查找但可以通过CustomLoader自定义加载路径和文件后缀。为了演示方便本示例放在Resources/myapp目录下。

相关新闻

2026年AI量化提效,工具重点要跟阶段走

2026年AI量化提效,工具重点要跟阶段走

2026/7/10 1:50:48

AI 辅助量化开发时,很多效率来自把阶段任务拆清楚。已有经验者通常不缺基本概念,但可能会低估一个事实:在不同开发阶段,真正需要工具帮助的地方会变化,不能把所有阶段都当成同一种问题来处理。让 AI 先帮你把问题问清楚…

毕业季不再焦虑!7款一键生成论文工具1天实现毕业流程全通关

毕业季不再焦虑!7款一键生成论文工具1天实现毕业流程全通关

2026/7/10 1:40:48

先打破错观念:你正在用的“攒论文”方法,正在害你毕不了业 千万别再熬夜蹲图书馆攒论文了!也别再当“学术裁缝”东拼西凑剪别人的内容了!更别随便找个通用大模型直接生成全文直接用了! 这些看起来“省时间”的旧做法…

FPGA交通灯控制系统:从基础设计到带紧急按键与倒计时显示的3大功能扩展

FPGA交通灯控制系统:从基础设计到带紧急按键与倒计时显示的3大功能扩展

2026/7/10 1:40:48

FPGA交通灯控制系统:从基础设计到三大功能扩展实战在数字系统设计领域,交通灯控制系统作为经典的教学案例,涵盖了状态机、时序控制、外设驱动等核心知识点。本文将从一个基础交通灯控制方案出发,逐步实现紧急按键响应、倒计时显示…

AI系统监控预警:从数据漂移到模型行为的四层实战体系

AI系统监控预警:从数据漂移到模型行为的四层实战体系

2026/7/10 3:10:51

1. 项目概述:从“有监控”到“会监控”的认知跃迁在AI应用架构师的日常工作中,系统监控预警是保障服务稳定性的生命线。然而,我见过太多团队,投入了大量资源搭建了看似完备的监控体系,告警灯却依然在半夜不合时宜地闪烁…

DC-DC电感选型误区解析:5个常见计算错误与实测验证

DC-DC电感选型误区解析:5个常见计算错误与实测验证

2026/7/10 3:10:51

DC-DC电感选型误区解析:5个常见计算错误与实测验证在DC-DC电源设计中,电感选型是决定转换效率、输出纹波和系统稳定性的关键环节。然而,即使经验丰富的工程师也常陷入一些计算误区。本文将剖析五个典型错误,结合MP2330C评估板的实…

ROS Noetic + Cartographer 实战:Raspberry Pi 3B+ 与 Kinect 融合建图避坑 3 要点

ROS Noetic + Cartographer 实战:Raspberry Pi 3B+ 与 Kinect 融合建图避坑 3 要点

2026/7/10 3:10:51

ROS Noetic Cartographer 实战:Raspberry Pi 3B 与 Kinect 融合建图避坑 3 要点在低成本SLAM小车的开发中,将激光雷达与深度相机数据融合是一个极具挑战性的任务。本文将聚焦于Raspberry Pi 3B这一嵌入式平台,分享在ROS Noetic环境下使用Car…

vivo探索游戏推荐“最后一公里”:大模型助力用户从“给结果”走向“帮决策”

vivo探索游戏推荐“最后一公里”:大模型助力用户从“给结果”走向“帮决策”

2026/7/10 3:10:51

排序精准,用户仍难选择做游戏分发时,有两类场景反复出现。一类是用户打开页面看到几款游戏,排序虽把最可能被点的放前面,但用户因几款游戏看着差不多而犹豫;另一类是用户拿玩过的游戏当锚找相似的,却需自己…

【软件工程】软件生命周期(SDLC)各阶段划分

【软件工程】软件生命周期(SDLC)各阶段划分

2026/7/10 3:10:51

考点频率:★★★★☆(基础概念,选择题常考,也是理解开发模型的前提) 难度:⭐ 建议:掌握软件生命周期的六个阶段及各阶段的主要任务与产出物,理解“文档驱动”的含义 1️⃣ 什么是软件…

Ultralytics Docker镜像选型与YOLO训练部署实战指南

Ultralytics Docker镜像选型与YOLO训练部署实战指南

2026/7/10 3:00:51

1. 为什么是 Ultralytics Docker?——不是“又一个容器教程”,而是解决实际训练卡点的钥匙Ultralytics Docker 不是单纯把 YOLOv8/YOLOv10 搬进容器里走个过场,它是我在过去三年带团队落地 27 个工业视觉项目时,反复验证后沉淀下来…

解锁AMD Ryzen处理器深层性能:SMU Debug Tool完全指南

解锁AMD Ryzen处理器深层性能:SMU Debug Tool完全指南

2026/7/9 19:40:56

解锁AMD Ryzen处理器深层性能:SMU Debug Tool完全指南 【免费下载链接】SMUDebugTool A dedicated tool to help write/read various parameters of Ryzen-based systems, such as manual overclock, SMU, PCI, CPUID, MSR and Power Table. 项目地址: https://gi…

6个月转型AI工程师:实战路径与核心技能

6个月转型AI工程师:实战路径与核心技能

2026/7/9 18:28:30

1. 项目概述:6个月转型AI工程师的可行性路径在2023年大模型技术爆发的背景下,AI工程师岗位需求同比增长217%(LinkedIn数据)。不同于传统算法工程师需要3-5年培养周期,现代AI工程师更侧重工程化落地能力。我在硅谷科技公…

YOLOv5模型剪枝与量化实战:边缘设备部署优化

YOLOv5模型剪枝与量化实战:边缘设备部署优化

2026/7/8 14:04:34

1. 项目背景与核心价值在计算机视觉领域,YOLOv5因其出色的实时检测性能成为工业界宠儿。但当我们尝试将其部署到边缘设备(如树莓派、Jetson Nano或手机终端)时,立刻会遇到两个致命问题:模型体积庞大(原始YO…

终极原神FPS解锁器完整指南:轻松突破60帧限制

终极原神FPS解锁器完整指南:轻松突破60帧限制

2026/7/10 0:00:42

终极原神FPS解锁器完整指南:轻松突破60帧限制 【免费下载链接】genshin-fps-unlock unlocks the 60 fps cap 项目地址: https://gitcode.com/gh_mirrors/ge/genshin-fps-unlock 原神FPS解锁器是一款专为《原神》玩家设计的开源工具,通过先进的Wri…

YesPlayMusic:如何用高颜值播放器重塑你的网易云音乐体验?[特殊字符]

YesPlayMusic:如何用高颜值播放器重塑你的网易云音乐体验?[特殊字符]

2026/7/10 0:00:42

YesPlayMusic:如何用高颜值播放器重塑你的网易云音乐体验?🎵 【免费下载链接】YesPlayMusic 高颜值的第三方网易云播放器,支持 Windows / macOS / Linux :electron: 项目地址: https://gitcode.com/gh_mirrors/ye/YesPlayMusic…

从零实现红黑树:手写C++的set与map容器

从零实现红黑树:手写C++的set与map容器

2026/7/10 0:00:42

1. 项目概述:从STL容器到自研轮子在C的日常开发中,std::set和std::map是我们再熟悉不过的伙伴了。它们一个负责管理不重复的集合,一个负责维护键值对映射,底层都依赖一颗高效的红黑树来保证数据的有序性和操作的性能。但你是否曾想…