Всё что нужно знать про ECS

Привет, Хабр! В этой статье я расскажу всё, что знаю про Entity-Component-System и попытаюсь развеять различные предубеждения об этом подходе. Здесь вы найдете много слов о преимуществах и недостатках ECS, об особенностях этого подхода, о том как с ним подружиться, о потенциальных граблях, о полезных практиках, а также в отдельном разделе коротко посмотрим на ECS фреймворки для Unity/C#.

Я совсем не дизайнер, поэтому все картинки в статье честно украденыЯ совсем не дизайнер, поэтому все картинки в статье честно украдены

Статья очень неспешно собиралась в течении двух лет, из-за чего вышла очень большой. Она хорошо подойдет для тех, кто хочет/начинает знакомиться с ECS. Люди же вкусившие ECS я надеюсь тоже смогут подчеркнуть для себя что-то новое. Если же вы делаете игры на любом отличном от C# языке, статья всё равно может быть вам полезна. Здесь не будет примеров кода и ни слова про историю паттерна, только мои опыт, рассуждения и наблюдения, а также опыт других ECS-фанатиков, за что им всем отдельное огромное спасибо :)

Содержание

Что такое ECS

Entity-Component-System — это архитектурный паттерн, созданный специально для разработки игр, он отлично подходит для описания динамического виртуального мира. Из-за его особенностей, некоторые считают его чуть ли не новой парадигмой программирования, это скорее не так, но мозг перестраивать скорее всего потребуется.
ECS возводит в абсолют принцип Composition Over Inheritance(композиция важнее наследования) и может являться частным примером Data Oriented Design(ориентированного на данные дизайна, далее DOD), однако это уже зависит от интерпретации паттерна конкретной реализацией.

5f8261306f893340ebaed79c39e31622.png

Расшифруем название этого паттерна:

  • Entity — сущность, максимально абстрактный объект. Условный контейнер для свойств, определяющих чем будет являться эта сущность. Зачастую представляется в виде идентификатора для доступа к данным.

  • Component — компонент, свойство с данными объекта. Компоненты в ECS должны содержать исключительно чистые данные, без единой капли логики. Тем не менее часть разработчиков допускает использование разнообразных геттеров и сеттеров в компонентах, но лично я считаю, что для этих целей лучше подходят static utils (подробнее в Good Practices).

  • System — система, логика обработки данных. Системы в ECS не должны содержать никаких данных, только логика обработки данных. Но, опять же, часть разработчиков допускают это, чтобы определять некоторое вспомогательное поведение самой системы, например, константы или различного рода вспомогательные сервисы.

Как вы уже поняли из описанного выше: ECS строго отделяет данные от логики. Поведение объекта определяется не интерфейсам/контрактами/публичным API, как мы привыкли в классическом объектно-ориентированном программировании (далее ООП), а присвоенными объекту свойствами с данными + существующей отдельно логикой обработки. В ECS данные определяют всё — это и есть главное свойство, которое выделяет его на фоне других подходов к разработке: всё есть данные. И свойства объекта, и его характеристики, и даже события — всё это просто данные существующие в ECS-мире. Логика же является просто конвейерной обработкой всех этих данных. В некотором приближении, ECS можно сравнить с базой данных, которая обрабатывается каждый кадр потоком обработчиков написанных в стиле процедурного программирования : D

Entity-Component

Стоит отдельно проговорить, что нередко Entity-Component-System путают с очень близким архитектурным паттерном Entity-Component(иногда пишут как Entity-Component System, далее EC), но это большое заблуждение.
EC вы скорее всего уже встречали в различных движках, таких как Unigine или Unity (но их новый DOTS уже ECS). Главное отличие, как можно понять из названия — отсутствие выделенных под логику систем. В EC-подходе в компоненте хранятся и данные, и логика, а для изменения данных наружу торчит API. Компонент в таком случае уже не просто свойство объекта, а полноценное поведение, которое мы можем добавить нашей сущности. Каждый компонент, будучи отдельным объектом со своим API и ожидаемым поведением, зачастую сам обрабатывает или изменяет свои данные по чьей-либо просьбе.
Фактически, EC — классическое ООП с хорошей модульностью и сильным уклоном в сторону Composition Over Inheritance. Правда никто не запрещает сделать компоненты с чистой логикой и, как итог, получить опыт аналогичный ECS. Но вернемся к ECS…

