-
Notifications
You must be signed in to change notification settings - Fork 0
Entity Component System
- Overview
- Architecture Components
- Entity Lifecycle
- Component Masks
- Tag System
- Iteration Patterns
- Memory Layout
- Performance Considerations
The another-dunkan-engine implements a custom Entity-Component-System (ECS) architecture designed for high performance and type safety. Unlike traditional object-oriented approaches, ECS separates:
- Entities - Unique identifiers that represent game objects
- Components - Pure data containers (no logic)
- Systems - Logic that operates on entities with specific component combinations
This separation enables:
- Cache-friendly data access - Components stored in contiguous memory
- Flexible composition - Entities defined by component combinations, not inheritance
- Compile-time type safety - Template metaprogramming eliminates runtime type checks
classDiagram
class EntityManager {
-vector~Entity~ m_entities
-ComponentStorage m_components
+create_entity() Entity&
+add_component~T~(Entity&, args...) T&
+get_component~T~(Entity&) T&
+foreach~Components, Tags~(callback)
+kill(Entity&)
+refresh()
}
class Entity {
-uint64_t m_component_mask
-uint64_t m_tag_mask
-tuple~Keys...~ m_component_keys
-size_t id
-bool alive
+add_component~T~(key)
+has_component~T~() bool
+get_component_key~T~() key_type
+has_tag~T~() bool
+is_alive() bool
+kill()
}
class ComponentStorage {
-tuple~Slotmaps...~ m_component_tuple
-tuple~Singletons...~ m_singleton_tuple
+get_storage~T~() Slotmap~T~&
+get_singleton_storage~T~() T&
}
class Slotmap~T~ {
-array~key_type~ m_index
-array~T~ m_data
-array~index_type~ m_erase
+push_back(T) key_type
+operator[](key_type) T&
+erase(key_type) bool
+is_valid(key_type) bool
}
EntityManager o-- Entity : contains
EntityManager *-- ComponentStorage : owns
ComponentStorage *-- Slotmap~T~ : owns multiple
Entity ..> Slotmap~T~ : references via keys
The central hub that orchestrates all entity and component operations. Template parameters define the system's capabilities:
template <
typename COMPONENT_LIST, // Typelist of regular components
typename SINGLETON_LIST, // Typelist of singleton components
typename TAG_LIST = Typelist<>, // Typelist of tags (zero-size markers)
std::size_t CAPACITY = 10 // Max components per type in slotmaps
>
struct EntityManager;Engine Configuration (game/types.hpp):
using Components = ADE::META_TYPES::Typelist<
LightComponent,
PhysicsComponent,
RenderComponent,
ShadowComponent
>;
using SingletonComponents = ADE::META_TYPES::Typelist<
CameraComponent,
ConfigurationComponent
>;
using EntityManager = ADE::EntityManager<Components, SingletonComponents, Tags, 1024>;A lightweight handle containing:
- Component Mask - Bitfield indicating which components are attached
- Tag Mask - Bitfield indicating which tags are applied
- Component Keys - Generational indices for component lookup
- ID - Unique identifier
- Alive Flag - Marks entity for deferred deletion
struct Entity {
uint64_t m_component_mask{};
uint64_t m_tag_mask{};
tuple<key_type<PhysicsComponent>, key_type<RenderComponent>, ...> m_component_keys{};
size_t id{next_id++};
bool alive{true};
};A type-safe wrapper around a tuple of slotmaps. Uses template metaprogramming to:
- Transform
Typelist<A, B, C>→tuple<Slotmap<A>, Slotmap<B>, Slotmap<C>> - Map component types to compile-time indices
- Provide zero-overhead component access
template <typename COMPONENT>
constexpr auto& get_storage() noexcept {
constexpr auto id = component_info::template id<COMPONENT>();
return std::get<id>(m_component_tuple);
}stateDiagram-v2
[*] --> Created : create_entity()
Created --> Active : add_component()
Active --> Active : add_component()\nerase_component()
Active --> MarkedForDeath : kill()
MarkedForDeath --> Destroyed : refresh()
Destroyed --> [*]
note right of Active
Entity is usable
Components can be added/removed
Systems can process it
end note
note right of MarkedForDeath
alive = false
Still in m_entities vector
Systems skip it
Waiting for cleanup
end note
Creation:
auto& entity = entity_manager.create_entity();
// Entity added to m_entities vector
// Component mask = 0 (no components)
// alive = trueComponent Addition:
auto& physics = entity_manager.add_component<PhysicsComponent>(entity, x, y, z);
// 1. Component stored in slotmap
// 2. Key saved in entity.m_component_keys
// 3. Bit set in entity.m_component_maskComponent Access:
auto& physics = entity_manager.get_component<PhysicsComponent>(entity);
// 1. Check entity has component (assert)
// 2. Get key from entity
// 3. Lookup in slotmap using keyDeletion (Deferred):
entity_manager.kill(entity);
// 1. entity.alive = false
// 2. All component keys erased from slotmaps
// 3. Entity remains in vector (marked for removal)
entity_manager.refresh();
// Removes all dead entities from m_entities vectorComponent masks enable O(1) checks for component presence without memory indirection.
graph LR
subgraph "Component Typelist"
C0[PhysicsComponent #40;id=0#41;]
C1[RenderComponent #40;id=1#41;]
C2[LightComponent #40;id=2#41;]
C3[ShadowComponent #40;id=3#41;]
end
subgraph "64-bit Component Mask"
B0[...0]
B1[0]
B2[1]
B3[1]
B4[0]
B5[1]
end
C0 -.-> B5
C1 -.-> B4
C2 -.-> B3
C3 -.-> B2
style B3 fill:#90EE90
style B5 fill:#90EE90
style B2 fill:#FFB6C1
style B4 fill:#FFB6C1
Mask Generation (compile-time):
template <typename COMPONENT>
consteval static uint64_t mask() noexcept {
return 1 << component_info::template id<COMPONENT>();
}
// PhysicsComponent -> mask = 0b00001 (bit 0)
// RenderComponent -> mask = 0b00010 (bit 1)
// LightComponent -> mask = 0b00100 (bit 2)Mask Operations:
// Add component
entity.m_component_mask |= component_info::mask<PhysicsComponent>();
// Has component
bool has = entity.m_component_mask & component_info::mask<RenderComponent>();
// Remove component
entity.m_component_mask ^= component_info::mask<LightComponent>();Automatic Mask Type Selection: Based on the number of components, the smallest suitable integer type is chosen:
| Component Count | Mask Type | Memory Usage |
|---|---|---|
| ≤ 8 | uint8_t |
1 byte |
| ≤ 16 | uint16_t |
2 bytes |
| ≤ 32 | uint32_t |
4 bytes |
| ≤ 64 | uint64_t |
8 bytes |
This optimization is performed at compile-time via template metaprogramming.
Tags are zero-size markers for categorizing entities without additional memory overhead. They use a separate mask but no data storage.
// Define tags
using Tags = ADE::META_TYPES::Typelist<PlayerTag, EnemyTag, ProjectileTag>;
// Add tag to entity
entity.add_tag<PlayerTag>();
// Check for tag
if (entity.has_tag<EnemyTag>()) {
// Process enemy logic
}Memory Comparison:
- Component: Data stored in slotmap + key in entity + mask bit
- Tag: Only a mask bit (no data, no key)
Iterate over entities matching specific component and tag requirements:
sequenceDiagram
participant System
participant EntityManager
participant Entity
participant ComponentStorage
System->>EntityManager: foreach<Components, Tags>(callback)
loop For each entity
EntityManager->>Entity: is_alive()?
Entity-->>EntityManager: true
EntityManager->>Entity: has_component<C1>()?
Entity-->>EntityManager: check mask (true)
EntityManager->>Entity: has_component<C2>()?
Entity-->>EntityManager: check mask (true)
EntityManager->>ComponentStorage: get_component<C1>(entity)
ComponentStorage-->>EntityManager: C1&
EntityManager->>ComponentStorage: get_component<C2>(entity)
ComponentStorage-->>EntityManager: C2&
EntityManager->>System: callback(entity, C1&, C2&)
end
Usage Example:
// Iterate over entities with PhysicsComponent and RenderComponent
entity_manager.foreach<
META_TYPES::Typelist<PhysicsComponent, RenderComponent>, // Required components
META_TYPES::Typelist<> // Required tags
>([](Entity& entity, PhysicsComponent& physics, RenderComponent& render) {
// Components passed by reference
physics.x += physics.velocity_x * dt;
render.position = glm::vec3(physics.x, physics.y, physics.z);
});Implementation Details:
template <typename... C, typename... T>
void foreach_impl(auto&& process,
META_TYPES::Typelist<C...>,
META_TYPES::Typelist<T...>) {
for (auto& entity : m_entities) {
if (entity.is_alive()) {
// Fold expression: entity.has_component<C1>() && entity.has_component<C2>() && ...
auto has_components = (true && ... && entity.template has_component<C>());
if (has_components) {
// Fold expression: get_component<C1>(entity), get_component<C2>(entity), ...
process(entity, get_component<C>(entity)...);
}
}
}
}Iterate over all entities (no component filtering):
entity_manager.forall([](Entity& entity) {
std::cout << "Entity ID: " << entity.get_id() << std::endl;
});The ECS architecture optimizes for cache coherency by storing component data contiguously:
graph TD
subgraph "EntityManager"
EV[vector#60;Entity#62;]
CS[ComponentStorage]
end
subgraph "Entities Vector #40;scattered in memory#41;"
E0[Entity 0<br/>mask: 0b101<br/>keys: #123;P:3, R:5#125;]
E1[Entity 1<br/>mask: 0b011<br/>keys: #123;P:1, R:2#125;]
E2[Entity 2<br/>mask: 0b100<br/>keys: #123;P:4#125;]
end
subgraph "ComponentStorage Tuple"
direction LR
SM_P[Slotmap#60;Physics#62;]
SM_R[Slotmap#60;Render#62;]
SM_L[Slotmap#60;Light#62;]
end
subgraph "Slotmap#60;PhysicsComponent#62; #40;packed, cache-friendly#41;"
P0["#91;0#93; Entity1.Physics"]
P1["#91;1#93; Entity2.Physics"]
P2["#91;2#93; Entity0.Physics"]
end
subgraph "Slotmap#60;RenderComponent#62; #40;packed, cache-friendly#41;"
R0["#91;0#93; Entity0.Render"]
R1["#91;1#93; Entity1.Render"]
end
EV --> E0
EV --> E1
EV --> E2
CS --> SM_P
CS --> SM_R
CS --> SM_L
SM_P --> P0
SM_P --> P1
SM_P --> P2
SM_R --> R0
SM_R --> R1
E0 -.key.-> P2
E0 -.key.-> R0
E1 -.key.-> P0
E1 -.key.-> R1
E2 -.key.-> P1
style P0 fill:#E8F5E9
style P1 fill:#E8F5E9
style P2 fill:#E8F5E9
style R0 fill:#E3F2FD
style R1 fill:#E3F2FD
Key Observations:
- Entities - Stored in a vector, may not be cache-friendly during iteration
- Components - Stored in packed arrays within slotmaps, excellent cache locality
- Indirection - Entities store keys (generational indices), not direct pointers
- Safety - Keys include generation numbers to detect use-after-free
✅ Component Iteration - Iterating over all components of a type is cache-optimal
// All PhysicsComponents stored contiguously in memory
for (auto& physics : physics_slotmap) {
// Excellent cache performance
}✅ Compile-time Polymorphism - No vtables, no virtual function overhead
// Component types resolved at compile-time
entity_manager.foreach<Typelist<A, B>>(...); // Zero runtime type checks✅ Flexible Composition - Add/remove components dynamically without inheritance hierarchies
- Checking component masks (fast)
- Indirection through keys (moderate overhead)
std::vector, not as cache-friendly as component arrays
kill() + refresh() is deferred, trading immediate deletion for batched cleanup
-
Batch Operations - Call
refresh()once per frame, not after everykill() -
Reserve Capacity - Pre-allocate entity vector:
m_entities.reserve(1000) - Minimize Components - Fewer component types = smaller masks, less memory
- POD Components - Keep components trivially copyable for optimal performance
- System Ordering - Process systems in order of data dependencies for better cache usage
For global state (camera, configuration), use singleton components:
// Access singleton (no entity needed)
auto& camera = entity_manager.get_singleton_component<CameraComponent>();
camera.position = glm::vec3(0, 10, 0);Storage Difference:
- Regular Component: One instance per entity, stored in slotmap
- Singleton Component: One instance total, stored directly in tuple
void PhysicsSystem::update(EntityManager& em, float dt) {
// Iterate entities with physics components
em.foreach<
META_TYPES::Typelist<PhysicsComponent>,
META_TYPES::Typelist<>
>([dt](Entity& entity, PhysicsComponent& physics) {
// Update position based on velocity
physics.x += physics.velocity_x * dt;
physics.y += physics.velocity_y * dt;
physics.z += physics.velocity_z * dt;
// Apply gravity
physics.velocity_y -= 9.81f * dt;
});
}- Slotmap Implementation - How components are stored efficiently
- Template Metaprogramming - How typelists enable compile-time magic
- Component System - Component registration and management
- Usage Guide - Practical examples and patterns
Next: Learn about the Slotmap Implementation that makes component storage fast and safe.