![](https://habrastorage.org/getpro/habr/upload_files/672/a8f/d9f/672a8fd9f56312a4f9a95058857daa25.jpg)
Привет, Хабр!
На обложке демо-игра Megacity. Она содержит 4,5 млн элементов Mesh Renderer, 5000 динамических транспортных средств, 200 000 уникальных строительных объектов и 100 000 уникальных аудиоисточников. Но самое удивительное, что вся эта мощь запустилась на Iphone X при 60 кадрах в секунду . Как все это возможно?
Пару лет назад компания Unity представила свой стек DOTS, на котором и построен проект Megacity. Это некий список технологий, которые в совокупности позволяют колдовать и ускорять ваш проект в десятки раз. В корне всей магии лежат 2 простых заклинания:
Если правильно управлять данными, процессору будет легче их обрабатывать, а если их легче будет обрабатывать, то игрокам будет легче жить.
Количество ядер процессора растет, но код среднестатистического программиста не использует все ядра процессора. А значит игрокам все же живется туго. Фреймрейт ведет себя как Джокер - непредсказуемо.
![](https://habrastorage.org/getpro/habr/upload_files/b8e/ea6/b4c/b8eea6b4cb82b3d720840a5f7e852e7e.jpg)
Для того, чтобы Unity программистам удалось осуществить вышеописанные постулаты, компания выпустила дополнительные пакеты:
Дары смерти от Unity Technologies
Job System - Пишите многопоточный код, даже если вы не знаете что такое "Race condition" или "Context switch".
Burst Compiler - Ускорьте код в 10 раз, просто добавив атрибут и отказавшись от ссылочных типов.
Unity Mathematics - Специальная математика, адаптированная под компилятор Burst.
Native Container - Обертка над неуправляемой памятью. Это такие же List, Array, Queue, но живут в мире без мусора.
Мусор - это ваш код.Мусор - это то, что могут оставлять после себя мертвые ссылочные типы. Сборкой мусора в проекте занимается некий дворник - Garbage Collector, но он очень медленный и злой дядька . Да и Greenpeace советует убирать после себя трупы, не правда ли?
А сердцем DOTS является:
Entities - архитектурный паттерн Entity Component System (ECS), который ставит во главу угла данные, а не объекты, и тем самым переворачивает привычное представление о программировании среди ценителей ООП . Подобный подход развивает идею композиции над наследованием и позволяет легко адаптироваться под динамичные потребности гейм-дизайнера.
ECS стоит рассматривать в первую очередь как архитектурное решение. Скорость - это бонус.
На текущий момент DOTS выглядит скорее проклятьем Unity Technologies, чем волшебной палочкой. Стек до сих пор не вышел в официальный релиз. И неизвестно когда DOTS выйдет в официальный релиз. Сейчас это крайне нестабильная штука, которую точно не стоит использовать в Production проекте.
Поэтому, давайте попробуем применить подход Entity Component System (ECS) в своем проекте.
Производительность! Бесплатно и без регистрации
Entity Component System - это сердце нового подхода от Unity. Но одновременно и самая нестабильная часть всего стека DOTS . Что же это такое?
Из чего состоит ECS?
Entity - это все, что вас окружает. Ваша кошка, сын маминой подруги, пицца - это все entity.
Component - это то, что делает ваш Entity особенным. У кошки есть хвост, у сына маминой подруги - мозги, у пиццы - кетчуп. Это все - компоненты.
System - логика, которая управляет всеми entity, у которых есть тот или иной набор компонентов. Хвост помогает всем кошкам балансировать в пространстве, мозги помогают всем сыновьям маминой подруги быть круче вас, а кетчуп позволяет любой пицце быть вкусной.
Если перенести аналогию на игровые объекты, то ваш персонаж в игре - это Entity. Компонент физики - Rigidbody, а система - это то, что будет управлять всей физикой на сцене, включая вашего персонажа в игре.
Согласно официальной статье , снижение количества Update функций и переход на чистый c# автоматически добавит скорости в вашу игру. А Job System, Burst Compiler можно применять к любой архитектуре. И если у команды Unity пакет Entities пока что официально не выпущен, давайте обратимся к кулинарным профессионалам из сообщества и посмотрим, как же они предлагают нам приготовить этот самым ECS.
Рецепт ECS под соусом Leo
Поиск Гугл по рецептам приводит нас к самому вкусному результату:
Leo ECS - очень легкий и быстрый ECS фреймворк, который не требует какого-то специфичного игрового движка. Он прост, быстр и не форсирует вашу интеграцию с Unity Engine.
Помимо этого, на данный момент Leo ECS самый популярный ECS фреймворк после Entitas
![](https://habrastorage.org/getpro/habr/upload_files/324/967/c60/324967c60972da45c73a96cc21eba35f.png)
Entitas - аппетитный фреймворк с самым большим комьюнити, но вся его проблема в том, что он работает на классах. Добавлять этот ингредиент сейчас не модно, повара предпочитают использовать структуры на компонентах. Классы почти в 2 раза медленней структур в синтетических тестах, т.к хранят в памяти ссылку. А данные тесты демонстрируют отставание по скорости готовки от 10 до 20 раз.
Другие ECS рецепты, которые могут вас заинтересовать
ECS.ME - потрясающий ECS фреймворк для Unity, приготовленный для сетевых игр с элементом отката (RollBack). Эдакий некоммерческий аналог Quantum от Exit Games. Рецепт очень-очень крутой и вкусный. Но чтобы rollback не дал трещину, нужно четко следовать правилам данного фреймворка.
EgoECS - ECS фреймворк с сильнейшей интеграцией в Unity. С его помощью можно готовить MonoBehavior в качестве компонентов. Если вам не важна скорость работы, EgoECS выглядит весьма хорошим рецептом. Каждая система - это всего 1 фильтр, кому-то такой подход может скручивать руки при написании кода.
Svelto ECS - очень амбициозный рецепт, с очень сложным API. Но я должен был его упомянуть. Скорее всего он не подходит новичкам, но если вы заинтересовались, то на habr существует перевод Wiki данного фреймворка.
Actors - изначально разрабатывался как набор утилит. В рецепте присутствует шина событий(глобальный ивенты), которая может нарушить порядок вызовов функций и никак не относится к ECS. Реактивщина в ECS может сбить вас с толку при поиске багов / ошибок в вашей игре.
Вот мы и определились. Чистый C# и соус Leo.
ОБНОВЛЕНО! Насколько увеличится скорость?
Я провел классический эксперимент, создал 100 000 GameObject и двигал их по вектору Z на 0.1 значение.
Характеристики устройства
![](https://habrastorage.org/getpro/habr/upload_files/6b1/0f8/ef9/6b10f8ef91aba543e81a76a1032bb160.png)
Версия Unity при тестировании
![](https://habrastorage.org/getpro/habr/upload_files/3ee/955/7c3/3ee9557c3bfeb9786fdda3bc10b8ee82.png)
MonoBehavior Update
void Update()
{
var position = transform.localPosition;
transform.localPosition = new Vector3(position.x, position.y, position.z + 1);
}
![](https://habrastorage.org/getpro/habr/upload_files/e8b/d77/6be/e8bd776be028e638c99fb0410f161533.jpg)
Leo ECS
public void Run()
{
for (int idx = 0, idxMax = _filter.GetEntitiesCount(); idx < idxMax; idx++)
{
ref var move = ref _filter.Get1(idx);
move.vec.z += 1;
move.trans.localPosition = move.vec;
}
}
![](https://habrastorage.org/getpro/habr/upload_files/32b/a07/ccc/32ba07ccce46f5238ad64dd35302c98e.jpg)
R
Как видно из этих тестов, переход на ECS подарил в районе 20 FPS. Однако точно такой же тест от коллеги по цеху, запущенный на моем устройстве, демонстрирует увеличение производительности в 3 раза. Мне же не удалось достичь таких результатов.
Leo ECS еще один тест
![](https://habrastorage.org/getpro/habr/upload_files/4fa/82a/359/4fa82a359e95e0d53b98b85e09815b25.jpg)
После тщательного анализа исходного кода моего коллеги, я так и не нашел причины за счёт чего ему удалось увеличить скорость работы. Есть ощущение, что пакеты от DOTS, установленные на моей сборке, как-то замедляли весь процесс тестирования.
Нагрузка Update методов из Profiler
![](https://habrastorage.org/getpro/habr/upload_files/4a4/cee/ea4/4a4ceeea4af73a766bd99b6a5b0f0d53.jpg)
Однако я попробовал вынести работу в многопоток через Job System и получил буст производительности в 5-6 раз. При этом добавление атрибута от Burst никак не увеличила производительность. Видимо в моем коде нет как таковой логики чтобы ее Burstить.
Leo ECS + Job System
public void Run()
{
var newJob = new MoveJob {
moveVector = Vector3.forward
};
var handle = newJob.Schedule(this.transforms);
// JobHandle.ScheduleBatchedJobs();
handle.Complete();
}
[BurstCompile] // Атрибут Burst не увеличил производительность.
public struct MoveJob : IJobParallelForTransform {
public Vector3 moveVector;
public void Execute(int index, TransformAccess transform) => transform.position += this.moveVector;
}
![](https://habrastorage.org/getpro/habr/upload_files/af2/ff9/e66/af2ff9e66607daeb1190d3b0eac64dd6.jpg)
То есть, переход на ECS позволит процессору вашего целевого устройства легче выполнять работу, а вам - писать чистый код.
Превращаем GameObject в Entity
Если Leo ECS по умолчанию не конвертирует игровые объекты из Unity Engine, то как интегрировать его с этим движком? Можно ли как-нибудь очень-очень легко превращать наши игровые объекты в Entity, чтобы процесс готовки был простым и понятным.
В этом нам поможет моя собственная библиотека UniLeo . Я написал ее для того, чтобы сохранить привычный flow работы с игровым движком Unity.
UniLeo автоматически конвертирует ваши игровые объекты в Entity и позволяет настраивать компоненты прямо из инспектора.
Перейдем наконец-то к коду
Подключаем пакеты через Unity Package Manager
Добавьте ссылку в Packages/manifest.json
"com.leopotam.ecs": "https://github.com/Leopotam/ecs.git",
"com.voody.UniLeo": "https://github.com/voody2506/UniLeo.git",
Или Unity Editor -> Window -> Package Manager
![](https://habrastorage.org/getpro/habr/upload_files/d12/f0f/083/d12f0f0833fb41f6d0cde1379fbde029.png)
Компонент в Leo ECS - это обычная структура, но мы, благодаря UniLeo можем управлять ее содержимым в рамках редактора Unity.
Не забываем пространство имен
using Leopotam.Ecs;
using Voody.UniLeo;
Создаем первый компонент
[Serializable] // <- Данный атрибут необходим, чтобы управлять компонентом из редактора
public struct PlayerComponent {
public float health;
}
Пример компонента с элементами Unity
[Serializable]
public struct UnityExampleComponent {
public Rigidbody rigidBody;
public Transform transform
public GameObject unityObject
}
То есть, если в проекте нужно управлять физикой, необходимо создать компонент с переменной RigidBody. Таким образом можно управлять абсолютно любым элементом игрового движка Unity.
Теперь нам необходимо как-нибудь прикрепить наш компонент к игровому объекту, но как это сделать? Компоненты - это структуры. А движок Unity позволяет крепить к игровым объектам только классы, которые наследуется от MonoBehavior
.
Но в UniLeo можно создать класс-проводник, который должен наследоваться MonoProvider
и его можно крепить к игровым объектам.
Создаем класс-проводник
public sealed class PlayerComponentProvider : MonoProvider<PlayerComponent> { }
После того, как данный класс закреплен на игровом объекте, его можно предварительно настроить из редактора.
![](https://habrastorage.org/getpro/habr/upload_files/464/ed6/69e/464ed669e97300476d39d12f8570e62d.png)
Отлично, теперь мы можем без лишних проблем настроить наш компонент прямо из редактора Unity.
Настало время написать первую систему. Для этого необходимо просто создать класс и реализовать один из готовых интерфейсов, чаще всего используются только:
IEcsInitSystem // Срабатывает 1 раз при инициализации
IEcsRunSystem // Срабтаывает на Update или Fixed Update метод
Пишем первую систему
class PlayerHealthSystem : IEcsInitSystem, IEcsRunSystem {
// Переменная _world автоматически инициализируется
EcsWorld _world = null;
// В фильтре просто описываем с каким компонентом
будет работать система
EcsFilter<PlayerComponent> _filter = null;
public void Init () {
// Сработает на старте
}
public void Run () {
foreach (var i in _filter) {
// entity которые содержат PlayerComponent.
ref var entity = ref _filter.GetEntity (i);
// Get1 вернет ссылку на "PlayerComponent".
ref var player = ref _filter.Get1 (i);
player.helth = player.helth - 10;
}
}
}
Когда наша первая система создана, мы должны как-то запустить ее: для этого создадим стартап код ECS, это простой класс, который наследуется от MonoBehavior
.
Запускаем ECS
class Startup : MonoBehaviour {
EcsWorld _world;
EcsSystems _systems;
void Start () {
// create ecs environment.
_world = new EcsWorld ();
_systems = new EcsSystems (_world)
.ConvertScene() // Этот метод сконвертирует GO в Entity
.Add (new PlayerComponent ());
_systems.Init ();
}
void Update () {
// process all dependent systems.
_systems.Run ();
}
void OnDestroy () {
// destroy systems logical group.
_systems.Destroy ();
// destroy world.
_world.Destroy ();
}
}
Обратите внимание, что к вашему игровому объекты автоматически добавится компонент Convert To Entity
, который позволяет выбрать предпочтительный метод конвертации ваших игровых объектов.
Выбираем метод конвертации
![](https://habrastorage.org/getpro/habr/upload_files/dd9/62b/168/dd962b168aa2abeed4334b6da6a42be1.png)
Convert And Inject
- Создаст entity на основе компонентов, повешанных на игровой объект.
Convert And Destroy
- После превращения GO в Entity , объект автоматически удалится. Может быть полезно, когда необходимо просто добавить определенные компоненты в ECS мир..
После запуска игровой объект автоматически сконвертируется в Entity. Система начнет отрабатывать свои методы. Мы успешно интегрировали Leo ECS в наш проект. Поздравляю!
Наверное у вас еще остались вопросы, я попробую ответить на них.
Вопрос-Ответ
Как работать с Prefab?
Вы можете порождать prefab любым известным вам способом, конвертация пройдет автоматически.
GameObject.Instantiate(gameObject, position, rotation);
PhotonNetwork.Instantiate <- рабоатет и в сторонних библиотеках
Как работать с ивентами в ECS?
В ECS всё - данные . Ивенты тоже, необходимо просто создать компонент и запустить его в нужный момент. Ну и создать систему, которая бы реагировала на этот ивент.
// MonoBehavior
void OnCollisionEnter(Collision collision)
{
// Создаем новый entity и добавляем компонент
// Переменная _world хранится в Startup классе
EcsEntity entity = _world.NewEntity ();
var event = new EventComponent ();
entity.Replace (event);
// Теперь просто можно перехватить этот ивент в любой системе
}
В ECS очень важно не нарушать порядок, при котором выполняются системы, асинхронное срабатывание может привести к большим проблемам в будущем.
Очень часто ивент-компоненты нужно очищать в конце кадра, для этого в LeoECS есть готовый механизм.
void Start () {
var world = new EcsWorld ();
_update = new EcsSystems (world);
_update
.Add (new CalculateSystem ())
.Add (new UpdateSystem ())
.OneFrame<EventComponent> () // Этот компонент удалится
самостоятельно в этот момент времени
.Init ();
}
А что насчет многопоточности?
У Leo ECS есть готовая интеграция , которая позволяет запускать системы в разных потоках. Она не использует Job System и Burst Compiler , а использует стандартную библиотеку System.Threading.
Но вы вполне можете использовать и пакеты от компании Unity, они дадут намного больше производительности, но интеграция с ними не так легка.
На хабре уже существует отличный пост про Job систему. Вам необходимо вызвать ее внутри ваших Run методов в LeoECS.
Пример работы с Job System есть выше, в разделе про тестирование.
Я пишу сетевую игру, как мне работать с ECS
Готовой интеграции для LeoECS с популярными сетевыми библиотеками на данный момент не существует, но передача данных осуществляется через ивенты, а как работать с ивентами я уже отвечал.
В любом случается, на данный момент вам придется самостоятельно придумать как лучше всего интегрировать то или иное решение с ECS.
P.S
Да, возможно сторонние ECS решения не такие быстрые, как подход DOTS, но они позволяют существенно увеличить производительность относительно классического MonoBehavior подхода. Теперь вы знаете с чего начать свой Megacity проект!
Спасибо, что дочитали до конца!