Object-Oriented Programming (OOP) taught a generation of software engineers to model complex domains by creating class hierarchies. In games and real-time systems, we start with simple abstractions: a base NPC class extended by Player, EasyEnemy, Companion, or ToughEnemy.
Over time, this model breaks down. A Companion might need inventory management from Player and pathfinding from ToughEnemy. Single inheritance forces you to choose between code duplication or pushing specialized methods up into a bloated “God Class.”
Beyond code cleanliness, OOP suffers from a silent killer: poor CPU cache locality. Storing polymorphic objects as heap-allocated pointers (std::vector<std::unique_ptr<NPC>>) means your CPU spends more cycles chasing pointers through RAM than performing calculations.
Entity Component System (ECS) replaces deep inheritance trees with Data-Oriented Design (DOD):
- Entity: A lightweight ID handle (no data, no logic).
- Component: Plain-Old-Data (POD) structs stored contiguously in memory.
- System: Stateless functions that operate over flat arrays of components.
1. Concrete Mapping: OOP vs. ECS
Instead of defining class types, entities are composed at runtime by attaching data components:
Entity Type Position Velocity Health Inventory AIBehavior Player ✓ ✓ ✓ ✓ EasyEnemy ✓ ✓ ✓ ✓ ToughEnemy ✓ ✓ ✓ ✓ ✓ Companion ✓ ✓ ✓ ✓ ✓Notice how Companion simply reuses Inventory and AIBehavior without touching Player or inheriting from a rigid base class.
2. Sample C++17 ECS Implementation
This minimal implementation uses sparse sets to store component data in contiguous vectors, enabling O(1) lookup while keeping iteration memory-dense and CPU cache-friendly.
#include <iostream>
#include <vector>
#include <unordered_map>
#include <typeindex>
#include <memory>
#include <cstdint>
#include <cassert>
using Entity = std::uint32_t;
constexpr Entity NULL_ENTITY = 0xFFFFFFFF;
// ============================================================================
// 1. DATA COMPONENTS (POD - Pure Data)
// ============================================================================
struct Position { float x{0.0f}, y{0.0f}; };
struct Velocity { float dx{0.0f}, dy{0.0f}; };
struct Health { int current{100}, max{100}; };
struct Inventory {
std::vector<int> itemIDs;
int capacity{10};
};
enum class AIType { Easy, Tough, Companion };
struct AIBehavior {
AIType type{AIType::Easy};
float aggroRadius{10.0f};
};
// ============================================================================
// 2. SPARSE SET COMPONENT POOL
// ============================================================================
class IPool {
public:
virtual ~IPool() = default;
virtual void Remove(Entity entity) = 0;
};
template <typename T>
class ComponentPool : public IPool {
public:
void Insert(Entity entity, T component) {
if (entity >= m_Sparse.size()) {
m_Sparse.resize(entity + 1, NULL_ENTITY);
}
m_Sparse[entity] = static_cast<Entity>(m_DenseData.size());
m_DenseEntities.push_back(entity);
m_DenseData.push_back(component);
}
void Remove(Entity entity) override {
if (!Has(entity)) return;
// Swap with the last element to maintain contiguous memory
Entity indexToRemove = m_Sparse[entity];
Entity lastEntity = m_DenseEntities.back();
m_DenseData[indexToRemove] = m_DenseData.back();
m_DenseEntities[indexToRemove] = lastEntity;
m_Sparse[lastEntity] = indexToRemove;
m_Sparse[entity] = NULL_ENTITY;
m_DenseData.pop_back();
m_DenseEntities.pop_back();
}
bool Has(Entity entity) const {
return entity < m_Sparse.size() && m_Sparse[entity] != NULL_ENTITY;
}
T& Get(Entity entity) {
assert(Has(entity) && "Entity does not have requested component!");
return m_DenseData[m_Sparse[entity]];
}
// Direct access to contiguous memory for high-speed cache execution
std::vector<T>& GetData() { return m_DenseData; }
const std::vector<Entity>& GetEntities() const { return m_DenseEntities; }
private:
std::vector<Entity> m_Sparse; // Entity ID -> Index in Dense vector
std::vector<Entity> m_DenseEntities; // Index -> Entity ID
std::vector<T> m_DenseData; // Contiguous Component Data
};
// ============================================================================
// 3. REGISTRY (Entity & Component Manager)
// ============================================================================
class Registry {
public:
Entity CreateEntity() {
return m_EntityCounter++;
}
template <typename T>
void AddComponent(Entity entity, T component) {
GetPool<T>()->Insert(entity, component);
}
template <typename T>
T& GetComponent(Entity entity) {
return GetPool<T>()->Get(entity);
}
template <typename T>
bool HasComponent(Entity entity) {
return GetPool<T>()->Has(entity);
}
template <typename T>
ComponentPool<T>* GetPool() {
std::type_index typeKey = typeid(T);
auto it = m_Pools.find(typeKey);
if (it == m_Pools.end()) {
it = m_Pools.emplace(typeKey, std::make_unique<ComponentPool<T>>()).first;
}
return static_cast<ComponentPool<T>*>(it->second.get());
}
private:
Entity m_EntityCounter{0};
std::unordered_map<std::type_index, std::unique_ptr<IPool>> m_Pools;
};
// ============================================================================
// 4. STATELESS SYSTEMS
// ============================================================================
namespace MovementSystem {
void Update(Registry& registry, float dt) {
auto* posPool = registry.GetPool<Position>();
auto* velPool = registry.GetPool<Velocity>();
// Cache-friendly loop over contiguous memory buffers
const auto& entities = velPool->GetEntities();
const auto& velocities = velPool->GetData();
for (size_t i = 0; i < entities.size(); ++i) {
Entity entity = entities[i];
if (posPool->Has(entity)) {
auto& pos = posPool->Get(entity);
const auto& vel = velocities[i];
pos.x += vel.dx * dt;
pos.y += vel.dy * dt;
}
}
}
}
namespace AISystem {
void Update(Registry& registry) {
auto* aiPool = registry.GetPool<AIBehavior>();
const auto& entities = aiPool->GetEntities();
auto& aiData = aiPool->GetData();
for (size_t i = 0; i < entities.size(); ++i) {
Entity e = entities[i];
switch (aiData[i].type) {
case AIType::Easy:
std::cout << "[AI] Entity " << e << " (EasyEnemy): Wandering casually.\n";
break;
case AIType::Tough:
std::cout << "[AI] Entity " << e << " (ToughEnemy): Aggressively flanking player.\n";
break;
case AIType::Companion:
std::cout << "[AI] Entity " << e << " (Companion): Following player and offering support.\n";
break;
}
}
}
}
// ============================================================================
// 5. EXECUTION & VERIFICATION
// ============================================================================
int main() {
Registry registry;
// 1. Create Player
Entity player = registry.CreateEntity();
registry.AddComponent(player, Position{0.0f, 0.0f});
registry.AddComponent(player, Velocity{1.5f, 0.0f});
registry.AddComponent(player, Health{100, 100});
registry.AddComponent(player, Inventory{{101, 102}, 20});
// 2. Create Tough Enemy
Entity toughEnemy = registry.CreateEntity();
registry.AddComponent(toughEnemy, Position{10.0f, 5.0f});
registry.AddComponent(toughEnemy, Velocity{-0.5f, -0.5f});
registry.AddComponent(toughEnemy, Health{250, 250});
registry.AddComponent(toughEnemy, Inventory{{201}, 5});
registry.AddComponent(toughEnemy, AIBehavior{AIType::Tough, 15.0f});
// 3. Create Companion
Entity companion = registry.CreateEntity();
registry.AddComponent(companion, Position{1.0f, 0.0f});
registry.AddComponent(companion, Velocity{1.2f, 0.0f});
registry.AddComponent(companion, Health{150, 150});
registry.AddComponent(companion, Inventory{{301, 302, 303}, 15});
registry.AddComponent(companion, AIBehavior{AIType::Companion, 8.0f});
std::cout << "=== INITIAL STATE CREATED ===\n\n";
// Simulate 1 Frame tick
float dt = 0.016f; // ~60 FPS
std::cout << "--- Executing AISystem ---\n";
AISystem::Update(registry);
std::cout << "\n--- Executing MovementSystem ---\n";
MovementSystem::Update(registry, dt);
std::cout << "\nPlayer Position after movement: ("
<< registry.GetComponent<Position>(player).x << ", "
<< registry.GetComponent<Position>(player).y << ")\n";
return 0;
}
Enter fullscreen mode Exit fullscreen mode
3. Why ECS Extends Beyond Game Engines
While ECS originated in video games to solve object composition and frame-budget limits, its underlying paradigm – Data-Oriented Design (DOD) – is equally critical in other performance-sensitive domains:
Robotics & Autonomous Systems
Modern robots process dozens of heterogeneous sensors (LiDAR points, IMU telemetry, camera frames, motor feedback) at high frequencies. Modeling a robot platform via OOP inheritance leads to synchronization lock-ups. ECS allows sensor data to stream into continuous arrays where perception, planning, and motor-control systems run as parallel data pipelines.High-Frequency Financial Systems
Order-matching engines and market-data aggregators process millions of financial instruments per second. Using ECS, order entities contain dynamic state tags (Active,MarginCall,PendingCancel). Systems iterate through contiguous pools of bid/ask values without pointer indirection, minimizing instruction cache misses and latency spikes.CAD & Mechanical Simulations
Engineering applications must simulate millions of structural nodes subject to heat, tension, and fluid dynamics. By modeling nodes as entities with components likeThermalState,Vector3DForce, orMaterialStress, finite-element solvers sweep through contiguous arrays using SIMD vector instructions for optimal hardware usage.
Key Takeaways
- Composition over Inheritance: Eliminate monolithic base classes. Add or remove behaviors at runtime simply by attaching or detaching components.
- CPU Cache Optimization: Storing components in flat arrays allows hardware prefetchers to load memory lines efficiently, eliminating O(N) pointer chasing.
- Stateless Logic: Systems remain clean and decoupled – they don’t care what an entity is, only that it has the components required for processing.