9#include <glm/gtc/quaternion.hpp>
10#include <glm/gtc/type_ptr.hpp>
12#include "Application.h"
15#include "btBulletDynamicsCommon.h"
16#include "physics/PhysicsManager.h"
17#include "GLFW/glfw3.h"
19#include "backends/imgui_impl_glfw.h"
20#include "backends/imgui_impl_opengl3.h"
23#include "input/InputManager.h"
24#include "io/FileSystem.h"
25#include "scene/GameObject.h"
26#include "scene/Scene.h"
27#include "scene/components/CameraComponent.h"
28#include "scene/components/LightComponent.h"
29#include "scene/components/MeshComponent.h"
30#include "scene/components/PlayerControllerComponent.h"
38 ImGui::CreateContext();
39 ImGuiIO &io = ImGui::GetIO();
40 io.ConfigFlags |= ImGuiConfigFlags_NavEnableKeyboard;
41 ImGui::StyleColorsDark();
45 if (!ImGui_ImplGlfw_InitForOpenGL(window,
true))
47 LOG_ERROR(
"ImGui GLFW backend init failed");
52 LOG_ERROR(
"ImGui OpenGL3 backend init failed");
66 ImGui_ImplOpenGL3_Shutdown();
67 ImGui_ImplGlfw_Shutdown();
68 ImGui::DestroyContext();
69 m_initialized =
false;
79 GLFWwindow *w = glfwGetCurrentContext();
81 static bool wasDown =
false;
82 bool down = (w !=
nullptr) && (glfwGetKey(w, GLFW_KEY_F1) == GLFW_PRESS);
94 int desired = m_visible ? GLFW_CURSOR_NORMAL : GLFW_CURSOR_DISABLED;
95 int current = glfwGetInputMode(w, GLFW_CURSOR);
96 if (current != desired)
98 glfwSetInputMode(w, GLFW_CURSOR, desired);
102 glfwGetCursorPos(w, &x, &y);
104 vec2 p(
static_cast<f32>(x),
static_cast<f32>(y));
105 im.SetMousePositionOld(p);
106 im.SetMousePositionCurrent(p);
107 im.SetMousePositionChanged(
false);
111 ImGui_ImplOpenGL3_NewFrame();
112 ImGui_ImplGlfw_NewFrame();
125constexpr f32 kLeftWidth = 320.0F;
126constexpr f32 kRightWidth = 280.0F;
127constexpr f32 kBottomHeight = 280.0F;
128constexpr f32 kConsoleFrac = 0.62F;
130constexpr ImGuiWindowFlags kDockedFlags = ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoResize
131 | ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoBringToFrontOnFocus;
142DockRects ComputeDock(
f32 menuH)
144 const ImGuiViewport *vp = ImGui::GetMainViewport();
145 f32 vx = vp->WorkPos.x;
146 f32 vy = vp->WorkPos.y + menuH;
147 f32 vw = vp->WorkSize.x;
148 f32 vh = vp->WorkSize.y - menuH;
151 f32 leftW = (vw < kLeftWidth + kRightWidth + 240.0F) ? vw * 0.24F : kLeftWidth;
152 f32 rightW = (vw < kLeftWidth + kRightWidth + 240.0F) ? vw * 0.22F : kRightWidth;
153 f32 bottomH = (vh < kBottomHeight + 240.0F) ? vh * 0.30F : kBottomHeight;
156 r.settingsPos = ImVec2(vx, vy);
157 r.settingsSize = ImVec2(leftW, vh);
159 r.inspectorPos = ImVec2(vx + vw - rightW, vy);
160 r.inspectorSize = ImVec2(rightW, vh);
162 f32 bottomY = vy + vh - bottomH;
163 f32 centerX = vx + leftW;
164 f32 centerW = vw - leftW - rightW;
165 f32 consoleW = centerW * kConsoleFrac;
167 r.consolePos = ImVec2(centerX, bottomY);
168 r.consoleSize = ImVec2(consoleW, bottomH);
170 r.hierarchyPos = ImVec2(centerX + consoleW, bottomY);
171 r.hierarchySize = ImVec2(centerW - consoleW, bottomH);
174 constexpr f32 kStatsW = 190.0F;
175 constexpr f32 kStatsH = 90.0F;
176 r.statsPos = ImVec2(vx + leftW + centerW - kStatsW - 10.0F, vy + 10.0F);
177 r.statsSize = ImVec2(kStatsW, kStatsH);
184 if (!m_initialized || !m_visible)
191 f32 menuH = ImGui::GetFrameHeight();
192 DockRects d = ComputeDock(menuH);
196 ImGui::SetNextWindowPos(d.hierarchyPos, ImGuiCond_Always);
197 ImGui::SetNextWindowSize(d.hierarchySize, ImGuiCond_Always);
202 ImGui::SetNextWindowPos(d.inspectorPos, ImGuiCond_Always);
203 ImGui::SetNextWindowSize(d.inspectorSize, ImGuiCond_Always);
208 ImGui::SetNextWindowPos(d.consolePos, ImGuiCond_Always);
209 ImGui::SetNextWindowSize(d.consoleSize, ImGuiCond_Always);
214 if (m_showRender || m_showEngine || m_showPhysics || m_showPlayer)
216 ImGui::SetNextWindowPos(d.settingsPos, ImGuiCond_Always);
217 ImGui::SetNextWindowSize(d.settingsSize, ImGuiCond_Always);
222 ImGui::SetNextWindowPos(d.statsPos, ImGuiCond_Always);
223 ImGui::SetNextWindowSize(d.statsSize, ImGuiCond_Always);
228 ImGui::ShowDemoWindow(&m_showDemo);
231 for (
auto &p : m_customPanels)
233 if (ImGui::Begin(p.first.c_str()))
248 ImGui_ImplOpenGL3_RenderDrawData(ImGui::GetDrawData());
253 return m_initialized && m_visible && ImGui::GetIO().WantCaptureMouse;
258 return m_initialized && m_visible && ImGui::GetIO().WantCaptureKeyboard;
263void Editor::DrawMenuBar()
265 if (!ImGui::BeginMainMenuBar())
269 if (ImGui::BeginMenu(
"Windows"))
271 ImGui::MenuItem(
"Hierarchy",
nullptr, &m_showHierarchy);
272 ImGui::MenuItem(
"Inspector",
nullptr, &m_showInspector);
273 ImGui::MenuItem(
"Console",
nullptr, &m_showConsole);
274 ImGui::MenuItem(
"Render",
nullptr, &m_showRender);
275 ImGui::MenuItem(
"Stats",
nullptr, &m_showStats);
276 ImGui::MenuItem(
"Engine",
nullptr, &m_showEngine);
277 ImGui::MenuItem(
"Physics",
nullptr, &m_showPhysics);
278 ImGui::MenuItem(
"Player",
nullptr, &m_showPlayer);
280 ImGui::MenuItem(
"ImGui Demo",
nullptr, &m_showDemo);
283 ImGui::Text(
" | F1: toggle editor");
284 ImGui::EndMainMenuBar();
289void Editor::DrawObjectNode(GameObject *obj)
295 const auto &children = obj->GetChildren();
297 ImGuiTreeNodeFlags flags = ImGuiTreeNodeFlags_OpenOnArrow | ImGuiTreeNodeFlags_SpanAvailWidth;
298 if (children.empty())
300 flags |= ImGuiTreeNodeFlags_Leaf;
302 if (m_selected == obj)
304 flags |= ImGuiTreeNodeFlags_Selected;
307 bool open = ImGui::TreeNodeEx(obj, flags,
"%s", obj->GetName().c_str());
308 if (ImGui::IsItemClicked() && !ImGui::IsItemToggledOpen())
314 for (
auto &c : children)
316 DrawObjectNode(c.get());
322void Editor::DrawHierarchy()
324 if (!ImGui::Begin(
"Hierarchy", &m_showHierarchy, kDockedFlags))
330 if (scene ==
nullptr)
332 ImGui::TextUnformatted(
"No active scene");
335 for (
auto &root : scene->GetRootObjects())
337 DrawObjectNode(root.get());
347 if (!ImGui::CollapsingHeader(
"Transform", ImGuiTreeNodeFlags_DefaultOpen))
352 if (ImGui::DragFloat3(
"Position", glm::value_ptr(pos), 0.05F))
358 vec3 euler = glm::degrees(glm::eulerAngles(rot));
359 if (ImGui::DragFloat3(
"Rotation (deg)", glm::value_ptr(euler), 0.5F))
365 if (ImGui::DragFloat3(
"Scale", glm::value_ptr(s), 0.01F, 0.0F, 0.0F))
373 if (!ImGui::CollapsingHeader(
"CameraComponent", ImGuiTreeNodeFlags_DefaultOpen))
380 if (ImGui::SliderFloat(
"FOV", &fov, 10.0F, 170.0F))
384 if (ImGui::DragFloat(
"Near", &near, 0.01F, 0.001F, far - 0.01F))
388 if (ImGui::DragFloat(
"Far", &far, 1.0F, near + 0.01F, 100000.0F))
396 if (!ImGui::CollapsingHeader(
"LightComponent", ImGuiTreeNodeFlags_DefaultOpen))
401 if (ImGui::ColorEdit3(
"Color", glm::value_ptr(col), ImGuiColorEditFlags_Float | ImGuiColorEditFlags_HDR))
409 if (!ImGui::CollapsingHeader(
"PlayerControllerComponent", ImGuiTreeNodeFlags_DefaultOpen))
416 if (ImGui::DragFloat(
"Move Speed", &ms, 0.05F, 0.0F, 1000.0F))
420 if (ImGui::DragFloat(
"Sensitivity", &sens, 0.1F, 0.0F, 500.0F))
424 if (ImGui::DragFloat(
"Jump Speed", &jump, 0.05F, 0.0F, 1000.0F))
428 ImGui::Text(
"On ground: %s", c->
OnGround() ?
"yes" :
"no");
433 if (!ImGui::CollapsingHeader(
"MeshComponent"))
437 ImGui::TextUnformatted(
"(mesh + material — read-only)");
440void Editor::DrawInspector()
442 if (!ImGui::Begin(
"Inspector", &m_showInspector, kDockedFlags))
447 if (m_selected ==
nullptr)
449 ImGui::TextUnformatted(
"Nothing selected");
454 ImGui::Text(
"Name: %s", m_selected->
GetName().c_str());
456 bool active = m_selected->
IsActive();
457 if (ImGui::Checkbox(
"Active", &active))
464 if (
auto *cam = m_selected->
GetComponent<CameraComponent>())
468 if (
auto *lit = m_selected->
GetComponent<LightComponent>())
472 if (
auto *pc = m_selected->
GetComponent<PlayerControllerComponent>())
476 if (
auto *mc = m_selected->
GetComponent<MeshComponent>())
486void Editor::DrawConsole()
488 if (!ImGui::Begin(
"Console", &m_showConsole, kDockedFlags))
494 static bool showInfo =
true;
495 static bool showWarn =
true;
496 static bool showError =
true;
497 static bool autoScroll =
true;
498 static char filter[128] = {0};
500 ImGui::Checkbox(
"Info", &showInfo);
502 ImGui::Checkbox(
"Warn", &showWarn);
504 ImGui::Checkbox(
"Error", &showError);
506 ImGui::Checkbox(
"Auto-scroll", &autoScroll);
508 if (ImGui::Button(
"Clear"))
513 ImGui::SetNextItemWidth(-1.0F);
514 ImGui::InputTextWithHint(
"##filter",
"filter", filter,
sizeof(filter));
518 if (ImGui::BeginChild(
"console_scroll", ImVec2(0, 0),
false, ImGuiWindowFlags_HorizontalScrollbar))
521 for (
const auto &e : entries)
541 if (filter[0] != 0 && e.text.find(filter) == std::string::npos)
550 col = ImVec4(0.75F, 0.85F, 0.75F, 1.0F);
553 col = ImVec4(1.00F, 0.85F, 0.40F, 1.0F);
556 col = ImVec4(1.00F, 0.45F, 0.45F, 1.0F);
559 col = ImVec4(1.00F, 0.20F, 0.20F, 1.0F);
562 ImGui::PushStyleColor(ImGuiCol_Text, col);
563 ImGui::TextUnformatted(e.text.c_str());
564 ImGui::PopStyleColor();
566 if (autoScroll && ImGui::GetScrollY() >= ImGui::GetScrollMaxY() - 1.0F)
568 ImGui::SetScrollHereY(1.0F);
577void Editor::DrawSettings()
580 ImGuiWindowFlags flags = kDockedFlags | ImGuiWindowFlags_NoTitleBar;
581 if (!ImGui::Begin(
"##settings",
nullptr, flags))
587 if (ImGui::BeginTabBar(
"settings_tabs", ImGuiTabBarFlags_FittingPolicyScroll))
589 if (m_showRender && ImGui::BeginTabItem(
"Render"))
594 if (m_showEngine && ImGui::BeginTabItem(
"Engine"))
599 if (m_showPhysics && ImGui::BeginTabItem(
"Physics"))
604 if (m_showPlayer && ImGui::BeginTabItem(
"Player"))
616void Editor::DrawRenderBody()
620 ImGui::SeparatorText(
"Clear");
621 ImGui::ColorEdit4(
"Clear color", glm::value_ptr(rs.clearColor));
623 ImGui::SeparatorText(
"Pixel art");
624 ImGui::Checkbox(
"Use Pixelation", &rs.useInternalRes);
625 ImGui::SliderInt(
"Pixel Size", &rs.pixelSize, 1, 32);
626 rs.pixelSize = std::clamp(rs.pixelSize, 1, 32);
627 ImGui::Text(
"Scene target: %d x %d", rs.internalW, rs.internalH);
628 ImGui::Checkbox(
"Use Outline", &rs.useOutline);
631 ImGui::BeginDisabled(!rs.useOutline);
632 ImGui::SliderFloat(
"Normal Edge Strength", &pp.normalEdgeStrength, 0.0F, 2.0F);
633 ImGui::SliderFloat(
"Depth Edge Strength", &pp.depthEdgeStrength, 0.0F, 2.0F);
634 pp.normalEdgeStrength = std::clamp(pp.normalEdgeStrength, 0.0F, 2.0F);
635 pp.depthEdgeStrength = std::clamp(pp.depthEdgeStrength, 0.0F, 2.0F);
636 ImGui::EndDisabled();
639 ImGui::SeparatorText(
"Debug view");
640 const char *kDebugViewLabels[] = {
"Color",
"Normals (view-space)",
"Depth (linearised preview)" };
641 int debugViewIdx =
static_cast<int>(rs.debugView);
642 if (ImGui::Combo(
"Scene target", &debugViewIdx, kDebugViewLabels, IM_ARRAYSIZE(kDebugViewLabels)))
644 rs.debugView =
static_cast<DebugView>(debugViewIdx);
647 ImGui::SeparatorText(
"Display");
648 if (ImGui::Checkbox(
"VSync", &m_vsyncEnabled))
650 glfwSwapInterval(m_vsyncEnabled ? 1 : 0);
652 if (ImGui::Checkbox(
"Wireframe", &m_wireframe))
654 glPolygonMode(GL_FRONT_AND_BACK, m_wireframe ? GL_LINE : GL_FILL);
656 if (ImGui::Checkbox(
"Lock cursor", &m_cursorLocked))
660 m_cursorLocked ? GLFW_CURSOR_DISABLED : GLFW_CURSOR_NORMAL);
666void Editor::DrawEngineBody()
670 ImGui::SeparatorText(
"Time");
671 float scale =
eng.GetTimeScale();
672 if (ImGui::SliderFloat(
"Time scale", &scale, 0.0F, 4.0F,
"%.2fx"))
674 eng.SetTimeScale(scale);
677 if (ImGui::SmallButton(
"1x"))
679 eng.SetTimeScale(1.0F);
682 bool paused =
eng.IsPaused();
683 if (ImGui::Checkbox(
"Pause (dt = 0)", &paused))
685 eng.SetPaused(paused);
688 ImGui::SeparatorText(
"Scene");
689 Scene *scene =
eng.GetScene();
690 if (scene !=
nullptr)
692 ImGui::Text(
"Root objects: %zu", scene->GetRootObjects().size());
695 ImGui::TextDisabled(
"No active scene");
698 ImGui::SeparatorText(
"Application");
699 if (ImGui::Button(
"Quit"))
701 if (
auto *app =
eng.GetApplication())
703 app->SetNeedsToBeClosed(
true);
710void Editor::DrawPhysicsBody()
713 if (world ==
nullptr)
715 ImGui::TextDisabled(
"Physics world not available");
719 btVector3 g = world->getGravity();
720 float grav[3] = {g.x(), g.y(), g.z()};
721 if (ImGui::DragFloat3(
"Gravity", grav, 0.1F, -100.0F, 100.0F))
723 world->setGravity(btVector3(grav[0], grav[1], grav[2]));
725 if (ImGui::Button(
"Earth (0,-9.81,0)"))
727 world->setGravity(btVector3(0.0F, -9.81F, 0.0F));
730 if (ImGui::Button(
"Zero-G"))
732 world->setGravity(btVector3(0.0F, 0.0F, 0.0F));
735 int bodies = world->getNumCollisionObjects();
736 ImGui::Text(
"Collision objects: %d", bodies);
741void Editor::DrawStats()
743 if (!ImGui::Begin(
"Stats", &m_showStats, kDockedFlags | ImGuiWindowFlags_NoTitleBar))
748 f32 dt = ImGui::GetIO().DeltaTime;
749 f32 fps = (dt > 0.0F) ? (1.0F / dt) : 0.0F;
750 m_fpsSmoothed = (m_fpsSmoothed == 0.0F) ? fps : (m_fpsSmoothed * 0.9F + fps * 0.1F);
752 ImGui::Text(
"FPS: %6.1f", m_fpsSmoothed);
753 ImGui::Text(
"Frame time: %6.2f ms", dt * 1000.0F);
754 ImGui::Text(
"Draw calls: %d", m_lastDrawCount);
762PlayerControllerComponent *FindPlayer(GameObject *obj)
768 if (
auto *player = obj->GetComponent<PlayerControllerComponent>())
772 for (
const auto &child : obj->GetChildren())
774 if (
auto *found = FindPlayer(child.get()))
783void Editor::DrawPlayerBody()
786 PlayerControllerComponent *player =
nullptr;
787 if (scene !=
nullptr)
789 for (
const auto &root : scene->GetRootObjects())
791 player = FindPlayer(root.get());
792 if (player !=
nullptr)
798 if (player ==
nullptr)
800 ImGui::TextDisabled(
"No PlayerControllerComponent in scene");
804 ImGui::SeparatorText(
"Look");
805 f32 sens = player->GetSensitivity();
806 if (ImGui::SliderFloat(
"Sensitivity (deg/px)", &sens, 0.01F, 1.0F,
"%.3f"))
808 player->SetSensitivity(sens);
811 ImGui::SeparatorText(
"Movement");
812 f32 moveSpeed = player->GetMoveSpeed();
813 if (ImGui::SliderFloat(
"Max speed (m/s)", &moveSpeed, 1.0F, 20.0F,
"%.2f"))
815 player->SetMoveSpeed(moveSpeed);
817 f32 jumpSpeed = player->GetJumpSpeed();
818 if (ImGui::SliderFloat(
"Jump x maxSpeed", &jumpSpeed, 0.0F, 2.0F,
"%.2f"))
820 player->SetJumpSpeed(jumpSpeed);
822 ImGui::Text(
" -> jump impulse: %.2f m/s", moveSpeed * jumpSpeed);
824 ImGui::SeparatorText(
"Acceleration");
825 f32 groundAccel = player->GetGroundAccel();
826 if (ImGui::SliderFloat(
"Ground accel", &groundAccel, 0.0F, 30.0F,
"%.2f"))
828 player->SetGroundAccel(groundAccel);
830 f32 airAccel = player->GetAirAccel();
831 if (ImGui::SliderFloat(
"Air accel", &airAccel, 0.0F, 30.0F,
"%.2f"))
833 player->SetAirAccel(airAccel);
835 f32 fric = player->GetFriction();
836 if (ImGui::SliderFloat(
"Friction", &fric, 0.0F, 12.0F,
"%.2f"))
838 player->SetFriction(fric);
840 f32 cap = player->GetAirCap();
841 if (ImGui::SliderFloat(
"Air wishspeed cap", &cap, 0.1F, 30.0F,
"%.2f"))
843 player->SetAirCap(cap);
845 ImGui::TextDisabled(
"Low cap (~0.76) = Q3 strafe-jump; high = HL air control");
847 bool hop = player->GetAutoHop();
848 if (ImGui::Checkbox(
"Auto bunny-hop (hold Space)", &hop))
850 player->SetAutoHop(hop);
853 ImGui::SeparatorText(
"Presets");
854 if (ImGui::Button(
"HL1"))
856 player->SetMoveSpeed(7.5F);
857 player->SetJumpSpeed(0.9F);
858 player->SetGroundAccel(10.0F);
859 player->SetAirAccel(10.0F);
860 player->SetFriction(4.0F);
861 player->SetAirCap(30.0F);
864 if (ImGui::Button(
"Quake3"))
866 player->SetMoveSpeed(8.0F);
867 player->SetJumpSpeed(1.0F);
868 player->SetGroundAccel(10.0F);
869 player->SetAirAccel(1.0F);
870 player->SetFriction(6.0F);
871 player->SetAirCap(0.76F);
874 if (ImGui::Button(
"Source"))
876 player->SetMoveSpeed(7.6F);
877 player->SetJumpSpeed(0.85F);
878 player->SetGroundAccel(10.0F);
879 player->SetAirAccel(10.0F);
880 player->SetFriction(4.0F);
881 player->SetAirCap(30.0F);
884 ImGui::SeparatorText(
"Live");
885 ImGui::Text(
"On ground: %s", player->OnGround() ?
"yes" :
"no");
constexpr const char * kGlslVersionDirective
In-game ImGui overlay: hierarchy, inspector, console, stats.
Console logging macros for all engine and game code.
#define LOG_INFO(fmt,...)
#define LOG_ERROR(fmt,...)
Per-frame renderer tuning knobs (PSX-style pixelation, fog, ambient).
Perspective camera — provides view and projection matrices each frame.
void EndFrame()
Renders the ImGui draw data to the current framebuffer.
void Shutdown()
Tear down ImGui state. Call before destroying the GL context.
bool WantsCaptureKeyboard() const
Same as WantsCaptureMouse() but for the keyboard.
void ToggleVisible()
Flip overlay visibility.
bool Init(GLFWwindow *window)
Initialise ImGui against the given GLFW window. Call once at startup.
void BeginFrame()
Call after glfwPollEvents.
bool WantsCaptureMouse() const
void Draw()
Builds all panels (no GL state changes yet).
PostProcess & GetPostProcess()
Outline / highlight post-pass; tunables exposed via the Editor render panel.
Scene * GetScene()
Returns the currently active scene, or nullptr if none is set.
InputManager & GetInputManager()
Returns the InputManager that tracks keyboard and mouse state.
static Engine & GetInstance()
Returns the single Engine instance (created on first call).
RenderSettings & GetRenderSettings()
Mutable render settings edited by the Editor's Render panel.
PhysicsManager & GetPhysicsManager()
Node in the scene graph: a named transform that owns components and children.
const vec3 & GetScale() const
const vec3 & GetPosition() const
void SetActive(bool active)
Show/hide this object (and stop Update calls).
void SetPosition(const vec3 &pos)
const str & GetName() const
Returns the object's display name.
void SetRotation(const quat &rot)
void SetScale(const vec3 &scale)
const quat & GetRotation() const
bool IsActive() const
Returns false if the object is hidden.
T * GetComponent()
Find and return the first component of type T attached to this object.
Point light that contributes position + colour to the render pass.
void SetColor(const vec3 &color)
Set the light's emission colour.
const vec3 & GetColor() const
Returns the linear RGB colour of this light (default: white {1,1,1}).
Makes a GameObject renderable by submitting its mesh each frame.
btDiscreteDynamicsWorld * GetWorld()
Raw pointer to the underlying Bullet world (for advanced/internal use).
void SetMoveSpeed(f32 speed)
f32 GetSensitivity() const
void SetSensitivity(f32 s)
glm::vec2 vec2
2-component float vector (e.g. UV coordinates, mouse position).
glm::vec3 vec3
3-component float vector (e.g. world position, RGB colour, normals).
glm::quat quat
Unit quaternion for rotation (avoids gimbal lock).
double f64
64-bit double — used for high-precision timing.
float f32
32-bit IEEE float — the standard GL scalar type.
DebugView
Renderer tuning struct passed to the post-process shader each frame.
static void InspectPlayer(PlayerControllerComponent *c)
static void InspectTransform(GameObject *obj)
void LogClear()
Clear the in-memory log buffer.
static void InspectMesh(MeshComponent *)
static void InspectLight(LightComponent *c)
static void InspectCamera(CameraComponent *c)
const std::deque< LogEntry > & LogGetEntries()
Snapshot of the in-memory log buffer (bounded ring).