Чем хорош ECS в Unity

Введение в ECS

ECS - это паттерн проектирования, а его аббревиатура расшифровывается как Entity Component System. Давайте рассмотрим его ключевые составляющие.

Источник <a href="https://api.dtf.ru/v2.8/redirect?to=https%3A%2F%2Fitch.io%2Fjam%2Fthe-dots-challenge&postId=2234884" rel="nofollow noreferrer noopener" target="_blank">itch.io</a>
Источник itch.io

Entity - это единица чего-либо в игре; обычно она является просто индексом. Компоненты - это набор данных без поведения, они относятся к Entity и определяют ее архетип. Системы - выполняют работу над набором компонентов (Query), которые находятся в одном архетипе.

Этот паттерн, в отличие от GameObjects, использует data-oriented подход. Для ECS характерно разделение данных и поведения, что облегчает повторное использование кода и расширение архитектуры, даже на поздних этапах разработки. Ещё одним отличием является хранение данных в cache-friendly форме, поскольку компоненты обычно хранятся в массивах.

Основными отличиями ECS от ООП являются:

  • предпочтение композиции наследованию
  • разделение данных и поведения
  • типы в ООП статические, типом же в ECS является набор компонентов, который может изменяться

Легко спутать ECS и EC framework, хотя в последних Entity являются классами и содержат в себе данные и поведение. Всем известным примером EC framework являются Unity GameObjects. Ниже представлена стандартная реализация:

class IComponent { public: virtual void update() = 0; }; class Entity { vector<IComponent*> components; public: void addComponent(IComponent *component); void removeComponent(IComponent *component); void updateComponents(); };

Введение в Unity DOTS

ECS for Unity входит в состав Unity DOTS (Data-Oriented Technology Stack) и поставляется в пакете Entities. В состав DOTS входят ещё два компонента: C# Job System и Burst Compiler, которые находятся в релизе уже достаточно давно. С выходом Unity 2022 LTS, ECS обновился с версии 0.51 до 1.0. На момент написания статьи последней версией является 1.0.16.

Я следил за версией ECS от Unity со времён Entities 0.51. Меня привлёк новый подход к разработке и, конечно же, производительность по умолчанию, которую даёт, ориентированный на данные, подход вместе с многопоточностью и компиляцией в машинный код. Но только с выходом релизной версии, нашей командой было принято решение сменить архитектуру игры.

В этой статье я буду рассматривать ECS как часть DOTS, поскольку они отлично работают вместе и дополняют друг друга. Начнем с минусов подхода, которых достаточно много.

О чем недоговаривает Unity

Я узнал об этом уже в процессе разработки, и это было немного неожиданно. Entities 1.0 является релизом по стабильности и рекомендуется самими Unity для использования в production (источник). Но пока в нем присутствует не все, что имеется при стандартном подходе, я имею ввиду при использовании GameObjects. И так, чeго у нас нет из коробки на данный момент:

  • Ui
  • Анимации
  • Спрайты

Но мы можем использовать сторонние пакеты, или написать свое решение. Для примера, в Asset Store есть пакет для анимации. В комментариях указали, что нет нативной поддержки спрайтов, поискав в интернете, нашел пакет на гитхабе, который специально сделан для Entities и DOTS NSprites - Unity DOTS Sprite Rendering Package.

Что касается графики, в документации к Enitities Graphics представлена feature matrix. Согласно этому списку пока отсутствуют:

  • Texture Streaming
  • Lod Crossfade
  • Только Forward+ рендеринг для URP
  • Streaming Virtual Texturing в HDRP

Burst Occlusion Culling и Mesh deformations (аналог Skinned Mesh Renderer) - находятся в экспериментальной фазе разработки. Их можно использовать, но реализация с большой вероятностью сильно поменяется в будущем.

Будьте готовы готовы к сложностям при разработке на ECS. Не надо переносить на них свои три в ряд и idle RPG, они нормально работают на GameObjects. Как максимум попробуйте гибридный подход, так как Jobs и Burst Compiler работают и без Entities.

Сложности в разработке

Программирование на ECS сложнее, чем на GameObjects. Для того чтобы разрабатывать игры с новой архитектурой, потребуются более опытные разработчики (источник).

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