Зачем ECS

Наверняка, на этом месте у вас уже возник вопрос:»А зачем мне этот ваш ECS вообще нужен? Какая от него польза? ». И чтобы помочь вам определиться читать ли вообще статью дальше, я расскажу чем лично мне так полюбился ECS. В дальнейшем раздел Pros and Cons раскроет подробнее и хорошие, и плохие стороны этого подхода, чтобы вы смогли окончательно определиться нужен ли вам ECS.

c9df32c55bc65abfc4eb1611bec651ad.jpg

Лично я люблю ECS за то, что…

С ECS ты просто садишься и делаешь игру, а не воюешь с архитектурой проекта. Нет нужды строить большие и «красивые» иерархии, продумывать кучу связей и париться про «X же не должен знать про Y». При этом принципы ECS защищают тебя (не на 100%, ессесно) от безвыходной ситуации, в которую заводит плохая архитектура, когда дальнейшее развитие проекта становится очень болезненным. И даже если всё таки что-то пошло не так — рефакторинг в ECS совсем не проблема. И это, на мой взгляд, самое кайфовое в ECS.

Код на ECS получается простым и понятным. Не нужно ползать по куче вызовов среди кучи классов, чтобы понять чем занимается конкретная система, всё видно сразу, особенно если грамотно разбивать фичу на системы, системы на методы и не переусложнять код. Вдобавок, ECS сильно упрощает профилирование: сразу видно какая логика (система) сколько времени кадра отнимает, не нужно искать источник лагов в глубине вызовов.

Очень легко манипулировать логикой. Добавление новой логики практически безболезненно — просто вставляешь новую систему в нужное место, не боясь напрямую повлиять на остальной код (стоит отметить, что возможно косвенное влияние через данные). Можно без каких-либо проблем использовать общую логику (системы) между клиентом и сервером, при сохранении используемых данных (компонентов), конечно. Можно легко переписывать системы, заменяя старые системы на отрефакторенные, при этом без какого-либо влияния на остальной код, не понравится результат — просто снова включаешь старую систему и выключаешь новую. Аналогичным механизмом можно легко устраивать A/B тесты.

Всё крутится вокруг данных. На поверку это оказывается дико удобно. С помощью прямого манипулирования данными на сущностях открываются широчайшие возможности для комбинаторики. Можно с помощью данных формировать сущность во что угодно: от камеры, которая убивает по касанию, до нематериального контейнера с конфигами. А если фреймворк предлагает инструментарий для просмотра данных на сущностях, то можно в любой момент изучить данные и их динамику на совершенно любой сущности без необходимости запуска дебаггера, чтобы заглянуть в память.

Как работать с ECS

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

339a00166fdcefd612c64a8bf6224352.jpg

Задача: создать объект, который двигается в направлении заданного вектора движения.

Первым делом определим данные, необходимые нам для работы. Для нашей задачи потребуются позиция объекта и задаваемый вектор движения. На языке ECS это будут:

Следующим шагом опишем логику. Создаём систему MovementSystem. В главном методе системы, в зависимости от реализации это может быть Run()/Execute()/Update() или что-либо другое, получаем все сущности в ECS, у которых есть PositionComponent и MovementComponent. Как именно это можно сделать зависит от фреймворка, но зачастую это похоже на своеобразный SQL-запрос вида GetAllEntities().With().With().
Запускаем цикл по полученным сущностям и для каждой производим изменение позиции: positionComponent.position += movementComponent.velocity. Можно добавить *deltaTime, если вы не хотите зависеть от частоты вызова системы.

Ну и наконец, мы просто создаем сущность(или даже 10 штук) с двумя нашими компонентами, задаем вектор движения отличный от нуля и теперь при каждом вызове системы MovementSystem(вне зависимости от того где и когда мы ее вызовем) наш объект будет менять позицию в направлении заданного вектора движения. Задача выполнена! :)
Зачастую системы так или иначе встраиваются в GameLoop проекта и дёргаются каждый кадр самим движком, но можно это делать и руками, и любым другим способом, тк это просто вызов метода. 

Посмотрим какие дополнительные возможности для разработки мы получили помимо решения основной задачи:

  • Любая другая наша система способна определить двигается ли объект простой проверкой на наличие свойства MovementComponent

  • Любая другая наша система способна получить вектор движения для своих нужд

  • Любая другая наша система сможет задать вектор движения для любой нашей сущности по своему желанию

