Vulkan Schnee uses EnTT for high-performance data-oriented ECS, combined with wrapper classes for game logic.
Core Concepts
Entity
entt::entity is a lightweight handle (32-bit integer). Entities own components stored in the registry.
Registry
Ecs::RegistryManager::get() provides access to the global EnTT registry. All component data lives here.
auto& registry = Ecs::RegistryManager::get();
entt::entity entity = registry.create();
registry.emplace<Ecs::LocalTransform>(entity, glm::mat4{1.0f});
Components
Components are pure data structs in the Ecs namespace (defined in Engine/include/Engine/Ecs/EcsData.h).
struct LocalTransform {
glm::mat4 matrix;
};
struct WorldTransform {
glm::mat4 matrix;
};
struct Tick {
EngineCore::ITickable* tickable;
};
}
Data structs for the Entity Component System.
Architecture Pattern
Vulkan Schnee uses composition for data, inheritance for type safety.
Data layer: EnTT components store raw data for cache-friendly iteration. Logic layer: C++ classes wrap entities and provide game logic.
Entity (Base Class)
EngineCore::Entity wraps an entt::entity handle.
Fields:
- entt::entity data - Handle to ECS entity
- std::vector<std::shared_ptr<LogicComponent>> components - Logic components (not ECS)
- bool allowTicking - Tick enable flag
Usage:
class Entity {
protected:
entt::entity data = entt::null;
};
Entities register themselves for ticking:
void Entity::enableTick(bool enable) {
auto& registry = Ecs::RegistryManager::get();
if (enable) {
registry.emplace<Ecs::Tick>(data, this);
}
}
Actor (Subclass)
EngineCore::Actor extends Entity with transform and scene graph.
Additional fields:
- std::shared_ptr<SceneNode> sceneNode - Scene graph node
- Transform worldTransform - Cached world transform
- std::vector<MeshComponent*> meshComponents - Rendering components
Usage:
class Actor : public Entity {
public:
glm::vec3 getActorLocation() const;
void setActorLocation(glm::vec3 newLocation);
glm::mat4 getWorldTransform() const;
};
Actors live in the scene graph. Transform changes propagate through hierarchy.
Data Flow
1. Actor Creation
Actor* actor = scene->spawnActor<MyActor>(spawnTransform);
2. Component Attachment
class MeshComponent {
entt::entity componentEntity;
Ecs::StaticMeshData* staticMeshData;
};
MeshComponent::MeshComponent(...) {
auto& registry = Ecs::RegistryManager::get();
componentEntity = registry.create();
staticMeshData = ®istry.emplace<Ecs::StaticMeshData>(componentEntity);
registry.emplace<Ecs::Parent>(componentEntity, actor->data);
}
3. Data Synchronization
Scene graph transforms sync to ECS:
registry.emplace_or_replace<Ecs::TransformDirty>(entity);
auto view = registry.view<Ecs::TransformDirty, Ecs::WorldTransform>();
for (auto entity : view) {
}
4. Rendering Query
Renderer iterates ECS components directly (no wrapper overhead):
auto view = registry.view<Ecs::MeshComponentRef>();
for (auto entity : view) {
auto& meshRef = view.get<Ecs::MeshComponentRef>(entity);
MeshComponent* mesh = meshRef.component;
}
Wrapper vs ECS Storage
Wrapper Classes (Slow Path)
Game logic uses wrappers: Actor, MeshComponent, Scene.
Purpose:
- Type-safe API
- Virtual functions (beginPlay, tick, endPlay)
- Complex operations (physics, input, animation)
Access pattern:
actor->setActorLocation(newPos);
ECS Storage (Fast Path)
Renderer uses ECS directly for hot loops.
Purpose:
- Cache-friendly iteration
- SIMD-friendly data layout
- Parallel processing
Access pattern:
auto view = registry.view<Ecs::WorldTransform, Ecs::MeshPrimitiveData>();
for (auto entity : view) {
}
Common Patterns
Registry Queries
auto& registry = Ecs::RegistryManager::get();
if (registry.any_of<Ecs::TransformDirty>(entity)) {
auto& transform = registry.get<Ecs::WorldTransform>(entity);
}
auto view = registry.view<Ecs::StaticMeshData, Ecs::Parent>();
for (auto entity : view) {
auto [mesh, parent] = view.get(entity);
}
auto view = registry.view<Ecs::Tick>(entt::exclude<Ecs::SimulatesPhysics>);
Dirty Flags
Transform changes use two-stage dirty tracking:
Stage 1: Scene graph sync
registry.emplace<Ecs::TransformDirty>(entity);
Stage 2: GPU upload
registry.emplace<Ecs::TransformDirtyRenderer>(entity);
Separation prevents race conditions when transforms change during tick.
Asset Loading
Asset pipeline uses staged components:
registry.emplace<Ecs::AssetRequested>(entity);
auto& meshData = registry.emplace<Ecs::MeshPrimitiveData>(entity);
registry.remove<Ecs::AssetRequested>(entity);
auto view = registry.view<Ecs::MeshPrimitiveData>(entt::exclude<Ecs::AssetRequested>);
Example: Tick System
void ExampleScene::tick(double dt) {
rat->rotateActor(glm::vec3(10.0f * dt, 90.0f * dt, 0.0f));
}
auto& registry = Ecs::RegistryManager::get();
auto tickView = registry.view<Ecs::Tick>();
for (auto entity : tickView) {
auto& tickComponent = tickView.get<Ecs::Tick>(entity);
tickComponent.tickable->tick(deltaTime);
}
Key Files
Performance Notes
EnTT provides O(1) component access and cache-friendly iteration. Use wrappers for game logic, ECS for hot loops.
Wrapper overhead: Acceptable for tick/input (runs once per entity per frame). ECS direct access: Required for rendering (processes thousands of primitives per frame).
Transform hierarchy sync runs once per frame before rendering. Dirty flags minimize work.