Asset Bundles и Addressables не полностью совместимы с новым подходом (источник), а новая система работы с контентом пока ещё сыровата.

Итак, я закончил с минусами и приглашаю в комментарии всех, кто работал с Unity ECS, рассказать о трудностях, с которыми вы столкнулись при разработке. А теперь перейдем к тому, в чем новый подход раскрывается в полной мере.

Открытость ECS for Unity

Код Unity Engine является закрытым, и это одна из причин, почему крупные студии предпочитают использовать Unreal Engine (источник). У Unreal Engine есть доступ к исходному коду, что предоставляет дополнительные возможности. В отличие от этого, ECS как пакет Unity распространяется вместе с исходными файлами, что позволяет пользователям исследовать, отлаживать и расширять его (источник). Благодаря этому вы можете проводить оптимизации и вносить любые изменения, специфичные для вашей игры.

Ссылка на исходный код Entities:

С закрытым исходным кодом вы остаетесь с тем, что вам предложили разработчики. Обычно движки являются многоцелевыми и не всегда удовлетворяют все потребности разных жанров и масштабов. В видео ниже Rasmus Höök, Software Engineer из Stunlock Studios, рассказывает о преимуществах открытости исходного кода, которые они использовали при работе над игрой VRising:

Сache-friendly

Как я уже рассказывал в своей предыдущей статье, одной из проблем GameObjects, как и любого объекта, является то, что он плохо использует способность процессора кэшировать информацию из ОЗУ. Так как объекты полны ссылок, процессор должен постоянно переходить по ним и доставать данные из памяти, а это намного медленнее, чем обращаться к кэшу (источник). Напротив, расположение компонентов в памяти в ECS устроено так, что они находятся в массивах последовательно. Следовательно, после загрузки данных в кэш, велика вероятность, что нужная информация для следующей операции уже будет там, так как компоненты находятся в массивах и обрабатываются системами последовательно. Чем меньше обращений к ОЗУ тем лучше. Разница во времени доступа к памяти огромна, особенно, если мы имеем в сцене большое количество объектов. Посмотрите на картинку ниже и обратите внимание на разницу во времени между доступом к кэшу (SRAM) и ОЗУ (DRAM). Поэтому я могу с уверенностью сказать, что здесь ECS работает с процессором лучше, чем GamoObjetcs.

Источник <a href="https://api.dtf.ru/v2.8/redirect?to=https%3A%2F%2Fcomputationstructures.org%2Flectures%2Fcaches%2Fcaches.html&postId=2234884" rel="nofollow noreferrer noopener" target="_blank">computationstructures.org</a>
Источник computationstructures.org

Лучшая производительность

В данном видео Justin Larabee CTO в студии Sunblink говорит о повышении производительности в своей игре HEROish. Он добился уменьшения времени обработки с 20 мс до 4,5 мс просто перенеся код ИИ на Native Containers и Jobs, при подключении Burst Compiler ему удалось сократить время еще в 10 раз, до 0,4 мс. Посмотрите видео с тайм-кодом ниже:

Нативная многопоточность

Как я уже говорил, одной из частей Unity DOTS является C# Job System. Она дает нам доступ к Unity native job system (С++) - системе параллелизации, которая используется внутри движка Unity. Эта система дает нам легкий и нативный способ использовать многопоточность. Jobs учитывают количество ядер CPU и предотвращает создание потоков, превышающих их количество. При использовании, например, Thread Pool легко создать лишние потоки, которые могут только ухудшить производительность (источник). Job System позволяет нам не заострять внимания на количестве ядер на целевой платформе и сосредоточиться на других задачах.

Особенностью паттерна ECS является то, что компоненты хранятся в массивах, а это отлично подходит для распараллеливания. Мы можем создавать выборку компонентов по фильтру, которая называется Query, и обрабатывать их в IJobsEntity на всех ядрах устройства. IJobEntity - это специальный вид параллельного Job, который работает напрямую с компонентами Entity. Так как при работе с GameObjects, мы ограничены основным потоком, а для многопоточности нам нужно создавать свои системы или использовать Jobs, а так же менять архитектуру приложения под это, в многопоточности однозначно выигрывает ECS.

Компиляция в машинный код

Другой немаловажной частью DOTS является Burst Compiler. В сочетании с нативной многопоточностью Job System компилятор даёт огромный прирост к производительности приложения. Давайте разберемся, как работает компиляция нашего кода, написанного на C#. Компилятор транслирует наш исходный код в универсальный IL (промежуточный язык), далее он обрабатывается JIT-компилятором для конкретной платформы. Стоит отметить, что эта прослойка накладывает некоторый оверхед своей работой в runtime. Burst компилятор транслирует наш код, написанный на HPC#, подмножестве языка C#, в машинный код целевой платформы при помощи LLVM. Burst так же дает нам использовать CPU intrinsics в критических для производительности местах в приложении. Нативный код исполняется процессором напрямую и намного более эффективно использует его ресурсы. При использовании обычного компилятора C# проблемой является то, что среда наших скриптов (managed code) отличается от среды движка Unity (unmanaged code). Это плохо тем, что передавая данные из C# в Unity Engine мы вынуждены производить специальные действия маршалера (источник) для конвертации данных из managed в unmanaged код. ECS же не использует управляемую память и в компоненты входят только те типы, которые передаются в движок Unity без конверсии. Так же часть кода, такая как управление трансформами и физикой вынесена в Entities Package и не является external call к движку Unity, а чем меньше обращений к движку, тем лучше.

Тесты производительности

ECS - это не просто архитектурный подход, также он позволяет улучшить производительность, а если подключить Burst компилятор, то разрыв можно увеличить еще больше. Как и обещал в прошлой статье, представляю вам сравнение производительности между ECS и GameObjects. В сценах используются машины с hovercraft физикой.

Сцена на GameObjects. Здесь представлено 175 машин:

Сцена на ECS, но без Burst компиляции, так же представлено 175 машин. Увеличение производительности в 5 раз:

Теперь подключаем Burst и видим, что FPS увеличился с 220 до 340, хоть количество машин возросло до 595:

Выводы

Итак, пробежимся по плюсам и минусам ECS for Unity еще раз.

Плюсы:

  • Многопоточность из коробки
  • Компиляция скриптов в машинный код
  • Cache-friendly компоновка данных
  • Расширяемость архитектуры

Минусы:

  • Сложнее для неопытных разработчиков
  • Не все features были реализованы для ECS

Заключение

Надеюсь вам понравилась статья, времени на нее потрачено было очень много. Учел ошибки прошлого раза и добавил много ссылок, поясняющих мои утверждения и просто полезной информации. В следующей статье будет демо с большим количеством объектов, постараемся подготовить ее в ближайшие 2 недели, подписывайтесь, чтобы не пропустить. Как всегда, оставляйте комментарии, если что-то непонятно или вы заметили ошибку, постараюсь ответить на все.

Другие мои статьи:

Полезные ссылки:

3939
32 комментария

В "Сложности в разработке" одно и то же (буквально) написано дважды.

3
Ответить

Я подумал что у меня шиза и читаю по два раза

1
Ответить
Автор

Спасибо, поправил

Ответить

Какой-то тотальный ПСИОП происходит. Но скажу так, возможно конечно моё мнение менее объективное, чем у автора. Но вот пописав свой проектик на дотс. Я понял, что это пиздец. Тебе столько кода лишнего нужно писать, столько ограничений странных.

К примеру у юнити нет из коробки поддержки рендеринга 2д спрайтов для дотс и скорее всего придется если не хватает навыков и понимания как работает рендер писать свой костыльный гибридный рендер через геймобджекты. И это только одна важная, но не единственная проблема.

Короче если вы индиразраб и вам нужна многопоточная обработка каких-то данных, по типу множественного поиска путей на огромной карте . Лучше всего, именно частично, выносить сложные вычисления в дотс, иначе сойдете с ума....

3
Ответить
Автор

Нет, тут наши мнения сходятся. Я описал, что Entities отстают фичам и разрабатывать игры сложнее, в некоторых местах. Если для вашего проекта отсутствуют критически важные компоненты и нет времени писать свои, то нужно возвращаться на GO.

Ответить
Автор

Забил в поисковик и нашел https://github.com/Antoshidza/NSprites. Как я уже и говорил, многое нужно искать или писать самому, Entities еще сыроваты.

Ответить

Комментарий недоступен

1
Ответить