Skip to content

Usage Guide

Dunkansdk edited this page Dec 8, 2025 · 1 revision

Usage Guide

Table of Contents

Getting Started

Basic Setup

#include "ecs/entitymanager.hpp"
#include "game/types.hpp"

// EntityManager is already configured in game/types.hpp
EntityManager entity_manager;

// Optional: Reserve capacity for entities
entity_manager = EntityManager(100);  // Pre-allocate space for 100 entities

Entity Manager Methods

Method Purpose
create_entity() Create a new entity
add_component<T>(entity, ...) Add component to entity
get_component<T>(entity) Access component data
erase_component<T>(entity) Remove component from entity
kill(entity) Mark entity for deletion
refresh() Remove all dead entities
foreach<Components, Tags>(callback) Iterate filtered entities
forall(callback) Iterate all entities
get_singleton_component<T>() Access singleton

Creating Entities

Basic Entity Creation

// Create an empty entity
auto& entity = entity_manager.create_entity();

std::cout << "Created entity ID: " << entity.get_id() << std::endl;
// Output: Created entity ID: 1

Entity with Components

// Create entity and add components immediately
auto& player = entity_manager.create_entity();

// Add physics component
auto& physics = entity_manager.add_component<PhysicsComponent>(
    player,
    0.0f, 5.0f, 0.0f,  // position (x, y, z)
    0.0f, 0.0f, 0.0f,  // velocity
    true               // grounded
);

// Add render component
auto& render = entity_manager.add_component<RenderComponent>(player, RenderComponent{
    nullptr,                          // texture (loaded separately)
    glm::vec4(0, 0, 64, 64),         // texture rect
    1.0f,                             // opacity
    1.0f,                             // scale
    "player_albedo",                  // albedo texture name
    "player_normal",                  // normal map name
    "player_height",                  // height map name
    "player_material"                 // material map name
});

Batch Entity Creation

// Create multiple entities efficiently
std::vector<Entity*> enemies;

for (int i = 0; i < 10; ++i) {
    auto& enemy = entity_manager.create_entity();
    
    entity_manager.add_component<PhysicsComponent>(
        enemy,
        float(i * 5), 0.0f, 0.0f,  // Spread horizontally
        0.0f, 0.0f, 0.0f,
        true
    );
    
    entity_manager.add_component<RenderComponent>(enemy, RenderComponent{
        nullptr,
        glm::vec4(0, 0, 32, 32),
        1.0f, 1.0f,
        "enemy_albedo", "enemy_normal", "enemy_height", ""
    });
    
    enemies.push_back(&enemy);
}

std::cout << "Created " << entity_manager.get_entities_count() << " entities" << std::endl;

Working with Components

Adding Components

auto& entity = entity_manager.create_entity();

// Method 1: Construct component in-place
auto& physics = entity_manager.add_component<PhysicsComponent>(
    entity,
    0.0f, 0.0f, 0.0f,   // Constructor arguments
    0.0f, 0.0f, 0.0f,
    false
);

// Method 2: Pass component struct
RenderComponent render_data{
    nullptr, glm::vec4(0, 0, 64, 64), 1.0f, 1.0f,
    "sprite", "sprite_normal", "", ""
};
auto& render = entity_manager.add_component<RenderComponent>(entity, render_data);

Important: If a component already exists, add_component returns the existing one:

auto& physics1 = entity_manager.add_component<PhysicsComponent>(entity, ...);
auto& physics2 = entity_manager.add_component<PhysicsComponent>(entity, ...);
// physics1 and physics2 reference the same component!

Accessing Components

// Get reference to component
if (entity.has_component<PhysicsComponent>()) {
    auto& physics = entity_manager.get_component<PhysicsComponent>(entity);
    physics.x += 10.0f;  // Modify component
}

// Direct access (assumes component exists)
auto& render = entity_manager.get_component<RenderComponent>(entity);
render.opacity = 0.5f;

Checking Component Presence

// Method 1: Via entity
if (entity.has_component<LightComponent>()) {
    std::cout << "Entity emits light" << std::endl;
}

// Method 2: Check multiple components
bool can_render = 
    entity.has_component<PhysicsComponent>() &&
    entity.has_component<RenderComponent>();

if (can_render) {
    // Render entity at physics position
}

