В этой статье пойдет речь о довольно интересной реализации Entity System Component (ECS), а именно о Data-Oriented ECS (DOD ECS). Эта статья подойдет для тех, кто хочет ознакомиться с ECS, а в частности с его DOD ECS реализацией. В этой статье не будут рассматриваться детали конкретных реализаций или оптимизаций, вместо этого в статье будет описана принципиальная разница между классической ECS и DOD ECS, приведены особенности, преимущества и недостатки DOD ECS.

Entity Component Systems in Elixir - DZone
Entity Component Systems

Что такое 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 должно основываться на требованиях к производительности, сложности проекта и опыте команды разработчиков.

Ссылки откуда я угнал картинки

Комментарии (2)


  1. Lekret
    21.07.2024 17:29
    +2

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

    Какие примеры библиотек реализуют DOD ECS, а какие нет?
    Наивный код с итерацией по всем сущностям и проверкой компонентов на лету я увидел, но в реальности такого не встречал, такое разве что в каких-то самопальных решениях есть.

    Компоненты по ссылкам видел только в Entitas (C#). LeoEcs, Morpeh, DOTS, Flecs (C++), Bevy (Rust) и тонна других решений независимо от языка данные хранят плотно, имеют запросы/фильтры и т.д.
    Значит ли это, что все популярные решения являются DOD ECS по умолчанию? Значит ли это, что используя их мы уже пришли к DOD?

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


    1. Deosis
      21.07.2024 17:29

      Это отличается от традиционного ECS, где компоненты могут быть разбросаны по памяти и связаны с сущностями через ссылки.

      Это очень спорное утверждение, так как все ECS пришли к плотному хранению данных.

      Само понятие системы, которая обрабатывает только несколько компонент сущности, подталкивает к такому архитектурному решению.

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

      auto entities = world.entities<Position, Velocity>();
      for (auto& pair: entities) {
        get<0>(pair) += get<1>(pair);
      }