Всем привет!

В игре может быть множество элементов интерфейса, всплывающих окон и т. д., и когда появится необходимость изменить общий стиль, например цвет кнопки или текста, то придется это менять во всех созданных элементах, если используется старая система UI Canvas - uGUI (IMGUI забудем как страшный сон). Не так давно Unity предоставили новую систему UI Toolkit, вдохновленную веб-технологиями (HTML-CSS vs UXML-USS) и позволяющую изменить цвет, шрифт и другие свойства всех элементов в игре одним движением. Преимуществ много, например можно подключить веб-дизайнера, и он тут быстро освоится.

Как и ожидается при появлении новой технологии, внятная документация отсутствует, статей в интернете мало и информации в них кусочками. Официальную документацию стараются делать, но явно не для людей. А так как мы все ценим свое драгоценное время, когда еще и горят дедлайны, мы хотим быстро и без лишних затрат использовать все удобства в разработке. Целью данной статьи как раз и является помощь в быстром освоении нового подхода Unity в создании интерфейсов, чтобы не отвлекаться на рысканье в потоках бессвязной документации и продолжать реализовывать свою фантазию или фантазию заказчика. Давно не доходили руки до написания статьи, но пора бы уже уронить свою каплю в море.

Для демонстрации основных часто используемых функций займемся созданием окошка с вводом текста, выбором из подгружаемого кодом списка и обработкой нажатия кнопки. Пусть это будет меню кафешки, когда нам захотелось перекусить. Это будет для вас отправной точкой для создания более сложных интерфейсов.

Установка UI Toolkit

Меню Window - Package Manager - Кнопка + - Add package from git URL... - com.unity.ui - Add

Изображение

После этого в меню Window появится новый пункт UI Toolkit.

Создание интерфейса

Меню Window - UI Toolkit - UI Builder

Изображение

Кратко пройдемся по редактору:

  • (1) - Viewport - рабочее пространство, видим что создаем

  • (2) - StyleSheets - перечень классов USS

  • (3) - Hierarchy - иерархия всех элементов на странице

  • (4) - Library - набор стандартных элементов управления

  • (5) - Inspector - визуальное средство редактирования свойств

Начнем с корневого элемента, назовем его Panel:

Изображение

Добавим класс, тут же попросит сохранить:

Изображение

На видео получилась такая последовательность сохранений:

  1. Сохраняется файл стилей USS.

  2. Сохраняется страница с разметкой UXML.

  3. Опять потребовало сохранить UXML, с первого раза видимо ему непонятно, не знаю как у вас, но у меня так всегда.

  4. Здесь я нажал Ctrl-S для очередной попытки, т.к. в панели Viewport (1) продолжала висеть звездочка рядом с названием файла - это означает, что сохранение не произошло.

Он так себя ведет первый раз, потом в процессе работы просто нажимайте Ctrl-S, чтобы сохранить весь прогресс.

Теперь обязательно сделайте вот что:

  1. Переходим на сцену.

  2. Создаем пустой объект.

  3. Добавим в него компонент UI Document.

Изображение

  1. В панели Project - Create - UI Toolkit - Panel Settings Asset.

Изображение

  1. Переносим его в свойство Panel Settings компонента UI Document.

  2. Файл cafe.uxml переносим в свойство Source Asset компонента UI Document.

Изображение

  1. Закрываем панель UI Builder.

  2. Открываем панель UI Builder (уже весело).

Для чего вот это вот все? В панели Viewport (1) появился важный пункт Unity Default Runtime Theme. Он позволяет во время редактирования видеть, как будет выглядеть интерфейс в игре. Зачем они сделали Dark и Light Editor Theme, которые выглядят совсем иначе и не связаны с игрой, только им известно.

Изображение

Продолжаем веселье, заново создавая корневой элемент Panel, т.к. он потерялся из-за манипуляций с сохранениями. Не переживайте, потом ничто никуда не потеряется, только первый раз Unity решает покапризничать. Перетаскиваем класс .panel на элемент Panel, после чего окно инспектора должно выглядеть так:

Изображение

Видим, что класс добавлен в элемент.

Теперь щелкаем на название класса в окне StyleSheets (2), чтобы менять свойства. Не перепутайте, щелкая на элемент в иерархии (3), иначе вы будете менять свойства только одного элемента, а нам нужно изменять все окна в игре, редактируя класс.

