2.4 · Scene / ECS
The Scene is LuckyEngine's runtime world: an entt::registry of entities plus the orchestrators that drive physics, scripts, audio, and observation each tick.
Overview
Scene (Hazel/src/Hazel/Scene/Scene.h) is the runtime world container. It owns an
entt::registry, a TimeManager, a Data::Observer, a
ScriptStorage, a PrefabManager, and a SceneSettings block. Scene
inherits Asset so scenes serialize through the regular asset pipeline.
Entity (Entity.h) is a thin wrapper over an entt::entity handle plus a
Scene* back-pointer. All component access goes through the template helpers
AddComponent<T>, GetComponent<T>, TryGetComponent<T>,
HasComponent<T...>, and RemoveComponent<T>.
Default components
Every entity is guaranteed to have these five components the moment it exists:
| Component | Role |
|---|---|
IDComponent | Stable 64-bit UUID that survives save/load and scene duplication. |
TagComponent | Display name shown in the editor hierarchy. |
TransformComponent | Local position, rotation, scale. |
RelationshipComponent | Parent + child UUIDs that form the scene graph. |
DomainComponent | Client, Server, or Both — gates where the entity is active. |
Component categories
The full menu lives in Components.h. They divide into roughly seven buckets:
Physics (3D)
RigidBodyComponent, box/sphere/capsule/mesh colliders, CharacterControllerComponent.
MuJoCo
MujocoBodyComponent + proxy collider components that mirror Hazel shapes into the MuJoCo model.
Rendering
MeshComponent, StaticMeshComponent, the light family (Directional, Point, Spot, Sky), and CameraComponent.
Scripts
ScriptComponent for C# behaviours, plus RobotControllerComponent with motion-graph and policy slots.
Audio
AudioComponent emitters, AudioListenerComponent receiver.
2D
RigidBody2DComponent, BoxCollider2DComponent, CircleCollider2DComponent — pipe through Box2D.
Other
PrefabComponent, AnimationComponent, TextComponent.
Transform hierarchy
World↔local conversion goes through TransformHierarchy.* — shear-safe under non-uniform parent scale.
Lifecycle
The Scene has three runtime entry points. Scene::OnUpdate is intentionally minimal — all periodic work
lives in TimeManager runners so phase ordering stays explicit.
| Entry point | What it does |
|---|---|
OnRuntimeStart() | Duplicates the editor scene, creates physics scenes, initialises scripts, then starts the TimeManager. |
OnSimulationStart() | Physics only — used by headless / training runs that don't want C# script lifecycle. |
OnUpdateRuntime(ts) | Drives TimeManager. The actual work is in registered runners (UpdateJolt*, UpdateMujoco*, UpdateScripts*, UpdateRobotControllers, UpdateObserver*, UpdateBox2DPhysics, UpdateAnimation). |
Identity & references
Cross-frame and cross-process identity always goes through UUIDs. The raw
entt::entity handle is fast but ephemeral — duplicating a scene, reloading,
or restoring a snapshot rebuilds the registry and invalidates every handle.
Use UUID at any boundary that survives a frame — serialization, gRPC, recordings, prefab refs.
Use entt::entity only for tight inner loops where you have a registry in hand.
Threading
Scene mutation — adding entities, adding or removing components, destroying entities — is main-thread only.
entt::registry is not thread-safe. Background workers must marshal back through
Application::SubmitMainThreadQueue before touching the registry.
Transform hierarchy
Parent/child relationships live in RelationshipComponent, but the actual world↔local math is
centralised in Hazel/src/Hazel/Scene/TransformHierarchy.*. The helpers decompose the parent matrix
carefully so non-uniform parent scale doesn't shear children — naïve matrix multiplication does, which is why no
site in the engine should hand-roll the conversion.
| Helper | Purpose |
|---|---|
GetWorldTransform(entity) | Walks parents and returns the shear-safe world TRS. |
SetWorldTransform(entity, world) | Inverts through the parent chain to update the local TransformComponent. |
Pitfalls
Scene::OnUpdateThe Scene is orchestration-only. Per-frame work belongs in a TimeManager runner so phase ordering (Acquisition → Control → Physics → Validation → Export) stays observable and reorderable.
entt::entity across save/loadHandles are recycled and reassigned on duplication. Persist UUIDs; resolve to a handle on demand.
Scene may depend on Physics, Asset, and Core. It must not depend on Editor, Renderer, or ScriptEngine — those layers depend on Scene, not the other way around.
Adding a component
Six touch points — see the full playbook in Playbook:
- Declare the POD in
Components.h. - If it should be duplicated on scene copy, register it in
CopyableComponents.h. - Add ser/deser in
SceneSerializer. - Draw editor UI in
SceneHierarchyPanel::DrawComponents. - Optional C# mirror in
Hazel-ScriptCore. - Optional
ScriptGlueInternalCalls for that mirror.