Lucky Robots Blog Open Roles

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.

Module: Hazel/src/Hazel/Scene/ Scene.h Entity.h Components.h TransformHierarchy.h Owns: entt::registry
Scene Scene.h entt::registry entity store TimeManager phase runners Data::Observer obs vector ScriptStorage field map PrefabManager instances Entity entt::entity + Scene* IDComponent TagComponent TransformComponent RelationshipComponent DomainComponent + opt-in components (physics, mesh, script, …) Component categories Components.h Physics 3D / 2D MuJoCo Rendering & lights Scripts Audio Prefab / Anim / Text
Scene composition: registry + orchestrators on the left, the default entity shape in the middle, opt-in component categories on the right.

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:

ComponentRole
IDComponentStable 64-bit UUID that survives save/load and scene duplication.
TagComponentDisplay name shown in the editor hierarchy.
TransformComponentLocal position, rotation, scale.
RelationshipComponentParent + child UUIDs that form the scene graph.
DomainComponentClient, 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.

Startup OnRuntimeStart duplicate scene create physics init scripts Each tick — OnUpdateRuntime drives TimeManager Acquisition read transforms Control scripts / forces Physics step Jolt / MuJoCo Validation contacts / events Export write transforms Observer sample obs
Runtime lifecycle: a one-shot startup phase followed by a phased per-tick pipeline driven by TimeManager runner callbacks.
Entry pointWhat 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.

Rule of thumb

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

Main thread only

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.

HelperPurpose
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

Don't add per-frame logic to Scene::OnUpdate

The Scene is orchestration-only. Per-frame work belongs in a TimeManager runner so phase ordering (Acquisition → Control → Physics → Validation → Export) stays observable and reorderable.

Don't cache entt::entity across save/load

Handles are recycled and reassigned on duplication. Persist UUIDs; resolve to a handle on demand.

Don't include the wrong way

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:

  1. Declare the POD in Components.h.
  2. If it should be duplicated on scene copy, register it in CopyableComponents.h.
  3. Add ser/deser in SceneSerializer.
  4. Draw editor UI in SceneHierarchyPanel::DrawComponents.
  5. Optional C# mirror in Hazel-ScriptCore.
  6. Optional ScriptGlue InternalCalls for that mirror.