-
Notifications
You must be signed in to change notification settings - Fork 0
Usage Guide
Dunkansdk edited this page Dec 8, 2025
·
1 revision
- Getting Started
- Creating Entities
- Working with Components
- Iterating Over Entities
- Entity Lifecycle Management
- Singleton Components
- Best Practices
- Common Patterns
- Complete Examples
#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| 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 |
// 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// 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
});// 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;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!// 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;// 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
}// 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>());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
});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;
}
});
}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)
});
}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);
}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;
});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)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
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
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();// Get singleton (no entity needed)
auto& camera = entity_manager.get_singleton_component<CameraComponent>();
auto& config = entity_manager.get_singleton_component<ConfigurationComponent>();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
}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();
}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
});✅ 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!
}
}// 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);
}
};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);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();
}#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;
}// 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!
}
});
}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";
}
}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";
}- Entity-Component-System - ECS architecture overview
- Slotmap Implementation - Component storage details
- Template Metaprogramming - Compile-time magic
- Component System - Component management
Congratulations! You now have a comprehensive understanding of the another-dunkan-engine ECS. Start building and experiment with the patterns above!