Привет, Хабр!
На обложке демо-игра Megacity. Она содержит 4,5 млн элементов Mesh Renderer, 5000 динамических транспортных средств, 200 000 уникальных строительных объектов и 100 000 уникальных аудиоисточников. Но самое удивительное, что вся эта мощь запустилась на Iphone X при 60 кадрах в секунду . Как все это возможно?
Пару лет назад компания Unity представила свой стек DOTS, на котором и построен проект Megacity. Это некий список технологий, которые в совокупности позволяют колдовать и ускорять ваш проект в десятки раз. В корне всей магии лежат 2 простых заклинания:
Если правильно управлять данными, процессору будет легче их обрабатывать, а если их легче будет обрабатывать, то игрокам будет легче жить.
Количество ядер процессора растет, но код среднестатистического программиста не использует все ядра процессора. А значит игрокам все же живется туго. Фреймрейт ведет себя как Джокер - непредсказуемо.
Для того, чтобы 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
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 значение.
Характеристики устройства
Версия Unity при тестировании
MonoBehavior Update
void Update()
{
var position = transform.localPosition;
transform.localPosition = new Vector3(position.x, position.y, position.z + 1);
}
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;
}
}
R
Как видно из этих тестов, переход на ECS подарил в районе 20 FPS. Однако точно такой же тест от коллеги по цеху, запущенный на моем устройстве, демонстрирует увеличение производительности в 3 раза. Мне же не удалось достичь таких результатов.
Leo ECS еще один тест
После тщательного анализа исходного кода моего коллеги, я так и не нашел причины за счёт чего ему удалось увеличить скорость работы. Есть ощущение, что пакеты от DOTS, установленные на моей сборке, как-то замедляли весь процесс тестирования.
Нагрузка Update методов из Profiler
Однако я попробовал вынести работу в многопоток через 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;
}
То есть, переход на 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
Компонент в 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> { }
После того, как данный класс закреплен на игровом объекте, его можно предварительно настроить из редактора.
Отлично, теперь мы можем без лишних проблем настроить наш компонент прямо из редактора 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 PlayerHealthSystem ());
_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
, который позволяет выбрать предпочтительный метод конвертации ваших игровых объектов.
Выбираем метод конвертации
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 проект!
Спасибо, что дочитали до конца!