Skip to content

Latest commit

 

History

History
226 lines (170 loc) · 6.5 KB

File metadata and controls

226 lines (170 loc) · 6.5 KB

Explanation of Entity-Component-System (ECS)

The Entity-Component-System (ECS) architectural pattern is a way of organizing data and behavior in software, especially for games. It focuses on composition over inheritance.

// Components
struct Position {
  float x, y;
};

struct Velocity {
  float dx, dy;
};

// Entity is just an integer
using Entity = unsigned int;

// Component Storage
template <typename T> class ComponentStorage {
public:
  void add(Entity entity, const T &component) {
    components[entity] = component;
  }

  void remove(Entity entity) { components.erase(entity); }

  T *get(Entity entity) {
    if (components.find(entity) != components.end()) {
      return &components[entity];
    }
    return nullptr;
  }

private:
  std::unordered_map<Entity, T> components;
};

// ECS Manager
class ECSManager {
public:
  Entity createEntity() { return nextEntity++; }

  template <typename T> void addComponent(Entity entity, const T &component) {
    getStorage<T>()->add(entity, component);
  }

  template <typename T> void removeComponent(Entity entity) {
    getStorage<T>()->remove(entity);
  }

  template <typename T> T *getComponent(Entity entity) {
    return getStorage<T>()->get(entity);
  }

  // Process entities that have both Position and Velocity
  void updateSystems() {
    for (Entity entity : entitiesWithComponents<Position, Velocity>()) {
      auto *pos = getComponent<Position>(entity);
      auto *vel = getComponent<Velocity>(entity);
      if (pos && vel) {
        pos->x += vel->dx;
        pos->y += vel->dy;
      }
    }
  }

private:
  Entity nextEntity = 0;

  template <typename T> ComponentStorage<T> *getStorage() {
    static ComponentStorage<T> storage;
    return &storage;
  }

  // Find entities with specific components
  template <typename... Components>
  std::vector<Entity> entitiesWithComponents() {
    std::vector<Entity> entities;
    for (Entity entity = 0; entity < nextEntity; ++entity) {
      if ((getComponent<Components>(entity) && ...)) {
        entities.push_back(entity);
      }
    }
    return entities;
  }
};

int main() {
  ECSManager ecs;

  // Create entities
  Entity e1 = ecs.createEntity();
  Entity e2 = ecs.createEntity();

  // Add components
  ecs.addComponent(e1, Position{0, 0});
  ecs.addComponent(e1, Velocity{1, 1});

  ecs.addComponent(e2, Position{10, 10});
  ecs.addComponent(e2, Velocity{-1, -1});

  // Simulate a few updates
  for (int i = 0; i < 5; ++i) {
    ecs.updateSystems();

    auto *pos1 = ecs.getComponent<Position>(e1);
    auto *pos2 = ecs.getComponent<Position>(e2);

    std::cout << "Entity 1 Position: (" << pos1->x << ", " << pos1->y << ")\n";
    std::cout << "Entity 2 Position: (" << pos2->x << ", " << pos2->y << ")\n";
  }

  return 0;
}

Here’s a breakdown of the definition and how it relates to the example:


1. Entities

Entities are the unique identifiers for objects in the game world.

  • In the example:
    • Entity is a simple unsigned int (e.g., e1, e2).

    • It does not store any data or behavior itself. Instead, its properties are defined by the components associated with it.

    • For example:

      Entity e1 = ecs.createEntity();

      Here, e1 is an entity that can be associated with components like Position or Velocity.


2. Components

Components are plain data structures that store information. Each component type defines one aspect of an entity, such as position, velocity, health, or AI state.

  • In the example:

    • Position and Velocity are components. They define specific aspects of an entity:
      struct Position {
          float x, y;
      };
      
      struct Velocity {
          float dx, dy;
      };
    • For instance, adding Position and Velocity components to an entity e1 allows it to have position and movement behavior:
      ecs.addComponent(e1, Position{0, 0});
      ecs.addComponent(e1, Velocity{1, 1});
  • Composition over inheritance:

    • Instead of creating a complex hierarchy like MovingObject inheriting from GameObject, the ECS pattern allows behavior to emerge by combining components. For example:
      • A stationary object may only have a Position component.
      • A moving object may have both Position and Velocity.

3. Systems

Systems contain the logic that operates globally on all entities with specific components.

  • In the example:

    • The updateSystems() function acts as a system:
      void updateSystems() {
          for (Entity entity : entitiesWithComponents<Position, Velocity>()) {
              auto* pos = getComponent<Position>(entity);
              auto* vel = getComponent<Velocity>(entity);
              if (pos && vel) {
                  pos->x += vel->dx;
                  pos->y += vel->dy;
              }
          }
      }
    • This system processes all entities with both Position and Velocity components, updating their positions based on velocity.
  • Global Operation:

    • Systems do not care which entities they process, only that the entities have the required components. For instance, the movement system applies uniformly to all entities with Position and Velocity.

4. Principle of Composition over Inheritance

  • Traditional object-oriented programming might model game objects using a deep class hierarchy:

    class GameObject {};
    class MovingObject : public GameObject {};
    class Player : public MovingObject {};
    class Enemy : public MovingObject {};
    • This approach becomes rigid and hard to extend. For example, what if you want a "stationary enemy" that doesn't move? You'd need to refactor the hierarchy.
  • ECS avoids this by composing behavior from components:

    • If you want a stationary object, it only has a Position component.
    • If you want a moving object, you add both Position and Velocity.
    • This flexibility allows entities to be easily customized and modified at runtime.

Advantages of ECS in the Example

  1. Scalability:

    • You can add more components (e.g., Health, Sprite) without modifying the existing system.
  2. Reusability:

    • The updateSystems logic is reusable for any entity with the required components, regardless of its role in the game.
  3. Efficiency:

    • Components are stored separately in memory (ComponentStorage), allowing efficient access and iteration, especially when processing entities in bulk.
  4. Flexibility:

    • Entities can dynamically gain or lose components, adapting to changes in the game world.