Установим ширину окна, выравниваем его по центру экрана с помощью margin: auto (все как для веб), цвет настроения черный с легкой прозрачностью, и border-radius: 10px.

Изображение

Добавим заголовок окна. Переносим Label на нашу #Panel и убеждаемся, что она упала дочерним элементом, т.к. тут часто можно промахнуться, и элемент становится соседним, а не вложенным.

Изображение

В классе .panel установим цвет текста, чтобы цвет наследовался всем вложенным элементам, тут тоже все как в веб.

Изображение

Добавим класс для заголовка, просто установив шрифт чуть больше, и перетянем его на метку.

Изображение

В инспекторе можете сами пройтись по атрибутам, там все интуитивно понятно. Скажу только сразу, что разметка изначально настроена на технологию flex. Если вы не знакомы с flex, то гугл вам все расскажет. Все правила из веб разработки касаемо flex применимы и здесь.

Также здесь работает и масштабирование как в Canvas. Вначале мы создавали файл настроек Panel Settings Asset, и если на нем щелкнуть в панели Project, то в стандартном инспекторе мы увидим свойства Scale Mode как в Canvas. По умолчанию установлено Constant Physical Size, что для меня лично удобно, я всегда в Canvas переключал на это значение.

Обработчик событий: оживляем кнопку

Зачем нам это все, если мы не сможем это запрограммировать? Добавим обработчик на кнопку.

Добавим кнопку в наше окошко, сохраним и перейдем на сцену. Создаем скрипт с любым названием, например CafeUI. Ранее мы добавили пустой объект с компонентом UI Document. На него и вешаем скрипт. Переходим в редактор кода, например Visual Studio.

Unity рекомендует обрабатывать элементы страницы в методе OnEnable(), так и сделаем. В листинге ниже комментарии все объясняют.

CafeUI.cs
using UnityEngine;
using UnityEngine.UIElements; //ссылка на библиотеку для работы с UI Toolkit

public class CafeUI : MonoBehaviour
{
    Button okButton;

    void OnEnable()
    {
        //Получаем ссылку на компонент UIDocument
        var uiDocument = GetComponent<UIDocument>();
        //Находим кнопку таким запросом, в параметр передаем имя кнопки
        okButton = uiDocument.rootVisualElement.Q<Button>("okButton");
        //Регистрируем событие нажатия кнопки
        okButton.RegisterCallback<ClickEvent>(ClickMessage);
    }

    void ClickMessage(ClickEvent e)
    {
        //Реализуем тут любые действия при нажатии кнопки
        Debug.Log("ok");
    }
}

Единственного удобства uGUI в перетаскивании методов на кнопку тут нет, но и такой способ особо не напрягает.

Список товаров, связывание данных (ListView и Binding)

В кафе должен быть список товаров, и мы, конечно же, хотим формировать его динамически во время выполнения. Источник данных может быть любой, но в данном случае для простоты мы будем генерировать список в коде, чтобы отобразить его в окне.

Кидаем на страницу в UI Builder элемент ListView, установим ему атрибуты fixed-item-height="50" style="height: 120px; margin: 10px;". В панели Project создаем новый UXML-документ (Create - UI Toolkit - UI Document), задаем ему имя item.uxml и открываем его двойным щелчком или клавишей Enter. Это будет шаблон элементов списка. Он будет состоять из картинки, названия товара и цены. Должно выглядеть это так:

Изображение

Внизу в коде uxml видно, какие атрибуты выставлены элементам. Здесь можно обойтись без uss-стилей, т.к. других окон у нас не будет, но если у вас должно быть несколько окон со списками, то удобнее будет установить единый стиль для списков в одном uss-файле. Цвет текста будет наследоваться от класса .panel. Корневому элементу установлено расположение вложенных элементов в строку и выравнивание посередине. Картинка 50px на 50px, название товара занимает всю оставшуюся область (flex-grow: 1), и для цены установлена фиксированная ширина, иначе весь список будет кривой, т.к. цена может быть разной.

Далее нужно создать класс, который будет представлять данные о товаре:

Item.cs
public class Item
{
    public string Name { get; set; }
    public decimal Price { get; set; }
}

