...

пятница, 20 сентября 2019 г.

Как мы делали нашу маленькую Unity с нуля

У нашей компании есть свой игровой движок, который используется для всех разрабатываемых игр. Он предоставляет всю важную базовую функциональность: 

  • рендеринг;
  • работа с SDK;
  • работа с операционной системой;
  • с сетью и ресурсами. 

Однако в нем не хватало того, чем так ценится Unity, — удобной системы организации сцен и игровых объектов, а также редакторов к ним.

Здесь я хочу рассказать, как мы внедряли все эти удобства и к чему пришли.

Что есть сейчас


Сейчас у нас есть некоторое подобие компонентной системы в Unity со всеми важными подсистемами и редакторами. Однако, так как мы исходили из нужд наших конкретных проектов, существуют довольно значительные расхождения.

У нас есть визуальные объекты, которые хранятся в сценах. Эти объекты состоят из узлов, которые организованы в иерархию и каждый узел может иметь ряд сущностей, таких как:

  • Transform — трансформация узла;
  • Component — занимается отрисовкой и может быть только одна или не быть вовсе. Компоненты — это sprite, mesh, particle и прочие сущности, которые умеют отображаться. Ближайший аналог в Unity — это Renderer;
  • Behaviour — отвечает за поведение, и их может быть несколько. Это прямой аналог MonoBehaviour в Unity, в них пишется любая логика;
  • Sorting — это сущность, которая отвечает за порядок отображения узлов в сцене. Так как наша система должна была легко интегрироваться в уже запущенные игры, с существующей и разнообразной логикой отображения объектов, нужно было уметь встраивать новые сущности в старые. Так что sorting позволяет передать управление за порядком отображения внешнему коду.

Как и в 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 — возможность получить доступ к информации о типах в коде программы). Поэтому большую часть подобного технического кода мы генерируем. 

Генератор — это набор скриптов на python, которые парсят заголовочные файлы и на их основе генерируют нужный код. Для гибкой настройки генерации используются специальные теги в комментариях.

Мы умеем генерировать код для следующих подсистем:

  • Сериализация — используется для сохранения / загрузки данных с диска или при передаче по сети. Будет более детально рассмотрена позднее.
  • Биндинги для библиотеки рефлексии — используются для автоматического отображения редактора к данным. Будут рассмотрены в главе про редактор.
  • Код для клонирования сущностей — используется для клонирования сущностей как в редакторе, так и в игре.
  • Код для нашей легковесной runtime рефлексии.

→ Пример сгенерированного кода для одного класса можно посмотреть тут

Парсинг с++


Почти все варианты решения вопроса парсинга заголовочных файлов вели к парсингу кода с clang. Но после экспериментов стало понятно, что скорость работы такого решения нас совершенно не устраивает. Тем более что та мощь, которую предоставлял clang, была нам не нужна.

Поэтому было найдено другое решение: CppHeaderParser. Это python библиотека из одного файла, которая умеет читать заголовочные файлы. Она очень примитивна, не ходит по #include, пропускает макросы, не анализирует символы и работает очень быстро. 

Мы ее используем и по сей день, правда, пришлось внести порядочное количество правок, чтобы исправить баги и расширить возможности, в частности, была добавлена поддержка новшеств из C++17.

Нам хотелось избежать недоразумений, связанных с неопределенностью статуса генерации кода. Поэтому было решено, что генерация должна происходить полностью автоматически. Мы используем CMake, в котором генерация запускается при каждой компиляции (нам не удалось настроить запуск генерации только при изменении зависимостей). Чтобы это не отнимало много время и не раздражало, мы храним кеш с результатом парсинга всех файлов и содержимого каталогов. В результате холостой запуск кодогенерации выполняется всего несколько секунд.

Генератор кода


С генерацией все проще. Библиотек для генерации чего угодно по шаблону великое множество. Мы выбрали Templite+, так как она совсем небольшая, обладает нужной функциональностью и исправно работает.

Подхода к генерации было два. Первая версия содержала много условий, проверок и прочего кода, поэтому самих шаблонов было минимум, а большая часть логики и производимого текста была в python коде. Это было удобно, ведь в python код удобней писать, чем в шаблонах, и можно было легко навернуть сколь угодно хитрую логику. Однако это было и ужасно, потому что код на python вперемешку с огромным количеством строк с C++ кодом было неудобно ни читать, ни писать. Используемые python-генераторы упрощали ситуацию, но не устраняли проблему в целом.

Поэтому текущая версия генерации базируется на шаблонах, а python код просто готовит нужные данные и сейчас это выглядит сильно лучше.

Сериализация


Для сериализации рассматривались разные библиотеки: protobuf, FlexBuffers, cereal и др.

Библиотеки с генерацией кода (Protobuf, FlatBuffers и другие) не подошли, потому что у нас рукописные структуры и нет возможности интегрировать сгенерированные структуры в пользовательский код. А увеличивать количество классов в два раза только для сериализации — слишком расточительно. 

Библиотека cereal показалась самым лучшим кандидатом — приятный синтаксис, понятная реализация, удобно генерировать код сериализации. Однако её бинарный формат нам не подходил, как и формат большинства других библиотек. Важными требованиями к формату были — независимость от железа (данные должны читаться вне зависимости от порядка байт и от разрядности) и бинарный формат должен быть удобен для записи из python.

