// // HierarchyPanel.cpp:实体层级树实现(UE Outliner 语义)。 // // 树形展示(根 = 无有效父链接的实体,子 = HierarchyComponent 挂载序)、 // 点击选中、拖拽改父/拖到空白区挂根(keepWorld = 挂载前后世界位姿 // 不变)、右键 Create Child / Detach / Delete(级联随葬,延迟销毁)。 // #include "HierarchyPanel.h" #include "Engine/ECS/Components.h" #include "Engine/ECS/EntityHandle.h" // CreateEntity 返回完整 EntityHandle #include #include #include #include using namespace Engine; namespace { // 拖拽 payload 类型名(Editor 内部约定;实体位模式 4 字节) constexpr const char* kHierPayload = "HIER_ENTITY"; // 实体显示名 std::string DisplayName(const entt::registry& reg, entt::entity e) { if (const auto* n = reg.try_get(e); n && !n->name.empty()) { return n->name; } return "(unnamed)"; } // 递归绘制单节点(含子树)。返回值无意义,选中写 selected void DrawEntityNode(const EditorContext& ctx, entt::entity e, entt::entity& selected) { auto& reg = ctx.scene->GetRegistry(); // 稳定行 ID(UUID):折叠态跨帧保持 + 与 Inspector PushID 隔离 u64 rowId = static_cast(static_cast(e)); if (const auto* id = reg.try_get(e)) { rowId = id->uuid.Value(); } ImGui::PushID(reinterpret_cast(rowId)); // 子表(过滤悬空 + 无 Name 的防御性兜底) const auto* h = reg.try_get(e); std::vector kids; if (h) { for (entt::entity c : h->children) { if (reg.valid(c) && reg.all_of(c)) { kids.push_back(c); } } } const std::string label = DisplayName(reg, e); const ImGuiTreeNodeFlags flags = ImGuiTreeNodeFlags_OpenOnArrow | ImGuiTreeNodeFlags_SpanAvailWidth | (kids.empty() ? ImGuiTreeNodeFlags_Leaf : 0) | (selected == e ? ImGuiTreeNodeFlags_Selected : 0); const bool opened = ImGui::TreeNodeEx(label.c_str(), flags); const bool toggled = ImGui::IsItemToggledOpen(); if (ImGui::IsItemClicked(ImGuiMouseButton_Left) && !toggled) { selected = e; } // ---- 拖拽源:拖动本实体去改父 ---- if (ImGui::BeginDragDropSource()) { const u32 bits = static_cast(entt::to_integral(e)); ImGui::SetDragDropPayload(kHierPayload, &bits, sizeof(bits)); ImGui::TextUnformatted(label.c_str()); ImGui::EndDragDropSource(); } // ---- 拖放目标:挂到本实体下(keepWorld,世界位姿不变)---- if (ImGui::BeginDragDropTarget()) { if (const ImGuiPayload* pl = ImGui::AcceptDragDropPayload(kHierPayload)) { u32 bits = 0; std::memcpy(&bits, pl->Data, sizeof(bits)); const entt::entity dragged = static_cast(bits); // 拒挂自身/自身后代(SetParent 内部同样防环,这里提前 // 静默拦截,避免"看起来挂上了"的错觉) if (dragged != e && !ctx.scene->IsDescendantOf(dragged, e)) { ctx.scene->SetParent(dragged, e); } } ImGui::EndDragDropTarget(); } // ---- 右键菜单 ---- if (ImGui::BeginPopupContextItem("hier_ctx")) { if (ImGui::MenuItem("Create Child")) { // keepWorld=true:新实体出现在父的位置(UE attach 语义) const auto child = ctx.scene->CreateEntity("child entity"); ctx.scene->SetParent(child, e); selected = child; } if (ctx.scene->GetParent(e) != entt::null && ImGui::MenuItem("Detach to Root")) { ctx.scene->SetParent(e, entt::null); } if (ImGui::MenuItem("Delete")) { // 级联随葬:选中若是被删子树的一部分,一并清除 const bool selInSubtree = ctx.scene->IsDescendantOf(selected, e); ctx.scene->DestroyEntity(e); if (selInSubtree) { selected = entt::null; } } ImGui::EndPopup(); } // ---- 组件徽标(与行内同级显示)---- std::string badge; if (reg.all_of(e)) badge += "[M]"; if (reg.all_of(e)) badge += "[A]"; if (reg.all_of(e)) badge += "[C]"; if (reg.all_of(e)) badge += "[L]"; if (reg.all_of(e)) badge += "[P]"; if (!badge.empty()) { ImGui::SameLine(); ImGui::TextDisabled("%s", badge.c_str()); if (ImGui::IsItemHovered()) { ImGui::SetTooltip( "M=Mesh A=Animation C=Camera L=Light P=Physics"); } } if (opened) { for (entt::entity c : kids) { DrawEntityNode(ctx, c, selected); } ImGui::TreePop(); } ImGui::PopID(); } } // namespace void HierarchyPanel::Draw(const EditorContext& ctx, entt::entity& selected) { ImGui::Begin("Outliner"); // 工具栏:创建实体(在遍历之前,不修改遍历中的注册表结构) if (ImGui::Button("+ Create Entity")) { const auto e = ctx.scene->CreateEntity("Entity"); selected = e; } ImGui::Separator(); auto& reg = ctx.scene->GetRegistry(); // 树形遍历:根 = 无有效父链接的实体(creation 序),子按挂载序递归 reg.view().each( [&](const entt::entity e, const Engine::ECS::NameComponent&) { if (ctx.scene->GetParent(e) != entt::null) return; // 非根 DrawEntityNode(ctx, e, selected); }); // ---- 空白区 = 挂根落点(UE Outliner 拖到空白 = 顶层)---- ImVec2 rem = ImGui::GetContentRegionAvail(); if (rem.y > 4.0f) { ImGui::InvisibleButton("##hier_drop_root", rem); if (ImGui::BeginDragDropTarget()) { if (const ImGuiPayload* pl = ImGui::AcceptDragDropPayload(kHierPayload)) { u32 bits = 0; std::memcpy(&bits, pl->Data, sizeof(bits)); ctx.scene->SetParent(static_cast(bits), entt::null); } ImGui::EndDragDropTarget(); } } ImGui::End(); }