Кастомный движок, окно
Всем привет. 8 месяцев прошло с момента публикации последнго девлога, а это значить, что пора пилить ещё один! Да, я обещал, что выложу следующую часть бытрее, и кто-то может сказать, что я обманщик… Но давайте рассуждать так: 16-ричная система счисления не чужда нам, технически подкованным людям, а число 30 (дней в месяце) в такой системе означало бы 48 дней в десятичной.
цифра 10 в 16-ричной системе счисления получается так:
Таким образом 8 месяцев в 16-ричной системе составляло бы 48x8 = 384 дня! В реальности же прошло всего ~240. Я никого не обманывал, материал вышел почти в 1.6 раз быстрее!
В прошлый раз мы подобрали подходящие библиотеки и в итоге набросали встраиваемый в C++ приложение UI. И так, давайте же им воспользуемся и создадим себе сподручный класс окна, который будет:
- принимать и хранить ввод пользователя
- предоставлять другим частям программы доступ к этому вводу
— Хранить данные о том, был ли инпут в текущем кадре
— <...> изменилось ли состояние кнопки
- предоставлять on-screen лог
- предоставлять консольный лог
Поехали.
Начнём с топ-левел дизайна. Так как мы всё-таки делаем игровой движок, в нём с вероятностью 100% будет иметь место вот такой "геймплейный цикл":
У этого цикла есть "начало" и "конец" в практическом смысле - "взять инпут игрока" и "нарисовать фрейм". Для нашего окна это значит, что имеет смысл завести 2 функции:
Первая будет отвечать за все, что окну требуется в начале "цикла фрейма", вторя - соответственно, за конец. Они нам понадобятся!
Ввод пользователя.
Для хранения объявим такую структуру:
Держа в уме наш "цикл фрейма", мы объявлем коллбэки для событий нажатия и отпускания кнопки - их нашей программе будет присылать glfw, которая получит их от ОС - нам при этом не важно, на какой именно ОС выполняется наш код, чудеса прослоек!
Эта святая троица обеспечит нашим кнопкам доступ из других частей программы для "экспериметального" кейса - когда мы будем работать над чем-то, не затрагивая вопрос архитектуры и нам нужно просто максимально быстро написать драйвер-код, буквально вот так:
Так как мы занимаемся разработкой игор, то полезно посмотерть на концепт модели выполнения, стоящий за checkPress():
Время идёт вперёд, и каждая следующиая итерация цикла будет отличаться от предыдущего по крайней мере на квант времени. То есть, у нас есть концепт "фрейма", как cовокупности всех предикатов, описвающих его в конкретный момент времени!
Можно использовать его как У.Е., или баранов, или мотоциклы - считать их, смотерть время между ними, думать о "предыдущем" или "следующем" фрейме, все вот это вот. Как же ведут себя наши флаги между фреймами? А вот так вот:
В некотором фрейме, где кнопка не была нажата, клиентский код, вызывающий checkPress(), заставляет флаг checkPressFlag принять положение false. В том фрейме, когда кнопка меняет положение на "нажата", checkPress() избегает сетап флага checkPressFlag в false, выражение pressed && !checkPressFlag становится верным, в checkPressFlag пишется true, а клиентский код получает добро на запуск, чего там нужно было запустить! В следующем фрейме, когда кнопка все ещё не была отпущена, pressed = true предотвратит сетап checkPressFlag = false и checkPress() вернёт false, тк кнопка не изменяла свого положения именно в этом кадре.
Поотвлекались немного, и поехали дальше. А что же это за window.keyboard? Вот он:
Ничего сверхъестественного - контейнер для кнопок, и немного ситаксического сахара в виде оператора []. Так же есть возможность обратиться к последней нажатой кнопке:
Похожую структуру мы будем использовать для мышки, но она так же будет хранить текущие координаты курсора, дельту перемещения и предыдущие координаты. Может пригодится, кто знает:
Геймпад. Геймпад - это просто клавиатура со стиками
Итого, наш класс окна будет содержать все 3 структуры:
glfw будет посылать ввод нашему окну через коллбэки, так что нужно их сделать, но приводить их целиком здесь я не буду - они состоят в основном из делегирующего кода:
Логгирование.
Возможность печатать текст на экране - это неотъемлимая часть любого игрового движка, которая не только позволяет вывести нужную информацию в реальном времени, но и потешить своё эго личностным ростом, ведь до этого текст выводился только в консоль. Консоль, кстати, тоже нужна, но в готовой игре линковать с ней - дурной тон, так что она тоже будет встроенная. Используя редактор UI из предыдущего девлога, создадим 2 виджета - экранный лог, который будет очищаться в каждом кадре, и экранную консоль, которая будет хранить сообщения на протяжении всей "жизни" программы.
Я не буду останавливаться подробно на создании виджетов и особенностях работы с ними - можно посмотреть на сам редактор на гитхабе, в нём есть примеры. В самом же классе окна нам нужно хранить только имена виджетов:
С консольным логом всё просто - кроме его создания, окну надо добавлять элементы-строки в UIMap динамически. Так как имя элемента - это тоже символьная строка, будем создавать его из заранее согласованного префикса и номера строки, которая добавляется клиентским кодом:
Добавление строк в экранный лог выглядит точно так же, но в отличие от консольного, он должен очищаться между фреймами:
Кстати, возвращаясь к началу нашего повествования - очистка экранного лога это одно из того, что должна делать onFrameEnd()!
Разное, техническое.
Менее интересные вопросы, которое окно как-то должно орабатывать, но которые от нас будут требовать только написания делегирующего "пайпланового" кода, включают в себя:
- полноэкранный режим
- вертикальную синхронизацию
- языковую локаль
Кроме этих вопросов остался ещё счетчик fps - его мы напишем сами. Думаю, используя концепты времени и фрейма, читатель догадается, как оно должно работать (в вызове onFrameEnd()):
Для локали и полноэкранного режима нам потребуются следующе функции, которые будут использоваться кодом игр:
Внутри они просто будут вызывать соответствующие функции glfw.
То, что не хэндлится glfw - это системная локаль! Настройка, отвечающая за текущий язык в системе. Раз уж платформо-независимый код недоступен, придётся вооружится стрым добрым ifdef и написать платформо-зависимый код, который будет компилироваться по-разному для разных ОС:
Игра (или приложение, использующее окно) сможет проверить locale и выбрать нужный язык уже платформо-независимо.
Пожалуй, на этом можно закончить! Пощупать получившееся окно и посмотреть пропущенные в статье моменты можно на гитхабе:
Там же есть приложение-пример, использущий класс окна. На этом всё. Надеюсь, скоро увидимся.