Removing Components

// Remove single component
bool removed = entity_manager.erase_component<LightComponent>(entity);
if (removed) {
    std::cout << "Light component removed" << std::endl;
}

// Entity no longer has light component
assert(!entity.has_component<LightComponent>());

Iterating Over Entities

foreach - Filtered Iteration

Iterate over entities with specific component combinations:

// Syntax:
entity_manager.foreach<
    META_TYPES::Typelist<RequiredComponents...>,  // Components
    META_TYPES::Typelist<RequiredTags...>         // Tags (usually empty)
>([](Entity& entity, Component1& comp1, Component2& comp2, ...) {
    // Process entities
});

Example: Update All Physics Entities

void update_physics(EntityManager& em, float dt) {
    em.foreach<
        META_TYPES::Typelist<PhysicsComponent>,
        META_TYPES::Typelist<>
    >([dt](Entity& entity, PhysicsComponent& physics) {
        // Apply gravity
        physics.velocity_y -= 9.81f * dt;
        
        // Update position
        physics.x += physics.velocity_x * dt;
        physics.y += physics.velocity_y * dt;
        physics.z += physics.velocity_z * dt;
        
        // Ground collision
        if (physics.y <= 0.0f) {
            physics.y = 0.0f;
            physics.velocity_y = 0.0f;
            physics.grounded = true;
        }
    });
}

Example: Sync Render with Physics

void sync_render_to_physics(EntityManager& em) {
    // Only process entities with BOTH components
    em.foreach<
        META_TYPES::Typelist<PhysicsComponent, RenderComponent>,
        META_TYPES::Typelist<>
    >([](Entity& entity, PhysicsComponent& physics, RenderComponent& render) {
        // Render position matches physics position
        // (Render system will use this data)
    });
}

Example: Light System

void update_lights(EntityManager& em, VulkanRenderSystem& renderer) {
    std::vector<LightData> lights;
    
    // Collect all lights with positions
    em.foreach<
        META_TYPES::Typelist<PhysicsComponent, LightComponent>,
        META_TYPES::Typelist<>
    >([&lights](Entity& entity, PhysicsComponent& physics, LightComponent& light) {
        lights.push_back(LightData{
            .position = glm::vec3(physics.x, physics.y, physics.z),
            .color = light.color,
            .radius = light.radius,
            .intensity = light.intensity
        });
    });
    
    // Upload to GPU
    renderer.updateLights(lights);
}

forall - Unfiltered Iteration

Iterate over all entities regardless of components:

// Count entities
size_t alive_count = 0;
entity_manager.forall([&alive_count](Entity& entity) {
    if (entity.is_alive()) {
        alive_count++;
    }
});

std::cout << "Alive entities: " << alive_count << std::endl;

// Debug print all entity IDs
entity_manager.forall([](Entity& entity) {
    std::cout << "Entity " << entity.get_id() << ": ";
    if (entity.has_component<PhysicsComponent>()) std::cout << "Physics ";
    if (entity.has_component<RenderComponent>()) std::cout << "Render ";
    std::cout << std::endl;
});

Entity Lifecycle Management

Killing Entities

Entities are not immediately deleted. They are marked for removal:

// Mark entity for deletion
entity_manager.kill(entity);

// Entity is now dead
assert(!entity.is_alive());

// Components are immediately removed from slotmaps
assert(!entity.has_component<PhysicsComponent>());

// Entity still exists in m_entities vector (marked dead)

Refreshing Entity List

Call refresh() to actually remove dead entities:

// Game loop
while (running) {
    // Update systems (may kill entities)
    update_physics(entity_manager, dt);
    update_ai(entity_manager, dt);
    
    // Clean up dead entities (once per frame)
    entity_manager.refresh();
    
    render(entity_manager);
}

Why Deferred Deletion?

  • Safety: Prevents iterator invalidation during system updates
  • Performance: Batch deletion is more efficient than incremental
  • Consistency: All systems see the same entity set during a frame

Lifecycle Diagram

stateDiagram-v2
    [*] --> Created : create_entity()
    Created --> Alive : add_component()
    Alive --> Alive : add/remove components
    Alive --> Dead : kill()
    Dead --> Removed : refresh()
    Removed --> [*]
    
    note right of Alive
        Entity participates in systems
        has_component() works
        foreach() processes it
    end note
    
    note right of Dead
        Entity marked for deletion
        Components already removed
        foreach() skips it
        Still in m_entities vector
    end note