Если нам захочется, то мы ещё и любую другую сущность сможем заставить двигаться, просто повесив на неё PositionComponent и MovementComponent, при создании игр это бывает крайне полезно.

Pros and cons

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

81aadbd0ce92e8dc5a201f88a7aa4578.png

Преимущества

  • Слабая связность кода
    Это крайне полезное для игроделов свойство. Оно позволяет нам производить рефакторинг и расширение кодовой базы относительно просто и не ломая старых кусков кода. Мы всегда можем добавить новое поведение с использованием старых данных буквально сбоку, без нужды как либо вмешиваться в старую логику. ECS достигает такого эффекта благодаря тому, что все взаимодействие логики выражено данными в Entity, которая в свою очередь является максимально абстрактным объектом без каких-либо гарантий, как какой-нибудь Object в C#/Java.
    Однако стоит иметь ввиду, что в ECS порядок изменения данных (то есть порядок выполнения систем) играет важную роль, что в конечном счете может повлиять на сложность рефакторинга и таки поломать вашу старую логику, а-то и создать неприятные сайд-эффект баги, об этом ещё поговорим в Недостатках.

  • Идеальная модульность и тестируемость логики
    Свойство всё есть данные даёт нам ещё одно прекрасное преимущество: если всё взаимодействие выражено в чистых данных — наша логика всегда полностью отвязана от источника данных. Это позволяет нам как безболезненно перемещать логику из проекта в проект и использовать повторно (с сохранением формата данных, ессесно), так и запускать логику на каких-угодно вводных данных для тестирования ее работы.

  • Сложнее писать плохой код
    ECS менее требователен к архитектуре, тк задает рамки с которыми сложнее (но не невозможно) создать реально плохой дизайн кода. При этом, как было сказано выше, мы можем относительно безболезненно и с минимальным влиянием на остальной код исправить проблему даже если плохой дизайн таки случился. Что приводит нас к тому, что ECS позволяет тратить меньше времени на раздумья «как впихнуть эту логику в нашу архитектуру и не сломать ничего» и просто добавлять новые фичи.

  • Комбинаторика свойств
    Это преимущество очень обрадует ваших геймдизов. Именно это преимущество и делает ECS отличным вариантом для описания динамических миров. Вы только представьте: вы можете придать любое свойство (а следовательно и логику) любой вашей сущности без какого-либо геморроя!
    Захотели, чтобы у камеры появилось здоровье — пожалуйста, повесил на сущность камеры HealthComponent и готово: она может получать урон (если есть такая система). Повесил на сущность InFireComponent и она тут же начинает получать урон от горения, если у нее есть HealthComponent, красота! Нужно чтобы дом начал двигаться под управлением игрока? Да без проблем, где там мой PlayerInputListenerComponent
    Опытный разработчик тут заметит:»Пфф, с этим справится большинство Composition over Inheritance паттернов, чем тут ECS лучше? ». Отвечаю: ECS позволяет вам комбинировать свойства не только с точки зрения формирования сущности, но и для создания специфичной логики при комбинации нескольких свойств (компонентов) на одной сущности. Не говоря уже о возможности добавить совсем новую логику для старых данных без необходимости трогать компоненты на сущности.
    Границы возможностей комбинаторики в вашем проекте определяете вы сами, но об этом мы еще поговорим в разделе Good Practices.

  • Проще соблюдать Single Responsibility логики
    Когда логика у нас полностью отделена от данных и не привязана к какому-либо объекту/сущности, нам становится проще контролировать разбиение логики по ее назначению, а не месту в иерархии. Каждая система просто выполняет какую-то конкретную, свойственную только ей, задачу. Зачастую код системы вообще выглядит как вызов одного метода для множества компонентов одного типа. По итогу, код в большинстве своём легко читается и воспринимается.
    Лично я сторонник принципа «Не разделяй раньше времени», поэтому допускаю, что система может выполнять несколько функций, если эти функции плотно связаны и не используются отдельно друг от друга, но это каждый решает сам для себя, об этом мы еще поговорим в Good Practices.

  • Более наглядный профайлинг
    Благодаря тому, что за обработку у нас отвечают обособленные системы с присущей только им логикой, при профилировании мы сразу видим какая логика и сколько времени кадра отнимает. Нам не нужно идти вглубь стека вызовов, чтобы понять что больше всего занимает, например, система движения персонажа, мы сразу видим виновную CharMovementSystem.
    Стоит правда заметить, что это преимущество зависит от устройства ECS фреймворка, тк у самого фреймворка может быть свой стек вызовов, в котором иногда ещё поди сориентируйся.

  • ECS может дать хороший прирост производительности
    Многие считают, что хорошая производительность — основное преимущество ECS (спасибо пропаганде Unity). Это не совсем так. Скорость выполнения кода лишь приятный бонус, вытекающий из принципов паттерна: данные в одном месте — логика в другом + работа систем в духе SIMD (single instruction, multiple data), когда мы выполняем одну и ту же логику для множества одинаковых компонентов. А если фреймворк следует DOD при реализации ECS и добивается хорошей локальности данных, то мы дополнительно получаем кэш-френдли код, что однозначно порадует ваш процессор.
    Ключевое слово этого пункта — »может». Итоговая производительность ECS зависит от множества факторов: как именно фреймворк хранит данные, как фреймворк фильтрует сущности, насколько быстрый доступ систем к данным, ну и, конечно же, насколько быстро работает код внутри ваших систем. При этом последний пункт для большинства проектов дает самое большое влияние на время обработки кадра.
    Однако, если взглянуть в контексте разработки на Unity, ECS всегда будет быстрее привычного MonoBehaviour-подхода, особенно на большом объеме данных. Но не забывайте, что всё таки главное в производительности вашей игры не столько архитектурный паттерн или производительность фреймворка, сколько алгоритмическая сложность и производительность написанного вами кода.
    Да пребудет с вами Профайлер!

  • Легче распараллеливать обработку данных
    За счёт того, что логика у нас выделена в отдельный обработчик данных, а данные фактически представляют собой линейную последовательность, мы можем в рамках одной системы без особых проблем распараллелить обработку. Это бывает очень актуально, если система обрабатывает огромное количество сущностей одновременно и они между собой никак не пересекаются.
    Можно пойти ещё дальше и отправить в разные потоки логику, которая не пересекается в изменяемых данных, но это все куда сложнее контролировать и отслеживать + всё равно будет bottleneck в виде синхронизации с главным потоком для подготовки данных на отрисовку. К тому же, может оказаться, что накладные расходы на подготовку данных и распределение между потоками буду выше чем время выполнения кода в ваших системах, так что нужно ещё будет дать оценку стоит ли оно того вообще.

  • С чистыми данными очень легко работать
    Почти в каждой игре нам приходится что-то сохранять, загружать или сериализовывать для отправки по сети. Это всё куда проще совершать, когда данные отделены от логики. Нет необходимости думать «А как это должно попасть в приватные данные…», вызывать какие-то особые методы для правильной сериализации, просто сохраняешь/загружаешь дамп нужных компонентов на сущности, проще некуда, а системы потом допилят ее до нужного состояния сами, если сочтут нужным.

  • Можно менять ECS-фреймворки как перчатки
    ECS фреймворки похожи друг на друга, тк принципы в их основе одни и те же. Разработчик, который перестроил свой мозг под ECS и хорошо разобрался в одном фреймворке однажды, без особых проблем сможет работать и с другим ECS-фреймворком. Время уйдет лишь на изучение API (нередко бывает, что и API похож) и особенностей конкретного фреймворка, но голову под новый подход перестраивать будет не нужно.
    Справедливости ради, это преимущество может быть отзеркалено и к другим архитектурным паттернам, будь то DI или EC.

