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.
Pick a recipe
Add a new component
Define the struct, copy rule, serialize, UI, optional C# binding.
Add a new asset type
Enum, Asset subclass, serializer, importer, file-extension mapping.
Add a new editor panel
Subclass EditorPanel, give it an ID, register, add menu entry.
Add a new render pass
HLSL shader, cached pipeline, insert in PreRender / EndScene.
Add a new C# binding
ScriptGlue InternalCall + InternalCalls.cs decl + wrapper.
Add a MuJoCo integration
Extend MujocoSceneInstance with Hz / Mj-suffixed methods.
Register a fixed-rate system
Pick a TimeManager phase and frequency, register with the runner.
Add a long-running worker
The Job pattern: jthread last, stop_token, handoff via TakeResult.
Modify the build system
New vendor, Conan package, or platform.
PB.1 · Add a new component
- Define the struct in
Hazel/src/Hazel/Scene/Components.h. - Add it to
Hazel/src/Hazel/Scene/CopyableComponents.hif it should duplicate with the entity (prefab instantiation, scene copy, runtime spawn). - Serialize it in
Hazel/src/Hazel/Scene/SceneSerializer.cpp— bothSerializeEntityandDeserializeEntity. - Add a collapsing header in
Hazel/src/Hazel/Editor/SceneHierarchyPanel.cpp :: DrawComponents, plus an entry in the "Add Component" menu. - (Optional) Mirror the struct in
Hazel-ScriptCore/Source/Hazel/and addScriptGlueInternalCalls if scripts need to read/write it.
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
- Add a value to the
AssetTypeenum and updateAssetTypeFromString/AssetTypeToStringinAssetTypes.h. - Create an asset class deriving
Asset, overridingGetStaticType()andGetAssetType(). - Create a serializer deriving
AssetSerializer(handles both file → Asset and Asset → file). - Register it in
AssetImporter.cppso the importer dispatcher knows which serializer to call per type. - Add the file-extension mapping in
EditorAssetManagerso dragging the file into the content browser triggers auto-import.
PB.3 · Add a new editor panel
- Create a class deriving
EditorPanelinLuckyEditor/src/Panels/(project-specific) orHazel/src/Hazel/Editor/(engine-wide). ImplementOnImGuiRender(bool& isOpen); optionallySetSceneContextif the panel reacts to the active scene. - Add a panel ID constant in the
EditorPanelIDsnamespace (EditorLayer.h). - Register the panel in
EditorLayer::OnAttachviam_PanelManager->RegisterPanel<MyPanel>(EditorPanelIDs::MyPanel). - Add a menu entry in
EditorLayer::UI_DrawMenubarto toggle the panel's visibility.
PB.4 · Add a new render pass
- Create the HLSL shader in
Resources/Shaders/. (HLSL only — no other shader languages.) - Create the cached pipeline and material in
SceneRenderer::Init. - Insert the pass execution in
SceneRenderer::PreRender, or between existing passes inEndScene. - If the pass needs intermediate storage, create the framebuffer or image alongside the others in
Init. - Register shader dependencies via
Renderer::RegisterShaderDependencyso the hot-reload system rebuilds the pipeline when the shader changes.
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)
- Add the C++ implementation in
Hazel/src/Hazel/Script/ScriptGlue.cpp. The naming convention isClassName_MethodName. - Register the call in
ScriptGlue::RegisterInternalCallsviaHZ_ADD_INTERNAL_CALL(ClassName_MethodName). - Add the C# declaration in
Hazel-ScriptCore/Source/Hazel/InternalCalls.cswith the[InternalCall]attribute and the exactly matching name. - Add a C# wrapper class or method (in the appropriate
Hazel-ScriptCorefile) that calls the InternalCall with a friendly signature.
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
MujocoSceneInstanceis the integration point — add the new method there, not on free functions.- New body / geom queries go through
m_Modelandm_Datausing the MuJoCo C API. Suffix every public method withHz(returns Hazel space) orMj(returns MuJoCo space) so callers always know the frame. - New actuator type: extend
MujocoActuatorand the build path inMujocoSceneAsset::BuildActuatorList. - New proxy collider component: handle it in
MujocoSceneInstance::RebuildFromSceneso changes in the scene propagate to the MuJoCo model. - Convert coordinates using
m_AxisMjToHz/m_AxisHzToMj(and theHzToMj/MjToHzhelpers onMujocoSceneAsset). Never swizzle axes ad-hoc.
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
- Pick the right
TimeManagerphase —Acquisition,Control,Physics,Validation, orExport. See Threading for the full ordering. - Pick a frequency that matches the work. Typical rates: 500 Hz physics, 100 Hz control, 30 Hz recording. Don't assume 60 Hz.
- Register it:
TimeManager::RegisterWithRunner(registration)for a fixed-rate system, orRegisterFreeUpdate(callback, FreePhaseID::PostExport)for refresh-rate-bound work (rendering / UI side effects). - Don't add the system to
Scene::OnUpdateorEditorLayer::OnUpdate. Those files are protected entry points — extending them couples your system to the frame loop in ways that bypass phase scheduling.
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.
- The class owns a
std::jthread m_Workerdeclared 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. - The worker entry point is
RunBackground(std::stop_token st). Checkst.stop_requested()at every cancel checkpoint (file boundaries, loop iterations, after any expensive operation). - 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. - Result handoff: the worker sets
m_Done.store(true, std::memory_order_release)when finished. The main thread pollsIsDone(). Once true, the main thread callsTakeResult()exactly once. - 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.
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
- New vendor library: drop it into
Hazel/vendor/, add a path mapping inDependencies.lua, and link it inHazel/premake5.lua. - New Conan package: add it to
conanfile.py, then runconan install. - New platform: add platform detection in
premake5.luaand place the implementation underHazel/src/Hazel/Platform/<Platform>/.
See also
- Cross-Cutting — Ref / Scope, events, serialization, logging.
- Threading — TimeManager phases and jthread workers in detail.
- Directory Map — where these files live.