Loading

Example: Bullet Lifecycle

struct BulletComponent {
    float lifetime;
};

// Create bullet
auto& bullet = entity_manager.create_entity();
entity_manager.add_component<PhysicsComponent>(bullet, x, y, z, vx, vy, vz, false);
entity_manager.add_component<BulletComponent>(bullet, BulletComponent{5.0f});

// Update bullets
entity_manager.foreach<
    META_TYPES::Typelist<BulletComponent>,
    META_TYPES::Typelist<>
>([dt](Entity& entity, BulletComponent& bullet) {
    bullet.lifetime -= dt;
    
    if (bullet.lifetime <= 0.0f) {
        entity_manager.kill(entity);  // Mark for deletion
    }
});

// Clean up at end of frame
entity_manager.refresh();

Singleton Components

Accessing Singletons

// Get singleton (no entity needed)
auto& camera = entity_manager.get_singleton_component<CameraComponent>();
auto& config = entity_manager.get_singleton_component<ConfigurationComponent>();

Camera Control Example

void update_camera(EntityManager& em, float dt, GLFWwindow* window) {
    auto& camera = em.get_singleton_component<CameraComponent>();
    
    // Keyboard input
    if (glfwGetKey(window, GLFW_KEY_W) == GLFW_PRESS) {
        camera.position.z -= 10.0f * dt;
    }
    if (glfwGetKey(window, GLFW_KEY_S) == GLFW_PRESS) {
        camera.position.z += 10.0f * dt;
    }
    
    // Mouse input (requires mouse delta)
    double mouse_x, mouse_y;
    glfwGetCursorPos(window, &mouse_x, &mouse_y);
    // ... calculate camera rotation
}

Configuration Management

void render_imgui_settings(EntityManager& em) {
    auto& config = em.get_singleton_component<ConfigurationComponent>();
    
    ImGui::Begin("Settings");
    
    if (ImGui::SliderFloat("Gamma", &config.gamma, 0.5f, 3.0f)) {
        // Gamma changed, will affect next frame
    }
    
    if (ImGui::Checkbox("SSAO Enabled", &config.ssaoEnabled)) {
        // Toggle SSAO
    }
    
    const char* debug_views[] = {"Normal", "Albedo", "Depth", "SSAO"};
    ImGui::Combo("Debug View", &config.debugView, debug_views, 4);
    
    ImGui::End();
}

Best Practices

When to Use Tags vs Components

Use Components for:

  • Data that needs storage (position, health, sprite)
  • Per-entity properties
  • Data iterated by systems

Use Tags for:

  • Zero-size markers (Player, Enemy, Projectile)
  • Entity categorization
  • Filtering without additional data
// Example with tags (hypothetical)
using Tags = Typelist<PlayerTag, EnemyTag>;

entity.add_tag<PlayerTag>();

// Filter by tag
em.foreach<
    Typelist<PhysicsComponent>,
    Typelist<EnemyTag>  // Only enemies
>([](Entity& e, PhysicsComponent& p) {
    // AI logic for enemies only
});

Performance Considerations

DO:

// Good: Call refresh() once per frame
void game_loop() {
    update_all_systems();
    entity_manager.refresh();  // Batch cleanup
    render();
}

// Good: Cache component references
auto& physics = em.get_component<PhysicsComponent>(entity);
physics.x += 1.0f;
physics.y += 1.0f;
physics.z += 1.0f;

// Good: Use foreach for component iteration
em.foreach<Typelist<PhysicsComponent>, Typelist<>>([](Entity& e, PhysicsComponent& p) {
    // Excellent cache locality
});

DON'T:

// Bad: Refresh after every kill
entity_manager.kill(entity1);
entity_manager.refresh();  // Expensive!
entity_manager.kill(entity2);
entity_manager.refresh();  // Expensive!

// Bad: Repeatedly fetch same component
em.get_component<PhysicsComponent>(entity).x += 1.0f;
em.get_component<PhysicsComponent>(entity).y += 1.0f;  // Redundant lookup
em.get_component<PhysicsComponent>(entity).z += 1.0f;