Недостатки

  • Высокий порог вхождения для бывалых
    Несмотря на то, что сам концепт ECS можно описать в одном предложении, чтобы научиться варить его правильно может потребоваться много практики. ECS требует от вас забыть всё, что вы знали о проектировании раньше: все ваши вертикальные иерархии наследования, что поведение объекта определяется его интерфейсом, что объект представляет собой что-то конкретное и неизменяемое, что у объекта может быть личное (private) пространство, а логика может быть вызвана где только захочется.
    В ECS всё не так, он полная противоположность описанному выше. Тут все данные открыты, все сущности абстрактны и очень динамичны, их свойства лежат в одной плоскости и доступным каждому, логика работает по принципу конвейера, а поведение сущностей вообще меняется на ходу исходя из данных.
    На перестроение головы под это уходит время и пока этого не произойдет, ваш мозг будет активно сопротивляться, особенно если у вас за спиной большой опыт разработки. Он будет хотеть выстраивать наследование компонентов, делать им интерфейсы и методы, а также много всего другого, мы разберем это в разделе Ошибки новичка.
    При всём вышеописанном, если у человека за спиной нету багажа из спагетти-архитектур (чистая голова джуна), то ECS осваивается быстрее и менее болезненно, чем какой-нибудь MVC.

  • Слабая связность может мешать
    Если вам вдруг понадобилось тесное взаимодействие между двумя конкретными сущностями (например, гусеничный корпус и башня танка), то вы сталкиваетесь с проблемой, что сущности то у нас максимально абстрактны и вы не можете гарантировать на уровне компилятора что на другом конце будет именно гусеничный корпус. Это будет мешать, тк игры — место, где много тесных взаимодействий и всегда хочется иметь прямую ссылку с гарантией свойств и поведения. Надо будет проверять наличие компонента и как-то обрабатывать его отсутствие, получать доступ к компоненту из сущности, чтобы начать с ним взаимодействовать…
    НО, возвращаясь к слабой связности как преимуществу: это научит вас организовывать свой код так, чтобы на другом конце могла быть любая сущность, которую можно толкнуть отдачей и даже предусмотреть случай отсутствия такого компонента, чтобы игра всё равно смогла работать корректно без необходимости менять код, если вдруг ваш геймдиз захотел установить башню танка на здание.

  • Доступ к любым данным откуда угодно
    Мир ECS представляет собой полностью открытые коробки сущностей с доступными каждому данными в компонентах. Это, как и слабая связность выше, одновременно и плюс, и минус ECS.
    С одной стороны это дико удобно, ибо не нужно придумывать как обходить созданные ранее при проектировании ограничивающие самого себя рамки («X не должен знать об Y»), пытаясь натянуть сову на глобус и вытаскивая в public сокрытые ранее данные для решения какой-то сиюминутной задачи.
    С другой стороны, любой неопытный программист так и норовит изменить данные оттуда, откуда этого делать не стоит, но обычно командное взаимодействие включает в себя доверие к работе других, так что доверяй, но проверяй ;)

  • Системы работают исключительно в потоке, друг за другом
    При правильном следовании принципам ECS, вы не должны вызывать логику одной системы внутри других систем. Системы вообще не должны знать о существовании друг друга, иначе это будет приводить к появлению излишней связности кода и потенциально вредит вашему проекту. Однако такое ограничение бывает неудобным и иногда будет приводить к различным обходным решениям, которые не будут нарушать принципы ECS. Если вам всё же понадобилось вызвать какой-то код «здесь и сейчас», то просто сделайте обычный объект с методами и положите его в компонент, не мучайте себя.

  • Плохо работает с рекурсивной логикой
    Этот недостаток является следствием предыдущего. Из-за отсутствия возможности вызывать код систем вне потока и там, где захотим того мы, ECS делает почти невозможным создание рекурсивного кода за рамками какой-то одной конкретной системы.
    В качестве решения этого недостатка (aka обходной путь для соблюдения принципов ECS) я вижу только создание специализированной структуры/системы, которая будет вызывать определенный список систем в бесконечном цикле, пока будет соблюдаться конкретное условие, например, пока есть сущности с компонентом DoActionComponent. Если у вас есть более изящные обходные пути, буду рад прочитать о них в комментариях :)

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

  • Сложнее дебажить
    Это достаточно спорный пункт, особенно с современными умными IDE, но многие его подмечают. В ввиду отсутствия глубокого StackTrace (у нас же логика в системах и не привязана к сущности) и невозможности отследить как и кем менялись данные и состояние сущности, может возникнуть ситуация, когда сложно найти причину почему ваша система вдруг начинает работать не так, как было задумано: сложно понять, что к этому вызову привело, хотя это просто кто-то добавил компонент на сущность или сделал лишний ++.
    Если подвести черту, то в ECS без дебаг-инструментов сложно отследить почему и как менялись данные в компонентах, особенно когда у тебя тысячи сущностей, а проблемная только одна. Исправить этот недостаток могут дебаг-инструменты, которые фреймворки могут предоставить, но их может не быть из коробки и придётся писать самому или страдать.

  • Неудачный вариант для структур данных, особенно иерархических
    Реализация структур данных с помощью ECS трудна, неудобна и, мне кажется, вообще лишена смысла. Я не говорю, что это невозможно совсем (если постараться, то возможно всё), но таки это будет тернистый путь без особой выгоды в конце пути, будьте рациональны при выборе.
    Я перечислю несколько проблем, который будут мешать при попытках всё таки реализовать какую-то структуру данных на ECS:
    — В ECS все данные доступны отовсюду, что для таких штук как структуры данных, где требуется максимальная консистентность, может быть крайне опасно. Любой мимокрокодил может изменить любые внутренние данные в обход вашей логики, что начисто поломает вам структуру данных.
    — Если честно следовать принципам ECS, то мы не сможем вызвать логику нашей структуры данных «здесь и сейчас», как обычно требуется при работе с ними. Однако этот пункт таки можно забороть с помощью static utils/extensions, подробнее об этом в Good Practices.
    — ECS — представитель горизонтальных архитектур, все данные в нём лежат в одной плоскости, почти всегда просто одномерные массивы компонентов. Это затрудняет работу если ваша структура данных требует вертикальности/иерархии.
    — Нередко в структурах данных требуются ещё и перекрестные ссылки между элементами (иерархия). Но, как вы можете помнить, в ECS все крутится вокруг максимально абстрактной Entity, что затрудняет работу, тк нет гарантий, что на другом конце будет элемент нужного нам типа и это надо будет как-то отдельно обрабатывать.
    — Структуре данных и её элементам обычно не требуется менять формат данных в рантайме, как и не требуется комбинаторика, они достаточно ригидны. На каждой сущности структуры данных может по итогу лежать вообще один компонент. Из этого делается вывод:, а зачем тут вообще ECS?

    Если вам всё же потребовалась структура данных (а они вам так или иначе потребуются), то рекомендую просто создать её отдельным объектом с методами, как это предполагает ваш ЯП, а после положить этот объект в ваш компонент и просто работать с ним из систем как обычно.

  • Больше файлов и классов
    В ECS-подходе количество файлов в проекте растет быстрее, чем при аналогичном коде в классических подходах. Как минимум из-за того, что вместо одного класса с данными и логикой у вас появляется два: компонент и система (их правда всё равно можно запрятать в один файл). Как максимум, если во имя комбинаторики делать все компоненты атомарными (1 компонент — 1 поле), то будет очень-очень много файлов…

  • Бойлерплейт
    Этот недостаток сильно зависит от конкретной реализации ECS-фреймворка. В одних фреймворках приходится писать очень много технического кода, в других разработчик постарался сделать максимально простое API и минимизировать бойлерплейт. Но, если сравнивать с другими подходами, почти всегда остаётся хотя бы крошечная доля дополнительного кода, который приходится писать: объявление систем и компонентов, получение фильтра с нужными компонентами, получение сущностей из него, получение компонента из сущности и тд.

