Lucky Robots Blog Open Roles

PB · Implementation Playbook

Step-by-step recipes for the recurring tasks: adding a component, an asset type, an editor panel, a render pass, a C# binding, a MuJoCo integration point, a fixed-rate system, a background worker, or a build-system change. Each list is the minimum set of touchpoints — skipping any step typically causes a silent failure.

Recipes Components Assets Panels Render passes C# bindings MuJoCo TimeManager jthread Build
Typical "add a component" touchpoints Components.h define struct CopyableComponents.h enable duplication SceneSerializer Serialize / Deserialize SceneHierarchyPanel DrawComponents UI ScriptCore C# mirror (opt.) Skip any step → component is invisible, lost on save/load, or crashes on duplication.
The recipes below follow the same pattern: chain every touchpoint or fail silently.

Pick a recipe

PB.1 · Add a new component

  1. Define the struct in Hazel/src/Hazel/Scene/Components.h.
  2. Add it to Hazel/src/Hazel/Scene/CopyableComponents.h if it should duplicate with the entity (prefab instantiation, scene copy, runtime spawn).
  3. Serialize it in Hazel/src/Hazel/Scene/SceneSerializer.cpp — both SerializeEntity and DeserializeEntity.
  4. Add a collapsing header in Hazel/src/Hazel/Editor/SceneHierarchyPanel.cpp :: DrawComponents, plus an entry in the "Add Component" menu.
  5. (Optional) Mirror the struct in Hazel-ScriptCore/Source/Hazel/ and add ScriptGlue InternalCalls if scripts need to read/write it.
Silent failure modes

Forgetting CopyableComponents: component is dropped on duplication. Forgetting the serializer: lost on save/load. Forgetting the UI: invisible in the editor. None of these produce a compile error.

PB.2 · Add a new asset type

  1. Add a value to the AssetType enum and update AssetTypeFromString / AssetTypeToString in AssetTypes.h.
  2. Create an asset class deriving Asset, overriding GetStaticType() and GetAssetType().
  3. Create a serializer deriving AssetSerializer (handles both file → Asset and Asset → file).
  4. Register it in AssetImporter.cpp so the importer dispatcher knows which serializer to call per type.
  5. Add the file-extension mapping in EditorAssetManager so dragging the file into the content browser triggers auto-import.

PB.3 · Add a new editor panel

  1. Create a class deriving EditorPanel in LuckyEditor/src/Panels/ (project-specific) or Hazel/src/Hazel/Editor/ (engine-wide). Implement OnImGuiRender(bool& isOpen); optionally SetSceneContext if the panel reacts to the active scene.
  2. Add a panel ID constant in the EditorPanelIDs namespace (EditorLayer.h).
  3. Register the panel in EditorLayer::OnAttach via m_PanelManager->RegisterPanel<MyPanel>(EditorPanelIDs::MyPanel).
  4. Add a menu entry in EditorLayer::UI_DrawMenubar to toggle the panel's visibility.

PB.4 · Add a new render pass

  1. Create the HLSL shader in Resources/Shaders/. (HLSL only — no other shader languages.)
  2. Create the cached pipeline and material in SceneRenderer::Init.
  3. Insert the pass execution in SceneRenderer::PreRender, or between existing passes in EndScene.
  4. If the pass needs intermediate storage, create the framebuffer or image alongside the others in Init.
  5. Register shader dependencies via Renderer::RegisterShaderDependency so the hot-reload system rebuilds the pipeline when the shader changes.
Cache pipeline state

Pipeline state objects must be cached, not recreated per frame. Recreating them every frame will tank both CPU time and GPU memory churn.

PB.5 · Add a new C# script binding (InternalCall)

  1. Add the C++ implementation in Hazel/src/Hazel/Script/ScriptGlue.cpp. The naming convention is ClassName_MethodName.
  2. Register the call in ScriptGlue::RegisterInternalCalls via HZ_ADD_INTERNAL_CALL(ClassName_MethodName).
  3. Add the C# declaration in Hazel-ScriptCore/Source/Hazel/InternalCalls.cs with the [InternalCall] attribute and the exactly matching name.
  4. Add a C# wrapper class or method (in the appropriate Hazel-ScriptCore file) that calls the InternalCall with a friendly signature.
