В этой статье пойдет речь о довольно интересной реализации Entity System Component (ECS), а именно о Data-Oriented ECS (DOD ECS). Эта статья подойдет для тех, кто хочет ознакомиться с ECS, а в частности с его DOD ECS реализацией. В этой статье не будут рассматриваться детали конкретных реализаций или оптимизаций, вместо этого в статье будет описана принципиальная разница между классической ECS и DOD ECS, приведены особенности, преимущества и недостатки DOD ECS.
Что такое ECS
Прежде чем переходить к DOD версии, разберемся с классическим подходом. Entity-Component-System (ECS) – архитектурный паттерн, используемый в программировании для управления и организацией данных и логики с использованием принципа композиции объектов. Он был разработан как альтернатива традиционному объектно-ориентированному программированию.
Преимущества перед OOP:
Гибкость. Композиция объектов ECS позволяет легко добавлять сущностям новые компоненты, не увеличивая количества наследовании у объекта.
Масштабируемость. ECS позволяет создавать сложные взаимоотношения сущностей через системы.
Производительность. Разделение данных и логики ECS позволяет лучше использовать кеш память и ускоряет выполнение операций.
Основные принципы ECS:
Entity. Сущность представляет собой уникальный контейнер для компонентов. Она не содержит данных или логики сама по себе, а лишь связывает компоненты и системы. Сущности позволяют идентифицировать объекты в системе, такие как игровые персонажи, элементы интерфейса или другие объекты.
Component. Компонент является набором данных, описывающим определённую характеристику сущности. Он содержит только данные и не включает в себя логику их обработки. Например, компонент может включать информацию о позиции, скорости, цвете или здоровье сущности.
System. Система реализует логику обработки и взаимодействия между компонентами. Она принимает данные от компонентов и обрабатывает их. Системы работают с сущностями, которые содержат определённые компоненты, и выполняют действия на основе этих данных. Например, система может обновлять позицию сущностей в зависимости от их скорости или обрабатывать взаимодействия между сущностями.
Рассмотрим пример простого игрового движка. В ECS могут быть такие сущности, как игроки, враги и предметы. Компоненты для этих сущностей могут включать позицию, скорость, здоровье и т.д. Системы могут обрабатывать логику движения (используя компоненты позиции и скорости), логику столкновений (используя компоненты позиции и коллизии) и визуализацию (используя компоненты рендера).
Пример кода:
struct Component {};
// Пример компонента
struct Position : public Component {
std::array<float, 2> Pos;
};
// Пример сущности
struct Entity {
int id;
std::vector<Component*> components;
};
// Пример системы
class MovementSystem {
public:
void update(std::vector<Entity>& entities) {
for (auto& entity : entities) {
if (entity.hasComponent<Position>()) {
Position* pos = entity.getComponent<Position>();
pos->pos.first += 0.5f;
pos->pos.second += 0.5f;
}
}
}
};
Что такое Data-Oriented ECS
Data-Oriented ECS (DOD ECS) — это специализированная версия архитектурного паттерна ECS, ориентированная на оптимизацию хранения данных и повышение производительности. В отличие от традиционного ECS, который фокусируется на разделении данных и логики, DOD ECS акцентирует внимание на том, как данные организованы и обрабатываются для максимально эффективного использования аппаратных ресурсов, таких как кэш-память и процессорные ядра.
Основные принципы DOD ECS:
Организация данных. Данные компонентов хранятся в массивы (предполагает использование таких структур данных, как "Array of Structures"), что позволяет обеспечить более компактное и последовательное расположение данных в памяти. Это отличается от традиционного ECS, где компоненты могут быть разбросаны по памяти и связаны с сущностями через ссылки.
Оптимизация доступа к данным. Организация данных в массивы помогает минимизировать количество промахов кэша и улучшает производительность за счёт более последовательного доступа к памяти. Такая организация данных также позволяет эффективно использовать конвейеризацию процессора и улучшает многопоточность, так как данные для обработки могут быть организованы так, чтобы минимизировать блокировки и конкуренцию потоков.
Рассмотрим пример использования DOD ECS в игровом движке. В классическом ECS игрок может быть представлён сущностью, которая содержит компоненты Position, Velocity и Health. В DOD ECS данные этих компонентов будут храниться в отдельных массивах: массиве Positions, массиве Velocities, массиве Healths.
Например, MovementSystem отвечает за передвижение персонажа - система будет работать только с данными массивов Velocities и Positions. Поскольку данные хранятся в массивах, система может эффективно обрабатывать данные, обновляя позиции, минимизируя количество промахов кэша и обеспечивая более быструю обработку.
Пример кода
// Пример хранения данных в DOD ECS
struct PositionComponent {
std::vector<std::array<float, 2> pos;
};
// Пример системы в DOD ECS
class MovementSystemDOD {
public:
void update(PositionComponent& positions) {
for (size_t i = 0; i < positions.pos.size(); ++i) {
positions.pos[i].first += 0.5f;
positions.pos[i].second += 0.5f;
}
}
};
Зачем нам Data-Oriented ECS
DOD ECS предоставляет ряд значительных преимуществ по сравнению с традиционным ECS, однако он также имеет свои недостатки. Эта глава посвящена описанию плюсов и минусов DOD ECS, часть из них также затрагивает и классический ECS.
Преимущества
Оптимизация кэширования. Данные компонентов хранятся в массивы (например, Array of Structures), что улучшает локальность данных и уменьшает количество промахов кэша.
Упрощение обработки. Поскольку данные хранятся последовательно, доступ к ним становится более быстрым, что улучшает общую производительность системы.
Параллелизм: Организация данных в массивы упрощает распределение задач между потоками. Это позволяет эффективно использовать многопроцессорные системы и улучшает масштабируемость.
Снижение блокировок. Многопоточные системы могут обрабатывать данные параллельно, минимизируя блокировки и конкуренцию потоков за ресурсы
Легкость в добавлении новых систем. В DOD ECS добавление новых систем или компонентов часто не требует изменений в существующих частях кода.
Изоляция изменений. Поскольку данные и логика разделены, изменение одной системы или компонента не затрагивает другие части системы.
Минимизация фрагментации. Хранение данных в массивы помогает минимизировать фрагментацию памяти.
Недостатки
Сложность структурирования данных. Проектирование системы с использованием DOD ECS может быть более сложным из-за необходимости тщательно продумывать, как данные будут организованы и как системы будут взаимодействовать с ними.
Управление данными. Разделение данных и логики может усложнить код и его понимание, так как логика обработки данных находится в системах, а данные — в компонентах. Это может затруднить отладку и понимание кода.
Ограниченная адаптивность. В некоторых случаях структура данных может оказаться менее гибкой по сравнению с объектно-ориентированным подходом.
Интеграция с другими системами. Интеграция DOD ECS с другими архитектурными паттернами или существующими системами может быть сложной, особенно если другие системы ориентированы на объектно-ориентированное проектирование.
Заключение
Data-Oriented ECS предоставляет множество преимуществ, включая улучшенную производительность, эффективное использование многопоточности и гибкость в добавлении новых систем и компонентов. Однако, он также требует тщательного проектирования, может увеличивать сложность кода и иметь ограниченную гибкость в некоторых случаях. Принятие решения о применении DOD ECS должно основываться на требованиях к производительности, сложности проекта и опыте команды разработчиков.
Ссылки откуда я угнал картинки
Комментарии (3)
domix32
21.07.2024 17:29+2Это настолько плохо, что даже почти хорошо.
DOD ECS
ECS это подход, чтобы делать вашу программу ориентированной на данные. То есть этот ваш додик - масло маслянной из масла.
В статье несколько раз всплывает "расшифровка" DOD ECS, только автор почему-то ни разу не удосужился посчитать сколько у него букв в аббревиатуре. Судя по всему DOD должен быть Data oriented design, то бишь общий набор подходов, который включает ECS.
Поскольку данные хранятся последовательно, доступ к ним становится
более быстрым, что улучшает общую производительность системы.И это буквально то что описывается первым пунктом - последовательное расположение в памяти улучшает расположение данных по кэшлинии, уменьшая промахи на всякие случайные условия.
Параллелизм: Организация данных в массивы упрощает распределение задач между потоками.
В первую очерель ECS это про параллелизм данных, а не многопоточность. То есть хорошо структурированная программа приводит к автовекторизации и появлению SIMD инструкций. Либо помогает самому писать обработку посредством соответствующих интринсиков. Распараллеливание по потокам - в этом смысле довольно сказочная задача - многие из систем между собой связаны и не должны выполняться в параллель.
Изоляция изменений.
Вот уж чего там точно нет, так это изоляции изменений. Добавление новых компонентов и систем - довольно атомарные, но системы сами по себе могут затрагивать множество компонентов сразу и несколько систем могут быть завязаны на одни и те же компоненты, поэтому любые изменения в структуру компонентов начинают отражаться в соотвествующих системах (собственную же картиночку в тетрадке гляньте). Так что нет никакой изоляции изменений. Оно действительно более атомарно в сравнении с классическим ООП, но не более того.
Ну и собственно по поводу кода:
for (auto& entity : entities) { if (entity.hasComponent<Position>()) {
Это ровно то, от чего ECS пытается избавлять код - условия, которые будут ломать branch prediction и кэширование. В нормальной ситуации вы конструируете набор компонентов некоторой сущности и забываете про неё - всем остальным занимаются системы.
// где-то в самом начале программы инициализируется порядок вызова систем vector<Systems*> systems { PositionSystem, HealthSystem, ... RenderSystem, }; ... // создаётся набор каких-то сущностей с какими-то компонентами // сами сущности естественно хранятся в некоторой индексируемой // структуре - vector, hashmap, generation map auto entity = EntityBuilder::new() .with(PositionComponent(0.0, 0.0)) // прикрепляются компоненты .build(); entities.insert(entity.id, entity); // соотвественно последовательно обновляются системы // что-то исполняемое в разных потоках обычно // складируется в соседний набор систем for (auto system: systems) { system->update(); } // как примерно будет выглядеть пример с обновлением позиций class PositioSystem: public System { void update() override { auto pos = get_component<PositionComponent>(); pos[X] += 0.5f; pos[Y] += 0.5f; } };
Lekret
Спасибо за статью.
Хотя я увидел, что статья не затрагивает конкретные реализации, без них статья ощущается какой-то неполной.
Какие примеры библиотек реализуют DOD ECS, а какие нет?
Наивный код с итерацией по всем сущностям и проверкой компонентов на лету я увидел, но в реальности такого не встречал, такое разве что в каких-то самопальных решениях есть.
Компоненты по ссылкам видел только в Entitas (C#). LeoEcs, Morpeh, DOTS, Flecs (C++), Bevy (Rust) и тонна других решений независимо от языка данные хранят плотно, имеют запросы/фильтры и т.д.
Значит ли это, что все популярные решения являются DOD ECS по умолчанию? Значит ли это, что используя их мы уже пришли к DOD?
Я ожидал например увидеть сравнение архиетипных и спарс-сетных ECS, потому что это плоскость в которой разные реализации как раз таки сильно отличаются - скоростью разных операций, организацией данных под кеш, сложностью, гибкостью и удобством, что в целом можно обозначить как "больше/меньше заточен под DOD", или как то, что некоторые библиотеки намеренно продают какую-то часть своей дата-ориентированности в угоду простоте и удобству.
Deosis
Это очень спорное утверждение, так как все ECS пришли к плотному хранению данных.
Само понятие системы, которая обрабатывает только несколько компонент сущности, подталкивает к такому архитектурному решению.
В статье почему то нет примера работы сразу с несколькими компонентами, который может выглядеть так: