Кт? Imgui? Wxвиджеты? Мы Пишем Свои Собственные

Здравствуйте, хабровчане! Я хочу рассказать о своей UI-системе, которую я написал для своего игрового движка, на которой делаю для нее редактор.

Так:

Кт? ImGUI? wxвиджеты? Мы пишем свои собственные

Итак, я в который уже раз взялся за написание движка, и твёрдо решил, что на этот раз всё сделаю хорошо и правильно.

Одним из таких «хороших и правильных» является ВИЗИВИГ редактор а-ля Unity3D. Кстати, до этого у меня уже был опыт разработки подобных редакторов на Qt. И к тому времени я уже понимал, что задача непростая, если я хочу сделать по-настоящему хороший редактор.

А для этого нам нужна очень хорошая и гибкая UI-система, в которой я буду очень хорошо разбираться и знать всякие тонкости.

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

При этом в самом движке стояла задача создать хорошую UI-систему.

Поскольку движок для 2D игр, а в таких играх очень много интерфейсов (бизнес-логика игр, чаты, кланы, инвентари и т.д.), то и система UI в нем должна быть гибкой и удобной.

«Ну, почему бы не убить двух зайцев одним выстреломЭ» - Я думал.

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

В общем, я мог бы нарисовать какую-то простую картинку.

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

Думаю, не стоит описывать всю их эволюцию, а сосредоточимся лишь на окончательном виде.

Теперь система пользовательского интерфейса напрямую основана на следующих вещах:

  • Оказывать
  • Система обработки кликов
  • Иерархия сцен
  • Система виджетов пользовательского интерфейса
Давайте рассмотрим каждый отдельно.



Оказывать

Все элементы пользовательского интерфейса рисуются с использованием двух примитивов: спрайта и текста.

Они, в свою очередь, рисуются сетками из треугольников.

То есть каждый спрайт или текст представляет собой набор треугольников на плоскости с наложенной на них текстурой.



Кт? ImGUI? wxвиджеты? Мы пишем свои собственные

Также, чтобы отрисовать UI, нужно обрезать прямоугольник — внутри любых списков, полей ввода и т. д. Треугольники рендерятся с помощью пакетной обработки (группировки): сетки, отрендеренные с одинаковой текстурой и отсечением, группируются в большие сетки, чтобы оптимизировать отправку данных на видеокарту.

Если каждую мелкую сетку рисовать отдельно (вызов draw), то видеокарта будет простаивать, пока процессор подготавливает для нее команды, что и как рисовать.

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



Спрайт

В своей простейшей форме спрайт — это изображение, занимающее определенное положение на экране.

По сути, два треугольника объединены в квадрат с нанесенной текстурой.

Однако у такого простого спрайта есть недостаток: если начать его растягивать, он начинает плавать, очертания фигур нарушаются и закругленные края кнопки уже становятся овальными.



Кт? ImGUI? wxвиджеты? Мы пишем свои собственные

Чтобы избежать этого, используются 9-срезовые спрайты.

Это те же спрайты, но разделенные на 9 частей:

Кт? ImGUI? wxвиджеты? Мы пишем свои собственные

У такого спрайта при изменении размера углы всегда остаются исходного размера, зоны по краям растягиваются в одну сторону, а зона в середине растягивается во все стороны.

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



Кт? ImGUI? wxвиджеты? Мы пишем свои собственные

Здесь уже есть нюанс — как должен вести себя спрайт, если его размер стал меньше размера углов? Размеры углов (и сторон) следует пропорционально уменьшить.

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

Последнее оказывается очень полезным при рисовании индикаторов выполнения с закругленными краями.



Кт? ImGUI? wxвиджеты? Мы пишем свои собственные

Помимо этих двух режимов, есть и другие, которые используются реже, но иногда необходимы: Множество гифок со спрайтовыми режимами

Кт? ImGUI? wxвиджеты? Мы пишем свои собственные

Простой спрайт, только что растянутый

Кт? ImGUI? wxвиджеты? Мы пишем свои собственные

Спрайт из 9 фрагментов, пропорционально растягивается

Кт? ImGUI? wxвиджеты? Мы пишем свои собственные

Показывает прогресс с круговым заполнением

Кт? ImGUI? wxвиджеты? Мы пишем свои собственные

Вертикальное заполнение

Кт? ImGUI? wxвиджеты? Мы пишем свои собственные

Горизонтальное заполнение

Кт? ImGUI? wxвиджеты? Мы пишем свои собственные

Повторяющаяся текстура

Кт? ImGUI? wxвиджеты? Мы пишем свои собственные

Сохраняйте соотношение сторон и посадку Создание сетки для одного спрайта — довольно недорогая операция.

Однако если спрайтов тысячи, это становится дорогостоящим.

