Войти
Totem 4 Engine BlogСтатьи

Totem Engine 4 Blog. Статья 3. GUI.

Автор:

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

Причина, по которой я не использую сторонние реализации для интерфейса пользователя очень проста - у меня нет денег на Scaleform, а остальные реализации (CEGUI, MyGUI) не вызывают у меня должного уровня доверия.

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

1. Рабочие принципы.

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

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

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

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

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

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

Не использовать сообщения (events).

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

Порциальная работа с интерфейсом.

Как раз то, о чем я говорил выше. Игроку не нужно сразу много контролей. Интерфейс последнее время вообще стремится к минимализму. В любом случае, любое количество диалогов с контролями внутри них можно разделять на группы, а игроку работать всегда только с одной активной группой. У меня такая группа называется экран (screen).

Работа в рамках активной области.

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

С клавиатурой нужно учитывать возможности ввода для всех диалогов текущего экрана, это может показаться не оптимальным (типа сообщения лучше), но обработать даже 1000 контролей при нажатии какой-то клавиши очень быстро.

Поддержка множества разрешений экрана.

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

Для этого я использую несколько типов выравнивания для диалогов и контролей.

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

-Объект всегда имеет строгую позицию и размеры относительно родителя в процентном отношении его сторон, например границы будут выглядеть так: left – 10%, top – 40%, right – 90%, bottom – 60%.
-Объект всегда имеет свои размеры, но имеет смещение (либо в пикселях, либо в процентах экрана) относительно каких либо сторон родителя.

Смещение позволяет группировать диалоги по одной стороне экрана независимо от разрешения, если размеры диалогов постоянные (а они должны быть постоянные, чтобы текстуры выглядели красиво).
article_3_align | Totem Engine 4 Blog. Статья 3. GUI.

2. Отображение интерфеса.

Сложной в отображении интерфейса нет практически никаких. Нужно вывести на экран квад (два треугольника) с текстурой, чтобы отобразить любой тип контроля.

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

Для отображения всего интерфейса я использую один динамический буфер вершин (vertex buffer) и один буфер индексов (index buffer). Индексный буфер не динамический, в нем вначале находятся индексы для отображения отдельных квадов, а в конце находятся индексы для рисования линий.

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

Можно было бы воспользоваться инстансингом (instancing), но мне удобнее было им не пользоваться в случае интерфейса.

Все выше перечисленное позволяет сократить число DIP, если мы оптимизируем работу с текстурами. А для этого нам нужно использовать атласы (atlas). Для себя я написал собственный генератор атласов, его основная задача посчитать текстурные координаты элементов, которые будут занесены в атлас при генерации.

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

Отдельно надо сказать про отображения текста в движке. Текст я отображаю квадами — один квад на одну букву, в качестве текстуры используется алфавит, то есть текстура, в которой содержатся все символы текущего шрифта в определенной кодировке. Для генерации алфавитов я использую программу Bitmap Fonts Generator от AngelCode.

Некоторые генерируют для отображения текста текстуры прямо в рендере (render), но на мой взгляд этот вариант куда сложнее алфавита.

Важным моментом является отсечение внутренностей диалогов, чтобы за границами диалога не отображался ни один контроль, если это требуется. Простым scissor test в нашем случае не обойтись, потому что мы кучу контролей отображаем за один вызов. Я использую отсечение в пиксельном шейдере через VPOS (позиция пикселя на экране, требуется SM3.0), для этого для каждой вершины у меня выделен дополнительный float4 элемент, хранящий left, top, right, bottom области отсечения текущей вершины.

Вращение (rotation) контролей осуществляется в вершинном шейдере. Аналогично отсечению, вершина хранит свой угол поворота и центр вращения.

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


3. Обработка интерфейса.

article_3_dialog_proc | Totem Engine 4 Blog. Статья 3. GUI.

В основе обработки стоит объект экран (screen). Как уже было сказано выше — это группа диалогов, которые могут быть отображены и обработаны вместе в данный момент игрового времени.

У экрана может быть в любой момент времени только один активный диалог (active dialog). Активация диалога происходит при нахождении мыши в его границах или при  нажатии горячей клавиши клавиатуры одного из контролей диалога. Если нажать на активный диалог (или хоткей), то диалог становится верхним (top), то есть перекрывает все остальные. Кроме того диалог может быть заморожен (locked), в таком случае нельзя активировать мышью или клавиатурой другой диалог, пока не снята заморозка.

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

Диалоги имеют иерархическуюх (hierarchical) структуру, чтобы удобно было работать с видимостью и границами дочерних диалогов.

Границы диалогов задаются в локальном пространстве (local space) родителя (для диалога родитель может быть экран или другой диалог). Экранные границы (screen space borders - позиция при рендере) всех диалогов и контролей высчитываются в соответствии логике, описанной в первой части статьи. Благодаря хранению относительного к родителю размера, можно легко пересчитать размеры и позиции объектов при изменении родителя или разрешения экрана.

Контроли (controls) — самая важная часть системы, ведь именно с их помощью пользователь осуществляет воздействие на игру. Контролей может быть множество, но все они собираются из двух базовых — это картинка (image) и текст (text).

C картинкой все просто, а вот текст гораздо более сложная вещь и сложность его заключается в выравнивании (aligning). Когда я только столкнулся с этим аспектом, это вызвало у меня некоторые трудности. Но сейчас мой текст поддерживает все основные типы выравнивания — левый край, центр, правый край, по ширине, верх, центр, низ.

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

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

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

Обработка контролей разделяется на 3 типа:

-Обработка мыши.
-Обработка клавиатуры.
-Обработка состояния контроля.

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

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

Первые два типа обработки приводят к третьему, то есть при наведении мыши на контроль (скажем кнопку), последний меняет состояние на активное, при нажатии клавиши мыши или клавиатуры контроль так же меняет свое состояние (для кнопки на нажатое).

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

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

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

4. Обработка связей.

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

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

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

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

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


5. Заключение.

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

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

Но возможно кто-то опытный сможет почерпнуть что-то полезное.

Как всегда, открыт для критики.

#GUI, #Totem 4 Engine, #Игровые движки

31 июля 2011

Комментарии [1]