Теперь самое сложное. В официальной документации предоставили довольно сложноватый пример, состоящий из нескольких классов и запутанного кода. Я же не люблю плодить много файлов и классов, и максимально упростил пример, чтобы сосредоточиться только лишь на связывании данных. Сделаем все по порядку. Нумерация пунктов соответствует нумерации в комментариях кода.

  1. Инициализируем список товаров. Здесь для простоты создаем список из двух товаров, в реальной игре же источник данных может быть любой - база данных, api-запросы, файлы и т.д.

  2. Находим отображаемый список на странице. Это ListView с именем list на главной странице cafe.uxml.

  3. Связываем шаблон элементов списка с самим списком.

  4. Связываем данные с отображаемым списком.

  5. Регистрируем событие выбора из списка.

  6. Переносим шаблон item.uxml на переменную itemsListTemplate в инспекторе

Изображение

  1. Стилизуем список. Тут пара хитростей. В UI Toolkit есть встроенные классы для каждого контрола, их очень много и можно посмотреть по ссылке https://docs.unity3d.com/2021.3/Documentation/Manual/UIE-ElementRef.html

  • изменим цвет наведения мыши на элемент списка: .unity-list-view__item:hover { background-color: rgba(0, 0, 0, 0.2); }

  • изменим цвет выбранного элемента; второй селектор тут добавлен, чтобы при наведении мыши не менялся цвет выбранного элемента: .unity-collection-view__item--selected, .unity-collection-view__item--selected:hover { background-color: rgba(255, 255, 255, 0.2); }

Эти селекторы можно добавить прямо в файл index.uss.

CafeUI.cs
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
using UnityEngine.UIElements; //ссылка на библиотеку для работы с UI Toolkit

public class CafeUI : MonoBehaviour
{
    Button okButton;

    public VisualTreeAsset itemsListTemplate;   //uxml-шаблон элементов списка
    List<Item> items = new List<Item>();        //список товаров
    ListView itemsListView;                     //список на странице

    void OnEnable()
    {
        //Получаем ссылку на компонент UIDocument
        var uiDocument = GetComponent<UIDocument>();
        //Находим кнопку таким запросом, в параметр передаем имя кнопки
        okButton = uiDocument.rootVisualElement.Q<Button>("okButton");
        //Регистрируем событие нажатия кнопки
        okButton.RegisterCallback<ClickEvent>(ClickMessage);


        //Работаем со списком
        //1. Инициализируем список товаров
        items.Add(new Item { Name = "Пирожок", Price = 40 });
        items.Add(new Item { Name = "Мороженое", Price = 60 });

        //2. Находим отображаемый список на странице
        itemsListView = uiDocument.rootVisualElement.Q<ListView>("list");

        //3. Связываем шаблон элементов списка с самим списком
        itemsListView.makeItem = () =>
        {
            return itemsListTemplate.Instantiate();
        };

        //4. Связываем данные с отображаемым списком
        itemsListView.bindItem = (_item, _index) =>
        {
            //Связываем список товаров по индексу
            var item = items[_index];
            //Получаем доступ к визуальным элементам шаблона по именам, которые мы указали в шаблоне
            _item.Q<Label>("name").text = item.Name;
            _item.Q<Label>("price").text = $"{item.Price} руб";
            //В данном случае картинка товара должна лежать в папке Resources и иметь название такое же, как название товара
            _item.Q<VisualElement>("image").style.backgroundImage = Resources.Load<Texture2D>(item.Name);
        };

        //Здесь все стандартно
        itemsListView.itemsSource = items;

        //5. Регистрируем событие выбора из списка
        itemsListView.onSelectionChange += ItemsListView_onSelectionChange;
    }

    private void ItemsListView_onSelectionChange(IEnumerable<object> obj)
    {
        if (obj.Count() > 0)
        {
            var item = obj.First() as Item;
            Debug.Log($"{item.Name}: {item.Price} руб");
        }
    }

    void ClickMessage(ClickEvent e)
    {
        //Реализуем тут любые действия при нажатии кнопки
        Debug.Log("ok");
    }
}

Результат работы

USS как CSS

USS максимально схож с CSS, благодаря чему знакомый с веб-разработкой будет чувствовать себя здесь в своей стихии. В вашем распоряжении все возможности из мира CSS:

  1. Использование переменных. Например:

Hidden text
:root {
  --general-bg: #4D4D4D;
}
Button {
    background-color: var(--general-bg);
}

  1. Вложенность и комбинации селекторов (selector1 selector2; selector1 > selector2; selector1, selector2...)

  2. transform, translate, transition...

И т.д. и т.п. Все возможности перечислены в документации

Полезные ссылки

Создание собственных контролов

Встроенные переменные USS

Справочник USS

Справочник по элементам управления. Их на самом деле много, а в окне UI Builder Library выведены только несколько стандартных

