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.
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.
Tmust derive fromRefCounted. - 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...)— nevernew T. - Cast with
ref.As<Derived>(). Reach the raw pointer withref.Raw()only when an API forces it. - Parameter style: pass
Ref<T>by value when the callee will share ownership; passconst 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 freshRef<T>if the target is still alive, otherwise an empty ref.- Don't deref a
WeakRefdirectly — always go throughLock().
Scope<T> — unique ownership
- Plain alias for
std::unique_ptr<T>. NoRefCountedrequirement. - 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.
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
| Category | Typical events |
|---|---|
Application | Window close, resize, minimize, focus |
Input | Bucket flag — combined with Keyboard / Mouse |
Keyboard | Key pressed, released, typed |
Mouse | Button pressed / released, moved, scrolled |
Scene | Scene start, stop, asset reloaded, animation graph compiled |
Editor | Selection 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
- Add a value to the
EventTypeenum. - Declare the event class with the
EVENT_CLASS_TYPEandEVENT_CLASS_CATEGORYmacros. - Fire it from the producer (window callback, input layer, scene transition, etc.).
- Handle it in the relevant layer's
OnEventviaDispatch<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.
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 family | When |
|---|---|
HZ_CORE_TRACE/INFO/WARN/ERROR/FATAL | Engine 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/ERROR | Messages 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. CallsFatalBreakin 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 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_ASSERTfor invariants you want to catch only in debug builds. - Use
HZ_CORE_VERIFYfor invariants that must hold in Dist (and accept theFatalBreakif they don't). - Prefer RAII over manual resource management — file handles, GPU resources, locks, threads all have wrappers.
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 insidenamespace Hazel. Add it only when ambiguous. - No inline single-line
ifbodies. The body always lives on its own line, with or without braces. Same forelse / 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, checkHazel::FileSystem. BeforeImGui::PushStyleVar, checkImGuiEx::ScopedStyle. BeforeIM_COL32, checkColors::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.