Comments 4
Спасибо за статью.
Хотя я увидел, что статья не затрагивает конкретные реализации, без них статья ощущается какой-то неполной.
Какие примеры библиотек реализуют DOD ECS, а какие нет?
Наивный код с итерацией по всем сущностям и проверкой компонентов на лету я увидел, но в реальности такого не встречал, такое разве что в каких-то самопальных решениях есть.
Компоненты по ссылкам видел только в Entitas (C#). LeoEcs, Morpeh, DOTS, Flecs (C++), Bevy (Rust) и тонна других решений независимо от языка данные хранят плотно, имеют запросы/фильтры и т.д.
Значит ли это, что все популярные решения являются DOD ECS по умолчанию? Значит ли это, что используя их мы уже пришли к DOD?
Я ожидал например увидеть сравнение архиетипных и спарс-сетных ECS, потому что это плоскость в которой разные реализации как раз таки сильно отличаются - скоростью разных операций, организацией данных под кеш, сложностью, гибкостью и удобством, что в целом можно обозначить как "больше/меньше заточен под DOD", или как то, что некоторые библиотеки намеренно продают какую-то часть своей дата-ориентированности в угоду простоте и удобству.
Это отличается от традиционного ECS, где компоненты могут быть разбросаны по памяти и связаны с сущностями через ссылки.
Это очень спорное утверждение, так как все ECS пришли к плотному хранению данных.
Само понятие системы, которая обрабатывает только несколько компонент сущности, подталкивает к такому архитектурному решению.
В статье почему то нет примера работы сразу с несколькими компонентами, который может выглядеть так:
auto entities = world.entities<Position, Velocity>();
for (auto& pair: entities) {
get<0>(pair) += get<1>(pair);
}
Это настолько плохо, что даже почти хорошо.
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;
}
};
Вы постоянно говорите о AoS(array of structures), хотя по факту описываете SoA(structure of arrays), просто филды у нас не примитивы а компоненты.
Что такое Data-Oriented ECS