Следовательно, сетки спрайтов кэшируются и обновляются только при изменении преобразования спрайтов или других параметров спрайтов.

Вы можете увидеть код спрайта Здесь .



Текст

Как описано выше, текст представляет собой набор треугольников.



Кт? ImGUI? wxвиджеты? Мы пишем свои собственные

Здесь есть два основных вопроса:

  • Получить текстуру с символами символов
  • Создать сетку
Глифы можно получить двумя способами:
  • Нарисуйте его самостоятельно в графическом редакторе или утилите и сгруппируйте в одну текстуру.

  • Рендеринг глифов через FreeType из векторного шрифта
Я в основном использую второй подход, потому что.

он более гибкий и удобный.

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

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

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

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

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

Теперь вам нужно создать его сетку.

Данная задача разделена на два этапа:

  • Формирование посимвольного описания местонахождения персонажей
  • Формирование сетки по ранее созданному описанию локации.

Самое интересное, конечно, первая часть.

Здесь вход представляет собой прямоугольник, в который нужно ввести текст. А также его форматирование: высота символа, цвет, выравнивание по горизонтали и вертикали, тип обрезки.

И конечно сам текст. Алгоритм размещает символы построчно, начиная с точки, соответствующей выравниванию.

Учитываются межсимвольные интервалы, кернинг и межстрочный интервал.

Также, если включен режим обрезки через окончание на ".

", проверяется, поместится ли следующий символ, и заменяется тремя точками.

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

Здесь есть важный нюанс – строить новый текст очень дорого и затратно.

Следовательно, если текст сдвинулся на X пикселей в сторону, текст не регенерируется, достаточно сдвинуть вершины сетки на X пикселей.

Вы можете увидеть текстовый код Здесь .



IRectDrawable

Для унификации рендеринга спрайтов и текста они имеют общий интерфейс.

IRectDrawable .

Он отображает некую сущность, которая описывается прямоугольником (точнее, матрицей преобразования 2х3), которую можно нарисовать, включить или выключить и которая имеет цвет. Сам IRectDrawable наследует от IDrawable (объект, который можно нарисовать) и Трансформировать (описывает преобразование объекта матрицей 2x3, или Основа ).



Обрезка

На первый взгляд довольно простая задача, я дал ее Графический API прямоугольник для обрезки, затем сбросьте настройки и все готово.

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

Например, у нас есть прокручиваемая область, у которой есть видимая область, за пределами которой все обрезано.

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

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

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

Но все это можно решить с помощью стека прямоугольников отсечения на уровне системы рендеринга.

Алгоритм прост: мы добавляем в стек прямоугольник, обновляя текущий прямоугольник отсечения.

Текущий получается как пересечение предыдущего и нового.

И мы можем добавить столько, сколько захотим.

Затем мы извлекаем прямоугольники один за другим, восстанавливая подсчитанные промежуточные прямоугольники.

То есть родительский элемент сначала добавляет прямоугольник отсечения в начале рендеринга, без каких-либо вычислений.

Затем рисуются дочерние виджеты, которые добавляют свои вырезки в начале отрисовки.

На каждом уровне текущий прямоугольник пересчитывается с учетом предыдущего.

Затем, в конце рендеринга, каждый виджет извлекает свой прямоугольник обрезки из стека.

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

Забегая вперед, скажу еще, что интерфейс IDrawable в момент отрисовки запоминает текущий прямоугольник отсечения, чтобы потом корректно отмечать клики по этим объектам вне зоны отсечения.



Обработка ввода

Итак, у нас есть возможность что-то нарисовать.

Теперь я хочу сделать это интерактивным — дать возможность тыкать курсором по этим нарисованным объектам.

На входе мы имеем информацию о курсоре (точнее, для тач-скрина), событиях нажатия, перемещения и отпускания курсора.

А также сущности, нарисованные в определенном порядке.

Наша задача — понять, на кого кликает пользователь.

Здесь есть пара нюансов.

Сначала, как описано выше, обрезка.

Если спрайт обрезан, то щелчок по обрезанной части работать не должен.

Во-вторых, спрайты перекрывают друг друга.

Очевидно, что следует щелкать то, что нарисовано выше всех остальных под курсором, что и видит конечный пользователь.

Имеется специальный интерфейс для обработки кликов от пользователя.

КурсорОбластьСобытияСлушатель .

Он имеет солидный пакет виртуальных функций, которые вызываются в зависимости от реакции пользователя.

Здесь представлены щелчки, вход и выход курсора, для разных курсоров и клавиш мыши.

Также существует система, которая отвечает за корректную отправку этих сообщений.

Система событий , основная часть которого CursorAreaEventListenersLayer .

Как правильно понять, на кого именно попал курсор? Этот объект обрезан под курсором? Он заблокирован другими? Обычно это решается на уровне иерархии.

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

