Lucky Robots Blog Open Roles

CC · Cross-Cutting Concerns

Conventions and primitives that touch every subsystem in LuckyEngine: memory ownership through custom smart pointers, the event bus, serialization (YAML and binary), tagged logging, math + coordinate conventions, and the engine's stance on error handling.

Ref<T> Scope<T> RAII yaml-cpp spdlog GLM Y-up, RH No std::shared_ptr
Smart-pointer ownership at a glance Ref<T> intrusive, atomic refcount shared ownership T : RefCounted construct Ref<T>::Create(args...) downcast / observe .As<U>() .Raw() param style Ref<T> share / const Ref<T>& peek WeakRef<T> non-owning observer break ownership cycles held alongside Ref<T> check .IsAlive() promote .Lock() → Ref<T> Scope<T> alias for std::unique_ptr<T> unique ownership no RefCounted requirement construct CreateScope<T>(args...) move only std::move(scope) to pass weak of vs. Forbidden everywhere: raw new/delete · std::shared_ptr · std::unique_ptr · std::make_*
Pick the pointer by ownership shape: shared (Ref), observer (WeakRef), unique (Scope).

Smart pointers

LuckyEngine uses custom smart pointers, not the standard library equivalents. The relevant headers are Hazel/src/Hazel/Core/Ref.h and Hazel/src/Hazel/Core/Base.h. Choose by ownership intent:

Ref<T> — shared ownership

  • Intrusive refcount stored on the object. T must derive from RefCounted.
  • Refcount is atomic, so Ref<T> is safe to copy across threads (the pointee still needs its own synchronization).
  • Construct with Ref<T>::Create(args...) — never new T.
  • Cast with ref.As<Derived>(). Reach the raw pointer with ref.Raw() only when an API forces it.
  • Parameter style: pass Ref<T> by value when the callee will share ownership; pass const Ref<T>& when it only needs to look.

WeakRef<T> — observer

  • Non-owning view of a Ref<T>-managed object. Used to break ownership cycles.
  • IsAlive() reports liveness; Lock() returns a fresh Ref<T> if the target is still alive, otherwise an empty ref.
  • Don't deref a WeakRef directly — always go through Lock().

Scope<T> — unique ownership

  • Plain alias for std::unique_ptr<T>. No RefCounted requirement.
  • Construct via CreateScope<T>(args...).
  • Use for owned-by-one-place members (panels, layers, per-system caches) when nothing else needs to share the pointee.
Forbidden

Don't use raw new / delete, std::shared_ptr, std::unique_ptr, std::make_shared, or std::make_unique in engine code. Always go through Ref<T>::Create, CreateScope<T>, or RAII container types. The engine's allocator, leak tracking, and refcounting all depend on the wrappers.

Events

The event bus lives in Hazel/src/Hazel/Core/Events/Event.h. Event is the abstract base: every event carries an EventType, category flag bits, and a Handled flag a listener can set to stop propagation.

Categories & types

CategoryTypical events
ApplicationWindow close, resize, minimize, focus
InputBucket flag — combined with Keyboard / Mouse
KeyboardKey pressed, released, typed
MouseButton pressed / released, moved, scrolled
SceneScene start, stop, asset reloaded, animation graph compiled
EditorSelection changed, panel-specific signals

Dispatch

A layer's OnEvent(Event&) normally builds an EventDispatcher and routes by type:

void MyLayer::OnEvent(Event& e)
{
    EventDispatcher dispatcher(e);
    dispatcher.Dispatch<KeyPressedEvent>(HZ_BIND_EVENT_FN(MyLayer::OnKeyPressed));
    dispatcher.Dispatch<WindowResizeEvent>([this](WindowResizeEvent& we) {
        m_Viewport.Resize(we.GetWidth(), we.GetHeight());
        return false; // not handled — let others see it too
    });
}

Adding a new event

  1. Add a value to the EventType enum.
  2. Declare the event class with the EVENT_CLASS_TYPE and EVENT_CLASS_CATEGORY macros.
  3. Fire it from the producer (window callback, input layer, scene transition, etc.).
  4. Handle it in the relevant layer's OnEvent via Dispatch<YourEvent>.

Serialization

Two flavours, depending on audience:

YAML — human readable