Записывать бинарный файл из python было важно, так как мы хотели иметь платформонезависимый и проектно-независимый универсальный скрипт, который будет конвертировать данные из текстового вида в бинарный. Поэтому мы написали скрипт, который оказался очень удобным инструментом сериализации.

Основная идея взята от cereal, в её основе лежат базовые архивы для чтения и записи данных. От них создаются разные наследники которые реализуют запись в разные форматы: xml, json, binary. А код сериализации генерируется по классам и использует эти архивы для записи данных.

Редактор


Для редакторов у нас используется библиотека ImGui, на которой мы написали все основные окна редактора: содержимое сцены, просмотрщик файлов и ассетов, инспектор ассетов, редактор анимаций и пр.

Основной код редактора пишется руками, но для просмотра и редактирования свойств конкретных классов у нас используется библиотека rttr, сгенерированный для нее биндинг и обобщенный код инспекторов, который умеет работать с rttr.

Библиотека рефлексии — rttr


Для организации рефлексии в C++ была выбрана библиотека rttr. Она не требует вмешательства в сами классы, имеет удобный и понятный API, имеет поддержку коллекций и оберток над типами (такие как умные указатели) с возможностью регистрировать свои обертки и позволяет делать все, что необходимо (создавать типы, перебирать члены класса, менять свойства, вызывать методы и т.д.). 

Также она позволяет работать с указателями, как с обычными полями, и использует паттерн null object, что сильно упрощает работу с ней.

Минус библиотеки — она громоздкая и не очень быстрая, поэтому мы используем ее только для редакторов. В игровом коде для работы с параметрами объектов, например, для системы анимаций, мы используем простейшую библиотеку рефлексии собственного производства.

Библиотека rttr требует написания биндинга с объявлением всех методов и свойств класса. Это связывание генерируется из python кода для всех классов, для которых нужна поддержка редактирования. А благодаря тому, что в rttr для любой сущности можно добавить метаданные, генератор кода умеет задавать разные настройки для членов класса: тултипы, параметры допустимых границ значений для числовых полей, специальный инспектор для поля и др. Эти метаданные используются в инспекторе для отображения интерфейса редактирования.

→ Пример кода для объявления класса в rttr можно посмотреть тут

Инспектора


Код самих редакторов очень редко работает с rttr напрямую. Чаще всего используется прослойка, которая по объекту умеет отрисовать ImGui инспектор для него. Это рукописный код, который работает с данными из rttr и рисует для них ImGui контролы.

Для кастомизации отображения интерфейса редактирования данных, используются указанные при регистрации в rttr метаданные. У нас поддерживаются все примитивные типы, коллекции, есть возможность создавать объекты, хранимые по значению и по указателю. Если член класса является указателем на базовый класс, то при создании можно выбирать конкретного наследника.

Так же код инспекторов берет на себя поддержку отмены операций — при изменении значений создается команда на изменение данных, которую потом можно откатить. 

Пока у нас нет системы определения атомарных изменений с возможностью их просмотреть и сохранить. Это означает, что у нас нет поддержки сохранения измененных свойств объекта в сцену и применение этих изменений после загрузки префаба. А так же нет автоматического создания анимационных треков при изменении свойств объекта.

Окна и редакторы


В данный момент на базе наших редакторов, кодогенерации и системы создания ассетов создано много разных подсистем и редакторов:
  • Система игровых интерфейсов предоставляет гибкую и удобную вёрстку и включает в себя все необходимые элементы интерфейса. К ней была сделана система визуального скриптования поведения окон.
  • Система для переключения состояния анимаций, похожа на редактор состояний в анимациях в Unity, но несколько отличается по принципу работы и имеет более широкое применение. 
  • Дизайнер квестов и событий позволяет гибко настраивать игровые события, квесты и туториал, почти без участия программистов. 

При разработке всех этих подсистем и редакторов мы присматривались к Unity, Unreal Engine и старались брать от них самое лучшее. А некоторые из этих подсистем сделаны на стороне игровых проектов.

Подводим итоги


В заключении я хотел бы описать, как проводилась разработка. Первая рабочая версия была сделана и интегрирована в некоторые игровые проекты парой человек всего за два месяца. В ней ещё не было кодогенерации, и того обилия редакторов, которое есть сейчас. В то же время, это была рабочая версия, с которой и началось движение вперед. Нельзя сказать, что в то время это соответствовало основному вектору развития движка, всё держалось на энтузиазме нескольких людей и чётком понимании необходимости и правильности того что мы делали.

Вся последующая разработка велась очень активно и эволюционно, шаг за шагом, но всегда с учетом интересов игровых проектов. В данный момент над развитием «нашей небольшой Unity» трудится больше десяти человек и само собой разработка новой версии уже не такой быстрый и стремительный процесс, как это было в самом начале.

Тем не менее мы добились больших результатов всего за пару лет и не собираемся останавливаться. Желаю и вам двигаться вперед к тому, что вы считаете правильным и важным для себя и для компании в целом.

Let's block ads! (Why?)

Комментариев нет:

Отправить комментарий