Unity UI Toolkit: MVVM ннада?
Изображёные приложения вы можете найти на GitHub проекта в папке примеров
Ни для кого не секрет, что 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
.
Добавление UI документа на сцену
Затем создадим файл MyFirstView.uxml
. Это будет наша View.
Создание UXML файла
После того как файл MyFirstView.uxml
будет создан. Откройте его в UI Builder
и добавьте UI элемент BindableLabel
, установив в поле Binding Text Path
значение Text
(название нашего свойства из ViewModel).
UI Builder
Всё, наша View готова. Если открыть её в редакторе кода, то там будет примерно такое содержание:
Заключительным шагом будет создание класса MyFirstDocumentView
, который установит нашу ViewModel в качестве BindingContext
'а для созданной View.
using UnityMvvmToolkit.UITK;
public class MyFirstDocumentView : DocumentView
{
}
Этот класс необходимо будет довавить к UI Document’у на сцене и задать нашу View там же.
Конфигурирование UI документа на сцене
Запустив проект, мы увидим наш Hello World
.
Получившийся результат
Библиотека получилась довольно гибкой и расширяемой. В чём можно убедиться, изучив приложение Counter
из папки примеров, где в UI используются только кастомные элементы пользовательского интерфейса.
Вот так выглядит CounterView:
А так 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 CreateGetValueDelegate(
this PropertyInfo propertyInfo)
{
return (Func) Delegate.CreateDelegate(typeof(Func),
propertyInfo.GetMethod);
}
public static Action CreateSetValueDelegate(
this PropertyInfo propertyInfo)
{
return (Action) Delegate.CreateDelegate(typeof(Action),
propertyInfo.SetMethod);
}
}
Этот подход позволяет избежать упаковки (boxing) и распаковки (unboxing) для типов значений, что значительно улучшает производительность. В частности, он примерно в 65 раз быстрее, чем тот, который использует стандартные методы GetValue
и SetValue
, и вообще не приводит к выделению памяти.
Все исходники, примеры и документацию можно найти на GitHub, также пакет опубликован на площадке OpenUPM. Если у вас есть идеи, предложения или желание поучавствовать в разработке, добро пожаловать в дискуссии.
А что вы думаете о таком подходе создания пользовательских интерфейсов в Unity?