Изображёные приложения вы можете найти на GitHub проекта в папке примеров
Изображёные приложения вы можете найти на 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 в проект:

  1. Откройте Edit/Project Settings/Package Manager

  2. Добавьте новый Scoped Registry

Name      package.openupm.com
URL       https://package.openupm.com
Scope(s)  com.chebanovdd.unitymvvmtoolkit
  1. Откройте Window/Package Manager

  2. Выберите My Registries

  3. Установите пакет UnityMvvmToolkit

Первым делом создадим нашу ViewModel:

using UnityMvvmToolkit.Core;

public class MyFirstViewModel : ViewModel
{
    public string Text { get; } = "Hello World";
}

Далее добавим на сцену UI Document выбрав GameObject/UI Toolkit/UI Document.

Добавление UI документа на сцену
Добавление UI документа на сцену

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

Создание UXML файла
Создание UXML файла

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

UI Builder
UI Builder

Всё, наша 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 там же.

Конфигурирование UI документа на сцене
Конфигурирование UI документа на сцене

Запустив проект, мы увидим наш 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?

Комментарии (1)


  1. MultiTeemer
    15.12.2022 22:05

    Не так давно как раз думал над темой MVVM в UI Toolkit. Очень круто! С удовольствием приму участие в проекте, как найду время.