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:
Entities are the unique identifiers for objects in the game world.
- In the example:
-
Entity
is a simpleunsigned 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 likePosition
orVelocity
.
-
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
andVelocity
are components. They define specific aspects of an entity:struct Position { float x, y; }; struct Velocity { float dx, dy; };
- For instance, adding
Position
andVelocity
components to an entitye1
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 fromGameObject
, 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
andVelocity
.
- A stationary object may only have a
- Instead of creating a complex hierarchy like
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
andVelocity
components, updating their positions based on velocity.
- The
-
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
andVelocity
.
- 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
-
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
andVelocity
. - This flexibility allows entities to be easily customized and modified at runtime.
- If you want a stationary object, it only has a
-
Scalability:
- You can add more components (e.g.,
Health
,Sprite
) without modifying the existing system.
- You can add more components (e.g.,
-
Reusability:
- The
updateSystems
logic is reusable for any entity with the required components, regardless of its role in the game.
- The
-
Efficiency:
- Components are stored separately in memory (
ComponentStorage
), allowing efficient access and iteration, especially when processing entities in bulk.
- Components are stored separately in memory (
-
Flexibility:
- Entities can dynamically gain or lose components, adapting to changes in the game world.