- PVSM.RU - https://www.pvsm.ru -
У нашей компании есть свой игровой движок, который используется для всех разрабатываемых игр. Он предоставляет всю важную базовую функциональность:
Однако в нем не хватало того, чем так ценится Unity, — удобной системы организации сцен и игровых объектов, а также редакторов к ним.
Здесь я хочу рассказать, как мы внедряли все эти удобства и к чему пришли.
Сейчас у нас есть некоторое подобие компонентной системы в Unity со всеми важными подсистемами и редакторами. Однако, так как мы исходили из нужд наших конкретных проектов, существуют довольно значительные расхождения.
У нас есть визуальные объекты, которые хранятся в сценах. Эти объекты состоят из узлов, которые организованы в иерархию и каждый узел может иметь ряд сущностей, таких как:
Как и в Unity, программисты создают свои component, behaviour или sorting. Для этого достаточно просто написать класс, переопределить нужные события (Update, OnStart и др) и пометить нужные поля специальным образом. В UnrealEngine это делается макросами, а мы решили использовать теги в комментариях.
/// @category(VSO.Basic)
class SpriteComponent : public MaterialComponent
{
VISUAL_CLASS(MaterialComponent)
public:
/// @getter
const std::string& GetId() const;
/// @setter
void SetId(const std::string& id);
protected:
void OnInit() override;
void Draw() override;
protected:
/// @property
Color _color = Color::WHITE;
/// @property
Sprite _sprite;
};
Далее по классу, с учетом тегов, будет сгенерирован весь код, который необходим для сохранения и загрузки данных, для работы редакторов, для поддержки клонирования и других мелких функций.
Автоматическая сериализация и генерация редакторов поддерживается не только для сущностей, которые хранятся в визуальном объекте, но и для любого класса. Для этого достаточно его унаследовать от специального класса Serializable и отметить нужные свойства тегами. А если хочется, чтоб экземпляры класса были полноценными ассетами (аналог ScriptableObject из Unity), то класс должен быть унаследован от класса Asset.
В итоге библиотека предоставляет возможность быстро разрабатывать новую функциональность. И теперь часть работ по разработке игры, например, создание эффектов, верстка UI, дизайн игровых сцен, может быть передана специалистам, которые с ней справятся лучше чем программисты.

Для работы многих систем нужно писать довольно много рутинного кода, который необходим из-за отсутствия в C++ рефлексии (reflection [2] — возможность получить доступ к информации о типах в коде программы). Поэтому большую часть подобного технического кода мы генерируем.
Генератор — это набор скриптов на python, которые парсят заголовочные файлы и на их основе генерируют нужный код. Для гибкой настройки генерации используются специальные теги в комментариях.
Мы умеем генерировать код для следующих подсистем:
→ Пример сгенерированного кода для одного класса можно посмотреть тут [3]
Почти все варианты решения вопроса парсинга заголовочных файлов вели к парсингу кода с clang. Но после экспериментов стало понятно, что скорость работы такого решения нас совершенно не устраивает. Тем более что та мощь, которую предоставлял clang, была нам не нужна.
Поэтому было найдено другое решение: CppHeaderParser [4]. Это python библиотека из одного файла, которая умеет читать заголовочные файлы. Она очень примитивна, не ходит по #include, пропускает макросы, не анализирует символы и работает очень быстро.
Мы ее используем и по сей день, правда, пришлось внести порядочное количество правок, чтобы исправить баги и расширить возможности, в частности, была добавлена поддержка новшеств из C++17.
Нам хотелось избежать недоразумений, связанных с неопределенностью статуса генерации кода. Поэтому было решено, что генерация должна происходить полностью автоматически. Мы используем CMake, в котором генерация запускается при каждой компиляции (нам не удалось настроить запуск генерации только при изменении зависимостей). Чтобы это не отнимало много время и не раздражало, мы храним кеш с результатом парсинга всех файлов и содержимого каталогов. В результате холостой запуск кодогенерации выполняется всего несколько секунд.
С генерацией все проще. Библиотек для генерации чего угодно по шаблону великое множество. Мы выбрали Templite+ [5], так как она совсем небольшая, обладает нужной функциональностью и исправно работает.
Подхода к генерации было два. Первая версия содержала много условий, проверок и прочего кода, поэтому самих шаблонов было минимум, а большая часть логики и производимого текста была в python коде. Это было удобно, ведь в python код удобней писать, чем в шаблонах, и можно было легко навернуть сколь угодно хитрую логику. Однако это было и ужасно, потому что код на python вперемешку с огромным количеством строк с C++ кодом было неудобно ни читать, ни писать. Используемые python-генераторы упрощали ситуацию, но не устраняли проблему в целом.
Поэтому текущая версия генерации базируется на шаблонах, а python код просто готовит нужные данные и сейчас это выглядит сильно лучше.
Для сериализации рассматривались разные библиотеки: protobuf, FlexBuffers, cereal и др.
Библиотеки с генерацией кода (Protobuf, FlatBuffers и другие) не подошли, потому что у нас рукописные структуры и нет возможности интегрировать сгенерированные структуры в пользовательский код. А увеличивать количество классов в два раза только для сериализации — слишком расточительно.
Библиотека cereal [6] показалась самым лучшим кандидатом — приятный синтаксис, понятная реализация, удобно генерировать код сериализации. Однако её бинарный формат нам не подходил, как и формат большинства других библиотек. Важными требованиями к формату были — независимость от железа (данные должны читаться вне зависимости от порядка байт и от разрядности) и бинарный формат должен быть удобен для записи из python.
Записывать бинарный файл из python было важно, так как мы хотели иметь платформонезависимый и проектно-независимый универсальный скрипт, который будет конвертировать данные из текстового вида в бинарный. Поэтому мы написали скрипт, который оказался очень удобным инструментом сериализации.
Основная идея взята от cereal, в её основе лежат базовые архивы для чтения и записи данных. От них создаются разные наследники которые реализуют запись в разные форматы: xml, json, binary. А код сериализации генерируется по классам и использует эти архивы для записи данных.

