#include #include #include #include #include #include #include #include #include #include #include #include #include #include #include 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& 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() ? "" : object.GetName(); const bool isOpen = ImGui::TreeNodeEx( static_cast(&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& 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>& objectsSource, const std::vector>& pendingAdditions, ObjectId& selectedObjectId) { RegisterEngineComponents(); std::vector 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 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() ? "" : 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 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() ? "" : selectedObject->GetName(); ImGui::TextUnformatted(displayName.c_str()); ImGui::TextDisabled( "GameObject | ID %llu", static_cast(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: "); } 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); } const bool componentsOpen = ImGui::CollapsingHeader( "Components", ImGuiTreeNodeFlags_DefaultOpen); if (componentsOpen) { if (ImGui::Button("+ Add Component")) { ImGui::OpenPopup("AddComponentPopup"); } std::string addComponentError; if (ImGui::BeginPopup("AddComponentPopup")) { std::vector componentTypes = ComponentFactory::GetRegisteredTypeNames(); std::sort(componentTypes.begin(), componentTypes.end()); bool hasAvailableComponent = false; for (const std::string& componentType : componentTypes) { const bool alreadyAdded = std::any_of( selectedObject->GetComponents().begin(), selectedObject->GetComponents().end(), [&componentType](const auto& component) { return component != nullptr && !component->IsBeingDestroyed() && component->GetTypeName() == componentType; }); if (alreadyAdded) { const std::string label = componentType + " (already added)"; ImGui::BeginDisabled(); ImGui::MenuItem(label.c_str()); ImGui::EndDisabled(); continue; } hasAvailableComponent = true; if (ImGui::MenuItem(componentType.c_str())) { try { if (ComponentFactory::Create( componentType, *selectedObject) == nullptr) { addComponentError = "Component type is not registered: " + componentType; } else { ImGui::CloseCurrentPopup(); } } catch (const std::exception& exception) { addComponentError = exception.what(); } } } if (!hasAvailableComponent) { ImGui::TextDisabled("All components are already added."); } if (!addComponentError.empty()) { ImGui::TextWrapped("Failed to add component: %s", addComponentError.c_str()); } ImGui::EndPopup(); } 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(); } }