Разработчик ролевой игры Knights of Frontier Valley рассказал о причинах создания собственного движка, его разработке, основных компонентах и управлении неигровыми персонажами в игре.
Странствуя по Фронтир Вэлли в поисках приключений и славы, вы путешествуете по динамичному, постоянно меняющемуся миру. Вы увидите закат солнца на горизонте, качающиеся на ветру деревья и окрашивающие землю белым снежинки. Во время передвижения путь автоматически прокладывается как можно эффективнее.
Всё это (и не только) работает благодаря собственному, разработанному специально для Knights of Frontier Valley движку. В примерах выше время захода солнца управляется «Менеджером времени» — компонентом, отвечающим за течение времени в игровом мире и освещение окружения. Осадки и качающиеся на ветру деревья возможны благодаря «Менеджеру погоды», а снежинки отрисовываются рендерером карты с использованием системы частиц. Передвижение персонажей по карте осуществляется по специально разработанному алгоритму поиска пути.
Сегодня я расскажу, как работают некоторые из этих механик, и зачем для этого понадобился собственный движок. Или посмотрите видео:
Мотивация
Начнём с вопроса «Зачем». Создание игрового движка с нуля для такой игры — амбициозное начинание. Возможности движка включают отрисовку изометрического вида (2.5D), продвинутое управление памятью, процедурную генерацию различных типов карт, обработку сложного поведения персонажей и их взаимодействий, сохранение состояния игры (для последующей загрузки), пошаговые механики и в реальном времени, собственный алгоритм поиска пути и многое другое. Такого объёма работ хватит на несколько лет.
Так зачем всё это, если есть много готовых движков, а некоторые ещё и бесплатны? Всё дело в поставленных целях.
Когда я начал работать над игрой в 2016 году, то вдохновлялся первопроходцами индустрии, такими как Ричард Гэрриот, Сид Мейер и Питер Молиньё, разработавшими первые игры в золотой век RPG в 80-х и 90-х годах. Тогда они не использовали сторонние движки, и на мой взгляд пройти по их стопам означало написать игру с нуля. Мне не нравилась идея делать игру на основе чужого, не до конца понятного мне кода, который я не смогу полностью адаптировать под свои задачи. Кроме того, я подумал, что это весьма увлекательно (и в целом так и есть).
Хотя Knights of Frontier Valley — мой первый игровой проект, за плечами у меня двадцатилетняя карьера в разработке ПО для настольных компьютеров, интернет-сайтов, встраиваемых систем и мобильных устройств. В Кремниевой долине я занимался программами для работы с GPS, разрабатывал софт под первые iPhone и оригинальный iPad, а также информационно-развлекательные системы и автопилот для Tesla.
Итак, программировать я умею, но разработка игр — совершенно другое направление, и мне предстояло многому научиться. Я начал делать игру, вкалывая на основной работе, поэтому занимался этим лишь по вечерам и в выходные. Примерно через три месяца у меня появился прототип с основным игровым циклом, где персонаж мог ходить по карте и выполнять простейшие действия. Прототип работал, но я понял, что с таким подходом никогда не создам игру с интересной визуальной составляющей. Используемые технологии попросту не подходили для игровой разработки, поэтому я всё бросил и начал заново — используя libGDX, упрощающий рендеринг OpenGL и некоторые другие игровые функции открытый Java-фреймворк. Я выбрал Java и OpenGL, поскольку хорошо знал язык, а ещё эти технологии использовались в успешных играх вроде Minecraft и RuneScape.
Отныне я был на правильном пути, и последующие годы развивал как саму игру, так и её движок.
На создание движка с нуля в свободное от работы время ушло около пяти лет, и если бы меня спросили, стоит ли этим заниматься, я бы ответил так: кому как. Если вы не опытный программист, я бы не советовал. Но даже для опытных разработчиков преимущества полного контроля над кодом, спокойствие по поводу возможных отчислений, изучение сопутствующих технологий и удовлетворение от достигнутой цели нужно соизмерять с годами работы без дохода и с приличной долей разочарований. Если вы не уверены, стоит ли — вероятно, не стоит. Стремление завершить начатое должно быть непоколебимо, юный падаван, иначе ты рискуешь потратить прорву времени и всё бросить.
Основы движка
Но что такое «игровой движок»? Смотря кого спрашивать, но я считаю движком всё не связанное с логикой и внешним видом конкретной игры программное обеспечение. Это выделенная отдельно базовая функциональность, которую можно использовать и в других играх похожего типа.
Основы проектирования программного обеспечения: во-первых, чтобы код был читаемым, поддерживаемым и пригодным для многократного использования, функциональность должна быть инкапсулирована. Это означает, что отдельные части кодовой базы не должны содержать логику для разных функций. И во-вторых, поведение и визуализация всегда должны быть разделены.
Следуя этим принципам, я разработал движок в виде набора модулей, где каждый отвечает за определённую игровую функцию. Эти модули подключаются по необходимости и управляются центральным компонентом, связывающим всё воедино: так получается основной игровой цикл.
Основной цикл — это выполняемая несколько раз в секунду функция. На каждом проходе обновляется весь игровой мир, и на экране отрисовывается отображающий актуальное его состояние кадр. В идеале основной цикл должен выполняться минимум 60 раз в секунду (= 60 FPS), чего достаточно для старых мониторов с соответствующей частотой обновления изображения (= 60 Герц). Сегодня это число зачастую выше, обычно 144 Гц.
Если ваша система недостаточно мощна для 60 FPS (узким местом обычно является GPU), можно заметить задержки и прерывистую анимацию. Но с Knights of Frontier Valley волноваться о видеокарте не придётся: графика в основном предварительно отрендерена, так что GPU может «пойти отдохнуть». Я не вижу тормозов даже на своё старом ноуте со встроенной графикой.
Мой движок состоит из:
- Основной цикл
- Конечный автомат
- Управление памятью
- Обработчик сохранений
- Рендереры для карты и объектов
- Систему воспроизведения видео
- Вывод звука
- Обработку ввода мыши/клавиатуры
- Сетевые функции
- Управление игровым временем
- Поиск пути
- Моделирование карт
- Моделирование погоды
- Моделирование персонажей и очередей действий
- Моделирование перемещения персонажей
Не входит в движок:
- Всё связанное с сюжетом и квестами
- Графические ресурсы, музыка и звуковые эффекты
- Пользовательский интерфейс
- Логика процедурного построения карт
- Специфичные для игры персонажи, объекты и предметы
- Поведение NPC
- Специфика конкретной игры, в том числе создание персонажа, боевая механика, спецэффекты (например, магия), пользовательские шейдеры, мини-игры и многое другое
О двух модулях я расскажу подробнее: моделировании персонажей (и связанных с ним очередях действий) и компоненте управления перемещениями NPC. Дополнительные подробности — в дневниках разработки о поиске пути и рендеринге на сайте проекта.
Моделирование персонажей и очереди действий
Моделирование персонажей — это программный контейнер, хранящий всех персонажей, которым могут понадобиться действия — для обновления поведения их следует сначала загрузить в память. Это относится ко всем NPC на карте с игровым персонажем, а также к некоторым другим, которым может потребоваться действовать по иным причинам: лидерам фракций, путешествующим (об этом позже) и важным квестовым персонажам. Когда игровой персонаж находится в городе, модуль обрабатывает сотни NPC — одни находятся в зданиях, а другие прогуливаются снаружи.
Модуль обрабатывает далеко не всех персонажей игрового мира одновременно, но поскольку большинство из них находятся на других картах (то есть не видны игроку) и не имеют причин действовать, нет смысла держать их в памяти и тратить процессорное время на их обновление. Эти «спящие» персонажи хранятся в базе данных и загрузятся в модуль по необходимости.
Когда игровой персонаж перемещается на другую карту, модуль определяет, каких персонажей в неё добавить (а каких — исключить), и выполняет необходимые действия. Вновь добавленные персонажи проходят оценку последнего сохранённого состояния — если их положение и деятельность больше не соответствуют их текущему поведенческому циклу (работа, сон, досуг и так далее), их телепортирует в нужное место на карте и они приступят к соответствующей деятельности.
По сути, модуль управляет не отдельными персонажами, а целыми группами. Даже действующий в одиночку персонаж (включая игрового), всегда входит в группу — возможно, из одного участника. Если группа состоит из нескольких персонажей, они всегда остаются на одной карте и перемещаются согласно формации, следуя за лидером. Если член группы погибает, его тело перемещается в новую группу с одним участником. Если погибает лидер группы, определяется новый, исходя из силы и ранга.
Когда персонажам группы требуется действовать индивидуально (то есть не двигаться в формации) — например, во время тактического боя — группа временно разделяется, и каждый персонаж получает собственную группу с одним участником. После окончания боя выжившие NPC возвращаются в старые группы или формируют новые.
Теперь, когда вы знаете, какие персонажи хранятся в модуле и способны действовать, давайте рассмотрим, что означает «действие» в контексте движка.
У каждого персонажа есть «очередь» — список действий, которые он выполнит по порядку. В них входят «движение по траектории», «взаимодействие с объектом», «разговор», «атаку», «исполнение музыки», «чтение свитка» и так далее, а иногда это просто «ожидание». Когда персонаж не занят выполнением действия, из очереди берётся верхнее действие и запускается. Только если очередь пуста, персонаж действительно бездействует.
Очередь может содержать множество действий, и в начале их выполнения некоторые могут разделяться на несколько новых. Это сложная система, где каждое возможное действие персонажа необходимо задать заранее и оценить время исполнения. Сейчас существует 36 типов действий, и некоторые представляют собой наборы нескольких поддействий. Одно из них называется «пользовательское действие» и может быть настроено как угодно, что даёт безграничные возможности.
Если во время выполнения NPC своей очереди происходит что-то непредвиденное (например, атак игрока), очередь очищается, а поведение пересматривается: в качестве ответной реакции создаются новые действия. Когда игра сохраняется, очереди действий также сохраняются и восстанавливаются при загрузке, так что все персонажи продолжают прямо с того места, где и остановились.
Модуль перемещения персонажей
При моделировании NPC персонажи могут перемещаться в реальном времени в любое место, даже через не загруженные в память карты. «Модуль перемещения» чётко определяет необходимые для путешествия действия и помещает их в очередь лидера группы. Например, для перемещения с чердака дома в подвал другого дома в другом городе потребуется выполнить следующие действия:
- Запустить соответствующую анимацию движения (ходьба, бег или полёт... в других сценариях, возможно, также верховая езда или плавание)
- Подойти к лестнице вниз
- Спуститься по лестнице до входного уровня здания
- Подойти к входной двери
- Взаимодействовать с дверью для выхода из здания
- Подойти к точке выхода из города
- Покинуть город
- Идти по миру ко входу в город назначения
- Войти в город
- Пройти требуемые городские кварталы и добраться до нужного дома
- Подойти ко входу в дом
- Взаимодействовать с дверью для входа в здание
- Спуститься по лестнице в подвал
- Дойти до точки назначения в подвале
Эти действия выполнятся поочерёдно, но предварительный расчёт полного списка действий перед путешествием не оптимален. По пути может произойти что-то непредвиденное, что заставит персонажа пересмотреть поведение: важная дверь может оказаться запертой, или персонаж просто устанет и не сможет продолжить путь. Разумеется, путешествовать между городами в Долине небезопасно. Персонаж действительно не знает, что произойдёт во время путешествия, поэтому движок рассчитывает действия из списка выше лишь по одному за раз. В промежуточных точках ситуация переоценивается с учётом нового окружения персонажа, а затем, если не происходит чего-то более важного, выполняется следующее действие в очереди. Принятие решения о дальнейших действиях по промежуточным точкам повышает надёжность системы и позволяет избежать ненужных рассчётов. Это также распределяет вычислительную нагрузку, что хорошо, когда много персонажей путешествуют одновременно.
Путешествующие NPC остаются в модели персонажей, даже если покидают карту с игроком, ведь им необходимо действовать вплоть до пункта назначения. Пока персонаж находится на одной с игроком карте, все препятствия на его пути известны и загружены в память, и их можно обойти с помощью компонента поиска пути. Но если персонаж ушёл на другую карту, как система узнаёт варианты обхода препятствий и определит временные затраты, если эта карта не находится в памяти?
Я решил эту проблему, добавив концепцию «маршрутизации вне карты» — систему, использующую среднее время пути при перемещении на определённое расстояние по различным типам карт. Проложив огромное количество маршрутов на каждом типе карты, я вычислил среднее время их прохождения. Это означает, что если NPC покинет карту с игроком и исчезнет из виду, а игрок позднее последует за ним, он найдёт NPC именно в том месте, где тот должен находиться.
Это не реальный расчёт, а приближение и своего рода «подделка» части путешествия. Но она отлично работает, и игроки никогда не узнают, что части маршрута не обсчитывались в реальном времени, МУАХАХАХАХА. Ладно, ну, ВЫ теперь знаете об этом — считайте это наградой за чтение до конца. Ваша способность удерживать внимание достойна восхищения, и у вас, вероятно, есть всё необходимое для написания собственного движка.