unity dots что это
Физическая симуляция сотен тысяч частиц на Unity + DOTS
В какой-то момент во время блужданий по просторам всемирной паутины на одном из сайтов я обнаружил интерактивный JS-элемент — картинку, составленную из частиц, разлетающихся при приближении курсора мыши. Возникло желание запрограммировать симуляцию подобного поведения на Unity и проверить, какую производительность можно выжать из движка. Техническими решениями и своими наблюдениями, полученными в процессе реализации, делюсь с вами.
Результат выглядит следующим образом:
Для того, чтобы его добиться, нужно решить следующие проблемы:
Физика
Для описания желаемого поведения частиц нам потребуется сформулировать всего три принципа: стремление сократить расстояние до исходной точки, стремление «улететь» от курсора мыши и затухание движения. Нам не потребуются точные физические взаимодействия и формулы, необходимы лишь общие принципы, двигаясь в соответствии с которыми частица будет вести себя задуманным образом. Для простоты, из всех формул исключена масса — условимся считать её единичной.
Сокращение расстояния до начального положения
Для того, чтобы частица стремилась вернуться в исходную позицию, нам подойдет закон Гука: сила, направленная в сторону исходной позиции, будет линейно пропорциональна расстоянию до нее. В два раза дальше улетела частица от начальной позиции — в два раза сильнее её «тянет» назад, всё просто.
Отдаление от курсора
Чтобы было интересно, частицы должны каким-либо образом взаимодействовать с курсором, позволять себя «отодвигать» из исходного положения. За основу такого взаимодействия я взял гравитационное с обратным знаком: частицы будет отталкивать сила, обратно пропорциональная квадрату расстояния между положением курсора мыши и текущей позицией частицы и направленная по вектору от курсора к частице. , где
— некая константа, регулирующая взаимодействие.
Затухание
Если ограничиться двумя предыдущими формулами, частица после задания начальной амплитуды будет колебаться вечно, ведь терять энергию в рамках такой модели некуда. Будем симулировать затухание как силу вязкого сопротивления среды, которая, согласно закону Стокса, линейно пропорциональна скорости движения частицы и направлена в противоположную движению сторону. Нас устроят ограничения накладываемые на применимость данной формулы, так как нас в данном случае не интересует абсолютная точность физического взаимодействия, а лишь только принцип.
Результат
где — текущая позиция и скорость частицы,
— начальное положение,
— позиция курсора,
— коэффициенты притяжения, отталкивания и затухания соответственно.
Программирование поведения
Компоненты
Нам потребуются следующие компоненты:
С компонентами всё достаточно просто, переходим к системам:
Контролировать очередность работы систем можно с помощью атрибутов
Самой масштабной из систем, описывающих поведение, будет система обновления ускорения, действующая в соответствии с выведенной формулой:
Рендер
Несмотря на потенциально огромное количество частиц, которые нужно отобразить, для всех можно использовать один и тот же квадратный mesh из двух полигонов, а так же один и тот же материал, что позволило бы отрендерить их все за один draw call, если бы не одно «но»: у всех частиц, в общем случае, разные цвета, иначе получившаяся картинка будет очень скучной.
Стандартный Unity-шейдер «Sprites/Default» умеет использовать GPU Instancing для оптимизации рендера объектов с неодинаковыми спрайтами и цветами, объединяя их в один draw call, но в его случае ссылка на текстуру и цвет для каждого конкретного объекта должна задаваться из скрипта, к чему из ECS у нас доступа нет.
В качестве решения может выступить метод Graphics.DrawMeshInstanced, позволяющий отрисовать один mesh несколько раз за один draw call с разными параметрами материала, используя тот же GPU Instancing. Выглядит это следующим образом:
Для того, чтобы отрендерить группу объектов, используя данный метод, нужно собрать массив матриц перехода и тех свойств материала, которые предполагается варьировать. Система рендера, как можно заметить, выполняется после TrsMatrixCalculationSystem — вспомогательной системы, рассчитывающей матрицу перехода для каждой из частиц, которая выглядит очень просто:
Производительность
Пришло время поговорить о том, для чего (помимо, конечно, эстетического удовлетворения) это всё затевалось. В Unity, на данный момент, есть возможность выбрать из двух Scripting Backend (реализаций CLR ): старая-добрая Mono и более современное решение от разработчиков Unity — IL2CPP.
Сравним производительность билдов для этих двух реализаций рантайма:
Количество частиц на экране | Average frame rate, Mono | Average frame rate, IL2CPP |
---|---|---|
50000 | 128 | 255 |
100000 | 66 | 130 |
200000 | 31 | 57 |
Спецификации ПК:
QHD 2560×1440
Intel Core i5-9600K
16GB RAM
MSI GeForce RTX 2080 SUPER VENTUS XS OC 8.0 GB
160.000 частиц
Заметно, что, в рамках данного сферического в вакууме эксперимента, IL2CPP выигрывает у Mono примерно в два раза.
Согласно профилировщику, буквально все время, затрачиваемое на кадр, уходит на систему рендера, расчеты остальных систем обходятся практически «бесплатно»:
Unity Editor
Build
Заметно, что большая часть времени уходит на процедуры преобразования цвета из Color в Vector4 и добавления в список List.Add(). Избавиться от первой мы можем легко — перенесём преобразование на момент генерации частиц, так как цвет в процессе не меняется:
Это изменение позволило полностью избавиться от дорогой операции преобразования:
Количество кадров в секунду для 200.000 частиц после него выросло до 61.
Возможно, можно оптимизировать заполнение списков, например, хранить все массивы цветов, генерируя их единожды, но такое решение не кажется мне изящным, поэтому буду рад конструктивным предложениям в комментариях.
Заключение
ECS в целом и Unity DOTS в частности — отличные инструменты для определенных сценариев и классов задач. Свою роль в эффективной обработке огромного количества данных они исполняют великолепно и позволяют создавать симуляции, на которые в их отсутствие ушло бы значительно больше усилий. Не стоит, однако, считать DOTS «серебряной пулей» и кидаться камнями в разработчиков, придерживающихся традиционных для Unity концепций в новых проектах — DOTS подходит далеко не каждому и далеко не для каждой задачи.
Пакеты DOTS
Наш стек информационно-ориентированных технологий (DOTS) позволит использовать все возможности современных многоядерных процессоров без необходимости в сложных программных алгоритмах. DOTS находится в статусе предварительной версии, и мы постоянно добавляем новые пакеты в стек.
Придерживаясь курса переработки базовых систем Unity на основе DOTS, мы постоянно добавляем в стек новые функции. Предлагаем вам обзор важнейших пакетов DOTS, над которыми мы сейчас работаем.
Entity Component System (ECS) позволяет разрабатывать высокопроизводительный код C#, решающий только те задачи, которые вы ставите, а именно обработку данных и логики вашей игры.
ECS — это наш локомотив перехода от объектно-ориентированного подхода к информационно-ориентированному. ECS в Unity позволяет использовать систему задач C# и Burst Compiler, раскрывая весь потенциал современных многоядерных процессоров и их кеша.
Информационно-ориентированный подход также означает большие возможности по повторному использованию кода и облегчению его понимания и дополнения другими членами команды.
Информацию о разработке игрового процесса с помощью ECS смотрите в документации ECS. О вариантах подходов к программированию можно узнать из доклада «Возможности взаимодействий сущностей», представленного на Unite Copenhagen 2019. Мы также рекомендуем загрузить примеры Entity Component System с GitHub.
Пакет Entities также содержит инструмент Unity Live Link. Unity Live Link позволяет вносить изменения в редакторе, которые будут тут же отображаться на всех подключенных устройствах с запущенным приложением. Это позволяет получать информацию о производительности физического устройства напрямую.
Другой инструмент, Conversion Workflow, преобразует объекты GameObject в сущности одним щелчком, позволяя вам работать с DOTS, используя привычные вам GameObject.
Система задач C# использует преимущества многоядерных процессоров современных компьютеров. Система создана с целью помочь программистам C# в разработке безопасного, быстрого и параллелизуемого кода.
Использование многоядерных процессоров
Система задач C# раскрывает внутреннюю систему задач C++ Unity, позволяя скриптам C# выполняться в виде задач вместе с внутренними компонентами Unity.
Безопасная среда
Обеспечивает защиту от некоторых проблем многопоточных вычислений, например в условиях гонки.
Новая основанная на LLVM вычислительная технология бэкенд-компиляции превращает задачи C# в глубоко оптимизированный машинный код.
Лучшее от каждой платформы
Компилятор Burst оптимизирует сборку кода для целевой платформы.
Меньше ручного написания кода
С легкостью пользуйтесь преимуществами настроенного вручную ассемблерного кода на различных платформах.
Мы разработали новейший физический движок на основе технологий DOTS, благодаря которому вы сможете моделировать физику без ущерба производительности, совместимую с современными требованиями к сетевому обмену данными. Система Unity Physics еще находится в статусе предварительной версии и совместима с Unity 2019.1 и более поздними выпусками.
Для клиентов, которым необходимы крайне сложные физические вычисления, мы предлагаем Havok Physics для Unity. В основе решения используется ведущий в отрасли физический движок Havok Physics, работающий более чем в половине игр для нынешнего поколения консолей. Оно построено на основе платформы DOTS C#, как и Unity Physics, но обеспечивает функциональность, производительность, стабильность и надежность закрытого проприетарного движка Havok Physics, написанного на нативном C++.
Если вы планируете создать многопользовательскую игру в Unity, то для понимания наших достижений в этой области советуем ознакомиться с проектом DOTS Sample. Включенный в проект NetCode обеспечивает прогнозирование на стороне клиента, «авторитетный» сервер и интерполяцию, упрощая разработку шутера.
Узнайте о планах на развитие сетевых функций Unity из доклада на Unite Copenhagen 2019.
Основа готовящейся к выходу звуковой системы DOTS — DSPGraph (теперь в статусе предварительной версии). Наша новая низкоуровневая звуковая подсистема работает с компилятором Burst и полностью расширяется с использованием C#, что позволяет программистам и разработчикам звукового оформления создавать собственные звуковые системы.
Присоединяйтесь к обсуждению звуковой подсистемы DOTS на форуме, задавайте вопросы или делитесь своими требованиями к звуку.
Раннюю версию разрабатываемой системы анимации для DOTS можно увидеть в действии в проекте DOTS Sample. Этот пакет предлагает базовые функции анимации, включая смешивание, инверсную кинематику, слои и маски. Кроме того, мы работаем над добавлением новых возможностей.
С помощью Project Tiny, новой модульной среды выполнения Unity на основе DOTS, вы сможете создавать быстрые игры небольшого размера, легкие и производительные. Загрузите демо-проект Tiny Racing, чтобы узнать, над чем мы сейчас работаем. Он содержит предварительную версию возможностей 3D-рендеринга и позволяет подготовить сборки для iOS, Android, HTML5, Windows и Mac.
Создание зомби-шутера от третьего лица с помощью DOTS
Салют, хабровчане. Как мы уже писали январь богат на новые запуски и сегодня мы анонсируем набор на новый курс от OTUS — «Разработчик игр на Unity». В преддверии старта курса делимся с вами переводом интересного материала.
Мы пересобираем ядро Unity с помощью нашего стека технологий, ориентированного на данные (Data-Oriented Tech Stack). Как и многие игровые студии мы тоже видим большие преимущества в использовании Системы Entity Component (ECS), Системы задач C#( C# Job System) и Компилятора Burst. На Unite Copenhagen у нас появилась возможность пообщаться с Far North Entertainment и углубиться в то, как они реализуют этот функционал DOTS в традиционных проектах на Unity.
Far North Entertainment – шведская студия, совладельцами которой являются пять друзей-инженеров. С момента релиза Down to Dungeon for Gear VR в начале 2018 года компания работает над игрой, которая принадлежит классическому жанру игр для ПК, а именно над постапокалиптической игрой в режиме выживания про зомби. Что выделяет проект на фоне других, так это количество зомби, которые гонятся за вами. Видение команды на этот счет рисовало тысячи голодных зомби, идущих за вами огромными ордами.
Это заставило технического директора студии Андреса Эрикссона обратить внимание на DOTS и сменить тип мышления с объектно-ориентированного на ориентированное на данные. «Ключевая идея, которая помогла осуществить этот сдвиг, заключались в том, что нужно было перестать думать об объектах и иерархиях объектов и начать думать о данных, о том, как они преобразуются и о том, как получать к ним доступ», — сказал он. Его слова значат, что не нужно строить архитектуру кода с прицелом на объекты реальной жизни, таким образом, чтобы она решала самую общую и абстрактную задачу. У него есть много советов для тех, кто также, как и он, столкнулся с изменением мировоззрения:
«Спросите себя, в чем заключается реальная проблема, которую вы пытаетесь решить, и какие данные важны для получения решения. Будете ли вы раз за разом одинаковым образом преобразовывать один и тот же набор данных? Сколько полезных данных вы можете уместить в одну строку кэша процессора? Если вы вносите изменения в существующий код, оцените сколько мусорных данных вы вносите в строку кэша. Можно ли разделить вычисления на несколько потоков или нужно использовать одиночный поток команд?»
Команда пришла к пониманию того, что сущности в Системе Компонентов Unity – это просто поисковые идентификаторы в потоках компонентов. Компоненты – это просто данные, в то время как системы содержат всю логику и отфильтровывают сущности с определенной сигнатурой, известные как архетипы. «Я думаю, что одним из озарений, которое помогло нам визуализировать наши задумки, было представление ECS в виде базы данных SQL. Каждый архетип – это таблица, в которой каждый столбец – это компонент, а каждая строка – уникальная сущность. По сути вы используете системы для создания запросов для этих таблиц архетипов и выполняете операции над сущностями», — говорит Андерс.
Знакомство с DOTS
Чтобы прийти к этому пониманию, он изучил документацию по системе Entity Component, примеры ECS и пример, который мы сделали совместно с Nordeus и представили на Unite Austin. Общие материалы об архитектуре, ориентированной на данные, также были очень полезны команде. «Доклад Майка Эктона об архитектуре, ориентированной на данные с CppCon 2014 – это именно то, что первым открыло нам глаза на этот способ программирования.»
Команда Far North опубликовала то, что они узнали в своем Dev Blog, в сентябре этого года они приехали в Копенгаген, чтобы рассказать о своем опыте перехода к подходу, ориентированному на данные, в Unity.
Эта статья основана на докладе, она более подробно объясняет специфику их реализации ECS, Системы задач C# и компилятора Burst. Еще команда Far North любезно поделилась большим количеством примеров кода из своего проекта.
Организация данных зомби
«Проблема, с которой мы столкнулись, заключалась в выполнении интерполяции перемещений и вращений для тысяч объектов на стороне клиента», говорит Андерс. Их изначальный объектно-ориентированный подходом заключался в создании абстрактного скрипта ZombieView, который унаследовал общий родительский класс EntityView. EntityView — это MonoBehaviour, присоединенный к GameObject. Он действует как визуальное представление игровой модели. Каждый ZombieView отвечал за обработку своего собственного перемещения и интерполяции вращения в своей функции Update.
Это звучит нормально, до того момента, пока вы не поймете, что каждая сущность располагается в памяти в произвольном месте. Это означает, что, если вы обращаетесь к тысячам объектов, ЦП должен доставать их из памяти по одному, и происходит это крайне медленно. Если вы складываете свои данные в аккуратные блоки, расположенные последовательно, процессор может кэшировать целую кучу данных одновременно. Большинство современных процессоров могут получать из кэша около 128 или 256 бит за один цикл.
Команда решила преобразовать врагов в DOTS в надежде устранить проблемы производительности на стороне клиента. Первой в очереди была функция Update в ZombieView. Команда определила, какие её части следует разделить на разные системы и определила необходимые данные. Первой и наиболее очевидной вещью была интерполяция позиций и поворотов, поскольку игровой мир представляет из себя двумерную сетку. Две переменных float отвечают за то, куда направляется зомби, а последний компонент – это целевая позиция, он отслеживает положение сервера для врага.
Следующим шагом было создание архетипа для врагов. Архетип представляет из себя набор компонентов, которые принадлежат определенной сущности, другими словами — это сигнатура компонента.
В проекте используются префабы для определения архетипов, поскольку для врагов требуется больше компонентов, а некоторые из них нуждаются в ссылках на GameObject. Работает это так, что вы можете обернуть данные вашего компонента в ComponentDataProxy, который превратит его в MonoBehaviour, который в свою очередь можно присоединить к префабу. Когда вы создаете экземпляр с помощью EntityManager и передаете префаб, он создает сущность со всеми данными компонентов, которые были прикреплены к префабу. Все данные компонента хранятся в 16-килобайтных чанках памяти, называемых ArchetypeChunk.
Вот визуализация того, как будут организованы потоки компонентов в нашем чанке архетипа:
«Одним из основных преимуществ чанков архетипов является то, что вам не нужно часто заново аллоцировать кучу при создании новых объектов, так как память уже была выделена заранее. Это означает, что создание сущностей представляет из себя запись данных в конец потоков компонентов внутри чанков архетипа. Единственный случай, когда необходимо выполнить аллоцирование кучи снова — это при создании сущности, которая не вписывается в границы чанка. В этом случае либо будет инициировано выделение нового чанка архетипа размером 16 КБ, либо, если есть пустой фрагмент того же архетипа, его можно использовать повторно. Затем данные для новых объектов будут записаны в потоки компонентов нового чанка», — объясняет Андерс.
Многопоточность ваших зомби
Теперь, когда данные плотно были упакованы и размещены в памяти удобным для кеширования способом, команда могла легко воспользоваться системой задач C# для параллельного запуска своего кода на нескольких ядрах ЦП.
Следующим шагом было создание системы, которая отфильтровывала все сущности из всех блоков архетипов, имеющих компоненты PositionData2D, HeadingData2D и TargetPositionData.
Для этого Андерс и его команда создали JobComponentSystem и сконструировали свой запрос в функции OnCreate. Это выглядит примерно так:
Код объявляет запрос, который отфильтровывает все объекты в мире, имеющие позицию, направление и цель. Далее они хотели распланировать задания для каждого фрейма с помощью системы задач C#, чтобы распределить вычисления по нескольким рабочим потокам.
«Самое классное в системе задач C# то, что это та же система, которую Unity использует в своем коде, поэтому нам не нужно было беспокоиться о том, что исполняемые потоки блокируют друг друга, требуя одни и те же ядра процессора и вызывая проблемы с производительностью.», говорит Андерс.
Команда решила использовать IJobChunk, потому что тысячи врагов подразумевали наличие большого количества чанков архетипа, которые должны соответствовать запросу во время выполнения. IJobChunk распределяет правильные чанки по различным рабочим потокам.
Каждый фрейм новая задача UpdatePositionAndHeadingJob отвечает за обработку интерполяции позиций и поворотов врагов в игре.
Код для планирования заданий выглядит следующим образом:
Так выглядит сама задача:
Когда рабочий поток извлекает задание из своей очереди, он вызывает ядро выполнения этого задания.
Вот как выглядит ядро исполнения:
«Вы можете заметить, что мы используем select вместо ветвления, это позволяет избавиться от эффекта, называемого неправильным предсказанием ветвей. Функция select оценит оба выражения и выберет то, которое соответствует условию, и, если ваши выражения не так уж сложны для вычисления, я бы порекомендовал использовать select, поскольку это зачастую дешевле, чем ждать, пока ЦП восстановится после неверного прогноза ветвления.», отмечает Андерс.
Увеличение производительности с Burst
Последний шаг преобразования DOTS для позиции противника и интерполяции курса — это включение компилятора Burst. Андерсу задача показалось довольно простой: «Поскольку данные располагаются в смежных массивах и поскольку мы используем новую библиотеку математики из Unity, все, что нам нужно было сделать, это добавить атрибут BurstCompile в нашу задачу».
Компилятор Burst дает нам Single Instruction Multiple Data (SIMD); машинные инструкции, которые могут работать с несколькими наборами входных данных и создавать множество наборов выходных данных с помощью всего одной инструкции. Это помогает нам заполнить больше мест на 128-битной шине кэша правильными данными. Компилятор Burst в сочетании с удобной для кэша компоновкой данных и системой заданий позволили команде значительно повысить производительность. Вот таблица, которую они составили, замерив производительность после каждого шага преобразования.
Это означало, что Far North полностью избавились проблем, связанных с интерполяцией положения на стороне клиента и направлением зомби. Их данные теперь хранятся в удобном для кеширования виде, а строки кеша заполняются только полезными данными. Нагрузка распределяется на все ядра ЦП, а компилятор Burst выдает высокооптимизированный машинный код с SIMD-инструкциями.
Советы и рекомендации по DOTS от Far North Entertainment
Планы на будущее
«Мы хотим использовать DOTS в других областях нашей игры, и мы были в восторге от анонсов на Unite про DOTS анимации, Unity Physics и Live Link. Мы хотели бы научиться преобразовывать больше игровых объектов в объекты ECS, и похоже, что Unity добилась значительных успехов в реализации этого», — заключает Андерс.
Если у вас есть дополнительные вопросы к команде Far North, мы рекомендуем вам присоединиться к их Discord!
Ознакомьтесь с плейлистом Unite Copenhagen DOTS, чтобы узнать, как другие современные игровые студии используют DOTS для создания великолепных высокопроизводительных игр, и как компоненты на основе DOTS, такие как DOTS Physics, новый Conversion Workflow, и компилятор Burst работают вместе.
На этом перевод подошел к концу, а мы приглашаем вас посетить бесплатный вебинар, в рамках которого расскажем как создать свой зомби-шутер за час.