EntityManager
The EntityManager is the central hub for entity lifecycle management and component storage in the helios ECS. It coordinates between the EntityRegistry (handle allocation) and type-specific SparseSet containers (component data).
Overview
EntityManager provides a unified interface for:
- Entity Creation/Destruction - Delegates handle management to
EntityRegistry - Component Storage - Maintains type-indexed
SparseSetcontainers - Component Operations - Emplace, retrieve, remove, and query components
- Entity Cloning - Deep-copy all components from one entity to another
EntityRegistry registry;
EntityManager manager(registry);
// Create entity
auto entity = manager.create();
// Add components
auto* transform = manager.emplace<TransformComponent>(entity, position);
auto* health = manager.emplace<HealthComponent>(entity, 100.0f);
// Query and retrieve
if (manager.has<HealthComponent>(entity)) {
auto* hp = manager.get<HealthComponent>(entity);
}
// Remove component
manager.remove<HealthComponent>(entity);
// Destroy entity (removes all components)
manager.destroy(entity);
Architecture
┌─────────────────────────────────────────────────────────────────┐
│ EntityManager │
├─────────────────────────────────────────────────────────────────┤
│ │
│ ┌──────────────────┐ ┌──────────────────────────────────┐ │
│ │ EntityRegistry │ │ components_ (vector) │ │
│ │ ┌────────────┐ │ │ ┌─────────────────────────────┐ │ │
│ │ │VersionId[] │ │ │ │ [0] SparseSet<Transform> │ │ │
│ │ │ FreeList │ │ │ │ [1] SparseSet<Health> │ │ │
│ │ └────────────┘ │ │ │ [2] SparseSet<Renderable> │ │ │
│ └──────────────────┘ │ │ [3] nullptr │ │ │
│ │ │ │ [4] SparseSet<Collision> │ │ │
│ ▼ │ │ ... │ │ │
│ Handle Validation │ └─────────────────────────────┘ │ │
│ Create / Destroy └──────────────────────────────────┘ │
│ │ │
│ ▼ │
│ Component Storage │
│ (indexed by TypeId) │
└─────────────────────────────────────────────────────────────────┘
API Reference
Entity Operations
| Method | Description |
|---|---|
create() | Creates a new entity, returns EntityHandle |
destroy(handle) | Destroys entity and invalidates handle |
isValid(handle) | Checks if handle points to living entity |
handle(entityId) | Reconstructs handle from entity ID |
Creating Entities
auto entity = manager.create();
// entity.entityId = allocated ID
// entity.version = current version for that slot
Destroying Entities
bool destroyed = manager.destroy(entity);
// Returns false if entity was already invalid
Component Operations
| Method | Description |
|---|---|
emplace<T>(handle, args...) | Constructs component in-place |
emplaceOrGet<T>(handle, args...) | Returns existing or creates new |
get<T>(handle) | Returns pointer or nullptr |
has<T>(handle) | Checks component existence |
has(handle, typeId) | Runtime type ID version |
remove<T>(handle) | Removes component from entity |
raw(handle, typeId) | Returns void* to component data |
Emplacing Components
// Construct with arguments
auto* transform = manager.emplace<TransformComponent>(entity, position, rotation);
// Returns nullptr if component already exists
auto* duplicate = manager.emplace<TransformComponent>(entity); // nullptr!
// Use emplaceOrGet for upsert behavior
auto* existing = manager.emplaceOrGet<TransformComponent>(entity);
Retrieving Components
// Type-safe retrieval
auto* health = manager.get<HealthComponent>(entity);
if (health) {
health->takeDamage(10.0f);
}
// Raw pointer access (for reflection/serialization)
void* raw = manager.raw(entity, typeId);
Removing Components
bool removed = manager.remove<HealthComponent>(entity);
// Returns false if:
// - Entity invalid
// - Component not attached
// - onRemove() returned false
Component Queries
// Check single component
if (manager.has<HealthComponent>(entity)) { ... }
// Iterate all component types on entity
for (auto typeId : manager.componentTypeIds(entity)) {
void* component = manager.raw(entity, typeId);
// Process component...
}
Enable/Disable Components
// Enable specific component
manager.enable(entity, typeId);
// Disable specific component
manager.disable(entity, typeId);
// With explicit flag
manager.enable(entity, typeId, false); // Same as disable
Entity Cloning
Clone all components from one entity to another:
auto source = manager.create();
manager.emplace<TransformComponent>(source, position);
manager.emplace<HealthComponent>(source, 100.0f);
auto target = manager.create();
manager.clone(source, target);
// target now has copies of all source's components
Cloning uses the ComponentOps::clone function pointer, which:
- Invokes the copy constructor
- Calls
onClone()if the component implements it
SparseSet Access
For advanced use cases, direct access to the underlying storage:
// Get typed SparseSet
auto* transforms = manager.getSparseSet<TransformComponent>();
if (transforms) {
// Iterate all transforms efficiently
for (const auto& t : *transforms) {
// Process...
}
}
Component Storage
Components are stored in type-specific SparseSet containers:
std::vector<std::unique_ptr<SparseSetBase>> components_;
- Index =
ComponentTypeId::id<T>().value() - Value =
SparseSet<T>containing all instances of that component type - Lazy allocation - Sets created on first use
Memory Layout
components_[0] → SparseSet<TransformComponent>
├── sparse_: [_, 0, _, 1, _, 2] (entityId → denseIndex)
└── storage_: [T₁, T₄, T₆] (dense, cache-friendly)
components_[1] → SparseSet<HealthComponent>
├── sparse_: [0, _, 1, _]
└── storage_: [H₁, H₃]
components_[2] → nullptr (no entities have this type)
Lifecycle Hook Integration
EntityManager integrates with the lifecycle system:
// On component removal
template<typename T>
bool remove(const EntityHandle& handle) {
// ...
const auto& ops = ComponentOpsRegistry::ops(typeId);
// Allow component to intercept removal
if (ops.onRemove && !ops.onRemove(rawCmp)) {
return false; // Removal blocked
}
return sparseSet->remove(handle.entityId);
}
Capacity and Performance
EntityManager is constructed with an initial capacity hint:
explicit EntityManager(EntityRegistry& registry,
size_t capacity = ENTITY_MANAGER_DEFAULT_CAPACITY);
This capacity is passed to each SparseSet to pre-allocate storage, reducing reallocations during gameplay.
Thread Safety
EntityManager is not thread-safe. All operations must be performed from a single thread or externally synchronized.
Best Practices
-
Use GameObject for game code - EntityManager is lower-level, prefer
GameObjectfacade -
Batch operations - Create/destroy entities in batches when possible
-
Check validity - Always verify handles before operations
-
Prefer type-safe methods - Use
get<T>()overraw()when type is known -
Pre-register component types - Register all types during bootstrap to avoid runtime allocations
Example: System Implementation
void HealthSystem::update(EntityManager& em, float deltaTime) {
auto* healthSet = em.getSparseSet<HealthComponent>();
auto* regenSet = em.getSparseSet<HealthRegenComponent>();
if (!healthSet || !regenSet) return;
for (size_t i = 0; i < healthSet->size(); ++i) {
auto entityId = healthSet->entityAt(i);
if (regenSet->contains(entityId)) {
auto* health = healthSet->get(entityId);
auto* regen = regenSet->get(entityId);
health->heal(regen->rate() * deltaTime);
}
}
}
See Also
- GameObject - High-level entity wrapper
- EntityRegistry - Handle allocation and validation
- EntityHandle - Versioned entity reference
- View - Component-based entity queries
- SparseSet - Underlying storage
- Component Lifecycle - Lifecycle hooks
- Component Registry - Type registration