Мы начинаем с объектов верхнего уровня, спрашивая, попал ли курсор в них или в обрезанную часть.

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

Этот подход имеет ряд недостатков.

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

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

Либо вам придется отказаться от этой оптимизации и рассчитывать все дерево, что еще дороже.

Здесь также есть ряд недостатков, таких как сложная реализация «сквозных» событий, например, когда клик должна получать и прокручиваемая область, и кнопка внутри нее.

Фактически, для обработки ввода у нас есть много информации во время рендеринга.

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

Поэтому я применил другой подход, чем перемещение по иерархии.

При рендеринге объекта, который обрабатывает сообщения курсора, он сообщает системе ввода, что объект в данный момент визуализируется.

Система ввода хранит линейный список таких визуализированных объектов в том порядке, в котором они были нарисованы.

Плюс, как я писал выше, мы запоминаем текущий прямоугольник отсечения для каждой сущности.

Также у каждой такой сущности есть виртуальный метод проверки попадания точки в эту сущность.

Этот метод очень прост и работает без каких-либо обрезков и т. д. По сути, просто проверяется, находится ли IRectDrawable в прямоугольнике.



Кт? ImGUI? wxвиджеты? Мы пишем свои собственные

Альтернативный рендеринг с перекрытием Далее, после рендеринга сцены, «трассируется» каждый курсор.

Идем от конца списка нарисованных объектов, от последнего нарисованного к первому, проверяем, что курсор попадает в прямоугольник отсечения, проверяем, что он находится в геометрии.

Если все возвращает true — курсор попадает на эту сущность, алгоритм останавливается.

Если нет, возьмите следующий с конца и проверьте еще раз.

Далее, имея данные о состоянии нажатого курсора, о том, какая сущность трассировалась на предыдущем кадре, мы можем формировать сообщения о нажатиях, входе или выходе курсора.

Эта система работает быстро и стабильно с большим количеством объектов.

В этом случае пользователю достаточно перегрузить функцию попадания точки в геометрию (что в большинстве случаев даже не обязательно, если это IRectDrawable), и в момент рисования вызвать метод IDrawable::OnDrawn().

функция.

Далее система автоматически рассылает необходимые сообщения.



Иерархия сцен

В своем движке я выбрал типичный подход с иерархической сценой с компонентами.

Базовый элемент - Актер , примитивный объект, имеющий имя, преобразование, набор компонентов, определяющих его поведение, и список дочерних Актеров.

На сцене они выстраиваются в древовидную структуру.

Сцена — это просто список Актеров.

Актеры также могут быть разных типов.

То есть это всего лишь базовый интерфейс, от него можно наследовать и реализовывать свою логику.

Это можно сделать с помощью компонентов, но в отличие от того, что компонент не является основной сущностью, он является вспомогательным, и на Актере их может быть несколько.



Кт? ImGUI? wxвиджеты? Мы пишем свои собственные

Система пользовательского интерфейса построена на основе этой иерархии сцен.

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



Виджет

Это самый основной «строительный блок» системы интерфейсов.

Если Актер описывается простым преобразованием, включающим положение, вращение, размер и масштаб, то положение Виджета описывается более сложной структурой и уже зависит от родителя.

Положение виджета описывается структурой ВиджетМайаут , который является наследником АктерПреобразование .

То есть это надстройка над обычным положением, вращением, размером и масштабом Актера.

Эта надстройка включает в себя относительные привязки и смещения от них.

Якоря указываются в процентах и располагаются относительно родительского элемента.

Затем к этим привязкам добавляются смещения пикселей.

Это позволяет сделать адаптивную верстку.

Правильно установив якоря, можно добиться растягивания дочерних элементов вслед за родительским или, наоборот, «приклеить» дочерние элементы к определенным точкам, углам или центру.

Аналогичный принцип применяется в Графический интерфейс Unity .



Кт? ImGUI? wxвиджеты? Мы пишем свои собственные



ВиджетСлой

Помимо обычной структуры дочерних Актеров, внутри Виджета хранится список слоев.

ВиджетСлой .

Слой — это очень упрощенный виджет, который имеет встроенный IRectDrawable и структуру, аналогичную WidgetLayout, описывающую адаптивное положение слоя.

ВиджетСлойМайаут. Слоев может быть много, слои могут иметь дочерние слои.

За счет упрощения достигается оптимизация, ведь эти более легкие сущности гораздо быстрее и проще в эксплуатации.

Их легко можно заменить дочерними Виджетами, но это нарушит целостность концепции Виджета.

Например, кнопка воспринимается как кнопка, а не набор дочерних изображений.

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

Таким образом, слои — это графическая часть виджета, отделенная от его дочерних виджетов.



Виджетстате

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

При наведении курсора кнопка должна подсветиться, а при нажатии она должна потемнеть.

