Pull to refresh

Comments 3

Спасибо за статью.
Хотя я увидел, что статья не затрагивает конкретные реализации, без них статья ощущается какой-то неполной.

Какие примеры библиотек реализуют 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

  1. ECS это подход, чтобы делать вашу программу ориентированной на данные. То есть этот ваш додик - масло маслянной из масла.

  2. В статье несколько раз всплывает "расшифровка" 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;
  }
};

Sign up to leave a comment.

Articles