Engine & platform

На DOTS: Система компонентов сущности

LUCAS MEIJER / UNITY TECHNOLOGIESContributor
Mar 8, 2019|9 Мин
На DOTS: Система компонентов сущности
Эта веб-страница была переведена с помощью машинного перевода для вашего удобства. Мы не можем гарантировать точность или надежность переведенного контента. Если у вас есть вопросы о точности переведенного контента, обращайтесь к официальной английской версии веб-страницы.

Это один из нескольких постов о нашем новом технологическом стеке, ориентированном на данные (DOTS), в котором мы расскажем о том, как и почему мы оказались там, где находимся сейчас, и куда мы движемся дальше.

В своей последней заметке я говорил о HPC# и Burst как о низкоуровневых фундаментальных технологиях для Unity. Я люблю называть этот уровень нашего стека "движком для игры". Любой желающий может использовать этот стек для создания игрового движка. Мы можем. Мы будем. Вы тоже можете. Не нравится наш? Напишите свой собственный или измените наш по своему вкусу.

Система компонентов Unity

Следующий слой, который мы создаем, - это новая система компонентов. В основе Unity всегда лежали концепции компонентов. Добавьте компонент Rigidbody к объекту GameObject, и он начнет падать. Добавьте компонент Light к объекту GameObject, и он начнет излучать свет. Добавьте компонент AudioEmitter, и игровой объект начнет издавать звук.

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

Что не очень хорошо сохранилось, так это то, как мы внедрили нашу систему компонентов. Он был написан с учетом объектно-ориентированного мышления. Компоненты и GameObjects - это объекты "тяжелого c++". Для их создания/уничтожения требуется мьютексная блокировка для изменения глобального списка id->objectpointers. Все объекты GameObject имеют имя. Каждый из них получает объект-обертку C#, который указывает на объект C++. Этот объект C# может находиться где угодно в памяти. Объект C++ также может находиться в любом месте памяти. Промахи в кэше многочисленны. Мы стараемся как можно лучше смягчить симптомы, но сделать можно очень многое.

Ориентируясь на данные, мы можем добиться гораздо большего. Мы можем сохранить те же приятные с точки зрения пользователя свойства (добавьте компонент Rigidbody, и вещь упадет), но при этом получить потрясающую производительность и параллельность с нашей новой системой компонентов.

Эта новая система компонентов - наша система Entity Component System (ECS). Грубо говоря, то, что вы делаете с GameObject сегодня, вы делаете с Entity в новой системе. Компоненты по-прежнему называются компонентами. Что же изменилось? Схема расположения данных.

Давайте рассмотрим некоторые распространенные схемы доступа к данным
Типичный компонент, который вы напишете в Unity традиционным способом, может выглядеть так:

класс Орбита : MonoBehaviour
{
public Transform _objectToOrbitAround;

void Update()
{
//Пожалуйста, не обращайте внимания на то, что все математические вычисления не работают, это не главное :)
var currentPos = GetComponent<Transform>().position;
var targetPos = _objectToOrbitAround.position;
GetComponent<RigidBody>().velocity += SomehowSteerTowards(currentPos,targetPos)
}
}

Эта схема повторяется снова и снова. Компонент должен найти один или несколько других компонентов на том же GameObject и прочитать/записать некоторые значения на нем.

В этом есть много неправильного:

  • Метод Update() вызывается для одного компонента орбиты. Следующий вызов Update() может быть для совершенно другого компонента, что, скорее всего, приведет к вытеснению этого кода из кэша при следующем запуске этого кадра для другого компонента Orbit.
  • Update() должен использовать GetComponent(), чтобы найти свой Rigidbody. (Вместо этого его можно кэшировать, но тогда придется следить за тем, чтобы компонент Rigidbody не был уничтожен).
  • Другие компоненты, с которыми мы работаем, находятся в совершенно других местах памяти.

Схема расположения данных, используемая ECS, понимает, что это очень распространенная схема, и оптимизирует расположение памяти, чтобы сделать такие операции быстрыми.

Макет данных ECS

ECS группирует в памяти все сущности, имеющие одинаковый набор компонентов. Он называет такой набор архетипом. Примером архетипа является: "Position & Velocity & Rigidbody & Collider". ECS выделяет память кусками по 16 тыс. Каждый чанк будет содержать компонентные данные только для сущностей одного архетипа.

Вместо того чтобы пользовательский метод Update во время выполнения искал другие компоненты для работы с каждым экземпляром Orbit, в ECS вы должны статически объявить: "Я хочу выполнить некоторые операции со всеми сущностями, у которых есть Velocity, Rigidbody и компонент Orbit. Чтобы найти все эти сущности, мы просто находим все архетипы, которые соответствуют определенному "запросу на поиск компонентов". Каждый архетип имеет список чанков, в которых хранятся сущности этого архетипа. Мы перебираем все эти куски, и внутри каждого из них мы выполняем линейный цикл по плотно упакованной памяти, чтобы прочитать и записать данные компонента. Этот линейный цикл, выполняющий один и тот же код на каждой сущности, также предоставляет Burst возможность векторизации.

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