Scenes, prefabs, materials, project settings. Backed by yaml-cpp. Diff-friendly, hand-editable, the authoring format. Driven by SceneSerializer and helpers in YAMLSerializationHelpers.h (vec/mat/quat/UUID/asset-handle) and SerializationMacros.h (e.g. HZ_SERIALIZE_PROPERTY).

Binary AssetPack — runtime

Shipped game / sim builds use a single binary AssetPack instead of a tree of YAML and source assets. Compact, hashed, fast to mmap. Editor produces it; runtime consumes it through the same AssetManager facade.

Component schema versioning

Versioning is implicit: a missing key in the YAML deserializes to the C++ struct's default value. That makes adding fields cheap — old scenes still load. It also means removing or renaming a field silently drops data, so prefer additive changes.

Dataset schemas are different

Recording / dataset output is explicitly versioned, not implicit. See Recording Integrity § Schema fidelity for the rules — recorded data must be re-readable years later, so the schema is part of the file.

Logging

spdlog-based. Four logger sinks: Core (engine), Client (game / sim), and two EditorConsole sinks (App + Core) that surface inside the editor's console panel. Levels go Trace → Info → Warn → Error → Fatal. Per-tag filtering is driven by s_EnabledTags.

Which macro

Macro familyWhen
HZ_CORE_TRACE/INFO/WARN/ERROR/FATALEngine code, untagged.
HZ_CORE_*_TAG(tag, ...)Engine code, preferred — pass a system tag ("ContentVault", "Project", "ScriptBuilder", …) so the editor's tag filter can isolate the source.
HZ_TRACE/INFO/...Client / sandbox code.
HZ_CONSOLE_LOG_INFO/WARN/ERRORMessages that should appear in the in-editor console panel, not just the log file.

Asserts

  • HZ_CORE_ASSERT(cond, msg) — debug-only invariants. Compiles out in Release / Dist.
  • HZ_CORE_VERIFY(cond) — always-on invariants. Calls FatalBreak in Dist if violated.
  • HZ_CORE_ENSURE(cond) — soft contract: logs the violation and continues execution.

Math & coordinates

GLM is the math library: vec2 / vec3 / vec4, mat3 / mat4, quat. The engine coordinate system is Y-up, right-handed. Forward is -Z, right is +X.

MuJoCo is different

MuJoCo is Z-up and uses (w, x, y, z) quaternion order. Don't swizzle by hand — use MujocoSceneAsset::HzToMj / MjToHz, and the m_AxisHzToMj / m_AxisMjToHz rotation quaternions on MujocoSceneInstance. Method names on these classes are suffixed Hz or Mj to make the space explicit. Full rules in Physics (MuJoCo).

Error handling

The engine validates at system boundaries — file I/O, user input, network, plugin and RPC interfaces. Internal call sites are trusted. Don't sprinkle defensive checks for states that the caller has already ensured; that's noise that hides real bugs.

  • Handle NaN, divide-by-zero, and degenerate geometry where the data enters the system, not deep inside the math.
  • Use HZ_CORE_ASSERT for invariants you want to catch only in debug builds.
  • Use HZ_CORE_VERIFY for invariants that must hold in Dist (and accept the FatalBreak if they don't).
  • Prefer RAII over manual resource management — file handles, GPU resources, locks, threads all have wrappers.
No try / catch in the recording path

Recording code does not catch exceptions. A partially-recorded episode is worse than a missing one — corruption silently poisons downstream training data. Fail loud, delete the partial output, surface the failure. See Data / Observation and Recording Integrity.

Style essentials

The full rules live in Conventions; the most common ones in code review:

  • Drop the Hazel:: qualifier when already inside namespace Hazel. Add it only when ambiguous.
  • No inline single-line if bodies. The body always lives on its own line, with or without braces. Same for else / while / for / do.
  • Named C++ casts only. static_cast, reinterpret_cast, const_cast, dynamic_cast. Never (T)x. If a value needs both a numeric and a pointer conversion, spell both out.
  • Reuse helpers. Before reaching for std::filesystem, check Hazel::FileSystem. Before ImGui::PushStyleVar, check ImGuiEx::ScopedStyle. Before IM_COL32, check Colors::Theme.

See also

  • Threading — owning std::jthread, TimeManager phases, render thread.
  • Recording Integrity — versioned schemas, no try/catch in the data path.
  • Conventions — full style guide and helper-reuse index.