Skip to content

Component System

Dunkansdk edited this page Dec 8, 2025 · 1 revision

Component System

Table of Contents

Overview

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

Component Storage Architecture

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
Loading

Template Parameters

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;

Engine Configuration

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)

Dual Storage Model

The component system uses two different storage strategies:

Regular Components (Per-Entity)

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
Loading

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

Singleton Components (Global State)

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
Loading

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

Storage Comparison

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)

Component Registration

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!
Loading

Type-Safe Access

Getting a Component Storage

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>()

Getting a Singleton Component

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 instance

Supported Components

Regular Components

PhysicsComponent

Stores 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;

RenderComponent

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"
});

LightComponent

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
});

ShadowComponent

Marks entities that cast shadows.

Definition:

struct ShadowComponent {
    float shadowBias;
    bool castShadow;
};

Singleton Components

CameraComponent

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;

ConfigurationComponent

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
}

Adding New Components

Step-by-Step Guide

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.

Component Design Guidelines

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;
    }
};

Singleton Component Guidelines

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

Memory Layout Example

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.

Component Lifecycle Integration

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)
Loading

Performance Characteristics

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)

Related Documentation


Next: Check out the Usage Guide for practical examples and patterns.