1. 项目概述与核心价值
如果你在Unity开发中,厌倦了在UI脚本里写满GetComponent<Text>().text = ...,或者为每个按钮的点击事件手动拖拽一堆回调函数,那么UnityMvvmToolkit这个开源项目,很可能就是你一直在找的“解药”。我作为一个在Unity项目里摸爬滚打多年的开发者,经历过UI逻辑和业务逻辑搅成一团浆糊的痛苦,也尝试过自己手搓一套简陋的绑定系统,最终发现维护成本高得吓人。UnityMvvmToolkit的出现,算是把WPF、Xamarin.Forms这些成熟框架里验证过的MVVM(Model-View-ViewModel)模式,以一种非常“Unity友好”的方式带到了游戏和应用开发中。
简单来说,它就是一个为Unity量身定制的数据绑定框架。它的核心价值在于,让你能用声明式的方式,在UI(无论是老旧的uGUI还是新的UI Toolkit)和背后的数据(ViewModel)之间建立连接。你不再需要手动去查找UI组件、监听事件、然后更新数值;你只需要在ViewModel里定义好可观察的属性(Property)和命令(Command),然后在UI的UXML或Inspector面板里,像写配置一样指定“这个Label要显示那个属性”、“这个按钮要触发那个命令”。框架会自动帮你处理所有的同步工作。这带来的最直接好处就是关注点分离:UI设计师可以专注于布局和样式,程序员可以专注于业务逻辑和数据流,两者通过清晰的契约(ViewModel)进行协作,极大地提升了代码的可维护性和可测试性。
从GitHub上544个Star和活跃的更新来看,这个项目已经相当成熟,支持UI Toolkit和uGUI双后端,内置了常用的绑定控件,并且考虑到了性能(对象池)和扩展性(自定义控件、值转换器)。接下来,我会带你从零开始,彻底拆解这个工具包,不仅告诉你“怎么用”,更会深入分析“为什么这么设计”,以及在实际项目中可能遇到的“坑”和应对技巧。
2. 核心架构与设计思想拆解
要玩转UnityMvvmToolkit,不能只停留在“照猫画虎”的层面,必须理解其背后的设计哲学。它的核心架构清晰地遵循了MVVM模式,但针对Unity引擎的特性做了大量适配。
2.1 MVVM模式在Unity中的落地
传统的MVC或MVP模式在Unity中常常变形,因为MonoBehaviour本身就是一个强视图(View)倾向的组件。UnityMvvmToolkit巧妙地引入了几个核心接口和基类,重新划清了界限:
- Model: 就是你的纯数据对象,比如
PlayerData、InventoryItem。它不关心UI,只负责存储状态。 - ViewModel: 这是桥梁和核心。它继承自
IBindingContext接口,这是一个标记接口,告诉绑定系统“我这个类里有东西可以绑”。ViewModel包含IProperty<T>和ICommand,它从Model获取数据,转换成UI友好的格式(可观察属性),并暴露用户操作(命令)。关键点:ViewModel应该是纯粹的C#类,不继承MonoBehaviour,这保证了它的可测试性,你可以在单元测试中直接实例化并测试其逻辑。 - View: 负责展示。在UI Toolkit中,是继承自
DocumentView<TBindingContext>的类;在uGUI中,是继承自CanvasView<TBindingContext>的类。View的职责很轻:挂载到UI Document或Canvas上,声明它要绑定哪个ViewModel,框架会自动扫描其下的UI元素并建立绑定。
这种设计的优势非常明显:当你的游戏状态(Model)发生变化时,你只需要更新ViewModel中的Property值,所有绑定到这个Property的UI元素会自动刷新,无需编写任何UI更新代码。同样,用户点击按钮,触发的是ViewModel中的Command,Command内部的业务逻辑可以任意复杂,但View对此一无所知。
2.2 数据绑定的实现机制
框架实现绑定的核心是“观察者模式”的变体。IProperty<T>接口有一个ValueChanged事件。当你写下binding-text-path="Count"时,框架在运行时:
- 通过反射(或预编译的源生成器)在ViewModel中找到名为
Count的IProperty<int>。 - 为这个Label创建一个对该Property的监听器(订阅
ValueChanged事件)。 - 当
Count.Value被设置新值时,Property会触发事件,监听器收到通知,然后自动更新Label的text属性。
这个过程对于开发者是透明的。你可能会担心反射的性能,这也是为什么项目提供了源生成器(UnityMvvmToolkit.Generator)作为付费选项。源生成器能在编译时分析你的代码,直接生成高效的绑定代码,完全消除运行时反射的开销。对于性能敏感的项目,这投资是值得的。
2.3 对UI Toolkit和uGUI的双重支持
这是该项目一个非常务实的设计。Unity目前处于UI系统的过渡期,新旧项目并存。
- UI Toolkit路径(
DocumentView): 这是未来的方向,尤其适合运行时UI和编辑器扩展。框架提供了BindableLabel,BindableButton等一套与UI Toolkit原生控件对应的可绑定版本。 - uGUI路径(
CanvasView): 照顾了大量存量项目。它通过为GameObject添加特定的MonoBehaviour组件(如BindableLabelUGUI)来实现绑定,原理类似,但底层依赖于Unity的GameObject/Component系统。
在实际选型时,新项目强烈建议直接上UI Toolkit,它的声明式UXML和样式系统USS与MVVM模式是天作之合。老项目迁移,如果UI不复杂,可以逐步用CanvasView替换旧逻辑;如果复杂,则需要评估重构成本。
3. 环境配置与项目初始化实操
理论讲完了,我们动手把环境搭起来。这里我会以UI Toolkit为主进行讲解,因为它是更现代的选择。
3.1 安装UnityMvvmToolkit
官方推荐通过OpenUPM安装,这是最规范的方式。
添加Scoped Registry:
- 打开Unity,进入
Edit->Project Settings->Package Manager。 - 在
Scoped Registries区域,点击“+”。 - 填写如下信息:
Name:package.openupm.comURL:https://package.openupm.comScope(s): 添加两行,分别是com.cysharp.unitask和com.chebanovdd.unitymvvmtoolkit。注意:这里必须添加UniTask的Scope,因为UnityMvvmToolkit的异步命令依赖它。
- 点击“Apply”。
- 打开Unity,进入
从Package Manager安装:
- 打开
Window->Package Manager。 - 将左上角的包源从“Unity Registry”切换到“My Registries”。
- 你应该能看到
UniTask和UnityMvvmToolkit。务必先安装UniTask,然后再安装UnityMvvmToolkit。
- 打开
注意:如果你在“My Registries”里看不到包,请检查Scoped Registry的Scope是否填写正确,或者尝试重启Unity。网络问题也可能导致列表加载失败。
替代安装法(Git URL):如果你需要锁定特定版本,或者公司内网环境特殊,可以使用Git URL安装。在Package Manager中点击“+”,选择“Add package from git URL”,输入:https://github.com/ChebanovDD/UnityMvvmToolkit.git?path=src/UnityMvvmToolkit.UnityPackage/Assets/Plugins/UnityMvvmToolkit。如需指定版本,在后面加上#v1.1.9这样的标签。
3.2 处理IL2CPP限制(重要!)
UnityMvvmToolkit底层使用了泛型虚拟方法,在Unity 2021及更早版本使用IL2CPP后端编译时,如果“IL2CPP Code Generation”设置为Faster runtime,可能会导致运行时错误。
解决方案:
- 打开
File->Build Settings。 - 在右下角找到
IL2CPP Code Generation选项。 - 将其从
Faster runtime更改为Faster (smaller) builds。 - 这个设置会略微增加包体大小,但能确保泛型代码被正确编译。Unity 2022及以上版本已修复此问题,无需此操作。
3.3 创建你的第一个数据绑定场景
我们来复现官方那个经典的Counter示例,这会让你立刻感受到MVVM的魔力。
创建ViewModel: 在
Scripts文件夹下创建CounterViewModel.cs。using UnityMvvmToolkit.Core; using UnityMvvmToolkit.Core.Interfaces; public class CounterViewModel : IBindingContext // 1. 实现标记接口 { public CounterViewModel() { // 2. 创建可观察属性 Count = new Property<int>(); // 3. 创建命令,关联到私有方法 IncrementCommand = new Command(IncrementCount); DecrementCommand = new Command(DecrementCount); } // 4. 对外暴露接口,而非具体实现类,这是良好的解耦实践 public IProperty<int> Count { get; } public ICommand IncrementCommand { get; } public ICommand DecrementCommand { get; } private void IncrementCount() => Count.Value++; // 5. 业务逻辑 private void DecrementCount() => Count.Value--; }解释一下:
Property<int>包装了一个int值,当它的Value被设置时,会自动通知所有绑定者。Command包装了一个Action(这里是IncrementCount方法)。创建View (UI Toolkit):
- 在
UI文件夹下创建CounterView.uxml(右键Create->UI Toolkit->UI Document)。 - 用UI Builder打开它,或者直接编辑XML。我们需要引用框架的命名空间并使用其控件。
<ui:UXML xmlns:ui="UnityEngine.UIElements" xmlns:uitk="UnityMvvmToolkit.UITK.BindableUIElements"> <ui:VisualElement class="container"> <!-- 绑定到Count属性,需要IntToStrConverter --> <uitk:BindableLabel binding-text-path="Count" name="CountLabel" class="count-text"/> <!-- 绑定到增加命令 --> <uitk:BindableButton text="+" command="IncrementCommand" class="inc-btn"/> <!-- 绑定到减少命令 --> <uitk:BindableButton text="-" command="DecrementCommand" class="dec-btn"/> </ui:VisualElement> </ui:UXML>注意
binding-text-path="Count"和command="IncrementCommand",它们就像魔法咒语,将UI元素和ViewModel的属性/命令连接起来。- 在
创建View脚本: 创建
CounterView.cs脚本,继承DocumentView<CounterViewModel>。using UnityMvvmToolkit.UITK; public class CounterView : DocumentView<CounterViewModel> { // 我们需要提供一个值转换器,把int转换成string供Label显示 protected override IValueConverter[] GetValueConverters() { return new IValueConverter[] { new IntToStrConverter() }; } }DocumentView基类会帮你完成所有繁重的工作:加载UXML、查找所有IBindableElement、实例化CounterViewModel、建立绑定。组装场景:
- 在场景中创建一个空GameObject,命名为
CounterUI。 - 为其添加
UIDocument组件。 - 将
CounterView.uxml拖拽到UIDocument的Source Asset栏。 - 为
CounterUI添加CounterView组件。 - 运行游戏!点击按钮,你会看到Label的数字随之变化。整个过程,你没有在View脚本里写一句
Find或GetComponent,也没有手动为按钮添加onClick监听。
- 在场景中创建一个空GameObject,命名为
4. 核心组件深度解析与实战应用
掌握了基础流程,我们来深入看看框架提供的各种“武器”,以及如何在实际项目中灵活运用。
4.1 属性(Property)的进阶用法
Property<T>不仅仅是简单的包装器。
只读属性:当你有一个属性只应由ViewModel内部修改,而View仅负责显示时,使用ReadOnlyProperty<T>。这提供了更好的封装性。
public class PlayerViewModel : IBindingContext { private int _health; public PlayerViewModel(int maxHealth) { MaxHealth = new ReadOnlyProperty<int>(maxHealth); // View只能读,不能改 CurrentHealth = new Property<int>(maxHealth); } public IReadOnlyProperty<int> MaxHealth { get; } public IProperty<int> CurrentHealth { get; } }使用[Observable]属性简化代码:手动创建Property字段和公开IProperty属性很繁琐。你可以使用[Observable]特性。
public class ItemViewModel : IBindingContext { [Observable] // 自动生成名为“ItemName”的绑定路径(自动去除字段名的下划线/m_前缀) private readonly IProperty<string> _itemName = new Property<string>(); [Observable("DisplayPrice")] // 显式指定绑定路径名 private readonly IProperty<float> _price = new Property<float>(); // 公开一个传统属性,内部与可观察字段联动 public string ItemName { get => _itemName.Value; set => _itemName.Value = value; } }在UXML中,你可以直接绑定到ItemName和DisplayPrice。这减少了大量样板代码。
包装非可观察模型:这是非常常见的场景。你的数据来自网络或数据库,是一个普通的POCO类。你需要让它变得可观察。
public class UserModel { public string Name { get; set; } } public class UserViewModel : IBindingContext { private readonly UserModel _model; [Observable(nameof(Name))] private readonly IProperty<string> _name = new Property<string>(); public UserViewModel(UserModel model) { _model = model; _name.Value = model.Name; // 初始化 } public string Name { get => _model.Name; set { if (_name.TrySetValue(value)) // 尝试更新可观察属性 { _model.Name = value; // 如果成功(值确实变了),更新底层模型 // 这里可以触发保存到服务器等操作 } } } }TrySetValue方法会检查新值是否与旧值相等,只有不同时才更新并触发事件,避免了不必要的通知和UI刷新。
4.2 命令(Command)与异步命令(AsyncCommand)
命令是用户操作的抽象。
带参数的Command:按钮可能需要传递一个参数,比如删除某个特定ID的项。
public class TodoViewModel : IBindingContext { public TodoViewModel() { DeleteItemCommand = new Command<int>(DeleteItem); } public ICommand<int> DeleteItemCommand { get; } private void DeleteItem(int itemId) { // 根据itemId删除项 } }在UXML中,你需要配合ParameterValueConverter来传递参数:<uitk:BindableButton command="DeleteItemCommand, 123, ParameterToIntConverter" />。
强大的AsyncCommand:现代应用离不开异步操作,如下载、网络请求。AsyncCommand基于UniTask,用起来非常顺手。
public class DownloadViewModel : IBindingContext { public DownloadViewModel() { // DisableOnExecution = true 是黄金搭档,防止用户重复点击 DownloadCommand = new AsyncCommand(DownloadFileAsync, DisableOnExecution = true); Status = new Property<string>("就绪"); } public IAsyncCommand DownloadCommand { get; } public IProperty<string> Status { get; } private async UniTask DownloadFileAsync(CancellationToken cancellationToken) { Status.Value = "下载中..."; try { // 模拟下载 await UniTask.Delay(2000, cancellationToken: cancellationToken); Status.Value = "下载完成!"; } catch (OperationCanceledException) { Status.Value = "下载已取消"; } catch (Exception ex) { Status.Value = $"下载失败: {ex.Message}"; } } }DisableOnExecution: 命令执行时自动禁用关联的按钮,完美解决重复提交问题。WithCancellation(): 可以获取一个支持取消的AsyncCommand,结合一个“取消”按钮,实现操作中断。AllowConcurrency: 设置为true时,允许同一个命令被并发执行多次(谨慎使用)。
4.3 值转换器(Value Converter)——数据与UI的翻译官
UI显示的数据类型和ViewModel中的数据类型经常不一致。例如,ViewModel存的是enum ThemeMode { Light, Dark },但Toggle开关需要的是bool。这就是值转换器的用武之地。
创建自定义PropertyValueConverter:
// 定义枚举 public enum ThemeMode { Light, Dark } // 创建转换器 public class ThemeModeToBoolConverter : PropertyValueConverter<ThemeMode, bool> { // ViewModel -> UI (ThemeMode 转 bool) public override bool Convert(ThemeMode value) { return value == ThemeMode.Dark; // Dark -> true, Light -> false } // UI -> ViewModel (bool 转 ThemeMode) - 双向绑定需要 public override ThemeMode ConvertBack(bool value) { return value ? ThemeMode.Dark : ThemeMode.Light; } }在View中注册:
public class SettingsView : DocumentView<SettingsViewModel> { protected override IValueConverter[] GetValueConverters() { return new IValueConverter[] { new ThemeModeToBoolConverter() }; } }在UXML中使用:
<!-- 三种写法等效 --> <uitk:BindableToggle binding-value-path="ThemeMode, Converter={ThemeModeToBoolConverter}" /> <uitk:BindableToggle binding-value-path="ThemeMode, ThemeModeToBoolConverter" /> <uitk:BindableToggle binding-value-path="ThemeMode" /> <!-- 自动寻找匹配的转换器 -->内置转换器:框架提供了IntToStrConverter和FloatToStrConverter,这就是为什么之前Counter例子中Label能直接显示int类型的Count。
4.4 集合绑定:BindableListView与BindableScrollView
显示列表数据(如背包、聊天记录、排行榜)是UI的常见需求。框架提供了两种方式:
BindableListView:首选。它基于ListView,支持虚拟化。这意味着即使你有成百上千条数据,它也只会创建屏幕上可见的那几条Item对应的VisualElement,滚动时复用,性能极佳。BindableScrollView: 基于ScrollView,不支持虚拟化。它会为数据源中的每一项都创建一个VisualElement。只适合数据量很少(比如少于50条)的情况。
使用BindableListView的完整流程:
定义Item数据模型和ViewModel:
public class TodoItem : ICollectionItem // 实现ICollectionItem接口,提供唯一Id { public int Id { get; set; } // 必须,用于虚拟化复用时的标识 [Observable] private readonly IProperty<string> _title = new Property<string>(); [Observable] private readonly IProperty<bool> _isCompleted = new Property<bool>(); public string Title { get => _title.Value; set => _title.Value = value; } public bool IsCompleted { get => _isCompleted.Value; set => _isCompleted.Value = value; } }创建Item的UI模板(
TodoItemView.uxml):<ui:UXML xmlns:ui="UnityEngine.UIElements" xmlns:uitk="UnityMvvmToolkit.UITK.BindableUIElements"> <ui:VisualElement class="todo-item"> <uitk:BindableLabel binding-text-path="Title" class="title"/> <uitk:BindableToggle binding-value-path="IsCompleted" class="toggle"/> </ui:VisualElement> </ui:UXML>创建主ViewModel:
public class TodoListViewModel : IBindingContext { public TodoListViewModel() { var items = new ObservableCollection<TodoItem> { new() { Id = 1, Title = "学习UnityMvvmToolkit", IsCompleted = true }, new() { Id = 2, Title = "写一个示例项目", IsCompleted = false }, }; // 使用ObservableCollection,其变化能自动通知ListView TodoItems = new ReadOnlyProperty<ObservableCollection<TodoItem>>(items); } public IReadOnlyProperty<ObservableCollection<TodoItem>> TodoItems { get; } }创建主View并注册模板:
public class TodoListView : DocumentView<TodoListViewModel> { [SerializeField] private VisualTreeAsset _todoItemTemplate; // 拖入TodoItemView.uxml protected override IReadOnlyDictionary<Type, object> GetCollectionItemTemplates() { return new Dictionary<Type, object> { { typeof(TodoItem), _todoItemTemplate } }; } }在主UXML中使用BindableListView:
<ui:UXML xmlns:ui="UnityEngine.UIElements" xmlns:uitk="UnityMvvmToolkit.UITK.BindableUIElements"> <uitk:BindableListView binding-items-source-path="TodoItems" class="todo-list"/> </ui:UXML>运行后,一个功能完整、性能高效的Todo列表就出来了。增删改
TodoItems集合,UI会自动同步。
5. 高级技巧、性能优化与避坑指南
当项目规模变大,你需要更深入地了解这个工具,以规避陷阱并提升效率。
5.1 使用BindingContextProvider管理嵌套上下文
复杂的UI通常有层次结构。一个页面(MainViewModel)里可能包含一个用户信息卡片(UserViewModel)和一个设置面板(SettingsViewModel)。你不想把所有属性都塞进一个巨大的ViewModel里。
BindingContextProvider就是为解决这个问题而生。它允许你在UI树中的某个局部,切换当前绑定的上下文。
<ui:UXML xmlns:ui="UnityEngine.UIElements" xmlns:uitk="UnityMvvmToolkit.UITK.BindableUIElements"> <!-- 根上下文是MainViewModel --> <uitk:BindableLabel binding-text-path="PageTitle"/> <!-- 绑定到MainViewModel.PageTitle --> <uitk:BindingContextProvider binding-context-path="UserInfo"> <!-- 这个区域内的所有绑定,上下文切换为MainViewModel.UserInfo (类型是UserViewModel) --> <uitk:BindableLabel binding-text-path="UserName"/> <!-- 绑定到UserViewModel.UserName --> <uitk:BindableLabel binding-text-path="Level"/> <!-- 绑定到UserViewModel.Level --> </uitk:BindingContextProvider> <uitk:BindingContextProvider binding-context-path="Settings"> <!-- 这个区域内的所有绑定,上下文切换为MainViewModel.Settings --> <uitk:BindableToggle binding-value-path="IsSoundOn" /> </uitk:BindingContextProvider> </ui:UXML>对应的ViewModel:
public class MainViewModel : IBindingContext { public MainViewModel() { PageTitle = new Property<string>("主页"); UserInfo = new Property<UserViewModel>(new UserViewModel()); Settings = new Property<SettingsViewModel>(new SettingsViewModel()); } public IProperty<string> PageTitle { get; } public IProperty<UserViewModel> UserInfo { get; } // 子ViewModel public IProperty<SettingsViewModel> Settings { get; } // 子ViewModel }这实现了完美的模块化和解耦,每个功能块管理自己的数据和逻辑。
5.2 创建自定义可绑定控件
框架内置的控件有限,你肯定会需要自定义控件,比如一个绑定Texture2D的BindableImage。
手动创建(理解原理):
- 创建基础控件
ImageElement。 - 创建
BindableImage,实现IBindableElement接口,手动处理SetBindingContext和ResetBindingContext,订阅属性变化事件。 这个过程冗长且容易出错,代码量很大(如之前文档所示)。
使用源生成器(高效实践): 这才是推荐的方式。你需要安装可选的UnityMvvmToolkit.Generator(目前是赞助者专属)。使用后,代码变得极其简洁:
[UxmlElement] // 生成UxmlFactory public partial class ImageElement : VisualElement { public void SetImage(Texture2D image) => style.backgroundImage = new StyleBackground(image); } [BindableElement] // 标记为可绑定元素 public partial class BindableImage : ImageElement { [BindableProperty] // 标记需要绑定的属性 private IReadOnlyProperty<Texture2D> _imageProperty; partial void OnImagePropertyValueChanged(Texture2D value) { SetImage(value); // 属性变化时的回调 } }编译后,生成器会自动为你生成完整的UxmlFactory和绑定胶水代码。你只需要关注业务逻辑。这大大提升了开发效率和代码可维护性。
5.3 性能优化:对象池与预热
数据绑定在运行时不可避免会有对象分配(如属性访问器、转换器实例)。UnityMvvmToolkit内部使用了对象池来缓解这个问题。但你还可以主动进行“预热”。
在你的基础View类中重写GetObjectProvider方法:
public abstract class AppBaseView<T> : DocumentView<T> where T : class, IBindingContext { protected override IObjectProvider GetObjectProvider() { return new BindingContextObjectProvider(new IValueConverter[] { new IntToStrConverter(), new FloatToStrConverter() }) .WarmupAssemblyViewModels() // 预热本程序集中所有ViewModel .WarmupViewModel<MyFrequentlyUsedViewModel>() // 预热特定ViewModel .WarmupValueConverter<MyCustomConverter>(10); // 预热10个自定义转换器实例 } }预热的意义:在游戏初始化或场景加载时,提前创建好一批常用对象放入池中。当游戏运行时需要这些对象时,直接从池中取出,用完后归还,避免了频繁的垃圾回收(GC),对性能有显著提升,尤其是在移动设备上。
5.4 常见问题与排查技巧
绑定不生效,UI无变化:
- 检查点1:路径拼写。
binding-text-path="Count"中的Count必须与ViewModel中公开的IProperty<int> Count { get; }属性名完全一致,大小写敏感。 - 检查点2:值转换器。如果绑定的是
int到Label,确保View中注册了IntToStrConverter。 - 检查点3:Property值是否真的改变。确保你是通过
Property.Value = newValue来赋值,而不是直接修改一个局部变量。使用TrySetValue可以避免无变化赋值。 - 检查点4:UI Toolkit的USS影响。有时USS样式(如
display: none)会隐藏元素,让你误以为绑定失败。检查USS或使用UI Debugger。
- 检查点1:路径拼写。
命令不触发:
- 检查点1:命令名称。
command="MyCommand"中的MyCommand必须是ICommand类型的属性。 - 检查点2:CanExecute。
Command构造函数可以传入第二个参数Func<bool> canExecute。如果提供了且返回false,按钮会是禁用状态。检查你的canExecute逻辑。 - 检查点3:AsyncCommand的状态。如果
AsyncCommand正在执行且DisableOnExecution为true,按钮会被禁用。等待异步操作完成。
- 检查点1:命令名称。
列表(ListView)不显示或滚动卡顿:
- 确保Item实现了
ICollectionItem并提供了稳定的Id。这是虚拟化正确复用的关键。Id在列表生命周期内不能改变。 - 对于大量数据,务必使用
BindableListView而非BindableScrollView。 - 检查Item模板的复杂度。过于复杂的Item模板(嵌套过深、大量元素)会影响虚拟化性能。尽量简化。
- 确保Item实现了
在Unity 2021及以下版本IL2CPP打包后崩溃:
- 这是最容易被忽略的坑!务必确认已将
Build Settings中的IL2CPP Code Generation从Faster runtime改为Faster (smaller) builds。
- 这是最容易被忽略的坑!务必确认已将
自定义控件在UI Builder中不显示或属性不显示:
- 使用
[UxmlElement]特性并继承VisualElement的控件,通常需要编译一次项目后,才能在UI Builder的库中看到并拖拽使用。 - 确保自定义控件的程序集在UI Builder的查询范围内。
- 使用
我个人在几个中型项目中使用UnityMvvmToolkit后,最大的体会是:前期花时间理解和搭建MVVM结构是值得的。当UI复杂度增长时,它的优势会指数级体现。调试时,数据流非常清晰;修改UI时,几乎不用碰业务逻辑代码;测试ViewModel也变得异常简单。虽然学习曲线比直接写MonoBehaviour略陡,但它带来的长期维护收益和代码整洁度,绝对是面向生产的Unity项目应该认真考虑的选择。