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
Engine::Ecs::RegistryManager::get() provides access to the global EnTT registry. All component data lives here.
entt::entity entity = registry.create();
static entt::registry & get()
Gets the registry for all components.
Components
Components are pure data structs in the Engine::Ecs namespace (defined in Engine/include/Engine/Ecs/EcsData.h).
struct Transform {
};
struct WorldTransform {
};
};
}
Tag for everything which wants to receive tick events.
Engine::Core::ITickable * tickable
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)
Engine::Entities::Entity wraps an entt::entity handle.
Fields:
- entt::entity data_ - Handle to ECS entity
- std::vector<std::unique_ptr<Engine::Components::Logic>> components_ - Logic components, not ECS components
- bool allowTicking_ - Allows this entity to register for ticking
- int32_t tickPriority_ - Sort key for tick order
Usage:
class Entity {
protected:
entt::entity data_ = entt::null;
std::vector<std::unique_ptr<Engine::Components::Logic>> components_;
bool allowTicking_ = false;
int32_t tickPriority_ = 0;
};
Entities register themselves for ticking:
void Entity::enableTick(bool enable) {
if (enable && canEverTick()) {
}
}
Actor (Subclass)
Engine::Entities::Actor extends Entity with transform and scene graph.
Additional fields:
- std::optional<std::shared_ptr<SceneNode>> sceneNode - Scene graph node
- Scene* owningScene_ - Scene that owns the actor
- std::weak_ptr<Engine::Components::Scene> rootComponent_ - Optional root scene component
- bool hasEndedPlay_ - Prevents duplicate endPlay() execution
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;
};
MeshComponent::MeshComponent(...) {
componentEntity = registry.create();
}
Stores parent relationships.
3. Data Synchronization
Scene graph transforms sync to ECS:
for (auto entity : view) {
}
4. Rendering Query
Renderer iterates ECS components directly (no wrapper overhead):
for (auto entity : view) {
}
}
A component which can be attached as many times to an actor as one wants. It makes it possible to ren...
References to MeshComponents for ECS-based iteration. Multiple mesh components can share one SceneNod...
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:
for (auto entity : view) {
}
Common Patterns
Registry Queries
}
for (auto entity : view) {
auto [mesh, parent] = view.get(entity);
}
auto view = registry.view<
Engine::Ecs::Tick>(entt::exclude<Engine::Ecs::SimulatesPhysics>);
Dirty Flags
Transform changes use two-stage dirty tracking:
Stage 1: Scene graph sync
Stage 2: GPU upload
static void push(entt::entity entity)
Separation prevents race conditions when transforms change during tick.
Asset Loading
Asset pipeline uses staged components:
Is a tag for asset loading. When an asset has been requested for loading but isn't yet created this t...
Example: Tick System
void ExampleScene::tick(double dt) {
rat->rotateActor(glm::vec3(10.0f * dt, 90.0f * dt, 0.0f));
}
for (auto entity : tickView) {
}
virtual void tick(double deltaTimeSeconds)
Function called each frame when a tick component has been registered with EnTT Ecs::Tick.
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.