Ошибки новичка

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

bc20d22e3edadd07e96719a83f24474b.jpg

  • Наследование компонентов и интерфейсы
    Наследование компонентов и использование интерфейсов — это очень частая ошибка новичков. Почему же это вредно для разработки?
    Во-первых, абстракция на уровне компонентов не даёт абсолютно никакой выгоды в сравнении с ECS-абстракцией через данные (о которой я расскажу в следующем пункте).
    Во-вторых, создаёт вам ограничения: вам будет сложнее расширять такие компоненты и дорабатывать связанную с ними логику.
    В-третьих, приводит к ситуации, когда в компоненте появляется своя логика, а это, как мы помним, нарушение принципов ECS, чего делать не стоит.
    В-четвертых, приводит к беспричинной необходимости наследовать системы или делать всякие switch-case по типам.
    Сразу замечу, что наследовать систем — это не всегда хорошая идея, но и плохого она ничего не даёт, в отличии от наследования компонентов. Так что если вам захотелось понаследовать компоненты — не надо так, подумайте ещё раз как можно решить задачу иным путём.

  • Неумение в ECS-абстракцию
    ECS абстракция — это когда мы общие данные (которые должны наследоваться в ООП) просто выносим в отдельный компонент. «Наследника» такого компонента мы делаем просто: добавляем новый компонент с нужными нам данными и фильтруем сущности, где есть BaseComponent и InheritorComponent. Всё элементарно: если у вас появились какие-то общие данные между компонентами/сущностями — почти всегда их можно вынести в отдельный компонент и чем раньше это сделаешь, тем лучше.

  • Включать/отключать системы для изменения логики
    ECS устроен так, что мир и системы, которые его обрабатывают, статичны и существуют всегда, а вот сущности и их данные очень динамичны. И если вам нужно отключить какую-то логику, отключать для этого систему — некорректное решение, к тому же из систем зачастую нету доступа к другим системам (и это хорошо). Куда более практичный вариант — создать какой-то компонент-маркер (подробнее в Good Practices), который будет говорить, что логика системы не должна отрабатывать для сущности с маркером или больше: если вообще в мире существует хотя бы одна сущность с нашим маркером.
    На этом месте многие новички заявляют:»Но ведь если у меня нету сущностей для системы, то зачем этой системе работать? Не лучше ли её отключить оптимизации ради? ». Нет, не лучше. Если допускается, что сущностей может не быть, то проще в самое начало главного метода системы добавить if (entities.Length < 1) return;, в масштабах игры вызов функции и сравнение двух интов — капля в море, которая никак не повлияет на вашу производительность.
    Единственные легитимные случаи отключения систем в рантайме: A/B тестирование и дебаггинг/тестирование конкретных систем (большинство фреймворков даёт инструменты, чтобы делать это не из кода, а из окна редактора).

  • Возводить ECS в абсолют
    Только Ситхи возводят всё в абсолют!
    Стоит помнить, что ООП не запрещён для ECS-адептов : D
    Когда работаешь с ECS, не стоит прямо совсем упарываться в ECS и переносить всёпревсё на него, ибо это бывает контрпродуктивно. К тому же, как я упоминал в недостатках ECS: не все структуры данных хорошо ложатся на ECS, поэтому лучше не трепать себе нервы и просто делать в таких случаях отдельный ООП-класс.
    Некоторые разработчики идут дальше и некоторые элементы проекта (например, UI) делают не по лекалам ECS, а немного в стороне любым другим удобным способом и после соединяют с ECS каким-нибудь мостом. Также всяческую вспомогательную логику (подгрузка конфигов, прямая работа с сетью, запись сохранения в файл) проще сделать ООП-сервисом и работать с ним напрямую из нужной системы, чем натягивать сову на глобус. Выбирать как именно поступать нужно исходя из здравого смысла. ECS должен помогать разработке, а не мешать ей.

  • Пытаться дословно перекладывать существующий код на ECS
    Очень часто новички пытаются дословно перенести их существующий код на ECS-рельсы. Это не самая хорошая идея, поскольку подходы к написанию кода в ECS таки отличаются от традиционных архитектурных паттернов. Результатом такого переноса обычно оказывается попоболь от ECS и очень кривой итоговый код.
    Если вам всё таки требуется перенести старый код на ECS, лучшим вариантом будет написать ту же логику с нуля на ECS, используя свои знания и существующий код как инструкцию что делать.

  • Использовать Delegates/Callbacks или реактивную логику в системах
    В ECS может быть опасно захватывать какую-то логику из систем и сохранять её в компонент для дальнейшего использования или моментально производить реакцию на какие-то изменения (например, реакция системы на добавление компонента в другой системе). Помимо того, что это добавляет излишней связанности системам (они начинают сильно зависеть от внешних вызовов), это ломает наш красивый конвейер процессинга данных, добавляя логику, вызов которой мы не то чтобы контролируем. В качестве альтернативы лучше использовать отложенную реактивность о которой расскажу подробнее в Good Practices.

  • Разбивать файлы в папки по типам
    Когда начинаешь работать с ECS, по первости хочется складывать новые файлы по типам: компоненты в папочку Components, а системы в папочку Systems. Но с опытом приходит понимание, что это способ сортировки далёк от эффективности. С ним сложно ориентироваться, понимать какие компоненты с какими системами связаны.
    Лучший вариант — разбиение по фичам, когда всё, что касается какой-то определенной фичи лежит в одной папке (может с внутренней иерархией components/systems). То есть все компоненты и системы связанные со здоровьем и нанесением урона будут лежать в папке Health. Это позволит одним взглядом на папку понять основной контекст данных для систем, которые в ней лежат и проще ориентироваться по проекту.

Good Practices

Сверху вы можете почитать о том как точно НЕ стоит делать при разработке на ECS, а тепер

© Habrahabr.ru