feat: Add "simple" inspector to the imgui panels

This commit is contained in:
2026-08-12 00:59:04 +02:00
parent 662940793e
commit 8c7302de49
26 changed files with 649 additions and 19 deletions
+3
View File
@@ -12,6 +12,8 @@ set(SRC_FILES
"src/Components/Physics/Rigidbody.cpp" "src/Components/Physics/Rigidbody.cpp"
"src/Components/Physics/BoxCollider.cpp" "src/Components/Physics/BoxCollider.cpp"
"src/Components/Physics/CapsuleCollider.cpp"
"src/Components/Physics/Collider.cpp"
"src/Components/Physics/SphereCollider.cpp" "src/Components/Physics/SphereCollider.cpp"
"src/Graphics/BindlessSetManager.cpp" "src/Graphics/BindlessSetManager.cpp"
@@ -64,6 +66,7 @@ set(SRC_FILES
"src/Serialization/ComponentRegistry.cpp" "src/Serialization/ComponentRegistry.cpp"
"src/Util/DeltaTime.cpp" "src/Util/DeltaTime.cpp"
"src/Util/ImGuiUtils.cpp"
"src/Physics/PhysicsWorld.cpp" "src/Physics/PhysicsWorld.cpp"
"src/Physics/SimplePhysicsWorld.cpp" "src/Physics/SimplePhysicsWorld.cpp"
@@ -17,6 +17,7 @@ public:
nlohmann::json Serialize() const override; nlohmann::json Serialize() const override;
void Deserialize(const nlohmann::json& data) override; void Deserialize(const nlohmann::json& data) override;
void ResolveReferences(const ObjectMap& objects) override; void ResolveReferences(const ObjectMap& objects) override;
void ImGuiInspector() override;
void SetMeshID(MeshID id); void SetMeshID(MeshID id);
MeshID GetMeshID() const { return meshID; } MeshID GetMeshID() const { return meshID; }
@@ -28,6 +28,7 @@ public:
nlohmann::json Serialize() const override; nlohmann::json Serialize() const override;
void Deserialize(const nlohmann::json& data) override; void Deserialize(const nlohmann::json& data) override;
void ImGuiInspector() override;
// optional setters // optional setters
void SetRadius(float r) { m_Radius = r; } void SetRadius(float r) { m_Radius = r; }
@@ -13,6 +13,7 @@ public:
nlohmann::json Serialize() const override; nlohmann::json Serialize() const override;
void Deserialize(const nlohmann::json& data) override; void Deserialize(const nlohmann::json& data) override;
void ImGuiInspector() override;
std::string GetTypeName() const override std::string GetTypeName() const override
{ {
@@ -17,6 +17,8 @@ public:
return "CapsuleCollider"; return "CapsuleCollider";
} }
void ImGuiInspector() override;
nlohmann::json Serialize() const override { nlohmann::json Serialize() const override {
auto data = SerializeCollider(); auto data = SerializeCollider();
data["radius"] = m_Radius; data["radius"] = m_Radius;
@@ -12,6 +12,8 @@ public:
~Collider() override = default; ~Collider() override = default;
void ImGuiInspector() override;
[[nodiscard]] virtual PhysicsShapeDesc BuildPhysicsShape() const = 0; [[nodiscard]] virtual PhysicsShapeDesc BuildPhysicsShape() const = 0;
[[nodiscard]] bool IsTrigger() const { return m_IsTrigger; } [[nodiscard]] bool IsTrigger() const { return m_IsTrigger; }
@@ -21,6 +21,7 @@ public:
nlohmann::json Serialize() const override; nlohmann::json Serialize() const override;
void Deserialize(const nlohmann::json& data) override; void Deserialize(const nlohmann::json& data) override;
void ImGuiInspector() override;
std::string GetTypeName() const override std::string GetTypeName() const override
{ {
@@ -14,6 +14,7 @@ public:
nlohmann::json Serialize() const override; nlohmann::json Serialize() const override;
void Deserialize(const nlohmann::json& data) override; void Deserialize(const nlohmann::json& data) override;
void ImGuiInspector() override;
std::string GetTypeName() const override std::string GetTypeName() const override
{ {
return "SphereCollider"; return "SphereCollider";
@@ -22,6 +22,7 @@ public:
nlohmann::json Serialize() const override; nlohmann::json Serialize() const override;
void Deserialize(const nlohmann::json& data) override; void Deserialize(const nlohmann::json& data) override;
void ImGuiInspector() override;
void SetDistance(float distance); void SetDistance(float distance);
void SetSpeed(float speed) { m_Speed = speed; } void SetSpeed(float speed) { m_Speed = speed; }
@@ -20,6 +20,7 @@ public:
nlohmann::json Serialize() const override; nlohmann::json Serialize() const override;
void Deserialize(const nlohmann::json& data) override; void Deserialize(const nlohmann::json& data) override;
void ImGuiInspector() override;
void SetAxis(const glm::vec3& axis) { void SetAxis(const glm::vec3& axis) {
m_Axis = glm::dot(axis, axis) > 0.000001f m_Axis = glm::dot(axis, axis) > 0.000001f
@@ -39,6 +39,8 @@ public:
virtual void LateUpdate(float dt); virtual void LateUpdate(float dt);
virtual void FixedUpdate(float fixedDt); virtual void FixedUpdate(float fixedDt);
// Called by the editor inspector while this component is selected. A
// component owns the controls for its editable runtime properties.
virtual void ImGuiInspector(); virtual void ImGuiInspector();
virtual void ImGuiRender(); virtual void ImGuiRender();
+2
View File
@@ -4,6 +4,7 @@
#include <functional> #include <functional>
#include <destrum/Event.h> #include <destrum/Event.h>
#include <destrum/ObjectModel/ObjectId.h>
#include <destrum/Scene/SceneManager.h> #include <destrum/Scene/SceneManager.h>
#include "destrum/Physics/JoltPhysicsWorld.h" #include "destrum/Physics/JoltPhysicsWorld.h"
@@ -120,6 +121,7 @@ private:
//Imgui vars //Imgui vars
bool m_ShowDemoWindow{false}; bool m_ShowDemoWindow{false};
ObjectId m_SelectedObjectId{InvalidObjectId};
std::function<void()> m_registerBindings; std::function<void()> m_registerBindings;
std::function<void()> m_unregisterBindings; std::function<void()> m_unregisterBindings;
+27
View File
@@ -0,0 +1,27 @@
#ifndef DESTRUM_IMGUIUTILS_H
#define DESTRUM_IMGUIUTILS_H
#include <memory>
#include <string_view>
#include <vector>
#include <glm/glm.hpp>
#include <destrum/ObjectModel/ObjectId.h>
class GameObject;
namespace ImGuiUtils {
[[nodiscard]] bool DrawVec3Control(
const char* label,
glm::vec3& value,
float speed = 0.1f);
void RenderSceneInspector(
std::string_view sceneName,
const std::vector<std::shared_ptr<GameObject>>& objects,
const std::vector<std::shared_ptr<GameObject>>& pendingAdditions,
ObjectId& selectedObjectId);
}
#endif // DESTRUM_IMGUIUTILS_H
+2
View File
@@ -194,6 +194,8 @@ void App::run()
{ {
ZoneScopedN("Debug ImGui"); ZoneScopedN("Debug ImGui");
SceneManager::GetInstance().RenderImgui();
ImGui::Begin("Debug"); ImGui::Begin("Debug");
ImGui::Text("FPS: %.2f", avgFPS); ImGui::Text("FPS: %.2f", avgFPS);
ImGui::End(); ImGui::End();
+53 -18
View File
@@ -8,6 +8,7 @@
#include <glm/gtc/matrix_transform.hpp> #include <glm/gtc/matrix_transform.hpp>
#include <glm/gtc/quaternion.hpp> #include <glm/gtc/quaternion.hpp>
#include <algorithm> #include <algorithm>
#include <imgui.h>
#include <optional> #include <optional>
#include <sstream> #include <sstream>
@@ -327,24 +328,58 @@ void Animator::Update(float dt) {
} }
void Animator::ImGuiInspector() { void Animator::ImGuiInspector() {
// ImGui::Text("Clip: %s", m_currentClipName.empty() ? "(none)" : m_currentClipName.c_str()); if (m_skeletonAsset.empty()) {
// ImGui::Text("Time: %.2f", m_current.time); ImGui::TextDisabled("Skeleton: <none>");
// ImGui::Text("Blend: %.2f", m_blendT); } else {
// ImGui::TextWrapped("Skeleton: %s", m_skeletonAsset.path.c_str());
// if (!isPlaying()) { if (!m_skeletonAsset.subresource.empty()) {
// ImGui::BeginDisabled(); ImGui::TextWrapped(
// ImGui::Button("Stop"); "Skeleton subresource: %s",
// ImGui::EndDisabled(); m_skeletonAsset.subresource.c_str());
// } else if (ImGui::Button("Stop")) { }
// stop(); }
// } ImGui::Text("Joints: %zu", m_skeleton.joints.size());
//
// ImGui::Separator(); if (!m_referencesResolved) {
// ImGui::Text("Clips:"); ImGui::TextDisabled("Asset references are waiting to be resolved.");
// for (auto& [name, _] : m_clips) { }
// if (ImGui::Selectable(name.c_str(), name == m_currentClipName))
// play(name); if (m_current.clip == nullptr) {
// } ImGui::TextDisabled("Playback: stopped");
} else {
ImGui::Text("Playing: %s", m_currentClipName.c_str());
float speed = m_current.speed;
if (ImGui::DragFloat("Playback speed", &speed, 0.05f)) {
m_current.speed = speed;
}
ImGui::Text("Time: %.2f / %.2f s", m_current.time, m_current.clip->duration);
ImGui::Text("Blend: %.2f", m_blendT);
}
const bool wasPlaying = isPlaying();
if (!wasPlaying) {
ImGui::BeginDisabled();
}
if (ImGui::Button("Stop")) {
stop();
}
if (!wasPlaying) {
ImGui::EndDisabled();
}
ImGui::Separator();
ImGui::Text("Clips: %zu", m_clips.size());
for (const std::string& name : m_clipOrder) {
if (!m_clips.contains(name)) {
continue;
}
ImGui::PushID(name.c_str());
if (ImGui::Selectable(name.c_str(), name == m_currentClipName)) {
play(name, 0.15f);
}
ImGui::PopID();
}
} }
void Animator::addClip(std::shared_ptr<SkeletalAnimation> clip) { void Animator::addClip(std::shared_ptr<SkeletalAnimation> clip) {
@@ -8,6 +8,8 @@
#include "destrum/ObjectModel/GameObject.h" #include "destrum/ObjectModel/GameObject.h"
#include "destrum/Util/GameState.h" #include "destrum/Util/GameState.h"
#include <imgui.h>
#include <stdexcept> #include <stdexcept>
MeshRendererComponent::MeshRendererComponent(GameObject& parent): Component(parent, "MeshRendererComponent") { MeshRendererComponent::MeshRendererComponent(GameObject& parent): Component(parent, "MeshRendererComponent") {
@@ -139,6 +141,30 @@ void MeshRendererComponent::ResolveReferences(const ObjectMap&) {
} }
} }
void MeshRendererComponent::ImGuiInspector() {
if (meshID == NULL_MESH_ID) {
ImGui::TextDisabled("Mesh: <none>");
} else {
ImGui::Text("Mesh ID: %zu", meshID);
ImGui::TextWrapped(
"Mesh key: %s",
meshKey.empty() ? "<not resolved>" : meshKey.c_str());
}
if (materialID == NULL_MATERIAL_ID) {
ImGui::TextDisabled("Material: <none>");
} else {
ImGui::Text("Material ID: %u", materialID);
ImGui::TextWrapped(
"Material key: %s",
materialKey.empty() ? "<not resolved>" : materialKey.c_str());
}
ImGui::Text(
"Skinning: %s",
m_skinnedMesh != nullptr ? "active" : "static");
}
void MeshRendererComponent::SetMeshID(MeshID id) { void MeshRendererComponent::SetMeshID(MeshID id) {
meshID = id; meshID = id;
meshKey.clear(); meshKey.clear();
+66
View File
@@ -9,6 +9,11 @@
#include "destrum/Components/MeshRendererComponent.h" #include "destrum/Components/MeshRendererComponent.h"
#include "destrum/Util/GameState.h" #include "destrum/Util/GameState.h"
#include "destrum/Util/DeltaTime.h" #include "destrum/Util/DeltaTime.h"
#include "destrum/Util/ImGuiUtils.h"
#include <imgui.h>
#include <algorithm>
static glm::vec3 RandomUnitVector(std::mt19937& rng) static glm::vec3 RandomUnitVector(std::mt19937& rng)
{ {
@@ -175,3 +180,64 @@ void OrbitAndSpin::Deserialize(const nlohmann::json& data) {
} }
BuildOrbitBasis(); BuildOrbitBasis();
} }
void OrbitAndSpin::ImGuiInspector() {
float radius = m_Radius;
if (ImGui::DragFloat("Radius", &radius, 0.05f, 0.0f)) {
SetRadius(radius);
}
glm::vec3 center = m_Center;
if (ImGuiUtils::DrawVec3Control("Center", center)) {
SetCenter(center);
}
glm::vec3 orbitAxis = m_OrbitAxis;
if (ImGuiUtils::DrawVec3Control("Orbit axis", orbitAxis)) {
if (glm::dot(orbitAxis, orbitAxis) >= 1e-8f) {
m_OrbitAxis = glm::normalize(orbitAxis);
BuildOrbitBasis();
}
}
float orbitSpeed = m_OrbitSpeed;
if (ImGui::DragFloat("Orbit speed", &orbitSpeed, 0.05f)) {
m_OrbitSpeed = orbitSpeed;
}
ImGui::DragFloat("Orbit phase", &m_OrbitPhase, 0.05f);
ImGui::Text("Orbit angle: %.2f rad", m_OrbitAngle);
float growSpeed = m_GrowSpeed;
if (ImGui::DragFloat("Grow speed", &growSpeed, 0.05f, 0.0f)) {
m_GrowSpeed = growSpeed;
}
float growMin = m_GrowMin;
if (ImGui::DragFloat("Grow min", &growMin, 0.01f, 0.0f)) {
m_GrowMin = std::min(growMin, m_GrowMax);
}
float growMax = m_GrowMax;
if (ImGui::DragFloat("Grow max", &growMax, 0.01f, 0.0f)) {
m_GrowMax = std::max(growMax, m_GrowMin);
}
glm::vec3 baseScale = m_BaseScale;
if (ImGuiUtils::DrawVec3Control("Base scale", baseScale, 0.05f)) {
m_BaseScale = baseScale;
}
glm::vec3 spinAxis = m_SpinAxis;
if (ImGuiUtils::DrawVec3Control("Spin axis", spinAxis)) {
if (glm::dot(spinAxis, spinAxis) >= 1e-8f) {
m_SpinAxis = glm::normalize(spinAxis);
}
}
float spinSpeed = m_SpinSpeed;
if (ImGui::DragFloat("Spin speed", &spinSpeed, 0.05f)) {
m_SpinSpeed = spinSpeed;
}
ImGui::Text("Grow phase: %.2f rad", m_GrowPhase);
ImGui::Text("Material ID: %u", m_MaterialID);
}
@@ -1,5 +1,9 @@
#include <destrum/Components/Physics/BoxCollider.h> #include <destrum/Components/Physics/BoxCollider.h>
#include <destrum/Util/ImGuiUtils.h>
#include <imgui.h>
nlohmann::json BoxCollider::Serialize() const { nlohmann::json BoxCollider::Serialize() const {
auto data = SerializeCollider(); auto data = SerializeCollider();
data["halfExtents"] = {m_HalfExtents.x, m_HalfExtents.y, m_HalfExtents.z}; data["halfExtents"] = {m_HalfExtents.x, m_HalfExtents.y, m_HalfExtents.z};
@@ -17,3 +21,12 @@ void BoxCollider::Deserialize(const nlohmann::json& data) {
}); });
} }
} }
void BoxCollider::ImGuiInspector() {
Collider::ImGuiInspector();
glm::vec3 halfExtents = m_HalfExtents;
if (ImGuiUtils::DrawVec3Control("Half extents", halfExtents)) {
SetHalfExtents(halfExtents);
}
}
@@ -0,0 +1,17 @@
#include <destrum/Components/Physics/CapsuleCollider.h>
#include <imgui.h>
void CapsuleCollider::ImGuiInspector() {
Collider::ImGuiInspector();
float radius = m_Radius;
if (ImGui::DragFloat("Radius", &radius, 0.05f, 0.0001f)) {
SetRadius(radius);
}
float height = m_Height;
if (ImGui::DragFloat("Height", &height, 0.05f, 0.0001f)) {
SetHeight(height);
}
}
@@ -0,0 +1,17 @@
#include <destrum/Components/Physics/Collider.h>
#include <imgui.h>
#include <destrum/Util/ImGuiUtils.h>
void Collider::ImGuiInspector() {
bool isTrigger = m_IsTrigger;
if (ImGui::Checkbox("Is trigger", &isTrigger)) {
SetTrigger(isTrigger);
}
glm::vec3 centerOffset = m_CenterOffset;
if (ImGuiUtils::DrawVec3Control("Center offset", centerOffset)) {
SetCenterOffset(centerOffset);
}
}
@@ -4,6 +4,8 @@
#include <destrum/ObjectModel/GameObject.h> #include <destrum/ObjectModel/GameObject.h>
#include <destrum/Scene/Scene.h> #include <destrum/Scene/Scene.h>
#include <imgui.h>
namespace { namespace {
[[nodiscard]] const char* RigidbodyTypeName(RigidbodyType type) { [[nodiscard]] const char* RigidbodyTypeName(RigidbodyType type) {
switch (type) { switch (type) {
@@ -128,3 +130,48 @@ void Rigidbody::Deserialize(const nlohmann::json& data) {
m_AllowSleep = data.at("allowSleep").get<bool>(); m_AllowSleep = data.at("allowSleep").get<bool>();
} }
} }
void Rigidbody::ImGuiInspector() {
static const char* typeNames[] = {"Static", "Dynamic", "Kinematic"};
int type = static_cast<int>(m_Type);
if (ImGui::Combo("Body type", &type, typeNames, IM_ARRAYSIZE(typeNames))) {
SetType(static_cast<RigidbodyType>(type));
}
float mass = m_Mass;
if (ImGui::DragFloat("Mass", &mass, 0.1f, 0.0001f)) {
SetMass(mass);
}
float friction = m_Friction;
if (ImGui::DragFloat("Friction", &friction, 0.01f, 0.0f)) {
SetFriction(friction);
}
float restitution = m_Restitution;
if (ImGui::SliderFloat("Restitution", &restitution, 0.0f, 1.0f)) {
SetRestitution(restitution);
}
bool useGravity = m_UseGravity;
if (ImGui::Checkbox("Use gravity", &useGravity)) {
SetUseGravity(useGravity);
}
bool allowSleep = m_AllowSleep;
if (ImGui::Checkbox("Allow sleep", &allowSleep)) {
SetAllowSleep(allowSleep);
}
if (HasPhysicsBody()) {
const glm::vec3 velocity = GetLinearVelocity();
ImGui::Text(
"Linear velocity: %.2f, %.2f, %.2f",
velocity.x,
velocity.y,
velocity.z);
ImGui::Text("Physics body: %u", m_Body.id);
} else {
ImGui::TextDisabled("Physics body: not registered");
}
}
@@ -1,5 +1,7 @@
#include <destrum/Components/Physics/SphereCollider.h> #include <destrum/Components/Physics/SphereCollider.h>
#include <imgui.h>
nlohmann::json SphereCollider::Serialize() const { nlohmann::json SphereCollider::Serialize() const {
auto data = SerializeCollider(); auto data = SerializeCollider();
data["radius"] = m_Radius; data["radius"] = m_Radius;
@@ -12,3 +14,12 @@ void SphereCollider::Deserialize(const nlohmann::json& data) {
SetRadius(data.at("radius").get<float>()); SetRadius(data.at("radius").get<float>());
} }
} }
void SphereCollider::ImGuiInspector() {
Collider::ImGuiInspector();
float radius = m_Radius;
if (ImGui::DragFloat("Radius", &radius, 0.05f, 0.0001f)) {
SetRadius(radius);
}
}
+27
View File
@@ -6,6 +6,9 @@
#include <glm/gtc/quaternion.hpp> // glm::quat, glm::angleAxis #include <glm/gtc/quaternion.hpp> // glm::quat, glm::angleAxis
#include <glm/gtx/quaternion.hpp> // operator*(quat, vec3) #include <glm/gtx/quaternion.hpp> // operator*(quat, vec3)
#include <destrum/Util/DeltaTime.h> #include <destrum/Util/DeltaTime.h>
#include <destrum/Util/ImGuiUtils.h>
#include <imgui.h>
glm::vec3 Rotator::MakePerpendicularUnitVector(const glm::vec3& axis) glm::vec3 Rotator::MakePerpendicularUnitVector(const glm::vec3& axis)
{ {
@@ -128,3 +131,27 @@ void Rotator::Deserialize(const nlohmann::json& data) {
m_InitialOffset = {value.at(0).get<float>(), value.at(1).get<float>(), value.at(2).get<float>()}; m_InitialOffset = {value.at(0).get<float>(), value.at(1).get<float>(), value.at(2).get<float>()};
} }
} }
void Rotator::ImGuiInspector() {
float distance = m_Distance;
if (ImGui::DragFloat("Distance", &distance, 0.05f, 0.0f)) {
SetDistance(distance);
}
float speed = m_Speed;
if (ImGui::DragFloat("Speed", &speed, 0.05f)) {
SetSpeed(speed);
}
glm::vec3 pivot = m_Pivot;
if (ImGuiUtils::DrawVec3Control("Pivot", pivot)) {
SetPivotPosition(pivot);
}
glm::vec3 axis = m_Axis;
if (ImGuiUtils::DrawVec3Control("Axis", axis)) {
SetAxis(axis);
}
ImGui::Text("Current angle: %.2f rad", m_CurrentAngle);
}
+17
View File
@@ -4,6 +4,9 @@
#include "destrum/ObjectModel/Transform.h" #include "destrum/ObjectModel/Transform.h"
#include "destrum/Util/DeltaTime.h" #include "destrum/Util/DeltaTime.h"
#include "destrum/Util/ImGuiUtils.h"
#include <imgui.h>
void Spinner::Update(float dt) void Spinner::Update(float dt)
{ {
@@ -33,3 +36,17 @@ void Spinner::Deserialize(const nlohmann::json& data) {
m_Angle = data.at("angle").get<float>(); m_Angle = data.at("angle").get<float>();
} }
} }
void Spinner::ImGuiInspector() {
glm::vec3 axis = m_Axis;
if (ImGuiUtils::DrawVec3Control("Axis", axis)) {
SetAxis(axis);
}
float speed = m_Speed;
if (ImGui::DragFloat("Speed", &speed, 0.05f)) {
SetSpeed(speed);
}
ImGui::Text("Current angle: %.2f rad", m_Angle);
}
+6 -1
View File
@@ -1,6 +1,6 @@
#include <destrum/Scene/Scene.h> #include <destrum/Scene/Scene.h>
#include <destrum/ObjectModel/GameObject.h> #include <destrum/ObjectModel/GameObject.h>
#include <destrum/Util/DeltaTime.h> #include <destrum/Util/ImGuiUtils.h>
#include <algorithm> #include <algorithm>
@@ -160,6 +160,11 @@ void Scene::Render(const RenderContext& ctx) {
} }
void Scene::RenderImgui() { void Scene::RenderImgui() {
ImGuiUtils::RenderSceneInspector(
m_name,
m_objects,
m_pendingAdditions,
m_SelectedObjectId);
} }
void Scene::CleanupDestroyedGameObjects() { void Scene::CleanupDestroyedGameObjects() {
+302
View File
@@ -0,0 +1,302 @@
#include <destrum/Util/ImGuiUtils.h>
#include <algorithm>
#include <cfloat>
#include <string>
#include <unordered_set>
#include <vector>
#include <imgui.h>
#include <glm/gtc/type_ptr.hpp>
#include <glm/gtx/quaternion.hpp>
#include <destrum/ObjectModel/Component.h>
#include <destrum/ObjectModel/GameObject.h>
#include <destrum/ObjectModel/Transform.h>
namespace {
[[nodiscard]] bool IsInspectableObject(const GameObject* object) {
return object != nullptr && !object->IsBeingDestroyed();
}
[[nodiscard]] bool HasInspectableChildren(const GameObject& object) {
return std::ranges::any_of(object.GetTransform().GetChildren(),
[](const Transform* child) {
return child != nullptr && IsInspectableObject(child->GetOwner());
});
}
void DrawGameObjectNode(
GameObject& object,
ObjectId& selectedObjectId,
std::unordered_set<const GameObject*>& visited,
bool defaultOpen = false) {
if (!IsInspectableObject(&object) || !visited.insert(&object).second) {
return;
}
const bool hasChildren = HasInspectableChildren(object);
ImGuiTreeNodeFlags treeFlags = ImGuiTreeNodeFlags_SpanAvailWidth;
if (object.GetId() == selectedObjectId) {
treeFlags |= ImGuiTreeNodeFlags_Selected;
}
if (defaultOpen) {
treeFlags |= ImGuiTreeNodeFlags_DefaultOpen;
}
if (!hasChildren) {
treeFlags |= ImGuiTreeNodeFlags_Leaf | ImGuiTreeNodeFlags_NoTreePushOnOpen;
}
const std::string displayName = object.GetName().empty()
? "<unnamed>"
: object.GetName();
const bool isOpen = ImGui::TreeNodeEx(
static_cast<const void*>(&object),
treeFlags,
"%s",
displayName.c_str());
if (ImGui::IsItemClicked()) {
selectedObjectId = object.GetId();
}
if (!object.IsActive()) {
ImGui::SameLine();
ImGui::TextDisabled("[inactive]");
} else if (!object.IsActiveInHierarchy()) {
ImGui::SameLine();
ImGui::TextDisabled("[parent inactive]");
}
if (isOpen && hasChildren) {
for (Transform* childTransform : object.GetTransform().GetChildren()) {
if (childTransform == nullptr) {
continue;
}
GameObject* child = childTransform->GetOwner();
if (IsInspectableObject(child)) {
DrawGameObjectNode(*child, selectedObjectId, visited);
}
}
ImGui::TreePop();
}
}
[[nodiscard]] GameObject* FindSelectedObject(
const std::vector<GameObject*>& objects,
ObjectId selectedObjectId) {
const auto it = std::find_if(
objects.begin(),
objects.end(),
[selectedObjectId](const GameObject* object) {
return object != nullptr && object->GetId() == selectedObjectId;
});
return it == objects.end() ? nullptr : *it;
}
}
namespace ImGuiUtils {
bool DrawVec3Control(const char* label, glm::vec3& value, float speed) {
if (label == nullptr) {
return false;
}
ImGui::PushID(label);
ImGui::AlignTextToFramePadding();
ImGui::TextUnformatted(label);
ImGui::SameLine();
ImGui::SetNextItemWidth(-FLT_MIN);
const bool changed = ImGui::DragFloat3(
"##value",
glm::value_ptr(value),
speed);
ImGui::PopID();
return changed;
}
void RenderSceneInspector(
std::string_view sceneName,
const std::vector<std::shared_ptr<GameObject>>& objectsSource,
const std::vector<std::shared_ptr<GameObject>>& pendingAdditions,
ObjectId& selectedObjectId) {
std::vector<GameObject*> objects;
objects.reserve(objectsSource.size() + pendingAdditions.size());
const auto appendInspectableObjects = [&objects](const auto& source) {
for (const auto& object : source) {
if (IsInspectableObject(object.get())) {
objects.push_back(object.get());
}
}
};
appendInspectableObjects(objectsSource);
appendInspectableObjects(pendingAdditions);
std::vector<GameObject*> roots;
roots.reserve(objects.size());
for (GameObject* object : objects) {
Transform* parent = object->GetTransform().GetParent();
if (parent == nullptr || !IsInspectableObject(parent->GetOwner())) {
roots.push_back(object);
}
}
if (FindSelectedObject(objects, selectedObjectId) == nullptr) {
selectedObjectId = InvalidObjectId;
}
ImGui::SetNextWindowSize(ImVec2(640.0f, 520.0f), ImGuiCond_FirstUseEver);
if (!ImGui::Begin("Inspector")) {
ImGui::End();
return;
}
const std::string displaySceneName = sceneName.empty()
? "<unnamed>"
: std::string{sceneName};
ImGui::Text("Scene: %s", displaySceneName.c_str());
ImGui::SameLine();
ImGui::TextDisabled("%zu GameObjects | %zu roots", objects.size(), roots.size());
ImGui::Separator();
const float availableWidth = ImGui::GetContentRegionAvail().x;
const float hierarchyWidth = std::max(180.0f, availableWidth * 0.45f);
if (ImGui::BeginChild(
"SceneHierarchy",
ImVec2(hierarchyWidth, 0.0f),
ImGuiChildFlags_Borders)) {
std::unordered_set<const GameObject*> visited;
visited.reserve(objects.size());
for (GameObject* root : roots) {
DrawGameObjectNode(*root, selectedObjectId, visited, true);
}
// Keep malformed or partially loaded hierarchies visible instead of
// silently dropping objects whose parent is not in this list.
bool hasUnvisitedObjects = false;
for (GameObject* object : objects) {
if (!visited.contains(object)) {
if (!hasUnvisitedObjects) {
ImGui::Separator();
ImGui::TextDisabled("OTHER OBJECTS");
hasUnvisitedObjects = true;
}
DrawGameObjectNode(*object, selectedObjectId, visited);
}
}
if (objects.empty()) {
ImGui::TextDisabled("This scene has no GameObjects.");
}
}
ImGui::EndChild();
GameObject* selectedObject = FindSelectedObject(objects, selectedObjectId);
if (selectedObject == nullptr) {
selectedObjectId = InvalidObjectId;
}
ImGui::SameLine();
if (ImGui::BeginChild(
"ObjectInspector",
ImVec2(0.0f, 0.0f),
ImGuiChildFlags_Borders)) {
if (selectedObject == nullptr) {
ImGui::TextDisabled("Select a GameObject to inspect it.");
} else {
const std::string displayName = selectedObject->GetName().empty()
? "<unnamed>"
: selectedObject->GetName();
ImGui::TextUnformatted(displayName.c_str());
ImGui::TextDisabled(
"GameObject | ID %llu",
static_cast<unsigned long long>(selectedObject->GetId()));
ImGui::Separator();
bool active = selectedObject->IsActive();
if (ImGui::Checkbox("Active", &active)) {
selectedObject->SetActive(active);
}
const Transform* parent = selectedObject->GetTransform().GetParent();
if (parent != nullptr && parent->GetOwner() != nullptr) {
ImGui::Text("Parent: %s", parent->GetOwner()->GetName().c_str());
} else {
ImGui::Text("Parent: <scene root>");
}
ImGui::Text("Children: %d", selectedObject->GetTransform().GetChildCount());
if (ImGui::CollapsingHeader(
"Transform",
ImGuiTreeNodeFlags_DefaultOpen)) {
Transform& transform = selectedObject->GetTransform();
bool transformChanged = false;
glm::vec3 localPosition = transform.GetLocalPosition();
if (DrawVec3Control("Position", localPosition, 0.05f)) {
transform.SetLocalPosition(localPosition);
transformChanged = true;
}
glm::vec3 localRotation = glm::degrees(
glm::eulerAngles(transform.GetLocalRotation()));
if (DrawVec3Control("Rotation", localRotation, 0.5f)) {
transform.SetLocalRotation(localRotation);
transformChanged = true;
}
glm::vec3 localScale = transform.GetLocalScale();
if (DrawVec3Control("Scale", localScale, 0.05f)) {
transform.SetLocalScale(localScale);
transformChanged = true;
}
if (transformChanged) {
selectedObject->RefreshPhysics();
}
const glm::vec3& worldPosition = transform.GetWorldPosition();
ImGui::Text(
"World position: %.2f, %.2f, %.2f",
worldPosition.x,
worldPosition.y,
worldPosition.z);
}
if (ImGui::CollapsingHeader(
"Components",
ImGuiTreeNodeFlags_DefaultOpen)) {
bool hasComponents = false;
for (const auto& component : selectedObject->GetComponents()) {
if (component == nullptr || component->IsBeingDestroyed()) {
continue;
}
hasComponents = true;
ImGui::PushID(component.get());
const std::string typeName = component->GetTypeName();
if (ImGui::CollapsingHeader(typeName.c_str())) {
bool enabled = component->isEnabled();
if (ImGui::Checkbox("Enabled", &enabled)) {
component->SetEnabled(enabled);
}
component->ImGuiInspector();
}
ImGui::PopID();
}
if (!hasComponents) {
ImGui::TextDisabled("No components.");
}
}
}
}
ImGui::EndChild();
ImGui::End();
}
}