Для редакторов у нас используется библиотека ImGui, на которой мы написали все основные окна редактора: содержимое сцены, просмотрщик файлов и ассетов, инспектор ассетов, редактор анимаций и пр.
Основной код редактора пишется руками, но для просмотра и редактирования свойств конкретных классов у нас используется библиотека rttr, сгенерированный для нее биндинг и обобщенный код инспекторов, который умеет работать с rttr.
Для организации рефлексии в C++ была выбрана библиотека rttr. Она не требует вмешательства в сами классы, имеет удобный и понятный API, имеет поддержку коллекций и оберток над типами (такие как умные указатели) с возможностью регистрировать свои обертки и позволяет делать все, что необходимо (создавать типы, перебирать члены класса, менять свойства, вызывать методы и т.д.).
Также она позволяет работать с указателями, как с обычными полями, и использует паттерн null object, что сильно упрощает работу с ней.
Минус библиотеки — она громоздкая и не очень быстрая, поэтому мы используем ее только для редакторов. В игровом коде для работы с параметрами объектов, например, для системы анимаций, мы используем простейшую библиотеку рефлексии собственного производства.
Библиотека rttr требует написания биндинга с объявлением всех методов и свойств класса. Это связывание генерируется из python кода для всех классов, для которых нужна поддержка редактирования. А благодаря тому, что в rttr для любой сущности можно добавить метаданные, генератор кода умеет задавать разные настройки для членов класса: тултипы, параметры допустимых границ значений для числовых полей, специальный инспектор для поля и др. Эти метаданные используются в инспекторе для отображения интерфейса редактирования.
→ Пример кода для объявления класса в rttr можно посмотреть тут [7]
Код самих редакторов очень редко работает с rttr напрямую. Чаще всего используется прослойка, которая по объекту умеет отрисовать ImGui инспектор для него. Это рукописный код, который работает с данными из rttr и рисует для них ImGui контролы.
Для кастомизации отображения интерфейса редактирования данных, используются указанные при регистрации в rttr метаданные. У нас поддерживаются все примитивные типы, коллекции, есть возможность создавать объекты, хранимые по значению и по указателю. Если член класса является указателем на базовый класс, то при создании можно выбирать конкретного наследника.
Так же код инспекторов берет на себя поддержку отмены операций — при изменении значений создается команда на изменение данных, которую потом можно откатить.
Пока у нас нет системы определения атомарных изменений с возможностью их просмотреть и сохранить. Это означает, что у нас нет поддержки сохранения измененных свойств объекта в сцену и применение этих изменений после загрузки префаба. А так же нет автоматического создания анимационных треков при изменении свойств объекта.
В данный момент на базе наших редакторов, кодогенерации и системы создания ассетов создано много разных подсистем и редакторов:
При разработке всех этих подсистем и редакторов мы присматривались к Unity [8], Unreal Engine [9] и старались брать от них самое лучшее. А некоторые из этих подсистем сделаны на стороне игровых проектов.
В заключении я хотел бы описать, как проводилась разработка. Первая рабочая версия была сделана и интегрирована в некоторые игровые проекты парой человек всего за два месяца. В ней ещё не было кодогенерации, и того обилия редакторов, которое есть сейчас. В то же время, это была рабочая версия, с которой и началось движение вперед. Нельзя сказать, что в то время это соответствовало основному вектору развития движка, всё держалось на энтузиазме нескольких людей и чётком понимании необходимости и правильности того что мы делали.
Вся последующая разработка велась очень активно и эволюционно, шаг за шагом, но всегда с учетом интересов игровых проектов. В данный момент над развитием «нашей небольшой Unity» трудится больше десяти человек и само собой разработка новой версии уже не такой быстрый и стремительный процесс, как это было в самом начале.
Тем не менее мы добились больших результатов всего за пару лет и не собираемся останавливаться. Желаю и вам двигаться вперед к тому, что вы считаете правильным и важным для себя и для компании в целом.
Автор: d9fault
Источник [10]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/razrabotka-igr/330694
Ссылки в тексте:
[1] Image: https://habr.com/ru/company/playrix/blog/467827/
[2] reflection: https://en.wikipedia.org/wiki/Reflection_(computer_programming)
[3] посмотреть тут: https://pastebin.com/hm4VtfZ8
[4] CppHeaderParser: https://pypi.org/project/CppHeaderParser/
[5] Templite+: https://git.joonis.de/snippets/4
[6] cereal: https://uscilab.github.io/cereal/
[7] посмотреть тут: https://pastebin.com/p8uNjEHU
[8] Unity: https://unity.com/
[9] Unreal Engine: https://www.unrealengine.com/
[10] Источник: https://habr.com/ru/post/467827/?utm_campaign=467827&utm_source=habrahabr&utm_medium=rss
Нажмите здесь для печати.