ECS делает всю эту работу за вас, вам нужно только предоставить код, который вы хотите запустить на каждой сущности. (При желании можно выполнить итерацию кусков вручную).

Когда вы добавляете/удаляете компонент из сущности, он меняет архетип. Мы перемещаем его из текущего чанка в чанк нового архетипа, а обратно меняем последнюю сущность предыдущего чанка, чтобы "заполнить дыру".

В ECS вы также статически объявляете, что вы собираетесь делать с данными компонента. ReadOnly или ReadWrite. Пообещав (обещание проверяется) читать только из компонента Position, ECS может более эффективно планировать свои задания. Другим заданиям, которые также хотят получить данные из компонента Position, не придется ждать.

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

Именно по этой причине демо-версия Megacity загружается на телефоне за несколько секунд.

Счастливые "случайности"

Хотя сущности могут делать то, что делают игровые объекты сегодня, они могут делать больше, потому что они очень легкие. В самом деле, что такое сущность? В раннем варианте этого поста я написал "мы храним сущности в кусках", а затем изменил его на "мы храним данные компонентов для сущностей в кусках". Это важное различие, которое нужно сделать, чтобы понять, что сущность - это просто 32-битное целое число. Для него не нужно ничего хранить или выделять, кроме данных его компонентов. Поскольку они так дешевы, вы можете использовать их для сценариев, для которых игровые предметы не подходили. Например, использование сущности для каждой отдельной частицы в системе частиц.

HPC#, Burst, ECS. Потрясающе, но где мой игровой движок?

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

Это может показаться неприятным. В каком-то смысле это так, но в другом - нет. Поскольку ECS и все, что создано на ее основе, написано на C#, она может работать внутри традиционной Unity. Поскольку он работает внутри Unity, вы можете писать компоненты ECS, которые используют функциональность, предшествующую ECS. На данный момент не существует чистой системы рисования сетки ECS. Однако вы можете написать ECS MeshRenderSystem, использующую в качестве реализации API доECS Graphics.DrawMeshIndirect, пока ждете выхода чистой версии ECS. Именно эту технику использует наша демоверсия "Мегаполиса". Загрузка/потоковая обработка/выборка/лоаддинг/анимация выполняются в чистых системах ECS, но окончательная прорисовка - нет.

Так что вы можете смешивать и сочетать. Это замечательно тем, что вы уже можете воспользоваться преимуществами кодегена Burst и производительности ECS для своего игрового кода, а не ждать, пока мы выпустим чистые ECS-версии всех подсистем. Что не очень хорошо, так это то, что на этом переходном этапе вы можете увидеть и почувствовать это трение, что вы "используете два разных мира, которые склеены вместе".

Мы будем поставлять весь исходный код для наших подсистем ECS HPC# в пакетах. Вы можете проверять, отлаживать, модифицировать каждую подсистему, а также более тонко контролировать, когда вы хотите обновить ту или иную подсистему. Например, можно обновить пакет подсистемы Physics, не обновляя ничего другого.

Что будет с игровыми объектами?

Игровые объекты никуда не денутся. Люди успешно выпускают на нем потрясающие игры уже более десяти лет. Этот фонд никуда не денется.

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

Удобство использования API / Шаблон

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

На горизонте маячит множество улучшений, которые призваны устранить необходимость в большинстве шаблонов и упростить выражение ваших намерений. Мы пока не реализовали многие из них, так как сосредоточились на фундаментальной производительности, но мы считаем, что нет никаких веских причин для того, чтобы код игр ECS содержал много шаблонного кода или был особенно более трудоемким, чем написание MonoBehaviour.

В Project Tiny уже реализованы некоторые из этих улучшений (например, API итераций на основе лямбд). Кстати говоря...

Как во все это вписывается ECS от Project Tiny?

Project Tiny будет поставляться на базе того же C# ECS, о котором шла речь в этом блоге. Проект Tiny станет для нас важной вехой в развитии ECS по нескольким направлениям:

  • Он сможет работать в полном окружении только ECS. Новый игрок без прошлого багажа.
  • Это означает, что он также является чистым ECS и должен поставляться со всеми подсистемами ECS, необходимыми настоящей (крошечной) игре.
  • Мы примем поддержку редактора Project Tiny для редактирования сущностей для всех сценариев ECS, а не только для tiny.
Присоединиться к нам?

У нас есть вакансии для всех частей стека DOTS, в частности в Бербанке и Копенгагене, смотрите на сайте careers.unity.com.

Также не забудьте присоединиться к нам на форуме Unity Entity Component System и C# Job System, чтобы оставить отзыв и получить информацию об экспериментальных и предварительных функциях.