Names must match exactly

The C++ registration symbol and the C# [InternalCall] name must be byte-identical, including case. Mismatches manifest as MethodMissingException at script-call time, not at build time. Always validate that the target entity is still alive before accessing its components from the bound implementation.

PB.6 · Add a new MuJoCo integration

  1. MujocoSceneInstance is the integration point — add the new method there, not on free functions.
  2. New body / geom queries go through m_Model and m_Data using the MuJoCo C API. Suffix every public method with Hz (returns Hazel space) or Mj (returns MuJoCo space) so callers always know the frame.
  3. New actuator type: extend MujocoActuator and the build path in MujocoSceneAsset::BuildActuatorList.
  4. New proxy collider component: handle it in MujocoSceneInstance::RebuildFromScene so changes in the scene propagate to the MuJoCo model.
  5. Convert coordinates using m_AxisMjToHz / m_AxisHzToMj (and the HzToMj / MjToHz helpers on MujocoSceneAsset). Never swizzle axes ad-hoc.
Ad-hoc swizzles are a long-running bug source

MuJoCo is Z-up with (w, x, y, z) quaternion order. Hazel is Y-up with (x, y, z, w). Hand-rolling the conversion in one place and not another produces drift that's invisible until you record a dataset. Always go through the conversion helpers — see Physics (MuJoCo).

PB.7 · Register a new periodic / fixed-rate system

  1. Pick the right TimeManager phase — Acquisition, Control, Physics, Validation, or Export. See Threading for the full ordering.
  2. Pick a frequency that matches the work. Typical rates: 500 Hz physics, 100 Hz control, 30 Hz recording. Don't assume 60 Hz.
  3. Register it: TimeManager::RegisterWithRunner(registration) for a fixed-rate system, or RegisterFreeUpdate(callback, FreePhaseID::PostExport) for refresh-rate-bound work (rendering / UI side effects).
  4. Don't add the system to Scene::OnUpdate or EditorLayer::OnUpdate. Those files are protected entry points — extending them couples your system to the frame loop in ways that bypass phase scheduling.
Phase matters for determinism

Acquisition reads sensors before Control consumes them. Validation runs after Physics so it sees the post-step world. Export is last so it never sees a half-updated frame. Putting work in the wrong phase produces datasets that look correct visually but are off-by-one in subtle ways.

PB.8 · Add a new long-running worker

Follow the *Job::Start pattern used by ImportProjectJob, CreateProjectFromTemplateJob, and similar jobs.

  1. The class owns a std::jthread m_Worker declared last in the member list. The destructor unwinds in reverse declaration order, so declaring it last guarantees the thread joins before any state it references is destroyed.
  2. The worker entry point is RunBackground(std::stop_token st). Check st.stop_requested() at every cancel checkpoint (file boundaries, loop iterations, after any expensive operation).
  3. Exchange state via atomics, or through a shared_ptr<Progress> under a mutex. No ImGui or NVRHI calls from the worker — both are main-thread-only.
  4. Result handoff: the worker sets m_Done.store(true, std::memory_order_release) when finished. The main thread polls IsDone(). Once true, the main thread calls TakeResult() exactly once.
  5. On failure, delete any partial output before reporting failure. Half-built project directories, partial recordings, and stub asset files are worse than a clean rollback.
Member-order discipline

If m_Worker is not the last member, the destructor may free state the still-running thread is reading. Symptoms: rare crashes on cancel, garbled error messages, debug-only heap corruption. Always declare it last. Full rules in Threading.

PB.9 · Modify the build system

  1. New vendor library: drop it into Hazel/vendor/, add a path mapping in Dependencies.lua, and link it in Hazel/premake5.lua.
  2. New Conan package: add it to conanfile.py, then run conan install.
  3. New platform: add platform detection in premake5.lua and place the implementation under Hazel/src/Hazel/Platform/<Platform>/.

See also