// Bad: Iterate entities to check components
for (auto& entity : all_entities) {  // NOT RECOMMENDED
    if (entity.has_component<PhysicsComponent>()) {
        // Use foreach instead!
    }
}

Memory Management Tips

// Reserve capacity upfront if you know entity count
EntityManager entity_manager(1000);  // Pre-allocate for 1000 entities

// Avoid creating/destroying entities every frame
// Instead, reuse entities with a pooling pattern:
struct EntityPool {
    std::queue<Entity*> inactive;
    
    Entity* spawn(EntityManager& em) {
        if (!inactive.empty()) {
            auto* entity = inactive.front();
            inactive.pop();
            // Reset components
            return entity;
        }
        return &em.create_entity();
    }
    
    void despawn(Entity* entity) {
        // Don't kill, just deactivate
        inactive.push(entity);
    }
};

Common Patterns

Entity Factory Pattern

class EntityFactory {
public:
    static Entity& create_player(EntityManager& em, float x, float y, float z) {
        auto& entity = em.create_entity();
        
        em.add_component<PhysicsComponent>(entity, x, y, z, 0, 0, 0, true);
        em.add_component<RenderComponent>(entity, RenderComponent{
            nullptr, glm::vec4(0, 0, 64, 64), 1.0f, 1.0f,
            "player_albedo", "player_normal", "player_height", "player_material"
        });
        
        return entity;
    }
    
    static Entity& create_enemy(EntityManager& em, float x, float y, float z) {
        auto& entity = em.create_entity();
        
        em.add_component<PhysicsComponent>(entity, x, y, z, 0, 0, 0, true);
        em.add_component<RenderComponent>(entity, RenderComponent{
            nullptr, glm::vec4(0, 0, 32, 32), 1.0f, 1.0f,
            "enemy_albedo", "enemy_normal", "", ""
        });
        
        return entity;
    }
    
    static Entity& create_light(EntityManager& em, float x, float y, float z, glm::vec3 color) {
        auto& entity = em.create_entity();
        
        em.add_component<PhysicsComponent>(entity, x, y, z, 0, 0, 0, false);
        em.add_component<LightComponent>(entity, LightComponent{color, 20.0f, 1.0f});
        
        return entity;
    }
};

// Usage
auto& player = EntityFactory::create_player(em, 0, 0, 0);
auto& enemy = EntityFactory::create_enemy(em, 10, 0, 5);

System Pattern

class PhysicsSystem {
public:
    void update(EntityManager& em, float dt) {
        em.foreach<
            META_TYPES::Typelist<PhysicsComponent>,
            META_TYPES::Typelist<>
        >([dt](Entity& entity, PhysicsComponent& physics) {
            // Gravity
            physics.velocity_y -= 9.81f * dt;
            
            // Integration
            physics.x += physics.velocity_x * dt;
            physics.y += physics.velocity_y * dt;
            physics.z += physics.velocity_z * dt;
            
            // Ground collision
            if (physics.y <= 0.0f && physics.velocity_y < 0.0f) {
                physics.y = 0.0f;
                physics.velocity_y = 0.0f;
                physics.grounded = true;
            } else {
                physics.grounded = false;
            }
        });
    }
};

class RenderSystem {
public:
    void render(EntityManager& em, VulkanContext& ctx) {
        em.foreach<
            META_TYPES::Typelist<PhysicsComponent, RenderComponent>,
            META_TYPES::Typelist<>
        >([&ctx](Entity& entity, PhysicsComponent& physics, RenderComponent& render) {
            // Render sprite at physics position
            ctx.draw_sprite(
                render.texture,
                glm::vec3(physics.x, physics.y, physics.z),
                render.scale,
                render.opacity
            );
        });
    }
};

// Game loop
PhysicsSystem physics_system;
RenderSystem render_system;

while (running) {
    physics_system.update(entity_manager, dt);
    render_system.render(entity_manager, vulkan_context);
    entity_manager.refresh();
}

Complete Examples

Example 1: Simple Game Loop

#include "ecs/entitymanager.hpp"
#include "game/types.hpp"

