Pull to refresh

Unity, ECS и все-все-все

Reading time 7 min
Views 106K


Сколько уже было мануалов "Как сделать игру на Unity за 3 часа", "Делаем Counter-Strike за вечер" и т.п.? Низкий порог входа — это, несомненно, главный плюс и минус Unity. Действительно, можно накидать “ассетов”, дописать несколько простых “скриптов”, обмотать синей изолентой и это даже будет как-то работать. Но когда проект обрастает игровыми механиками, сложной логикой поведения, то проблемы при подобном подходе нарастают как снежный ком. Для внедрения новых механик требуется переписывание кода во многих местах, постоянная проверка и переделывание префабов из-за побившихся ссылок на компоненты логики, не говоря уже об оптимизации и тестировании всего этого. Разумеется, архитектуру можно продумать изначально, но на практике это всегда недостижимая цель — дизайн-документ довольно часто меняется, какие-то части выкидываются, добавляются абсолютно новые и никак не связанные со старой логикой поведения. Компоненты в Unity — это шаг в правильном направлении в виде декомпозиции кода на изолированные блоки, но особенности реализации не позволяют достичь необходимой гибкости, а самое главное, производительности. Разработчики придумывают свои фреймворки и велосипеды, но чаще всего останавливаются на ECS (Entity Component System). ECS – одно из решений, продолжающее идею компонентной модели Unity, но придающее ей ещё больше гибкости и сильно упрощающее рефакторинг и дальнейшее расширение приложения новым функционалом без кардинальных изменений в текущем коде.


Что такое ECS


ECS — это шаблон проектирования "Сущность Компонент Система" (Entity Component System, не путать с Elastic Cloud Storage :). Если совсем по-простому, то есть “Сущности” (Entity) — объекты-контейнеры, не обладающие свойствами, но выступающие хранилищами для “Компонентов”. “Компоненты” — это блоки данных, определяющие всевозможные свойства любых игровых объектов или событий. Все эти данные, сгруппированные в контейнеры, обрабатываются логикой, существующей исключительно в виде “Систем” — “чистых” классов с определенными методами для выполнения. Данный паттерн является независимым от какого-либо “движка” и может быть реализован множеством способов. Все “сущности”, “системы” и “компоненты” должны где-то храниться и каким-то образом инициализироваться — все это является особенностями реализации каждого ECS решения для конкретного “движка”.


Постойте, скажете вы, но ведь в Unity всё так и есть! Действительно, в Unity Сущность — это GameObject, а Компонент и Система — это наследники MonoBehaviour. Но в этом и заключается основное различие между компонентной системой Unity и ECS — логика в ECS обязательно должна быть отделена от данных. Это позволяет очень гибко менять логику (даже удалять / добавлять её), не ломая данные. Другой бонус — данные обрабатываются “потоком” в каждой системе и независимо от реализации в “движке”, в случае с MonoBehaviour происходит довольно много взаимодействия с “Native”-частью, что съедает часть производительности. Об особенностях внутреннего устройства вызова методов у наследников MonoBehaviour можно почитать в официальном блоге Unity: 10000 вызовов Update()


Пример работы ECS


Задача от дизайнера: “надо сделать перемещение игрока и загрузку следующего уровня, когда он доходит до точки Х”.
Разбиваем задачу на несколько подзадач, по одной на “систему”:


  • UserInputSystem — пользовательский ввод.
  • MovePlayerSystem — перемещение игрока на основе ввода.
  • CheckPointSystem — проверка достижения точки игроком.
  • LoadLevelSystem — загрузка уровня в нужный момент.

Определяем компоненты:


  • UserInputEvent — событие о наличии пользовательского ввода с данными о нем. Да, события — это тоже компоненты!
  • Player — хранение текущей позиции игрока и его скорости.
  • CheckPoint — точка взаимодействия на карте.
  • LoadLevelEvent — событие о необходимости загрузки нового уровня.

И вот как это всё примерно работает:


  • Загружается сцена и инициализируются все системы в указанной выше последовательности. Да, порядок обработки систем можно контролировать без сложных телодвижений — это ещё один приятный бонус.
  • Создаются сущности игрока (с добавлением на него компонента Player) и сущности контрольной точки (с добавлением на неё компонента CheckPoint).
  • Тут стартует основной цикл обработки систем — по сути аналог метода MonoBehaviour.Update.
  • UserInputSystem проверяет пользовательский ввод через стандартное Unity-api и создает новую сущность с компонентом UserInputEvent и данными о вводе (если он был).
  • MovePlayerSystem проверяет — есть ли сущности с компонентом UserInputEvent и есть ли сущности с компонентом Player. Если пользовательский ввод есть — обрабатываем всех найденных “игроков” (даже если он один) с полученными данными, а сущность с компонентом UserInputEvent удаляем полностью. Да, это работает очень быстро, не вызывает работы сборщика мусора — все уходит во внутренний пул для последующего переиспользования.
  • CheckPointSystem проверяет — есть ли сущности с компонентом CheckPoint и есть ли сущности с компонентом Player. Если есть и то и то — в цикле проверяет дистанции между каждым игроком и точкой. Если один из “игроков” находится достаточно близко для срабатывания — создает новую сущность с компонентом LoadLevelEvent.
  • LoadLevelSystem проверяет — есть ли сущности с компонентом LoadLevelEvent и выполняет загрузку новой сцены при наличии. Все сущности с таким компонентом удаляются перед этим.
  • Повторяем основной цикл обработки систем.

UPD: josinSbazin сделал реализацию этой тестовой задачи.


