Кастомный движок, UI, Immediate-mode библиотеки
Всем привет.
5 месяцев назад я написал очередной девлог, в котором обещал постепенно выкладывать куски движка в открытый доступ, параллельно с разработкой основного проекта - Gobby Island. Ну как, обещал ))0))0)
Я очень надеялся, что выложу все до Steam Next Fest, но вышло как вышло. Теперь же новая демка готова, она доступна на самом фестивале, а мне пожалуй пора начать рассказ про кастомный движок! Ответ на наиболее животрепещущий вопрос ("а зачем вы это сделали???") оставлю в конце статьи, в постскриптуме.
Вступление.
И так, мы собрались писать "свой" движок. Что нам будет нужно в первую очередь? Окно, которое обрабатывает ввод-вывод? Окей. Что ещё нужно в этом окне? Оконный лог! Мы же не хотим все время бегать в консоль за проверками аутпута событий, хотелось бы наблюдать их в самом окне. Кроме того, нужны элементы управления - тыкать в строго назначенную кнопку на клавиатуре, чтоб что-то запусить, тоже не очень хотелось бы, а значит нужно встроить GUI.
Но прежде, чем перейти к нему, давайте определимся с ещё одной парочкой чуть более низкоуровневых вещей.
Начало начал
Как основной язык был выбран C++ (потому што я с ним знаком), как бэкенд для будущей графики - OpenGL, так как он уже достаточно зрелый, поддерживается и предустановлен везде, где можно и нельзя, и по нему написано неимоверно огромное количество гайдов.
Исходя из таких вводных, открываем гугл и ищем библиотеки:
GLFW, для функций ввода-вывода (кроссплатформенная прослойка между нами и ОС):
glm, математика
Обе сделаны с оглядкой на Opengl, так что отлично подойдут.
Назад к GUI! Гуй должен быть бесплатным (а ещё достаточно простым, чтоб не перетягивать на себя внимание, отвлекая от нашей заветной цели - окна, в которое этот самый GUI встроен). На момент начала работы автору этой статьи был известен Qt, но он сразу был отметён, так как это не только UI, но и много чего ещё. К тому же, для работы с кнопками (которые сами по себе являются вспомогательным элементом) там нужно наследовать классы и писать коллбэки. А чего бы хотелось:
Довольно притягательным поначалу казался RmlUI - описываем юай в HTML/CSS, потом используем. Но на практике тоже выходит не то, чтоб очень тривиально. После некоторого ресерча я наткнулся на это:
Реализация того, о чем вещает товарищ - ImGui.
Immediate-Mode библиотеки.
Модель выполнения Immediate-mode простая до безобразия - есть куча разных вызовов для UI элементов. Элемент рисуется во время вызова и в том же вызове проверяется его состояние (нажата кнопка или нет). Выходит очень похоже на желаемое:
У этого есть только один минус - фактически последовательность вызовов определяет дизайн юая, а этого тоже хотелось бы избежать, так как обращения к юаю планируются хаотичные (т.е. пока хз, где что будет и тем более хз, как оно должно выглядеть). К тому же, мы собираемся делать игры, и нужно нечто более визуальное чем код, для создания юая.
IndieGo UI
Нужен компромисс между здоровыми retained-mode библиотеками вроде Qt, которые позволяют запариваться с дизайном и легковесными и удобными для встраивания Immediate-mode библиотеками, дизайн для которых рисовать куда менее удобно.
Дизайн в Im-mode определяется последовательностью вызовов? Штош, мы вполне можем закодировать последовательность различных вызовов, сделав её параметром!
Итерируясь по call_ids и посылая каждый элемент на вход calIUIfunction, мы получаем способ управления элементами в рантайме - нет нужды хардкодить дизайн. Теперь давайте скажем, что UI элемент - это структура, которая хранит информацию о его состоянии после вызова calIUIfunction, а саму функцию сделаем членом структуры (хз, на сколько грамотно писать "член-функция структуры", но идея, думаю, ясна):
В момент вызова функций бэкенда (Im-библиотеки) мы можем запомнить состояние элемента:
Теперь можно создать HashMap из таких элементов, и проверять их состояние в любом месте программы, где этого захочется:
Рисование и апдейт состояния UI элементов же будет происходить при итерации по этому HashMap'у, один раз за "цикл рисования":
В реальности все немного сложнее, и кнопки должны быть расположены на виджетах, начало и конец которых так же обозначаются Immediate-mode вызовами (для ImGUI это ImGui::Begin() и ImGui::End()). Так что вводим понятие виджета, который будет хранить имена элементов, расположенных на нём. Итерироваться будем уже по элементам виджета, встраивая вызовы callUIfunction() между Begin() и End():
Есть ещё размер виджета, его цвет, возможная текстура, строки и столбцы - но логика для них обрабатывается примерно тем же образом (Immediate-mode вызовы в нужный момент - кому интересно, может глянуть в репозитории, ссылка в конце :))
Таким образом задача создания "оконного лога" сводится к описанию прозрачного виджета, на который можно добавлять строки текста (ImGui::Text()). Но само окно я буду описывать в другой статье, а пока....
UI editor
И так, для того, чтобы создать кнопки нам надо заполнить HashMap'ы: GUI (с UI элементами) и widgets (с виджетами). Можем ли мы заполнить их в рантайме? Можем. Можем ли мы заполнить их, тыкая другие кнопки, описанные на этапе компиляции? Почему бы и нет! Так появился UI editor:
Добавленные элементы с типом и свойствами сохраняются в файл, который потом можно прочитать в другом приложении и сразу использовать (далее сам GUI уже немного расширен с HashMap до структуры с доп. элементами и свойствами):
Сам UI editor вряд ли можно использовать как дизайнерский тул, тк кнопки на виджеты все ещё должны добавляться последовательно, да и мышкой их таскать нельзя:
Но вот рабочий интерфейс для приложения в нем создать можно - поддерживаются все основные элементы, а так же можно загружать изображения для скиннинга. Основаня задача UIMap - позволить добавлять элементы управления в окна быстро, особо не запариваясь на их счёт в архитектуре. Редактор же используется для создания UI в играх (кроме Gobby Island из начала статьи делаем ещё Elven City Simulator).
Если вы студент и вам нужно нечто достаточно простое для экспериментов, или вы пишите приложение на C++, для которого неполохо было бы иметь минимальный интерфейс - можете попробовать использовать редактор, почему нет. Помимо указанных выше библиотек, для сериализации "плана выполнения" была взята protobuf
Все зависимости лежат в собранном виде в репозитории. Сам репозиторий:
Вам потребуется CMake для сборки и Visual Studio минимум 19й версии. При наличии Cmake собирается просто (powershell):
На этом у меня всё! В следующий раз выложу и напишу про окно. Постараюсь сделать это побыстрее, честно
P.S.
Почему мы решили делать свой движок? За себя я отвечу картинкой: