Возможно, вам не нужен ECS

Привет хабр.

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

Набор мнений очень сильно варьируется, но главная мысль проста — вам следует попробовать, а вернее НЕТ, вам следует использовать ECS в вашей игре!
И если с первым я могу согласиться, то вот второе предлагаю обсудить.

de94e8096cf9fb58f09483e903262a00.png

Что такое ECS?

ЕCS расшифровывается как Entity Component System и является специализированной высокопроизводительной архитектурой, применяемой в играх. Название описывает основные компоненты:

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

  • Component — компоненты, содержат свойства сущностей.

  • System — системы, содержат логику обработки компонентов.

Тут важны детали. В классическом подходе в Unity также существуют сущности (GameObjects), компоненты и, по заветам инкапсуляции, методы по их обработке. При желании их можно вынести в отдельные статичные методы и отделить данные от логики, но обычно так делать не рекомендуют и на то есть веские причины. Так в чем же разница?

Почему ECS?

Производительность.

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

Сравнение скорости разных операций в компьютере

Для решения этой проблемы используют кэш процессора — очень быструю (и дорогую) память, расположенную обычно прямо на кристалле процессора. Работа с кэшем по скорости сопоставима с другими операциями процессора. Таким образом, когда вы, скажем, хотите сложить два числа, в кэш загружается целый блок памяти в несколько килобайт. Данные в программе обычно расположены достаточно локализовано — работая с объектом, вы используете его поля, работая с функцией — кусочек стека, массивы расположены в памяти по порядку, то есть такой подход значительно ускоряет вычисления.
Но в какой то момент вам могут потребоваться данные из блока, не входящего в кэш. Это называется «Промахом кэша». В этот момент процессор начнет простаивать, ожидая нужные данные из обычной памяти. Конечно это упрощенная схема, внутри современных процессоров какого волшебства только нет, но в целом, думаю, понятно. Вы даже можете легко проверить мои утверждения на классическом примере «Массив структур» vs «Структура Массивов». Можно сделать массив из 100000 экземпляров структуры из 10 float полей и 10 массивов по 100000 float чисел, после чего возвести в квадрат числа в первом поле / первом массиве. С большой вероятностью массив окажется значительно быстрее.

Так вот. Классическое ООП — злейший враг локальности кеша.

Объекты создаются в случайных местах в памяти, что вызывает постоянные обращения к различным блокам памяти и очень не оптимально.

Постоянно используя ООП с его полиморфизмом, большим количеством объектов в памяти, виртуальными функциями и прочими удобствами, мы платим достаточно большую цену в терминах производительности.

Как работает?

ECS призван это решить, и именно для этого нужны системы, компоненты, и другой способ обращения к объектам.

Теперь за выделением памяти следит ECS, она располагает все компоненты в чанки — массивы с заранее выделенной памятью и одним типов компонентов.

Для адресации вместо ссылок в памяти мы используем сущности. По сути это идентификатор, прикрепленный к компоненту в памяти, чтобы понимать к чему он логически относится.
Ну и наконец мы используем системы для групповой обработки компонентов. Важно то, что система это не просто статическая функция для определенного компонента. Это статические функции для массивов, которые выполняют не одну операцию, а одну операцию над каждым элементом в массиве.
Группируя данные близко друг к другу и обрабатывая их за один проход мы максимально используем процессорный кэш, а также можем эффективно использовать векторные инструкции процессора.
Ура!

На самом деле не все так просто. Точнее просто для простых случаев. Дело в том, что умножить 2 массива матриц это действительно просто и быстро, но обычно нам не это нужно. А нужно нам умножить их только для четных объектов. Или для объектов с определенным компонентом. Или если в компоненте жизней меньше 0. И сделать еще 38 компонентов и 80 систем для различных аспектов их обработки и игровой логики.
Поэтому системы предоставляют API для фильтрации и компоновки компонентов.

Но и это не решает всех проблем. Для одних случаев обработки будет оптимальнее использовать компонент в виде сочетания позиции и направления (операции по передвижению затрагивают обе переменных), для других их стоит разнести по разным. Оптимумы будут разными для разных игр и компонентов, более того, могут быть разными в разных режимах игры.
И в любом случае это довольно сильно зависит от вашей конкретной логики, а соответственно от вашего понимания, что и как организовать.

ECS имеет и другие преимущества.

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

И если вы не подумали про это на старте, реализация может оказаться весьма сложной, в то время как в ECS она есть из коробки.

Так как все компоненты открыты и системы имеют к ним равный доступ, вы можете легко организовать любые типы игровой логики, комбинируя любые компоненты и действия над ними.
При этом структурно ваша система очень проста — у вас есть только список компонентов и список систем, что может быть проще?

Почему вам может не подойти ECS?

Так почему ECS вам может не подойти?
Все просто — она вероятно не решит ВАШИХ проблем с производительностью, но создаст большие сложности с обслуживанием кода.

Производительность неигровой логики.

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

И это только рендер. Большая часть разнородных операций, ради которых мы, собственно, используем движки, работает на процессоре — композинг и проигрывание музыки и звуков, симуляции систем частиц и физики, разнородные логические компоненты, загрузка ресурсов, обслуживание дерева объектов, отрисовка интерфейсов. Сделать оптимальный рендер из миллиона треугольников не проблема, проблема сделать с ним игру.

Большая часть этих компонентов написана неплохо, вероятно хорошими разработчиками, и оптимизирована в ключевых местах. Разработчики аудио библиотек наверняка знают про эффективное использование массивов и кеш процессора. Но вопрос в количестве. Музыка будет все равно занимать больше килобайт и тратить больше тактов, чем типичная игровая логика движения монстров. Массив с параметрами символов шрифтов будет содержать больше элементов, чем средний массив с игровыми объектами.

Вам следует начать оптимизацию вашей игры с контроля этих величин, и никакой ECS вам не поможет.
Если вы инди — вам нужно следить за этим и попытаться разобраться, как это работает. Если вы в команде — вам следует объяснить это художникам, потому что никакая оптимизация объекта до 18 байт не поможет, если художник повесит на объект 15 систем частиц и сделает волшебный рейтрейсинг для вашей мобильной игры.
Unity не идеален, но весьма хорош, и некоторые его недостатки — продолжения его преимуществ.
На нем очень легко сделать тормозящий объект без единой строчки кода!

Наиболее эффективной стратегией будет тестировать, искать и лечить эти проблемы.

Есть и другой путь — вы можете переписать любой компонент, систему или функцию в кеш френдли манере, специализировать их для вашей игры. Таким образом вы можете оптимизировать и улучшить любой аспект игры.
Только придется переписать кучу готового кода. Нужно ли говорить, что пока это не тормозит, делать этого не нужно?

Производительность игровой логики

Отдельный вопрос, что многим играм в принципе не нужен ECS и в игровой логике. Посмотрите на популярные игры:
Шутеры? — Симуляция сотни игроков и тысяч пуль для ECS это даже не разминка, с этим легко справится обычный код, вы можете даже не получить преимущества.

РПГ? — Сотни монстров это скорее вопрос рендера, чем вопрос игровой логики монстра.

Стратегии? — даже тут это не всегда оправданно.
И мы даже не говорим про пиксельные инди рогалики и 3-в-ряд.
Отдельные части таких игр можно оптимизировать отдельно и при обычном ООП подходе, но большая часть в общем не требует оптимизации. Гораздо эффективнее организовать этот код в хорошо декомпозированные объекты и системы, с нормальными связями, понятными методами и хорошей иерархией.
Для сетевых игр будут полезны оптимизации структур и бинарная сериализация, но оптимизация заключается в том, какие данные передавать, а не в скорости сериализации. Если не передавать ненужные данные — они оптимизированы идеально.

Производительность альтернатив

Важно отметить, что обычное ООП тоже не прямо уж медленное. Компилятор умеет оптимизировать.

Случайные объекты в памяти располагаются не случайно — аллокаторы располагают объекты одинакового размера в одних блоках памяти (так проще потом их удалять и заменять, избегая фрагментации) таким образом объекты одного типа естественным образом попадают в одно место памяти.

Если мы создаем множество объектов за раз — они с большой вероятностью будут созданы в памяти по порядку. Таким образом создав массив с объектами мы получаем и локализованный массив со ссылками и с большой вероятностью локализованные объекты в этом массиве.

Часть объектов вообще не создаются в куче, потому что компилятор понимает что они короткоживущие. В то же время, ECS по сути сама занимается управлением памятью, и не факт, что во всех сценариях она делает это эффективнее.

Это может звучать грустно, что вам обычно нужно творить не крутые архитектуры, а убирать перерисовки в интерфейсах и искать жирные системы частиц, чтобы отругать технических художников. Это полезнее.

Резюмируя: в вашей игре, вероятно, тормозит не игровая логика, соответственно и оптимизировать нужно не ее.

Сложность

Многие пропагандируют ECS как решение по умолчанию, потому что «просто используйте ее и получите хорошую производительность за бесплатно».
Проблема в том, что эта серебряная пуля не бесплатная — ECS требует организовывать логику в весьма специфичной и организационно сложной форме, которая затрудняет чтение и написание кода.
С виду все кажется просто — 2 слоя, можно достучаться до любого компонента, что тут сложного?
Зато теперь у вас в проекте есть архитектура!
Но архитектура проекта это не только слои и паттерны, это ваши сущности и игровая логика. Ваши юниты, пули, эффекты и методы взаимодействия между ними — вот настоящая архитектура вашей игры.
Создание эффективных, удобных и понятных сущностей критически важно для вашей игры и ECS скорее мешает этому.

Во-первых, прежде удобные сущности, например, пулька, теперь разъединены на несколько компонентов и несколько систем. Помним про производительность? Позиция пульки и ее атрибуты должны храниться в разных компонентах, иначе вы потеряете в скорости. Порождение, передвижение, попадание — это теперь не разные функции в одном методе близко друг к другу, но 3 системы, где код организован не для вас, а для оптимальности прохождения массива.

