Как мы делали нашу маленькую 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» трудится больше десяти человек и само собой разработка новой версии уже не такой быстрый и стремительный процесс, как это было в самом начале.

    Тем не менее мы добились больших результатов всего за пару лет и не собираемся останавливаться. Желаю и вам двигаться вперед к тому, что вы считаете правильным и важным для себя и для компании в целом.
    Playrix
    75,18
    Разработчик мобильных free-to-play игр
    Поделиться публикацией

    Комментарии 9

      0
      Как движок назвали? Tinyti?
        +1
        EngineScapes
          0
          Хитро завернули! Опенсорцнуть не планируете в ближайщее время?
            0
            В ближайщее время не планируется никакой деятельности на то чтобы опенсорсить или как-то еще распространять наработки. Все силы на внутренней разработке.
          0
          Назвали VSO — Visual Scene Object
          +1
          Приятно видеть было вашу игру в топ 5 по сборам на мобильных платформах.
            +2

            Хочется побольше деталей. Как, например, основной цикл игры реализован? Как поддерживаете стабильный фпс? Как физика считается? И с примерами, примерами.

              0
              Отличная работа!
              А где можно это пощупать? Или Вы просто похвастаться?
                +2
                Хотелось бы подробностей
                1) как реализуете 3д и 2д объекты одновременно
                2) как устроена ваша сортировка.
                3) отличия анимационной системы от системы юнити

                Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                Самое читаемое