Не Unity единым, или Как мы в Playrix разрабатывали свой движок
Во времена обилия и доступности качественных игровых движков вроде Unity и Unreal необходимость в разработке собственных возникает редко. Об одном из исключений хочу рассказать в этой статье. Речь пойдет об игровом движке компании Playrix, в которой я работаю почти 5 лет. Расскажу о его прошлом и настоящем, текущей функциональности, о том, к каким техническим решениям мы пришли и почему не стали использовать Unity.
В первую очередь эта статья может быть интересна молодым студиям, которые еще не решили, по какому пути идти: создавать свой движок или выбрать существующее решение. А может, статья подбросит новые идеи, если вы уже используете движок собственного производства.
Для начала давайте познакомимся. Меня зовут Виталий, я программист визуальных эффектов в Playrix. В компанию пришел почти пять лет назад, в конце 2015 года. Начинал с портирования проекта Gardenscapes на Mac-платформу, а затем перешел на проект Fishdom, где целиком погрузился в работу с графикой, анимацией и программными эффектами.
Придя в компанию, обнаружил, что на всех проектах используется единый игровой движок собственного производства. Он имеет длинную историю и как отдельный проект начал свое существование в 2009 году из общей части игры Brickshooter Egypt. На момент написания статьи движку уже 11 лет! Можно сказать, он самый старый из работающих проектов Playrix.
Итак, немного о функционале: первоначально движок поддерживал только платформу Windows, а спустя несколько лет появилась поддержка мобильных платформ. Он покрывал весь базовый набор потребностей и был хорошо заточен под наши проекты. Что мы имели тогда:
- вывод графики;
- взаимодействие с SDK;
- работа с ОС;
- работа с ресурсами;
- работа с сетью.
Для комфортной работы не хватало только удобных редакторов и систем для организации игровых объектов. В старые времена у нас разные проекты работали с визуалом по-разному. Например, Gardenscapes применял систему окон, сделанную на Flash, что позволяло дизайнеру настраивать верстку и анимации. На Fishdom все было немного сложнее: отрисовка многих графических объектов велась напрямую из кода, что давало свои плюсы при создании визуальных эффектов. Но было ясно одно: нам нужна единая удобная система по работе с контентом игр.
Несколько раз поднимался вопрос использования Unity, но для этого понадобилось бы перевести уже существующие проекты с C++ на Unity. Этот процесс требует много времени и ресурсов. Избежать его не представлялось возможным, так как мы часто используем наработки одних проектов в других. Разные технологии сильно затруднили бы процесс.
В этой ситуации логичным решением стало развитие собственного инструмента — Visual Scene Object (VSO). В первой версии VSO еще не было ни кодогенерации, ни разнообразия редакторов. Он был написан за два месяца в связке с игровым проектом и стал основой дальнейшего развития. Опираясь на четкое понимание потенциала и востребованности продукта, команда из 4 человек активно работала над проектом, пусть это и не всегда совпадало с основным вектором разработки движка.
Дальше разработка шла эволюционно и постепенно, с учетом интересов и пожеланий проектов. Сейчас в команде VSO больше 10 человек, а времени на выход новых версий уходит намного больше, чем раньше. Помимо основной команды, в развитии VSO участвуют программисты других проектов. Когда Fishdom начал свой переход на VSO, я быстро включился в процесс написания новых behaviour-сценариев для упрощения процесса разработки. Часть этих наработок перекочевала в общую VSO-библиотеку.
Что мы имеем сейчас
Мы создали свою версию компонентной системы, в которой есть набор редакторов и различных подсистем. При ее разработке отталкивались от нужд и пожеланий игровых проектов и вдохновлялись лучшими из уже существующих движков.
Мы используем наборы объектов, собранных в сцены. Они состоят из узлов, которые, в свою очередь, состоят из ряда сущностей:
- Transform — трансформация узла (позиция, поворот, масштаб).
- Component — элемент отрисовки (не более одного, но может и отсутствовать). Это любой объект с возможностью отображения, например sprite или particle.
- Behaviour — элемент, описывающий поведение и любую логику, их количество не ограничено.
- Sorting — элемент управления порядком отрисовки узлов. Помимо регулирования порядка между объектами VSO, этот элемент помогает легко интегрироваться в запущенные проекты и обеспечить связь старых сущностей с новыми. С его помощью управлять порядком отображения может внешний код.
Программисты могут делать свои собственные component, behaviour, sorting. Более универсальные создает команда VSO, а узконаправленные — программисты внутри игровых проектов. Порой функционал из проекта переходит внутрь общего функционала VSO и становится частью библиотеки. Для написания своей версии класса требуется создать наследника от нужной базовой реализации и переопределить события (например, OnInit, OnClone). Вместо использования макросов, как это сделано в UnrealEngine, мы используем теги в комментариях.
Пример кода компоненты доступен тут.
Система, ориентируясь на класс, а также учитывая теги, генерирует код, необходимый для работы с данными (сохранение и загрузка), функционирования редакторов, поддержания клонирования и прочего функционала.
Генерацию и сериализацию редакторов используют не только для визуальных объектов, но и для любых других классов, наследуемых от Serializable. Нужно только пометить свойства тегами. Ну а если наследовать свой класс от класса Asset, получим полноценный ассет, похожий по функционалу на ScriptableObject в Unity. Такая модель библиотеки ускоряет разработку новой функциональности и делегирует часть задач профильным специалистам.
Основные блоки
Кодогенерация
Так как в языке C++ нет рефлексии (reflection — получение данных о типе из кода), значительную часть кода, обеспечивающую работу системы, приходится писать руками. Но мы смогли добиться генерации большей части этого рутинного кода.
Наш генератор — это питоновский скрипт, который парсит заголовочные файлы и на их основе создает нужный код. За счет тегов настройка генерации получилась гибкой. Для некоторых из наших систем код генерируется целиком, например:
- Инструмент сериализации (сохранение/загрузка с диска или по сети).
- Binding для библиотеки рефлексии (инструмент для создания редакторов).
- Код для клонирования объектов.
- Код, необходимый собственной runtime-рефлексии.
Пример кода сгенерированного класса доступен тут.
Парсинг C++
Одним из решений для разбора заголовочных файлов мог быть парсинг с Clang, но его скорости не хватало. Поэтому остановились на CppHeaderParser — это простая Python-библиотека, состоящая всего из одного файла. Она работает быстро, но имеет ряд ограничений, например, не ходит по #include и не обрабатывает макросы или символы. Поэтому мы ее серьезно доработали, среди прочего добавили нововведения из C++17, и используем до сих пор.
Чтобы избежать неопределенностей статуса при генерации кода, мы решили делать библиотеку полностью автоматической. Для этого выбрали CMake. Проводим генерацию при каждой компиляции. Для экономии времени сохраняем кэш с результатами парсинга, и холостой проход кодогенерации занимает всего пару секунд.
Генератор кода
Выбор библиотеки для генерации по шаблонам оказался довольно простым. Мы остановились на Templite+. Она не большая, но обладает всем необходимым функционалом.
К текущей версии генерации пришли не сразу. Изначально основную логику писали на Python, использовали шаблоны по минимуму, а код содержал множество условий и проверок. В таком подходе были свои плюсы: лучшая читабельность, чем в шаблонах, возможность реализовать хитрую логику. Но при этом Python-код соседствовал с большим количество кода на С++, и такая мешанина быстро утомляла. Генераторы на Python частично решали проблему, но не целиком.
По итогу все-таки перешли на генерацию с помощью шаблонов, и Python теперь только готовит данные.
Сериализация
Мы рассматривали целый ряд библиотек для сериализации, среди которых были FlatBuffers, cereal, Protobuf и другие.
Библиотеки с кодогенерацией, как FlatBuffers и Protobuf, подразумевают рукописные структуры, а сгенерированные структуры невозможно интегрировать в пользовательский код. Результатом использования этих библиотек было бы увеличение в два раза классов исключительно для сериализации.
В этой ситуации cereal выглядела лучшим кандидатом. У этой библиотеки приятный синтаксис, понятная реализация, в ней достаточно комфортная генерация кода для сериализации. Единственный крупный минус — ее бинарный формат. Основным требованием с нашей стороны была независимость формата от «железа» (порядок байтов или разрядность не должны влиять на чтение данных). А также удобство записи бинарного формата из Python. По этим критериям cereal нам также не подходил.
Взяв за основу идею cereal, мы создали библиотеку, в которой находятся базовые архивы чтения и записи данных. Наследники от этих архивов реализуют запись в нужном формате: json, xml. Конвертация из xml в бинарный формат выполняется простым Python-скриптом. Код сериализации в дальнейшем генерируется по классам и пользуется этими архивами при записи данных.
Редакторы
Для написания окон редакторов мы выбрали библиотеку ImGui. На ней созданы окно содержимого сцены, инспектор ассетов, редакторы анимаций и эффектов и прочее. Большая часть кода для редакторов пишется вручную, но для работы со свойствами классов, их просмотра или редактирования используется библиотека RTTR, а также биндинг, сгенерированный под нее, и обобщенный код инспекторов.
Библиотека рефлексии — RTTR
Для организации рефлексии в С++ мы выбрали библиотеку RTTR. Она имеет простой API, и ей не нужно вмешиваться в классы. RTTR поддерживает различные обертки над типами (например, умные указатели) и коллекции, а также позволяет регистрировать свои собственные обертки и обладает необходимой функциональностью.
Главная проблема этой библиотеки — ее громоздкость и медлительность. Поэтому применяем ее только для редакторов. Для игровых объектов сделали свою простенькую библиотеку.
В библиотеке RTTR требуется написание биндинга с объявлением всех методов и свойств класса. Этот биндинг генерируется из кода на Python для тех классов, которым нужна возможность редактирования. А за счет того, что RTTR поддерживает метаданные для всех сущностей, генератор создает различные настройки членам класса: специнспектор для поля, границы числовых полей, тултипы. В инспекторе эти метаданные применяются для отрисовки интерфейса редактирования.
Пример объявления класса в RTTR смотрите тут.
Инспектор
Обычно код внутри редактора не взаимодействует напрямую с RTTR. Для этого используют рукописные классы-прослойки, которые умеют рисовать ImGui-инспектор для объекта.
Метаданные, указанные в RTTR при регистрации, нужны, чтобы адаптировать для вывода редактируемые данные объекта. Мы можем создавать объекты, которые хранятся по значению или по указателю, а также имеем поддержку примитивных типов и коллекций.
В инспекторе мы заложили возможность отменить операции. Для этого используем команды, которые создаются при любом изменении данных и в которых заложен функционал возврата изначальных данных. Это дало Ctrl+Z.
Окна и редакторы
На основе кодогенерации, функционала редакторов и систем создания ассетов было создано большое количество подсистем и полезных игровых редакторов:
Редактор сцены — это гибкая система интерфейсов, которая позволяет наполнить сцену объектами, свойствами, настроить верстку и внешний вид.
Редактор событий и квестов — предназначен для создания различных туториалов, квестов и других событий. Может использоваться практически без участия программистов.
Редактор состояний
Редактор анимационных состояний — это набор состояний объекта, управляемых посредством анимации, и переходов между ними. Редактор позволяет также настраивать связи между этими состояниями, их длительность и тонкости перехода. Получив команду на смену состояния, контроллер автоматически найдет к нему путь и проиграет все анимации в промежуточных блоках.
Редактор анимаций
Редактор анимаций — основной инструмент для создания анимаций в VSO. Помимо управления трансформацией и цветом объектов, благодаря редактору можно привязать анимацию практически к любому из свойств или управлять их поведением. Например, регулировать скорость проигрывания flash-клипа или запустить эффект в нужный момент.
Редактор эффектов
Редактор эффектов позволяет создавать и редактировать эффекты частиц и является основным инструментом работы VFX-художников в компании.
В нашей реализации эффект — это набор нескольких систем однотипных частиц, которые изменяются во времени по одинаковым законам. У системы частиц есть как постоянные характеристики — текстура частиц, параметры эмиттера, предельное число частиц, так и переменные — положение (или скорость), размер, угол поворота, цвет. Из-за того, что некоторые параметры системы частиц при старте эффекта выбираются случайно, характеристики каждой отдельной частицы могут отличаться от характеристик другой частицы. Это создает иллюзию, что частицы живут сами по себе.
С помощью эффектов легко оживить статичную сцену, добавить красоты и расставить акценты.
Редактор ICS
Interactive Cutscenes (ICS) предназначен для создания сценариев, по которым на сцене будут происходить различные события в зависимости от заданных условий. Основная идея заключается не в том, чтобы перейти из одного состояния в другое, как это происходит в редакторе состояний, а чтобы проиграть сцену (набор действий). ICS позволяет вручную настроить маршрут, добавить развилки с зависимостью от условий, ожидать обработки кликов и многое другое. Программисты внутри проектов могут создавать особые блоки действий, расширяя функционал редактора.
Итоги
Мы понимаем, что Unity — это отличный движок, но для наших текущих разработок использовать его нецелесообразно из-за требуемых для перехода и поддержания усилий и средств. Использование чужого движка лишило бы возможности тонко настраивать инструменты под себя, создавать и развивать только те подсистемы, которые действительно нужны. Кроме этого, интеграция и распространение решений, созданных на проектах, была бы трудно реализуемой.
Таким образом, главное приобретение — это свобода действий. Поскольку движок создавался и развивался, ориентируясь на нужды проектов, в нем не освещена функциональность, которая не используется в играх Playrix. Например, в нашем движке нет поддержки больших 3D-сцен. Мы не используем статические и динамические тени, прямое и отложенное освещение — это большой пласт работ, на который мы сейчас не готовы. Однако если будем разрабатывать игру, которая требует использования, например, ландшафта или освещения, то обязательно вернемся к Unity, Unreal Engine или другого популярного движка.
Свой движок — это гибкость и свобода, но и большие затраты на разработку и поддержку. Выбор готового движка — быстрое внедрение, возможность найти специалиста с опытом взаимодействия, но и необходимость подстраиваться и идти на компромиссы. Решение остается за вами.