Списки должны расширяться, курсор ввода текста должен мигать и т. д. Желательно, чтобы все это происходило плавно и красиво.

Для этого в Виджете есть анимированные состояния - Виджетстате х.

Они работают по принципу простой машины состояний.

Каждое состояние представляет собой своего рода анимацию, имеющую две включенные позиции.

и выключается При включении.

Анимация воспроизводится вперед при выключении.

анимация воспроизводится задом наперед. Более того, анимация может идти в другую сторону, не дожидаясь конца.

За счет этого достигается быстрая реакция на действия пользователя.

Сами анимации довольно сложные и о них можно было бы написать отдельную статью.

Но суть их проста — они могут изменить любой параметр любого объекта внутри иерархии Actor, включая Widgets, Layouts, WidgetLayers и т.д. Здесь помогает собственная рефлексия, о которой когда-то уже говорилось.

написал .

Это позволяет искать нужные параметры, используя пути к запасам, например

children/0/transform/anchor

.

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

Например, для кнопки можно задать следующий список состояний

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



Внутренние виджеты

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

Например, кнопка закрытия в виде крестика на окне.

Окно представляет собой отдельный цельный виджет, и кнопка закрытия ощущается как его часть.

А дочерние элементы окна — это содержимое самого окна.

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

Поэтому Виджеты, помимо списка дочерних Виджетов, имеют список «внутренних» Виджетов.

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



Макет-Виджеты

Это специальные Виджеты, которые отвечают за определенные алгоритмы расположения дочерних Виджетов.

Например, Горизонтальный макет раскладывает своих детей в линию по горизонтали.

Работает так же Вертикальный макет , располагается только вертикально.

А также есть Макет сетки , который создает равномерную сетку.



Кт? ImGUI? wxвиджеты? Мы пишем свои собственные

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

Эти настройки позволяют охватить практически все возможные варианты адаптивной верстки.



Кт? ImGUI? wxвиджеты? Мы пишем свои собственные

Пример адаптивной верстки в редакторе настроек Алгоритм работы с этими макетами следующий:

  • Мы рекурсивно рассчитываем размеры дочерних виджетов.

    То есть мы как бы заглядываем в будущее, какого размера они будут. За основу берем минимальный размер элементов

  • Мы вычисляем пространство, которое могут занимать дочерние элементы.

    Берем текущий размер Layout, вычитаем все минимальные размеры дочерних элементов, вычитаем пробелы между ними.

    В результате получается «пространство, которое можно распределить».

  • Это свободное пространство мы пытаемся распределить между элементами.

    Но если для какого-то элемента это пространство оказывается слишком большим, то есть имеет ограниченный максимальный размер, он исключается из алгоритма и пространство распределяется без него.

  • Получив окончательные размеры дочерних элементов, им передаются соответствующие параметры WidgetLayout.


Типы виджетов

На основе этого простого элемента UI-системы построено множество других типов элементов, каждый из которых реализует определённую логику.

Стоит отметить, что базовый виджет не может обрабатывать входные сообщения.

Если виджету необходимо реагировать на ввод пользователя, он дополнительно наследуется от соответствующих интерфейсов.

Прослушиватель событий клавиатуры , КурсорОбластьСобытияСлушатель .

На данный момент поддерживается следующий список типов:

  • Кнопка
  • Флажок
  • Поле ввода текста, однострочное и многострочное
  • Выпадающий список
  • Список
  • Изображение
  • Надпись
  • Область прокрутки
  • Горизонтальный/вертикальный индикатор выполнения
  • Горизонтальная/вертикальная полоса прокрутки
  • Спойлер
  • Окно


Всплывающие окна

Всплывающее окно — это часть интерфейса, которая отображается поверх всего остального.

Для них существует отдельный алгоритм отложенного рендеринга.

Все всплывающие окна наследуются от общего класса PopupWidget .

Всплывающие окна также включают контекстные меню.

Текущее видимое всплывающее окно хранится внутри как статическая переменная.

Именно он и рисуется при отложенном рендеринге сцены.

Само всплывающее окно также может содержать дочернее всплывающее окно, которое будет нарисовано вместе с этим отложенным всплывающим окном.

Это используется, например, в контекстных меню: некоторые элементы могут открывать подпункты меню, которые являются дочерними всплывающими окнами.



Кт? ImGUI? wxвиджеты? Мы пишем свои собственные

Теги: #Разработка игр #Разработка мобильных приложений #gamedev #C++ #ui/ux #uikit

Вместе с данным постом часто просматривают:

Автор Статьи


Зарегистрирован: 2019-12-10 15:07:06
Баллов опыта: 0
Всего постов на сайте: 0
Всего комментарий на сайте: 0
Dima Manisha

Dima Manisha

Эксперт Wmlog. Профессиональный веб-мастер, SEO-специалист, дизайнер, маркетолог и интернет-предприниматель.