Что несет нам день грядущий или новые возможности UI Toolkit'а (с кодом и картинками)
О чем это статья?
В ноябре прошлого года, прошла конференция Unite 2023 и команда разработчиков демонстрировали новый функционал игрового движка.
Меня заинтересовало выступление про возможности UI Toolkit'а, которые появятся в будущем.
Меня порадовал функционал который там был представлен, он действительно ускоряет и упрощает работы с UI Toolkit'ом.
Поэтому я решил сделать текстовый пересказ видео + дополнения/разъяснения к нему.
Подготовка
Для начала, нужно установить себе версию Unity, не ниже 2023.2, так как именно в ней добавились новые фичи.
Улучшение фундамента
Именно с этого оглавления начинается рассказ про развитие UI Toolkit'а.
Команда разработчиков сделала изменение в трех аспектах разработки пользовательских интерфесов:
- Ускорение разработки
- Упрощение систем
- Дополнительные элементы (добавились новые UI элементы и контроллеры)
Давайте разбираться, что же они нам готовят в будущем?
Подключение данных к интерфейсу
Или как это называется в простонародье дата байндинг.
Как это работало раньше?
У нас было две сущности, первая это простая UXML в которой описана верстка, по-типу:
И код, который инициализировал и обновлял значения:
Нам приходилось искать нужный нам UI элемент по типу/по имени, а потом назначать ему значения (а если оно еще менялось, как часто это бывает, обновлять его, например через event'ы);
Как это работает сейчас?
У нас также есть верстка, но уже дополненная информацией, о том, какую информацию/тип должен отображать.
Рассмотрим на примере создания биндинга для Scriptable Object через UI Builder.
В нашем случае, мы хотим добавить текстовому полю PlayerName связь с типом PlayerInformation, для этого надо добавить Data Source и указать Data Source Path.
После чего, нам нужно добавить само связывание в поле Text.
Это делается кликом правой кнопкой и нажатие Add Binding и тогда откроется следующее окно:
И сразу после этого, у нас отобразится результат в верстке:
И как видите, для того чтобы отобразить какой-то статический текст, нам не нужен код.
А что тогда с динамическими данными?
Честно, надеялся вы не спросите, но раз спрашиваете...
– А слабо биндинг без своего UI Builder'а сделать, только через код?
Сделаем. Соответственно, у нас для этого чистый UXML без связываний, только UI элементы:
А вот с кодом интереснее выходит:
Давайте более подробно разберем это на примере текстового поля playerName:
У любого VisualElement'а (от которого наследуется Label), есть метод SetBinding(), который регистрирует информацию о привязке – какую информацию он должен отображать.
В нашем случае, мы хотим, чтобы _playerName привязался к PlayerData.Name.
В аргументах метода SetBinding, мы сперва видим BindingId что равнозначно PropertyPath, т.е к какому полю визуального элемента его привязывают, в рамках текстового поля, это Label.text.
А вторым параметром идет класс Binding, который объясняет интерфейсу:
- Какой режим связывания?
- Источник данных
- Что именно из данных мы хотим связать?
И после этого мы запускаем игру и видим, что наш код сработал:
Также отмечу, что у BindingMode есть 4 разных режима работы, которые решают ту или иную задачу в рамках построения интерфейсов.
У нас выбран BindingMode.ToTarget, именно благодаря нему, при изменение данных, оно сразу отобразится в интерфейсе (когда игрок повысил рейтинг или потерял).
Контроллируем обновление
Версионирование: когда обновлять
– А вот, Линар, ты сказал, мол оно само обновляется при изменение данных и отображается в интерфейсах, магия какая-то.
Согласен, тут есть нюанс, что оно обновляется при любом изменение данных, что не самый оптимальный путь.
By default, the binding system continuously polls the data source and updates the UI on every modification, without knowing if anything has actually changed since the last update
– Вот ты и попался, давай рассказывай, как сделать это лучше.
Интегрируем управление версиями и отслеживание изменений
Для того, чтобы интегрировать это нам нужно всего лишь...
Использовать интерфейс IDataSourceViewHashProvider, который обязывает нас реализовать следующий метод:
И тут начинается самое прикольное, так как можно сильно кастомизировать реализацию и подстраивать под свои нужды.
Давайте приведу примеры:
Помните наш класс PlayerData, (в котором имя и рейтинг игрока), так вот предположим, что мы хотим отображать изменения сразу при любом изменение любых значений, тогда мы сделаем что-то:
Опытный пользователи компьютера, вспомнят аналогию с методом GetHasCode(), у ссылочных типов.
– Простите, это конечно, все интересно, а что здесь происходит?
Прощаю.
Мы для нашего класса гененируем уникальное число (хэш) которое зависит от наших значений (Name, Rating) и если хоть одно из них изменится, тогда изменится сам хэш, а значит => надо обновить интерфейс актуальными значениями.
А если не изменился, значит обновлять ничего не надо.
Вот такой способ есть оптимизации, когда мы обновляет значения только при прямом изменение хэша нашего класса.
– Ага, ага, а че еще есть? Я вот люблю по экзотичнее
Такой как раз есть, называется буффер изменений.
Предположим, вы хотите обновлять интерфейс не всегда, а условно, когда поменяется оба поля (Name и Rating)
Тогда для этого нужно сделать некоторые измения в коде:
У нас тут появился метод CommitChanges(), который мы будем вызывать каждый раз, когда хотим обновить значения в интерфейсе.
И как раз, метод GetViesHasCode() возвращает текущую версию изменений (состояние класса) и если оно изменится, как и в предыдущем примере, вызовится обновление интерфейса.
Более глубже про этот функционал можно почитать здесь.
Представление данных
– А бывало ли у вас такое, что хотите отобразить данные разными способами?
Предположим, что мы хотим дополнительно отображать символ звезды(*) в интерфейсе, за каждые 20 рейтинга:
Тогда нам в помощь приходят Converter'ы, про них не буду много писать, только приведу пример:
Как помните, у нас был класс PlayerInformation, для него можно таким хитрым способом добавить поддержку альтернативного отображения одних и тех же данных.
В текущем примере, я создал Stars Converter Group, который надо будет выбрать в окне Edit Binding, в Advanced Settings.
Получится, что-то типа такого:
Cоздаем свои способы связывания
Также, мы можем создавать свои способы связывания, это может пригодится, когда у нас свои кастомные типы, либо когда не используется Data Source.
Рассмотрим пример из документации:
Это кастомный байндинг который позволяет отображать текущее время.
Основой для создания является наследование класса CustomBinding.
Нам нужно реализовывать метод Update и возвращать BindingResult.
Не буду сильно заострять внимание на детали, просто покажу как это использовать, для этого нам надо добавить информацию в верстку:
То же самое можно сделать через код:
И все, у вас будет отображаться текущее время.
Улучшение сериализации UXML
– Ура, товарищи, на нашей улице перевернулся камаз с аттрибутами.
У нас появилось два важных аттрибута, это [UxmlElement] и [UxmlAttribute] .
Теперь, когда мы создаем свой кастомный элемент интерфейса, мы можем сделать так:
Он появится в иерархии в UI Builder
И у него появилось наше поле:
Работает быстро, дешево и качественно. Всем советую.
Предфинальный
И на этой ноте, я бы хотел закончить статью, так как она перешла все мыслимые масштабы.
Если хочется глубже и более плотно ознакомиться с фичами, рекомендую посмотреть видео и читать документацию :)
Послесловье
Если у вас остались вопросы или что-то не понятно, пишите в комментариях – рассмотрим и разберемся.
Если статья понравилась тоже пишите. Если не понравилась тоже напишите, почему бы и нет.
Спасибо за внимание и до скорых встреч.