История разработки. Часть 1: User Area
К сожалению в последний месяц у меня почти не оставалось времени для работы над движком, да и сама имплементация формата анимированных моделей не предполагает какой-либо интересной информации, о которой можно было бы рассказать в процессе работы, поэтому я решил начать с самого начала и написать ряд заметок о том как проходила разработка движка, какие задачи ставились и как они решались, тем более что систематизированной информации об этом, я как-то не уделял внимания.
Планируемые технические характеристики и особенности движка:
- Движок для 3D игр, основной упор делается на FPS, однако при некоторой доработке так же годится для TPS, 2D игр, теоретически может быть пригоден и для стратегий, однако объем работ по изменению пользовательской части вырастет пропорционально. Ядро остаётся прежним.
- Освещение: только лайтмапы, лайтмапы в сочетании с динамикой, чистая динамика. На усмотрение пользователя.
- Нагрузочная способность: до 10 миллионов полигонов в кадре. В основном зависит от видеокарты и сложности шейдеров. На данный момент имеется система авто-лодов, планируется введение системы импосторов.
- Сетевая поддержка: UDP-соединение, возможность создания кастомного протокола средствами пользовательской части.
- Физическая симуляция: физика твёрдых тел, физика тряпичной куклы, плавучесть объектов. На стороне видеокарты также возможна симуляция тканей и жидкости.
- Форматы хранения уровней и моделей: собственные, с возможностью пользовательского расширения и\или кастомизации, без изменения ядра.
- Собственный редактор уровней с возможностью работы с примитивами (брашы, меши, кривые Безье)
- Система материалов без языка описания. Язык описания материалов формируется пользователем.
- Двухуровневая пользовательская часть. Бакэнд на С++ и игровые объекты на скриптовом языке.
- Полный набор утилит окружения для компиляции, миграции и просмотра ресурсов.
- Меню, базирующееся на собственном оконном менеджере, без использования сторонних библиотек
- Возможность создания игры не прибегая к программированию или ограничившись скриптовым языком
- Система растровых шрифтов, с возможностью хранения произвольной информации (шрифты, иконки, лайтмапы)
Из вышеперечисленного обойден вниманием только редактор анимированных моделей, предполагается использование сторонних (Blender, 3DSMax), что обычно хорошо согласуется с требованиями самих разработчиков. Ключевая идея движка заключается в наличии максимально абстрактного ядра, которое не потребует изменений под изменившиеся требования условий игры и мощной пользовательской части, меняя которую, сам пользователь сможет ориентировать будущую игру под определённый жанр и привести в соответствие своим требованиям. Между ядром и пользовательской частью имеются абстрактные интерфейсы, которые не меняются при обновлении ядра, однако существует потенциальная возможность расширения, за счёт появления новых интерфейсов.
Если не считать бесплодных попыток 2012-го и 2015-го годов, настоящая разработка XashNT началась 13-го февраля 2019-го года. В качестве основы была взята последняя версия Xash3D (build 4511). Это было сделано лишь с единственной целью продолжать итеративный процесс разработки и иметь на каждом этапе рабочий продукт. Первым делом ядро было портировано на С++, поскольку XashNT не предполагает использование чистого Си. Основная сложность заключалась в том, что у итерации был слишком большой временной шаг - фактически мне предстояло из движка с архитектурой из далёкого 1998-го года сделать движок с архитектурой 2014-го (примерно). Однако большая часть механизмов за это время так и не утратила своей актуальности, следовательно процесс не обещал быть особенно сложным, скорее муторным. В первую очередь время было уделено именно пользовательской части. Если вы знакомы с устройством Xash3D, который в свою очередь копирует механизмы обращения с объектами из GoldSrc, то должны помнить, что большую часть описания объекта занимает структура entvars_t, которая в свою очередь была унаследована из виртуальной машины Quake1 (Quake engine). Причём механизм взаимодействия между системной и пользовательской частью предполагал, что поля из этой структуры могут модифицироваться как, непосредственно, ядром, так и пользователем. Поведение было никак не детерминировано и не снабжено никакой документацией. Что в свою очередь порождало известные проблемы у пользователей. В XashNT сама концепция объектов существует только и исключительно в пользовательской части - ядро про них ничего не знает и никак с ними не взаимодействует. Это позволяет менять саму концепцию объектов на усмотрение пользователя. Механизм передачи значений по сети, аналогично находится в пользовательской части, ядро просто предоставляет функции обратной связи, из которой можно записать\прочитать значения в сетевом потоке. Механизм кодирования этих значений так же полностью отдан на откуп пользователю. Возможна реализация как дельта-компрессии, так и простой передачи (надёжность доставки этих значений дополнительно гарантируется самим ядром). Так, к примеру, для пошаговых стратегий или в играх с редко обновляющимся миром, дельта-компрессия просто приводит к перерасходу памяти. Это же касается и чисто сингл-плеерных игр. Аналогично в пользовательскую часть был вынесен и механизм сохранения\загрузки текущего игрового процесса. Следует так же отметить, что сам формат сохранений тоже находится в пользовательской части и ядру об этом ничего не известно. Это позволяет его изменять под текущие нужды. Механизм смены\загрузки уровней в ядре реализован через систему очереди. Это означает, что пользователь отправляет в ядро запросы о смене\загрузке уровня или начале новой игры, после чего начинает процесс завершения текущего уровня и очередном вызове ряда каллбэков в пользовательской части.
Секвенция смены уровня при этом выглядит следующим образом:
->LoadLevel // запрос ядра на смену уровня
...
...
<-SpawnServer // каллбэк из ядра
<-ExecuteLoadLevel // каллбэк из ядра
Каллбэки так же обладают возвратным значением для прерывания процесса загрузки, если с точки зрения пользователя что-то пошло не так. Физическая подсистема также разделена на две части для более плотной интеграции.
Коллижен детектор встроен в ядро и является частью имплементации загрузчика каждого из доступных форматов моделей с абстрактным интерфейсом. Комплексная коллизия сквозь все игровые объекты реализована уже в пользовательской части, потому что, во-первых, как я уже упоминал, ядро лишено какого-либо представления об игровых объектах, а во вторых, очень часто возникают ситуации в потребности совершенно различной обработке разных классов объектов, вплоть до игнорирования физической симуляции для каких-то из них. Законченные физические движки, как правило предоставляют весьма скудный набор для контролирования этих параметров (маски, каллбэки), что приводит к невозможности создания того или иного поведения для объектов. К тому же парадигма большинства физических движков основана на максимально приближённой к реальности физической симуляции всего окружения, тогда как в реальных игровых проектах дело обстоит строго наоборот и физическая симуляция на всех объектах без исключения, лишь увеличивает энтропию и расходует процессорное время. Так же наличие солвера в пользовательской части позволяет менять тип симуляции вплоть до полного отказа, например от симуляции твёрдых тел, например, если жанр игры этого не предусматривает.
Система AI и поиска пути так же полностью находится внутри пользовательской части, впрочем это давно уже стало традицией и ни у кого вопросов не
вызывает.
Пожалуй осталось лишь упомянуть о том, как же происходит взаимодействие ядра с объектами, если, как я уже упоминал, ядро не имеет о них никакого представления, а нам надо их как-то визуализировать.
Для этого существует три каллбэка:
AddEntitiesToScene // вызывается когда приходит очередное обновление на клиент
RenderScene // рендеринг сцены с использованием объектов, добавленных во время вызова предидущего каллбэка
EndFrame // конец текущего кадра, кадр может включать мультипроходной рендеринг
Сама функция добавления объектов в сцену оперирует абстрактным классом IGameObject, от которого берут наследие все игровые объекты в пользовательской части. Этот класс не содержит в себе никаких переменных, однако содержит ряд каллбэков, которые ядро может вызывать для своих нужд в случайном порядке и которые пользователь должен так или иначе обеспечить. Это касается следующих аспектов:
1. проверка на видимость
2. проверка на слышимость
3. абсолютная позиция в пространстве
4. абсолютные углы в пространстве
5. локальный AABB объекта
6. глобальный AABB объекта
7. матрица трансформации объекта в мировом пространстве
8. массив предрассчитанных костей (только для анимированных моделей)
9. некоторый набор переменных (вероятно в дальнейшем будет заменён на абстрактный доступ по имени переменной)
10. указатель на используемую модель
11. указатель на описание параметров прохода рендеринга (позиция и углы камеры, размер вьюпорта, угол обзора). Это используется для как для задания основного вида из глаз игрока в случае FPS-жанра, так и для реализации объектов, требующих мультипроходного рендеринга, например порталов или камер видеонаблюдения.
Рендерер использует стек сцен, поэтому во время рендеринга вложенных информация о предидущих не теряется. Это в свою очередь позволяет реализовать такие эффекты, как зеркала с многократным переотражением. В самом эффекте нет ничего особенного, я просто лишь хочу подчеркнуть, что архитектура ядра изначально строилась с учетом подобных ситуаций.
Таким образом ядро не копирует к себе настройки объектов, а лишь ссылается на них. Список обнуляется по наступлении следующего кадра либо по завершении текущего уровня. Также мельком (в дальнейшем это будет освещено более подробно), хочу упомянуть, что рендер ядра не базируется на особенностях формата уровня, которые используются для эффективного отсечения невидимых объектов, как это чаще всего реализовано в большинстве архитектур. В качестве модели уровня может выступать и анимированная модель, на этот момент принципиально не налагается никаких ограничений. Но естественно, механизмы отсечения уже работать не будут. Впрочем, как я уже говорил, это в первую очередь зависит от предпочтений пользователя и особенностей самой игры.
Вторая функция RenderScene как правило вызывает рендеринг сцены из самого ядра, функция рендеринга предоставлена в пользовательском интерфейсе. Смысл заключается лишь в подмене и\или модификации параметров камеры и вьюпорта, если в этом возникнет необходимость.
В EndFrame производятся любые операции на усмотрение пользователя, обычно очистка каких-то кэширующих флагов.
Всё вышеописанное оформлено в виде единой библиотеки под названием progs.dll. Название прописано внутри ядра и не может быть изменено. Вторая половина пользовательской части представляет собой доигровое меню и оформлено в виде библиотеки GameUI.dll, название которой аналогично прописано внутри ядра. О ней мы поговорим позже.
В заключение хочу ответить на вопрос, который у вас наверняка возникнет: о какой пользовательской части может идти речь, если я пообещал раскрыть все исходники проекта? На самом деле наличие исходников никоим образом не отменяет разделения на системную и пользовательскую часть, т.к. модификация ядра без достаточной квалификации может привести к непредсказуемым последствиям, к тому же такое разделение предполагает, что пользовательская часть останется замороженной в виду своей законченности, а вот ядро может обновляться (без нарушения совместимости), таким образом его модификация со стороны пользователя будет нежелательна в любом случае.
Вышеописанная пользовательская часть была первым этапом разработки XashNT и заняла ориентировочно 6 месяцев с некоторыми перерывами и промежутками. До сентября 2019-го года. Если у вас остались какие-то вопросы - спрашивайте. В следующий раз мы поговорим о системе материалов, её концепции и долгом поиске оптимального подхода, через который мне пришлось пройти.
Продолжение следует.
Chukcha
А планируется ли в движке многопоточность? Например, распараллелить игру полностью на все ядра процессора. Если да, то как примерно в планах вы бы хотели это реализовать? Тасковая система, будут ли новомодные lock-free и даже wait-free алгоритмы для этого?
Apr 12 2021 16:59
Дядя Миша
Движок проектируется с учётом будущей многопоточности, но вот будет ли она внедрена на самом деле - это большой вопрос. Я не люблю подход к проектированию, когда многопоточность становится единственной надеждой выровнять производительность. Т.е. делаю в расчете на одно ядро, но если можно будет что-то распараллелить - почему нет. Просто пока не вижу таких задач.
Apr 13 2021 10:41