
Ни для кого не секрет, что Unity сейчас активно работают над новой системой создания пользовательского интерфейса UI Toolkit. Это инструмент разработки интерфейсов вдохновлённый стандартными подходами веб-разработки.
Пользовательский интерфейс состоит из двух основных частей:
UXML документ – язык разметки, основанный на HTML и XML, определяет структуру пользовательского интерфейса.
Unity Style Sheets (USS) – таблицы стилей, похожи на каскадные таблицы стилей CSS, применяют визуальные стили и поведение к пользовательскому интерфейсу.
И всё бы хорошо, но каково было моё удивление, что, проделав такую работу, они не предоставили механизма связывания данных, работающего в runtime. Формально, механизм связывания есть, но он работает только при создании интерфейсов для редактора.
А мне так хотелось вновь прикоснуться к WPF и MVVM, но в контексте Unity, что было решено разработать собственный механизм data-binding'а. Вдохновлялся я .NET Community Toolkit, так что если вы уже работали с этим набором инструментов, для вас всё будет максимально знакомо.
В результате получилась библиотека которая позволяет реализовать:
Связывание данных работающее в runtime.
Привязку нескольких свойств у одного UI элемента.
Поддержку кастомных UI элементов.
Совместима с UniTask для реализации асинхронных команд.
Библиотека содержит набор стандартных классов, которые облегчают создание приложений с использованием шаблона MVVM.
В этот набор входят:
Также реализован базовый набор UI элементов:
Давайте рассмотрим работу библиотеки на примере простого HelloWorld'а.
Для этого добавим UnityMvvmToolkit в проект:
Откройте
Edit/Project Settings/Package ManagerДобавьте новый
Scoped Registry
Name package.openupm.com URL https://package.openupm.com Scope(s) com.chebanovdd.unitymvvmtoolkit
Откройте
Window/Package ManagerВыберите
My RegistriesУстановите пакет
UnityMvvmToolkit
Первым делом создадим нашу ViewModel:
using UnityMvvmToolkit.Core; public class MyFirstViewModel : ViewModel { public string Text { get; } = "Hello World"; }
Далее добавим на сцену UI Document выбрав GameObject/UI Toolkit/UI Document.

Затем создадим файл MyFirstView.uxml. Это будет наша View.

После того как файл MyFirstView.uxml будет создан. Откройте его в UI Builder и добавьте UI элемент BindableLabel, установив в поле Binding Text Path значение Text (название нашего свойства из ViewModel).

Всё, наша View готова. Если открыть её в редакторе кода, то там будет примерно такое содержание:
<ui:UXML xmlns:uitk="UnityMvvmToolkit.UITK.BindableUIElements" xmlns:ui="UnityEngine.UIElements"> <uitk:BindableLabel binding-text-path="Text" /> </ui:UXML>
Заключительным шагом будет создание класса MyFirstDocumentView, который установит нашу ViewModel в качестве BindingContext'а для созданной View.
using UnityMvvmToolkit.UITK; public class MyFirstDocumentView : DocumentView<MyFirstViewModel> { }
Этот класс необходимо будет довавить к UI Document'у на сцене и задать нашу View там же.

Запустив проект, мы увидим наш Hello World.

Библиотека получилась довольно гибкой и расширяемой. В чём можно убедиться, изучив приложение Counter из папки примеров, где в UI используются только кастомные элементы пользовательского интерфейса.
Вот так выглядит CounterView:
<UXML> <BindableContentPage binding-theme-mode-path="ThemeMode" class="counter-screen"> <VisualElement class="number-container"> <BindableCountLabel binding-text-path="Count" class="count-label count-label--animation" /> </VisualElement> <BindableThemeSwitcher binding-value-path="ThemeMode, Converter={ThemeModeToBoolConverter}" /> <BindableCounterSlider increment-command="IncrementCommand" decrement-command="DecrementCommand" /> </BindableContentPage> </UXML>
А так CounterViewModel:
public class CounterViewModel : ViewModel { private int _count; private ThemeMode _themeMode; public CounterViewModel() { IncrementCommand = new Command(IncrementCount); DecrementCommand = new Command(DecrementCount); } public int Count { get => _count; set => Set(ref _count, value); } public ThemeMode ThemeMode { get => _themeMode; set => Set(ref _themeMode, value); } public ICommand IncrementCommand { get; } public ICommand DecrementCommand { get; } private void IncrementCount() => Count++; private void DecrementCount() => Count--; }
В заключение немного технической информации. Под капотом UnityMvvmToolkit использует reflection, но получение и установка значений свойств реализована через делегаты.
public static class PropertyInfoExtensions { public static Func<TObjectType, TValueType> CreateGetValueDelegate<TObjectType, TValueType>( this PropertyInfo propertyInfo) { return (Func<TObjectType, TValueType>) Delegate.CreateDelegate(typeof(Func<TObjectType, TValueType>), propertyInfo.GetMethod); } public static Action<TObjectType, TValueType> CreateSetValueDelegate<TObjectType, TValueType>( this PropertyInfo propertyInfo) { return (Action<TObjectType, TValueType>) Delegate.CreateDelegate(typeof(Action<TObjectType, TValueType>), propertyInfo.SetMethod); } }
Этот подход позволяет избежать упаковки (boxing) и распаковки (unboxing) для типов значений, что значительно улучшает производительность. В частности, он примерно в 65 раз быстрее, чем тот, который использует стандартные методы GetValue и SetValue, и вообще не приводит к выделению памяти.
Все исходники, примеры и документацию можно найти на GitHub, также пакет опубликован на площадке OpenUPM. Если у вас есть идеи, предложения или желание поучавствовать в разработке, добро пожаловать в дискуссии.
А что вы думаете о таком подходе создания пользовательских интерфейсов в Unity?