Ни для кого не секрет, что 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?