int main() {
    EntityManager entity_manager;
    
    // Create player
    auto& player = entity_manager.create_entity();
    entity_manager.add_component<PhysicsComponent>(player, 0, 5, 0, 0, 0, 0, false);
    entity_manager.add_component<RenderComponent>(player, /* ... */);
    
    // Create ground
    auto& ground = entity_manager.create_entity();
    entity_manager.add_component<PhysicsComponent>(ground, 0, 0, 0, 0, 0, 0, true);
    entity_manager.add_component<RenderComponent>(ground, /* ... */);
    
    // Game loop
    float dt = 0.016f;  // 60 FPS
    for (int frame = 0; frame < 1000; ++frame) {
        // Physics update
        entity_manager.foreach<
            META_TYPES::Typelist<PhysicsComponent>,
            META_TYPES::Typelist<>
        >([dt](Entity& e, PhysicsComponent& p) {
            if (!p.grounded) {
                p.velocity_y -= 9.81f * dt;
            }
            p.x += p.velocity_x * dt;
            p.y += p.velocity_y * dt;
            p.z += p.velocity_z * dt;
        });
        
        // Render (simplified)
        entity_manager.foreach<
            META_TYPES::Typelist<RenderComponent>,
            META_TYPES::Typelist<>
        >([](Entity& e, RenderComponent& r) {
            std::cout << "Rendering entity " << e.get_id() << std::endl;
        });
        
        entity_manager.refresh();
    }
    
    return 0;
}

Example 2: Projectile System

// Projectile component
struct ProjectileComponent {
    float lifetime;
    float damage;
    int owner_id;
};

// Add to typelist in types.hpp
using Components = Typelist<..., ProjectileComponent>;

// Spawn projectile
Entity& spawn_projectile(EntityManager& em, glm::vec3 pos, glm::vec3 vel) {
    auto& projectile = em.create_entity();
    
    em.add_component<PhysicsComponent>(
        projectile,
        pos.x, pos.y, pos.z,
        vel.x, vel.y, vel.z,
        false
    );
    
    em.add_component<ProjectileComponent>(projectile, ProjectileComponent{
        .lifetime = 5.0f,
        .damage = 10.0f,
        .owner_id = -1
    });
    
    return projectile;
}

// Update projectiles
void update_projectiles(EntityManager& em, float dt) {
    em.foreach<
        META_TYPES::Typelist<ProjectileComponent>,
        META_TYPES::Typelist<>
    >([dt](Entity& entity, ProjectileComponent& proj) {
        proj.lifetime -= dt;
        
        if (proj.lifetime <= 0.0f) {
            // Don't use entity_manager here! Capture it.
            entity.kill();  // This won't compile - need EntityManager&
        }
    });
}

// Correct version with captured EntityManager
void update_projectiles(EntityManager& em, float dt) {
    em.foreach<
        META_TYPES::Typelist<ProjectileComponent>,
        META_TYPES::Typelist<>
    >([dt, &em](Entity& entity, ProjectileComponent& proj) {
        proj.lifetime -= dt;
        
        if (proj.lifetime <= 0.0f) {
            em.kill(entity);  // Correct!
        }
    });
}

Debugging Tips

Print Entity State

void debug_print_entity(EntityManager& em, Entity& entity) {
    std::cout << "Entity " << entity.get_id() << ":\n";
    std::cout << "  Alive: " << entity.is_alive() << "\n";
    
    if (entity.has_component<PhysicsComponent>()) {
        auto& physics = em.get_component<PhysicsComponent>(entity);
        std::cout << "  Position: (" << physics.x << ", " << physics.y << ", " << physics.z << ")\n";
    }
    
    if (entity.has_component<RenderComponent>()) {
        std::cout << "  Has Render Component\n";
    }
}

Entity Count Summary

void print_entity_summary(EntityManager& em) {
    size_t total = em.get_entities_count();
    size_t with_physics = 0;
    size_t with_render = 0;
    
    em.forall([&](Entity& e) {
        if (e.has_component<PhysicsComponent>()) with_physics++;
        if (e.has_component<RenderComponent>()) with_render++;
    });
    
    std::cout << "Entities: " << total << "\n";
    std::cout << "  With Physics: " << with_physics << "\n";
    std::cout << "  With Render: " << with_render << "\n";
}

Related Documentation


Congratulations! You now have a comprehensive understanding of the another-dunkan-engine ECS. Start building and experiment with the patterns above!