Эта архитектура навязывает вам чрезмерную декомпозицию и когнитивная сложность понимания такого кода очень высока.

Чтобы понять полное поведение сущности, нужно найти все ее компоненты, все системы, обновляющие ее, и сопоставить их в голове в порядке выполнения (который настраивается порядком систем). При плохой декомпозиции может быть довольно сложно понять, где заканчивается юнит и начинается пулька.
А чтобы модифицировать ее, нужно не только понять, как оно работает, но и придумать необходимые модификации и, возможно, создать для них целую систему или новый компонент.

Во-вторых — у вас нет базовых примитивов. Хотите делегат? Их нет, используйте компонент. Полиморфизм? Нет, это enum и несколько систем. Фильтр по циклу? Вам нужен компонент + система. Нужно дерево? Деревьев нет, используйте компоненты со ссылками на сущности. Ах да, и быстро это будет работать только если вы организуете дерево правильно, произвольные связи поломают вам локальность кэша.
У вас нет половины современных инструментов программирования, только императивное программирование и базовые структуры.
Либо, вы можете использовать все, что захотите — тогда у вас будет мутант, который не имеет преимуществ ECS.


В-третьих — у вас нет привычных инструментов разработки. Для классического программирования есть нотации, области видимости, переходы по ссылкам, поиск классов, наследников и интерфейсов.
В ООП у вас есть приватные переменные и интерфейсы для сокрытия деталей, понятные паттерны организации кода, иерархическая организация. Конечно, классический код тоже можно сделать запутанным и с плохой декомпозицией, но для ECS его очень легко сделать таким.
Конечно, вы можете придумать какие то нотации, различные правила оформления объектов. Но вероятность ошибок, сложность понимания и расширения такого кода очень велики и быстро растут с ростом проекта.

Когда все же использовать ECS

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

Вы делаете Factorio.

Некоторые специфичные игры, действительно, могут требовать максимальной производительности игровой логики и взаимодействия большого количества игровых объектов. В блоге авторов Factorio можно почитать, какие хаки они применяли (они не используют ECS, просто своя реализация на C++), чтобы обрабатывать все эти тысячи брусочков меди на конвейерах в реальном времени и по сети. Нужно ли говорить, что это специфические и сложные игры? Возможно, вам не нужно представлять десятки тысяч слитков?

Вы делаете сложную сетевую игру.

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

Вы учитесь.

Думаю, тут понятно, попробуйте сами, не обязательно верить мне на слово. Только пожалуйста, на чем нибудь маленьком и дешевом.

Вы знаете, что делаете.

Думаю, есть достаточно людей, которые лучше меня разбираются в описанных выше вопросах. Жгите.

Вы используете ECS только для части проекта.

Важно понимать, что вся игра не обязательно должна быть написана в этом стиле. Возможно, оправданным будет как раз обратный подход — вы точно выделяете то, к чему есть эти супер требования. Стейт игры, синхронизируемая по сети модель сессии боя. Остальной код игры, интерфейс, мета часть, могут быть написаны в обычном ООП стиле, с упором на удобство и понятность кода, либо удобство и понятность расширения, редактирования дизайнерами и прочими требованиями.

Альтернативы

Кроме того, нужно помнить очень важную часть — вы можете оптимизировать отдельно любой специализированный аспект игры без ECS.
Практически все ее преимущества могут быть использованы отдельно от нее.

Хотите генерировать 3д воксельный ландшафт и генерировать к нему 3д меши? Возможно, вам не нужен ECS, а нужен специализированный компонент. Он может работать на нативных массивах и бинарных преобразованиях и использовать Jobs для параллельных вычислений.

Хотите супер быструю систему событий? Возможно, только ее и нужно написать специализированно.

Хотите миллион пулек? Вы можете сделать систему пулек очень быстрой, используя массив атрибутов, массив позиций и массив направлений, и сгруппировав передвижения в один метод. Останется только одна проблема — отрисовать их, и для этого не нужен ECS, а нужен DrawInstance.

Хотите высокой производительности для поиска пути? Вам нужно выполнение задач в других потоках.
Кстати, DOTS не требует ECS, его инструменты типа jobs можно использовать отдельно.

Вместо заключения

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

Юнити — отличный инструмент. У него есть много недостатков, но большая часть — это продолжение его достоинств:

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

  • Он мощен и позволяет собирать уровни, сущности и любые игровые запчасти почти без кода, используя стандартные компоненты и комбинируя их со скриптами программистов. Но это же позволяет сделать тормозящую игру без единой строчки кода! Художники и гейм дизайнеры могут создавать тормоза прямо в редакторе, и мы должны дать им такую возможность, потому что это упрощает разработку и действительно раскрывает их потенциал.

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

Не стремитесь за хайпом, постарайтесь понять, что нужно вам.

© Habrahabr.ru