-
Notifications
You must be signed in to change notification settings - Fork 0
Component System
- Overview
- Component Storage Architecture
- Dual Storage Model
- Component Registration
- Type-Safe Access
- Supported Components
- Adding New Components
The Component System manages the storage and lifecycle of all component data in the ECS. It provides:
- Type-safe component access - Compile-time verification of component types
- Efficient storage - Components stored in cache-friendly slotmaps
- Dual storage model - Regular components (per-entity) and singletons (global)
- Automatic memory management - No manual allocation/deallocation needed
classDiagram
class ComponentStorage~COMPONENT_LIST, SINGLETON_LIST, TAG_LIST, Capacity~ {
-tuple~Slotmaps...~ m_component_tuple
-tuple~Singletons...~ m_singleton_component_tuple
+get_storage~T~() Slotmap~T~&
+get_singleton_storage~T~() T&
}
class Slotmap~PhysicsComponent~ {
+push_back(PhysicsComponent) key_type
+operator[](key) PhysicsComponent&
+erase(key) bool
}
class Slotmap~RenderComponent~ {
+push_back(RenderComponent) key_type
+operator[](key) RenderComponent&
+erase(key) bool
}
class Slotmap~LightComponent~ {
+push_back(LightComponent) key_type
+operator[](key) LightComponent&
+erase(key) bool
}
class Slotmap~ShadowComponent~ {
+push_back(ShadowComponent) key_type
+operator[](key) ShadowComponent&
+erase(key) bool
}
class CameraComponent {
+glm::vec3 position
+glm::vec3 rotation
+float fov
}
class ConfigurationComponent {
+float gamma
+bool ssaoEnabled
+int debugView
}
ComponentStorage *-- Slotmap~PhysicsComponent~ : contains
ComponentStorage *-- Slotmap~RenderComponent~ : contains
ComponentStorage *-- Slotmap~LightComponent~ : contains
ComponentStorage *-- Slotmap~ShadowComponent~ : contains
ComponentStorage *-- CameraComponent : contains
ComponentStorage *-- ConfigurationComponent : contains
template <
typename COMPONENT_LIST, // Typelist of per-entity components
typename SINGLETON_LIST, // Typelist of singleton components
typename TAG_LIST, // Typelist of tags (zero-size markers)
std::size_t Capacity = 10 // Slotmap capacity
>
struct ComponentStorage;From game/types.hpp:
using Components = ADE::META_TYPES::Typelist<
LightComponent,
PhysicsComponent,
RenderComponent,
ShadowComponent
>;
using SingletonComponents = ADE::META_TYPES::Typelist<
CameraComponent,
ConfigurationComponent
>;
using Tags = ADE::META_TYPES::Typelist<>; // No tags currently
using EntityManager = ADE::EntityManager<Components, SingletonComponents, Tags, 1024>;This configuration instantiates:
- 4 slotmaps (one per regular component), each with capacity 1024
- 2 singleton instances (one per singleton component)
The component system uses two different storage strategies:
Stored in slotmaps for efficient per-entity access:
graph TB
subgraph "Regular Components #40;One Instance Per Entity#41;"
PC[PhysicsComponent]
RC[RenderComponent]
LC[LightComponent]
SC[ShadowComponent]
end
subgraph "Storage Strategy"
SM1[Slotmap#60;PhysicsComponent, 1024#62;]
SM2[Slotmap#60;RenderComponent, 1024#62;]
SM3[Slotmap#60;LightComponent, 1024#62;]
SM4[Slotmap#60;ShadowComponent, 1024#62;]
end
subgraph "Entity References"
E1[Entity 1<br/>key: Physics=#123;0,5#125;]
E2[Entity 2<br/>key: Render=#123;1,3#125;]
E3[Entity 3<br/>key: Light=#123;2,1#125;]
end
PC --> SM1
RC --> SM2
LC --> SM3
SC --> SM4
E1 -.-> SM1
E2 -.-> SM2
E3 -.-> SM3
style PC fill:#E8F5E9
style RC fill:#E8F5E9
style LC fill:#E8F5E9
style SC fill:#E8F5E9
Characteristics:
- Each entity can have 0 or 1 instance of each component
- Components stored in packed arrays within slotmaps
- Entities reference components via generational keys
- Excellent cache locality during iteration
Stored directly in the tuple, one instance total:
graph TB
subgraph "Singleton Components #40;One Instance Total#41;"
CC[CameraComponent]
CONF[ConfigurationComponent]
end
subgraph "Storage Strategy"
Direct["std::tuple#60;<br/> CameraComponent,<br/> ConfigurationComponent<br/>#62;"]
end
subgraph "All Entities Access Same Instance"
EM[EntityManager]
S1[System 1]
S2[System 2]
end
CC --> Direct
CONF --> Direct
EM -.get_singleton.-> Direct
S1 -.get_singleton.-> Direct
S2 -.get_singleton.-> Direct
style CC fill:#FFF9C4
style CONF fill:#FFF9C4
style Direct fill:#E3F2FD
Characteristics:
- One instance shared across the entire application
- No keys needed - accessed directly by type
- Used for global state (camera, configuration, game state)
- No per-entity duplication
| Aspect | Regular Component | Singleton Component |
|---|---|---|
| Instances | One per entity (up to CAPACITY) |
One total |
| Storage | Slotmap (indirected) | Direct in tuple |
| Access | Via entity + key | Via type only |
| Iteration | Per component type | N/A (single instance) |
| Memory | sizeof(Component) * CAPACITY |
sizeof(Component) |
| Use Case | Per-entity data (position, health) | Global state (camera, settings) |
Components are registered via template type parameters, not runtime registration:
// In game/types.hpp
// Step 1: Define your component struct
struct MyNewComponent {
int value;
float multiplier;
};
// Step 2: Add to appropriate typelist
using Components = ADE::META_TYPES::Typelist<
LightComponent,
PhysicsComponent,
RenderComponent,
ShadowComponent,
MyNewComponent // ← Added here
>;
// That's it! The template metaprogramming handles the rest.What Happens at Compile-Time:
sequenceDiagram
participant User
participant Compiler
participant Templates
participant Storage
User->>Compiler: Add MyNewComponent to Typelist
Compiler->>Templates: Instantiate component_traits
Templates->>Templates: Assign ID = 4
Templates->>Templates: Generate mask = 0b10000
Compiler->>Templates: Instantiate ComponentStorage
Templates->>Templates: mp_transform to Slotmaps
Templates->>Templates: replace_t to std::tuple
Compiler->>Storage: Allocate std::tuple#60;..., Slotmap#60;MyNew#62;#62;
Storage-->>User: Ready to use!
Note over Compiler,Storage: All happens at compile-time!
template <typename COMPONENT>
constexpr auto& get_storage() noexcept {
constexpr auto id = component_info::template id<COMPONENT>();
return std::get<id>(m_component_tuple);
}Example:
auto& physics_storage = component_storage.get_storage<PhysicsComponent>();
// Compiler expands to:
// constexpr id = 1; (PhysicsComponent is at index 1 in typelist)
// return std::get<1>(m_component_tuple);Compile-Time Guarantees:
// Valid: PhysicsComponent is in typelist
auto& storage1 = component_storage.get_storage<PhysicsComponent>(); // ✅
// Compile error: UnknownComponent not in typelist
auto& storage2 = component_storage.get_storage<UnknownComponent>(); // ❌
// Error: static_assert failed in component_info::id<UnknownComponent>()template <typename COMPONENT>
constexpr auto& get_singleton_storage() noexcept {
return std::get<COMPONENT>(m_singleton_component_tuple);
}Example:
auto& camera = component_storage.get_singleton_storage<CameraComponent>();
camera.position = glm::vec3(0, 10, 0);
// All systems see the same camera instanceStores entity position and velocity.
Definition:
struct PhysicsComponent {
float x, y, z; // Position
float velocity_x, velocity_y, velocity_z; // Velocity
bool grounded;
};Usage:
auto& entity = entity_manager.create_entity();
auto& physics = entity_manager.add_component<PhysicsComponent>(
entity,
0.0f, 5.0f, 0.0f, // position
0.0f, 0.0f, 0.0f // velocity
);
// Update in system
physics.x += physics.velocity_x * dt;Stores rendering information for visual representation.
Definition:
struct RenderComponent {
VulkanTexture* texture;
glm::vec4 textureRect;
float opacity;
float scale;
std::string textureName;
std::string normalTextureName;
std::string heightTextureName;
std::string materialTextureName;
};Usage:
entity_manager.add_component<RenderComponent>(entity, RenderComponent{
nullptr,
glm::vec4(0, 0, 64, 64), // texture rect
1.0f, // opacity
1.0f, // scale
"player_albedo",
"player_normal",
"player_height",
"player_material"
});Defines light sources in the scene.
Definition:
struct LightComponent {
glm::vec3 color;
float radius;
float intensity;
// ... additional properties
};Usage:
entity_manager.add_component<LightComponent>(entity, LightComponent{
glm::vec3(1.0f, 0.8f, 0.6f), // warm light
20.0f, // radius
1.5f // intensity
});Marks entities that cast shadows.
Definition:
struct ShadowComponent {
float shadowBias;
bool castShadow;
};Global camera state.
Definition:
struct CameraComponent {
glm::vec3 position;
glm::vec3 rotation;
float fov;
float nearPlane;
float farPlane;
};Usage:
// Access without an entity
auto& camera = entity_manager.get_singleton_component<CameraComponent>();
camera.position = glm::vec3(player.x, player.y + 10.0f, player.z + 5.0f);
camera.rotation.y += mouse_delta_x * sensitivity;Global rendering configuration.
Definition:
struct ConfigurationComponent {
float gamma;
bool ssaoEnabled;
int debugView;
bool vsyncEnabled;
// ... additional settings
};Usage:
auto& config = entity_manager.get_singleton_component<ConfigurationComponent>();
if (ImGui::SliderFloat("Gamma", &config.gamma, 0.5f, 3.0f)) {
// Gamma updated, will affect next frame render
}1. Define Component Struct
Create header file dunkan/include/game/components/mycomponent.hpp:
#pragma once
#include <glm/glm.hpp>
struct MyComponent {
glm::vec2 direction;
float speed;
int state;
};2. Add to Typelist
Edit dunkan/include/game/types.hpp:
#include "game/components/mycomponent.hpp" // Add include
using Components = ADE::META_TYPES::Typelist<
LightComponent,
PhysicsComponent,
RenderComponent,
ShadowComponent,
MyComponent // ← Add here
>;3. Use in Game Code
// Create entity with new component
auto& entity = entity_manager.create_entity();
auto& my_comp = entity_manager.add_component<MyComponent>(
entity,
glm::vec2(1.0f, 0.0f), // direction
5.0f, // speed
0 // state
);
// Access in systems
entity_manager.foreach<
META_TYPES::Typelist<MyComponent>,
META_TYPES::Typelist<>
>([dt](Entity& entity, MyComponent& comp) {
comp.direction = glm::normalize(comp.direction);
// ... update logic
});That's it! No manual storage management, no registration functions.
✅ DO:
- Keep components as POD (Plain Old Data) when possible
- Use structs, not classes (public by default)
- Make components trivially copyable for best performance
- Group related data together
- Use descriptive names
❌ DON'T:
- Add logic to components (logic goes in systems)
- Store pointers or references (prefer IDs or keys)
- Create deep inheritance hierarchies
- Make components depend on other components directly
Good Component:
struct HealthComponent {
float current;
float maximum;
float regenRate;
};Bad Component (has logic):
struct HealthComponent {
float current;
float maximum;
void takeDamage(float amount) { // ❌ Logic in component
current -= amount;
}
};Use singletons for:
- Global state (camera, input, configuration)
- Unique resources (audio manager, resource loader)
- Shared data accessed by all systems
Don't use for:
- Per-entity data
- Data that could have multiple instances
For the engine configuration with 10 entities:
ComponentStorage<Components, Singletons, Tags, 1024>
│
├─ m_component_tuple: std::tuple<
│ Slotmap<LightComponent, 1024>, // 4 lights → uses 4 slots
│ Slotmap<PhysicsComponent, 1024>, // 10 entities → uses 10 slots
│ Slotmap<RenderComponent, 1024>, // 8 rendered → uses 8 slots
│ Slotmap<ShadowComponent, 1024> // 4 shadows → uses 4 slots
│ >
│
└─ m_singleton_component_tuple: std::tuple<
CameraComponent, // 1 instance (always)
ConfigurationComponent // 1 instance (always)
>
Memory Usage (Approximate):
Slotmap<LightComponent, 1024>:
- m_index: 8 KB (1024 * 8 bytes)
- m_data: 48 KB (assuming 48 bytes per component)
- m_erase: 4 KB (1024 * 4 bytes)
Total: ~60 KB per slotmap
4 slotmaps: ~240 KB
Singletons: ~200 bytes
Total ComponentStorage: ~240 KB
Despite 1024 capacity, only used slots consume CPU cache during iteration.
sequenceDiagram
participant Game
participant EntityManager
participant ComponentStorage
participant Slotmap
Game->>EntityManager: add_component#60;Physics#62;(entity, ...)
EntityManager->>ComponentStorage: get_storage#60;Physics#62;()
ComponentStorage-->>EntityManager: Slotmap#60;Physics#62;&
EntityManager->>Slotmap: push_back(PhysicsComponent{...})
Slotmap-->>EntityManager: key {id, gen}
EntityManager->>EntityManager: entity.add_component(key)
EntityManager->>EntityManager: entity.m_mask |= Physics_MASK
EntityManager-->>Game: PhysicsComponent&
Note over Game,Slotmap: Component is now active
Game->>EntityManager: erase_component#60;Physics#62;(entity)
EntityManager->>EntityManager: key = entity.get_component_key()
EntityManager->>ComponentStorage: get_storage#60;Physics#62;()
ComponentStorage-->>EntityManager: Slotmap#60;Physics#62;&
EntityManager->>Slotmap: erase(key)
Slotmap-->>EntityManager: true
EntityManager->>EntityManager: entity.m_mask ^= Physics_MASK
EntityManager-->>Game: true (success)
| Operation | Complexity | Cache Performance |
|---|---|---|
| Add Component | O(1) | Excellent |
| Get Component | O(1) | Good (2 indirections) |
| Remove Component | O(1) | Good |
| Iterate Components | O(n) | Excellent (packed array) |
| Get Singleton | O(1) | Excellent (direct access) |
Iteration Benchmark (conceptual):
// Iterate 1000 PhysicsComponents
for (auto& physics : physics_storage) {
physics.x += physics.velocity_x * dt;
}
// ~1-2 CPU cycles per component (cache-friendly)- Entity-Component-System - How components fit into ECS
- Slotmap Implementation - Component storage details
- Template Metaprogramming - How storage is generated
- Usage Guide - Practical component usage patterns
Next: Check out the Usage Guide for practical examples and patterns.