Создание шаблонов. Один шаблон мы уже с вами сделали

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


  1. ElenaSotnikova
    26.12.2022 17:47

    Очень интересная статья, хоть я игры и не разрабатываю ))) очень подробная и наглядная. Даже захотелось попробовать окунуться в этот мир разработки


    1. alexxxproof Автор
      28.12.2022 12:54

      Попробуйте, не пожалеете, это очень интересно и увлекательно. Я вот жалею, что раньше не занялся разработкой игр


  1. Ka33yC
    26.12.2022 17:47
    +1

    Как жаль, что юнитеки занимаются изобретением велосипедов никому не нужных. А нам с этим работать


  1. ScratchUA
    27.12.2022 06:51

    В игре может быть множество элементов интерфейса, всплывающих окон и т. д., и когда появится необходимость изменить общий стиль, например цвет кнопки или текста, то придется это менять во всех созданных элементах, если используется старая система UI Canvas - uGUI 

    Ну да, ну да, префабы и варианты префабов используют же только слабаки. UI Toolkit - очередная бесполезная блажь от Unity Technologies, которая мало кому интересна. Бесплатный UI Toolkit Sample – Dragon Crashers - полноценный мобильный UI на UI Toolkit - собрал аж 8 оценок и отзывов за 3,5 месяца (на данный момент) с момента публикации в Asset Store. Настолько высок интерес к продукту...

    Ну и, как обычно, оно часто ломается от версии к версии. В 2022.2 сломали ширину PropertyFields и разрабы, вместо того, чтобы пофиксить баг, присоветовали знакомому разработчику весьма популярного ассета добавить класс  .unity-base-field__aligned во все контролы. В упомянутом ассете, все инспекторы свойств созданы при помощи UI Toolkit и контролов там далеко за пару сотен, а то и больше.

    Переходить на UI Toolkit для создания игрового UI, как по мне, это не самая лучшая идея, пока идеально работает связка UGUI + Modern UI Pack, а вот для создания инспекторов и сервисных UI для Unity Editor подходит неплохо.


    1. alexxxproof Автор
      28.12.2022 12:52

      В окнах обычно разные наборы элементов, и тогда будет сотня префабов вместо сотни окон. После перехода на UI Toolkit стало намного проще и интереснее делать интерфейсы.

      Интерес невысок, потому что много скептиков, да и я долго обходил его стороной, пока не появился вопрос от заказчика - а что если потом я захочу поменять цвет интерфейса?

      В USS много разных селекторов и комбинаций, а не только название встроенного класса, чтобы что-то поменять.


      1. ScratchUA
        28.12.2022 21:04

        Вы неправильно меня поняли. Я говорю о префабах не окон, а конкретных uGUI контролов: кнопок, текстовых полей, переключателей, слайдеров и т.д. В Unity есть вариации префабов, а совсем недавно появились еще и вариации материалов и вам не нужно создавать множество материалов с идентичными параметрами, которые отличаются, например, только цветом. Достаточно создать базовый прототип материала с определенными настройками Diffusion / Normal / Metallic / Height и т.д. и потом создавать вариации материалов, в которых меняется только цвет Diffusion.

        Извиняюсь за прямоту, но, по всей видимости, у вас было совсем немного практики в создании UI на uGUI, если задача по смене цвета интерфейса вызвала у вас необходимость переходить на UI Toolkit. USS и UXML я изучал с самых первых версий, так что про комбинации селекторов я знаю. Но в итоге побаловался, ощутил непередаваемую мощь привнесённых Web-технологий, и вернулся к uGUI. Хотя безработным верстальщикам-формошлёпам, стремящимся войти в геймдев, вполне себе зайдет. Осталось еще только вернуть Unity Script (специфический Java Script для Unity), благополучно выпиленный в Unity 2018 и будет полное счастье.


        1. alexxxproof Автор
          28.12.2022 21:12

          Получается, что тут уже речь идет о личных предпочтениях, а не о работоспособности технологии, а о вкусах не спорят. От себя скажу, что работает UI Toolkit прекрасно, я продолжаю сейчас с ним работать, все задачи выполняются в короткий срок и никаких нареканий нет.


  1. bustedbunny
    28.12.2022 12:38

    Зачем они сделали Dark и Light Editor Theme, которые выглядят совсем иначе и не связаны с игрой, только им известно.

    Потому что UI Builder также используется для создания интерфейса для Editor.