Как я создаю игры на своём 3D движке в одиночку
Много лет назад я занимался созданием маленьких Flash игр и публиковал их на сайте Newgrounds. Сейчас я делаю полноценные игры для ПК.
На сегодняшний день у меня 4 законченных коммерческих игр в Steam, и самая последняя из них — выпущенная в 2021 году Pilie Pals, о процессе создания которой я расскажу в этой статье. Я работал над игрой всего примерно 6 месяцев, по вечерам после работы и на выходных.
Я занимаюсь дизайном, программированием, графикой, звуками и музыкой в одиночку. Мне это нравится, и таким образом я могу часто переключаться с одного вида деятельности на другой, благодаря чему не теряю интерес к разработке игры.
Я написал собственный 3D игровой движок YUME, используя Haxe, C++ и OpenGL, и на данный момент он используется тремя моими играми. Подробности и причины создания собственного движка приведены в отдельной статье. Такой подход меня вполне удовлетворяет, и я не планирую его менять.
Разработка Pilie Pals
Трейлер к игре. Доступна бесплатная демо версия игры, а также есть страница в Steam.
В начале разработки Pilie Pals я скопировал папку проекта своей предыдущей игры Phantom Path и удалил все ресурсы и файлы кода, связанные с игрой. Остался "чистый" движок, с которым можно экспериментировать, чтобы создать прототип следующей игры.
Перед тем, как описывать создание Pilie Pals, я объясню несколько главных принципов работы моего движка.
Хранение игровых данных в текстовых файлах
Мой движок почти полностью основывается на внешних данных (data-driven). Это значит, что я создаю набор файлов с данными, а движок их читает и обрабатывает.
Я стараюсь избегать жёсткого прописывания чего-либо в исходном коде игры. По возможности, вся информация хранится в отдельных файлах: игровые объекты, уровни, переводы текстов, визуальные и звуковые эффекты, и даже некоторая игровая логика, написанная на собственном языке сценариев YumeScript. Большая часть данных хранится в формате JSON.
Самое большое преимущество такого подхода заключается в том, что я могу создавать и править игру, не выходя из неё. При изменении какого-либо файла с данными движок автоматически перегружает ту часть данных, которая изменилась, без необходимости перезапускать или пересобирать что-то вручную. То же самое происходит с другими ресурсами, например, с текстурами, 3D моделями и звуками.
Ещё один плюс — это потенциальная поддержка пользовательских модификаций игры. Недавно я выложил новое обновление для игры, которое добавляет русскую локализацию, а также возможность создавать неофициальные модификации для поддержки других языков.
Сущности
Два главных компонента сцены игры в моём движке — это сущности и карты.
Сущность — игровой объект, который я описываю в JSON файле, чтобы впредь создавать экземпляры объектов такого вида. Например, персонаж игрока — это сущность.
Файл описания сущности содержит информацию:
Какие 3D модели содержит данная сущность, и как их отображать
Какие анимации могут совершать модели этой сущности, и как осуществляются переходы из одного состояния в другое
Какие эффекты может запускать данная сущность
Какие звуки может воспроизводить данная сущность
Какие области соприкосновения есть у данной сущности
Какие у сущности могут быть состояния, и каково поведение сущности в разных состояниях
Другие данные для использования в игровой логике: специальные тэги, группы, и т.д.
Стоит отметить, что у каждого экземпляра сущности есть собственная машина состояний, и у каждого состояния есть последовательность действий, которую сущность может проигрывать.
Например, можно задать состояние "walk" (ходьба) для сущности персонажа игры, и последовательность действий этого состояния может содержать команды, которые запускают нужную анимацию 3D модели, проигрывают звуки шагов и показывают эффект поднимающийся с земли пыли. Всё это описывается в текстовом файле, который можно редактировать и сразу тестировать, и это сильно ускоряет процесс разработки.
Карты
Карта — это файл данных, который содержит описание расположения сущностей. В этом файле также есть многоуровневая сетка "плиток", из которой можно построить ландшафт. Я не меняю файлы карт вручную — карты создаются с помощью встроенного редактора.
Так как движок знает всю информацию о сущностях, которые располагаются на карте, он в состоянии автоматически оптимизировать некоторые вещи. Например, если сущность является статичной и никогда не двигается (например, дерево или ландшафт), то движок автоматически объединяет её вместе с другими статичными сущностями, которые используют одинаковую текстуру, и создаёт одну общую 3D модель. Это значительно сокращает количество отображаемых видеокартой объектов, что сильно улучшает производительность игры.
Игровая логика
Некоторая часть игровой логики может быть описана в текстовых файлах, но она используется только для создания игровых сценариев, а не основной функциональности игры. Такая логика, как система соприкосновений, поведения искусственного интеллекта, правила игрового процесса и т.д. — программируется в исходном коде Haxe, который превращается в C++ при компиляции.
Есть 2 категории файлов кода, связанных с логикой:
Процессоры логики сущностей — могут быть присоединены к отдельным сущностям, используются для обработки индивидуальной логики объектов (например, искусственного интеллекта). Не каждой сущности нужен процессор логики.
Ядро — одиночный класс, который описывает общую логику правил игрового процесса.
Первый месяц: Прототип
У меня была идея о пошаговой игре-головоломке, в которой игрок может управлять сразу несколькими персонажами, способными поднимать и переносить всякие предметы. Логика игры должна была быть пошаговой, потому что я хотел записывать каждый игровой шаг, чтобы дать игроку возможность отменить свои шаги, т.е. вернуться в прошлое.
Сначала я написал ядро логики и реализовал пошаговую систему. Для каждой сущности, которая является частью головоломки, ядро выставляет состояние.
Игрок может выделить персонажа и выполнить действие (например, переместиться, или поднять предмет), которое меняет состояние игрового мира. Игра потом вычисляет, является ли возможным новое состояние мира (например, не столкнулся ли игрок с препятствием), и проверяет, нужна ли какая-либо реакция на это изменение, со стороны других элементов головоломки (например, если игрок положил какой-то предмет на кнопку, то у кнопки должно измениться состояние на "нажатая"). Если все проверки пройдены, то изменения применяются и добавляются в историю шагов.
Игрок может отменить последний шаг, просто вернувшись в предыдущее состояние мира.
В этом заключается основная функциональность игрового ядра. На самом деле, всё немного сложнее, потому что мне нужно обеспечить плавные, анимированные переходы между состояниями, позволить элементам быть переносимыми другими элементами, и делать другие интересные вещи — но всё это добавляется к базовому "фундаменту" логики игры.
Реализация такой системы и всех крайностей заняла у меня примерно неделю. Оставшееся время заняли размышления о том, какую игру я хочу сделать. В это время у меня появилась идея о том, что персонажи могли бы переносить других персонажей, и даже создавать стопки из друг друга.
Я создал 4 уровня игры, используя мой существующий редактор карт, чтобы убедиться, что игровой процесс действительно интересный. Результат мне понравился, и я продолжил разработку.
Второй месяц: Графика
Теперь у меня был рабочий прототип игры, и можно было начинать экспериментировать с художественными стилями. Я остановился на мультяшном визуальном стиле, и весь месяц занимался созданием 3D моделей, анимаций, эффектов, интерфейса, переходами, и т.д. Игра разбита на 5 тематических миров.
Я использую Blender для создания 3D моделей и анимаций, и GIMP для создания текстур.
Пластиковый вид игры достигнут с помощью написанного мною шейдера, который применяет заранее приготовленные данные об освещении к моделям. Работает это так: берётся заготовленная картинка освещённой сферы, применяется к некоторым частям модели, смешивая цветовые данные текстуры на основе нормалей модели в пространстве экрана.
Идея такого эффекта появилась у меня после работы с программами 3D моделирования и скульптуры. Подобный эффект в таких программах иногда называется MatCap. Мой подход заключается в том, что я смешиваю такой приём с традиционными алгоритмами освещения в реальном времени. Получается интересный эффект, который обрабатывается видеокартой очень быстро.
Третий месяц: Полировка
Весь месяц я улучшал User Experience игры: добивался плавности анимаций, хорошей чувствительности управления, чистоты графических элементов, удобности интерфейса.
Было реализовано меню паузы, меню выбора уровня, система последовательности уровней и системы сохранения — практически весь этот функционал у меня уже был создан для предыдущих игр, поэтому можно было повторно использовать часть этого кода, откорректировав некоторые визуальные вещи.
Работать над интерфейсами довольно утомительно, но плохой UX раздражает игроков, поэтому важно сделать всё правильно с самого начала.
Я решил сделать отполированный, полноценный "вертикальный срез" как можно быстрее. Таким образом я смог бы проверить, как бы выглядел готовый проект. Так как в игре на тот момент было мало контента, можно было свободно вносить глобальные изменения без особых проблем.
Четвёртый месяц: Музыка, звуки и демо версия
Я пишу музыку и создаю звуки в виртуальном модульном синтезаторе SunVox. Я — самоучка, и создание музыки у меня занимает довольно много времени.
В моём движке есть система динамического звукового окружения, которая может генерировать окружающий шум в реальном времени, используя подготовленные звуки (например, шум волн или крики птиц), меняя их частоту и воспроизводя в разных направлениях и в разных комбинациях. Эта информация описана в отдельном JSON файле.
На тот момент у меня была полностью "отполированная" игра, в которой было всего 4 уровня. Я создал ещё 6, и выпустил демо версию игры.
Так я стал собирать отзывы от игроков на ранней стадии разработки, и смог на их основе улучшить User Experience игры.
Примерно в это время я добавил систему подсказок в игру. О ней я подробно написал в отдельной статье.
Пятый и шестой месяцы: Контент игры
Следующие два месяца я делал новые уровни, добавлял новые игровые элементы, используя ту систему сущностей, которую я описал выше. В исходном коде игры уже практически не было никаких изменений, потому что ядро игры уже было полностью готово.
Эти два месяца я в основном работал в редакторе карт.
Вывод
В целом я удовлетворён результатом.
В начале разработки игры у меня уже было довольно чёткое представление о том, какую игру я хочу сделать. Поэтому процесс разработки прошёл намного плавнее и быстрее, чем обычно. Так я осознал важность наличия чёткой цели с самого начала.
Также оказалось хорошей идеей сделать отполированный вертикальный срез как можно скорее. Таким образом, я мог начать делать скриншоты, видео и даже выложить демо версию хорошего качества всего после 4 месяцев работы. Я получил несколько полезных отзывов от игроков ещё до того, как большая часть контента была готова, поэтому было проще вносить изменения, ничего при этом не ломая.
В этот раз я практически ничего не менял в ядре своего движка, и большая часть времени ушла непосредственно на создание игрового контента. Я буду продолжать использовать свой движок в будущих проектах.