Выглядит как чрезмерное усложнение кода по сравнению с одним “MonoBehaviour” классом в десяток строк, но изначально:


  • Позволяет отделить ввод от остальной логики. Мы можем поменять модель ввода с клавиатуры на мышь, контроллер, тачскрин и остальной код не поломается.
  • Позволяет расширять поведение по обработке игрока новыми способами без ломания текущих. Например, мы можем добавить зоны замедления / ускорения на карте путем добавления еще одной или нескольких систем и изменением параметра скорости в компоненте Player для определенных сущностей.
  • Позволяет иметь на карте сколько угодно контрольных точек, а не только одну, как просил дизайнер.
  • Позволяет даже иметь несколько игроков, управляющихся одним способом. Тоже может быть частью игровой механики, как в BinaryLand:


Особенности ECS


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


Плюсы


  • Гибкость и масштабируемость (добавление новых, удаление старых систем и компонентов).
  • Эффективное использования памяти (особенность реализации, мы можем переиспользовать инстансы “чистых” C#-классов как угодно в отличие от “MonoBehaviour”).
  • Простой доступ к объектам (выборка (фильтрация) сущностей с определенными компонентами производится ядром ECS без потери скорости и аллокаций памяти — это именно то, чего не хватает компонентной системе Unity).
  • Понятное разделение логики и данных.
  • Проще тестировать (легко воспроизводить тестовое окружение).
  • Возможность использования логики на сервере без Unity (нет зависимостей от самого “движка”).

Минусы


  • Больше кода
  • Для событий самой Unity необходимо каким-то образом пробрасывать их в ECS-окружение через “MonoBehaviour”-обертки.

Для многих, кто долго работал с Unity и ни разу не использовал ECS, поначалу будет сложно привыкнуть к такому подходу. Но вскоре, начинаешь “думать” компонентами / системами и всё собирается быстрее и легче, чем при сильно связанных компонентах на базе “MonoBehaviour”.


Встроенное ECS-решение в Unity


Сейчас даже сами разработчики Unity поняли, что пора что-то менять в их компонентной системе, чтобы повысить производительность приложений. Где-то год назад было анонсировано, что ведётся разработка собственной ECS и C# Job system. И вот, в 2018.1 версии, мы уже можем примерно представить, что же это будет в будущем, пусть даже и в Preview статусе.



Со штатной Unity ECS – пока ничего не понятно. Разработчики нигде не пишут, что она подходит только для ограниченного спектра задач, но когда возникают вопросы в результате переписывания с других ECS-решений — отвечают в стиле “вы неправильно используете ECS”. Т.е. по сути это получается не “multipurpose”-решение, что довольно странно. Релиза не было, всё еще могут поменять несколько раз, есть проблемы с передачей ссылочных типов (например, string), поэтому я не могу порекомендовать делать что-то большое на штатной ECS в её текущем состоянии.


Альтернативные ECS-решения для Unity


ECS-паттерн был придуман не вчера и на https://github.com можно найти множество его реализаций, включая версии для Unity. Относительно свежие и обновляющиеся:



Я имел дело только с двумя первыми вариантами.


Entitas — самое популярное и поддерживаемое большим сообществом решение (потому что было первым). Оно достаточно быстрое, есть интеграция с Unity-редактором для визуализации ECS-объектов, присутствует кодогенерация для создания оберток с удобным api поверх пользовательских компонентов. За последний год кодогенератор отделился в независимый проект и стал платным, так что это скорее минус. Еще один достаточно весомый минус (особенно для мобильных платформ) — память выделяется под все возможные варианты компонентов на каждой сущности, что не очень хорошо. Но в целом, он хорош, отлично документирован и готов к использованию на реальных проектах. Размер: 0.5mb + 3mb поддержки редактора.
Примеров с использованием Entitas достаточно много, но и существует / пиарится проект давно. Из примеров с исходниками можно посмотреть Match 1.
Общая производительность Entitas оценивается примерно так:


С LeoECS я знаком лучше, потому что делаю на нём новую игру. Оно компактное, не содержит закрытого кода в виде внешних сборок, поддерживает assembly definitions из Unity 2017, более оптимизировано по использованию памяти, практически нулевой GC (только на первичном наборе пулов), никаких зависимостей, C# v3.5 с опциональной поддержкой inline-ов для FW4.6. Из приятных вещей: DI через разметку атрибутами, интеграция с Unity-редактором для визуализации ECS-объектов и готовая обвязка для событий uGUI. Размер: 18kb + 16kb поддержки редактора.
В качестве готового примера с исходниками можно посмотреть классическую игру "Змейка".
Сравнение скорости Entitas и LeoECS: результаты достаточно близки с небольшим перевесом в ту и другую сторону.


Заключение


Я не эксперт в данном вопросе (только недавно начал использовать Unity в связке с ECS), поэтому и решил поделиться своими наблюдениями и мыслями в первую очередь с теми, кто "собирает" игры на Unity из ассетов с кучей скриптов на каждом. Да, это работает. Сам такой был. Но если вы делаете не прототип или какую-нибудь одноразовую игру без необходимости её поддержки и дальнейшего развития, то подумайте 10 раз — вам же потом во всём этом разбираться и переделывать.


Используя ECS я даже получаю удовольствие от процесса рефакторинга :) В игру легко добавляются новые фичи, изменяются старые — и всё это без боли и конфликтов с дизайнером, решившим добавить новую зубодробительную механику или удалить старые, наигравшись с ними.

Tags:
Hubs:
If this publication inspired you and you want to support the author, do not hesitate to click on the button
+25
Comments 30
Comments Comments 30

Articles