diff --git a/App/editor/HierarchyPanel.cpp b/App/editor/HierarchyPanel.cpp index 31f067f..58d2b4e 100644 --- a/App/editor/HierarchyPanel.cpp +++ b/App/editor/HierarchyPanel.cpp @@ -1,5 +1,9 @@ // -// HierarchyPanel.cpp:实体列表实现。 +// HierarchyPanel.cpp:实体层级树实现(UE Outliner 语义)。 +// +// 树形展示(根 = 无有效父链接的实体,子 = HierarchyComponent 挂载序)、 +// 点击选中、拖拽改父/拖到空白区挂根(keepWorld = 挂载前后世界位姿 +// 不变)、右键 Create Child / Detach / Delete(级联随葬,延迟销毁)。 // #include "HierarchyPanel.h" @@ -8,16 +12,137 @@ #include +#include +#include +#include + using namespace Engine; namespace { - struct ScopedID { - explicit ScopedID(const u64 uuid) { - ImGui::PushID(reinterpret_cast(uuid)); + + // 拖拽 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; } - ~ScopedID() {ImGui::PopID(); } - }; -} + 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) { @@ -33,55 +158,29 @@ void HierarchyPanel::Draw(const EditorContext& ctx, entt::entity& selected) auto& reg = ctx.scene->GetRegistry(); + // 树形遍历:根 = 无有效父链接的实体(creation 序),子按挂载序递归 reg.view().each( - [&](const entt::entity e, const Engine::ECS::NameComponent& name) + [&](const entt::entity e, const Engine::ECS::NameComponent&) { - const bool isSelected = (selected == e); - - u64 rowId = static_cast < uint64_t >( static_cast < uint32_t >(e)); - if (const auto * id = reg.try_get(e)) { - rowId = id->uuid.Value(); - } - const ScopedID scope (rowId); - - const std::string label = name.name.empty() ? "(unnamed)" : name.name; - if (ImGui::Selectable(label.c_str(), isSelected)) - { - selected = e; - } - - // 右键菜单:删除(延迟销毁,帧尾统一 flush) - if (ImGui::BeginPopupContextItem("entity_ctx")) - { - if (ImGui::MenuItem("Delete")) - { - ctx.scene->DestroyEntity(e); - if (selected == e) - { - 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 (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(); } diff --git a/App/editor/HierarchyPanel.h b/App/editor/HierarchyPanel.h index c38de23..22fd53c 100644 --- a/App/editor/HierarchyPanel.h +++ b/App/editor/HierarchyPanel.h @@ -1,8 +1,9 @@ // -// HierarchyPanel:实体列表面板。 +// HierarchyPanel:实体层级树面板。 // -// 点击选中(写 Editor 的选中状态)、工具栏创建实体、右键删除 -//(延迟销毁,帧尾 flush,遍历安全)。每项尾缀显示组件构成徽标。 +// 树形展示父子层级(UE Outliner)、点击选中、拖拽改父/拖到空白挂根 +//(keepWorld)、右键 Create Child / Detach / Delete(级联随葬,帧尾 +// flush,遍历安全)。每项尾缀显示组件构成徽标。 // #ifndef HELLOVULKAN_HIERARCHYPANEL_H diff --git a/App/editor/InspectorPanel.cpp b/App/editor/InspectorPanel.cpp index b66eb5f..b10dea4 100644 --- a/App/editor/InspectorPanel.cpp +++ b/App/editor/InspectorPanel.cpp @@ -215,6 +215,17 @@ void InspectorPanel::Draw(const EditorContext& ctx, entt::entity selected) { if (ComponentHeader("Transform", nullptr, false, nullptr)) { + // 父级提示(UE Details 语义:此处编辑的是 Relative 值) + if (ctx.scene) { + const Engine::ECS::Entity p = ctx.scene->GetParent(selected); + if (p != entt::null) { + std::string pname = "(unnamed)"; + if (const auto* n = reg.try_get(p)) { + pname = n->name; + } + ImGui::TextDisabled("parent: %s", pname.c_str()); + } + } glm::vec3 pos = t->GetPosition(); if (DragVec3Row("position", pos, 0.05f)) { diff --git a/App/editor/StatsPanel.cpp b/App/editor/StatsPanel.cpp index b15bb1c..8d6797a 100644 --- a/App/editor/StatsPanel.cpp +++ b/App/editor/StatsPanel.cpp @@ -32,7 +32,8 @@ void StatsPanel::Draw(const EditorContext& ctx) Engine::ECS::TransformComponent* camT = nullptr; if (ctx.scene->GetPrimaryCamera(&camT) != nullptr && camT != nullptr) { - const glm::vec3& pos = camT->GetPosition(); + // world 位姿(相机实体可能挂父层级下) + const glm::vec3& pos = camT->GetWorldPosition(); ImGui::Text("Game camera pos"); ImGui::Text("(%.3f, %.3f, %.3f)", pos.x, pos.y, pos.z); } diff --git a/App/editor/ViewportPanel.cpp b/App/editor/ViewportPanel.cpp index 72478f4..69334e5 100644 --- a/App/editor/ViewportPanel.cpp +++ b/App/editor/ViewportPanel.cpp @@ -164,14 +164,15 @@ std::optional ViewportPanel::Draw(const EditorContext &ctx, if (ImGui::IsKeyPressed(ImGuiKey_F) && tr != nullptr) { // F = 焦点选中实体(frame-selection):编辑器相机 // 环绕 pivot 设为选中实体位置,沿视线平移对准(环 - // 绕距离沿用当前值,滚轮可调) - ctx.editorCamera->FocusOn(tr->GetPosition()); + // 绕距离沿用当前值,滚轮可调)。世界位姿(层级 + // 实体 = 父链组合结果) + ctx.editorCamera->FocusOn(tr->GetWorldPosition()); } } // ---- Gizmo 操纵杆(ImGui drawlist 叠加,无 GPU pass)---- - // 选中实体 + 有 Transform 才出杆;矩阵分解直接写回 - // TransformComponent(与 Inspector 同源,下一帧数值自动跟随) + // 选中实体 + 有 Transform 才出杆;分解 world 矩阵经 + // SetWorldPose 写回(与 Inspector 同源,下一帧数值自动跟随) // PIE 运行态不出杆(编辑操作冻结) if (tr != nullptr && !playing) { // 相机矩阵:跟随渲染源(编辑器形态 = EditorCamera,游戏 @@ -263,8 +264,8 @@ std::optional ViewportPanel::Draw(const EditorContext &ctx, snapping ? snap : nullptr); gizmo.dragging = ImGuizmo::IsUsing(); - // 拖拽生效:分解 world 矩阵写回 SRT(Set* 标脏 → - // UpdateTransforms 重算 → 渲染下帧生效)。旋转用 + // 拖拽生效:分解 world 矩阵 → world SRT → 沿父链反解 + // local 写回(SetWorldPose;根实体 = 直写)。旋转用 // decompose 出的 quat 直写(UE FTransform 语义)—— // 不经欧拉往返,拖拽零漂移、无万向锁 if (ImGuizmo::IsUsing()) { @@ -272,9 +273,8 @@ std::optional ViewportPanel::Draw(const EditorContext &ctx, glm::quat rot; glm::vec4 persp; if (glm::decompose(m, scale, rot, transl, skew, persp)) { - tr->SetPosition(transl) - .SetRotationQuat(rot) - .SetScale(scale); + ctx.scene->SetWorldPose(selected, transl, rot, + scale); } } } @@ -382,7 +382,7 @@ void ViewportPanel::LogPick(const EditorContext &ctx, entt::entity e) { ? "static" : "dynamic", rb->boxHalfExtents.x, rb->boxHalfExtents.y, rb->boxHalfExtents.z, rb->sphereRadius, - tr.GetPosition().x, tr.GetPosition().y, tr.GetPosition().z); + tr.GetWorldPosition().x, tr.GetWorldPosition().y, tr.GetWorldPosition().z); } else { ENGINE_INFO("[pick] '{}' (hitproxy) | (no collider)", name); } diff --git a/Engine/include/Engine/ECS/Components.h b/Engine/include/Engine/ECS/Components.h index 737338a..d375089 100644 --- a/Engine/include/Engine/ECS/Components.h +++ b/Engine/include/Engine/ECS/Components.h @@ -13,6 +13,8 @@ #include #include +#include + #include #include #include @@ -40,22 +42,29 @@ namespace Engine::ECS { std::string name; }; - // 世界变换:UE USceneComponent 的 encapsulation 模式—— - // SRT 分量私有,所有写入口标脏;world 矩阵由帧循环的 - // Scene::UpdateTransforms() 只对脏的实体重算(transform-update - // 阶段),并自动向未来的子层级传播。渲染热路径只读 World()。 - // 层级(父子变换)留给后续的层级同步系统。 + // 变换(UE FTransform 语义):TRS 分量 = 相对父的 **local** 值 + //(无父实体 local 即 world,旧场景零迁移)。分量私有,所有写入口 + // 标脏;world 缓存(SRT 三分量 + 矩阵)由帧循环的 + // Scene::UpdateTransforms() 从根 DFS 只对脏子树重算(transform- + // update 阶段)。子 world = 父 world SRT 组合而非矩阵乘法—— + // 非均匀父缩放下组合式仍是干净 SRT(矩阵分解会引入剪切)。 + // 渲染热路径只读 World();灯光/相机/物理/Gizmo 读 world 三分量。 // // 旋转内部存储 = 四元数(UE FTransform 语义,不存欧拉角): // 欧拉往返是漂移/万向锁之源(Gizmo 拖拽乱跳的根因)。欧拉只在 // 表皮出现——UI 显示(GetRotation 派生)与 UI 输入(SetRotation // 转存);Gizmo/物理热路径走 quat 直写(SetRotationQuat)零往返。 struct TransformComponent { - // ---- 读取 ---- + // ---- local 读取(相对父;UI/游戏逻辑用)---- const glm::vec3& GetPosition() const { return m_Position; } const glm::quat& GetRotationQuat() const { return m_Rotation; } const glm::vec3& GetScale() const { return m_Scale; } - const glm::mat4& World() const { return m_World; } // 要求已由 UpdateTransforms 计算 + + // ---- world 缓存读取(要求已由 UpdateTransforms 计算)---- + const glm::mat4& World() const { return m_World; } + const glm::vec3& GetWorldPosition() const { return m_WorldPosition; } + const glm::quat& GetWorldRotationQuat() const { return m_WorldRotation; } + const glm::vec3& GetWorldScale() const { return m_WorldScale; } // 欧拉角(度)视图:从 quat 派生(仅 UI 显示/编辑用,勿做存储)。 // 按引擎 YXZ 序(R = Ry·Rx·Rz)从旋转矩阵反解;glm 列主序索引: @@ -120,25 +129,66 @@ namespace Engine::ECS { bool IsDirty() const { return m_Dirty; } + // 显式标脏:层级系统改父/挂载后触发 world 重算用 + //(local 值未变但 world 缓存已失效) + TransformComponent& MarkDirty() { m_Dirty = true; return *this; } + // SRT 分量 → world 矩阵(T * R * S),清除脏标记。 - // 由 Scene::UpdateTransforms 调用(或层级系统),勿手动 + // 根实体版(world = local);由 Scene::UpdateTransforms 调用, + // 勿手动 TransformComponent& Compose() { - const glm::mat4 t = glm::translate(glm::mat4(1.0f), m_Position); - const glm::mat4 s = glm::scale(glm::mat4(1.0f), m_Scale); - m_World = t * glm::mat4_cast(m_Rotation) * s; + m_WorldPosition = m_Position; + m_WorldRotation = m_Rotation; + m_WorldScale = m_Scale; + RebuildWorldMatrix(); + m_Dirty = false; + return *this; + } + + // 子实体版:world = 父 world SRT 组合(UE FTransform 语义): + // wPos = pPos + pRot·(pScale⊙lPos) wRot = pRot·lRot wScl = pScale⊙lScl + // 组合式(非矩阵分解)保证非均匀父缩放下结果仍是干净 SRT。 + // 要求父已 Compose(DFS 父先于子保证)。由 Scene 调用,勿手动 + TransformComponent& ComposeFromParent(const TransformComponent& parent) + { + m_WorldPosition = parent.m_WorldPosition + + parent.m_WorldRotation * (parent.m_WorldScale * m_Position); + m_WorldRotation = parent.m_WorldRotation * m_Rotation; + m_WorldScale = parent.m_WorldScale * m_Scale; + RebuildWorldMatrix(); m_Dirty = false; return *this; } private: + void RebuildWorldMatrix() + { + const glm::mat4 t = glm::translate(glm::mat4(1.0f), m_WorldPosition); + const glm::mat4 s = glm::scale(glm::mat4(1.0f), m_WorldScale); + m_World = t * glm::mat4_cast(m_WorldRotation) * s; + } + + // ---- local TRS(相对父)---- glm::vec3 m_Position{0.0f}; glm::quat m_Rotation{1.0f, 0.0f, 0.0f, 0.0f}; // 单位四元数(w,x,y,z) glm::vec3 m_Scale{1.0f}; + // ---- world 缓存(UpdateTransforms 重算)---- + glm::vec3 m_WorldPosition{0.0f}; + glm::quat m_WorldRotation{1.0f, 0.0f, 0.0f, 0.0f}; + glm::vec3 m_WorldScale{1.0f}; glm::mat4 m_World{1.0f}; bool m_Dirty = true; // 出生即脏:首次 UpdateTransforms 计算 world }; + // 层级链接(UE Actor attach):父句柄 + 子表。惰性创建—— + // 只有参与父子关系的实体才有本组件,独立实体零存储成本。 + // entt 句柄仅在 registry 存续期有效;序列化换 UUID,加载后重链 + struct HierarchyComponent { + entt::entity parent = entt::null; // entt::null = 根实体 + std::vector children; // 挂载顺序 + }; + // 网格渲染体:引用共享的 Model 资产。 // 存中性 AssetRef(Core 层纯值类型,Unity InstanceID 思路)—— // ECS 不感知资产系统内部;解析归 AssetManager,悬空由 generation 拦截。 diff --git a/Engine/include/Engine/ECS/Scene.h b/Engine/include/Engine/ECS/Scene.h index 03ef101..3cb0f98 100644 --- a/Engine/include/Engine/ECS/Scene.h +++ b/Engine/include/Engine/ECS/Scene.h @@ -52,7 +52,8 @@ namespace Engine::ECS { // 返回句柄表皮:e.AddComponent() / e.GetComponent() / e.Destroy() EntityHandle CreateEntity(std::string name = "new entity"); - void DestroyEntity(Entity e); // 延迟:帧尾统一销毁 + // 延迟:帧尾统一销毁。层级级联(UE 语义):销毁父 → 子树随葬 + void DestroyEntity(Entity e); void FlushDestroyQueue(); // 每帧末尾由 Application 调用 // 整场景换血(序列化加载):立即销毁全部实体并清空延迟队列。 @@ -66,10 +67,33 @@ namespace Engine::ECS { // 帧循环统一重算脏 transform 的 world 矩阵(UE transform-update // 阶段模式)——由 Application 在 OnUpdate 之后、渲染之前调用, - // 用户代码永远不需要手动 Compose + // 用户代码永远不需要手动 Compose。层级语义:从根 DFS,脏实体 + // 或脏祖先的后代才重算(父先于子,ComposeFromParent 组合) void UpdateTransforms(); bool IsAlive(Entity e) const { return m_Registry.valid(e); } + // ---- 层级(UE Actor attach 语义)---- + + // 改父/挂根。keepWorld=true(编辑器拖拽语义):换算 local 保持 + // world 位姿不变;false(序列化加载语义):local 已就位只建链。 + // 防环(新父是 child 后代 → 拒绝);成功返回 true + bool SetParent(Entity child, Entity parent, bool keepWorld = true); + + // 父句柄;根/悬空/无效实体返回 entt::null + Entity GetParent(Entity e) const; + + // e 是否在 ancestor 的子树内(含相等)。防环检测/面板用 + bool IsDescendantOf(Entity e, Entity ancestor) const; + + // 世界位姿写入(UE SetActorLocation 语义):world SRT → 沿父链 + // 反解 local。物理写回/Gizmo 拖拽用 + void SetWorldPose(Entity e, const glm::vec3& worldPos, + const glm::quat& worldRot, const glm::vec3& worldScale); + + // 沿父链自顶向下补算脏 world(层级写读 world 前调用—— + // UpdateTransforms 之外的时机读取/换算 world 前保证缓存新鲜) + void EnsureWorldFresh(Entity e); + // 按稳定 UUID 查实体(序列化/跨引用场景);找不到返回 entt::null Entity FindEntityByUUID(UUID uuid); diff --git a/Engine/include/Engine/Renderer/EditorCamera.h b/Engine/include/Engine/Renderer/EditorCamera.h index 073431d..42101cf 100644 --- a/Engine/include/Engine/Renderer/EditorCamera.h +++ b/Engine/include/Engine/Renderer/EditorCamera.h @@ -51,8 +51,9 @@ namespace Engine { // 一次性对齐版)。环绕 pivot 已失效,清除待 MMB 重建 void AlignTo(const ECS::TransformComponent& target) { - m_Transform.SetPosition(target.GetPosition()); - m_Transform.SetRotationQuat(target.GetRotationQuat()); + // world 位姿复制(目标实体可能在层级子树下) + m_Transform.SetPosition(target.GetWorldPosition()); + m_Transform.SetRotationQuat(target.GetWorldRotationQuat()); m_Transform.Compose(); m_Controller.ResetOrbit(); } diff --git a/Engine/include/Engine/Serialization/SceneSerializer.h b/Engine/include/Engine/Serialization/SceneSerializer.h index fb8288f..6c48db5 100644 --- a/Engine/include/Engine/Serialization/SceneSerializer.h +++ b/Engine/include/Engine/Serialization/SceneSerializer.h @@ -30,8 +30,9 @@ namespace Engine::Assets { class AssetManager; } namespace Engine::Serialization { - // 当前场景文件格式版本(字段/结构变更时递增) - inline constexpr int kSceneVersion = 1; + // 当前场景文件格式版本(字段/结构变更时递增)。 + // 2 = 层级(可选键 Parent: ,缺省 = 根实体,旧档零迁移) + inline constexpr int kSceneVersion = 2; class SceneSerializer { public: diff --git a/Engine/src/ECS/Scene.cpp b/Engine/src/ECS/Scene.cpp index 86e16c5..ad1b364 100644 --- a/Engine/src/ECS/Scene.cpp +++ b/Engine/src/ECS/Scene.cpp @@ -8,6 +8,9 @@ #include +#include +#include + namespace Engine::ECS { EntityHandle Scene::CreateEntity(std::string name) @@ -37,8 +40,19 @@ namespace Engine::ECS { ENGINE_FATAL("Scene::DestroyEntity: stale entity ({})", static_cast(e)); } - // 延迟到帧尾,保证遍历期间池结构不变 - m_PendingDestroy.push_back(e); + // 层级级联(UE 语义):销毁父 → 附着子树随葬。整棵子树入队, + // 重复入队由 FlushDestroyQueue 的 valid 检查消化 + std::vector stack{e}; + while (!stack.empty()) { + const Entity cur = stack.back(); + stack.pop_back(); + m_PendingDestroy.push_back(cur); + if (const auto* h = m_Registry.try_get(cur)) { + for (Entity c : h->children) { + if (m_Registry.valid(c)) stack.push_back(c); + } + } + } } void Scene::FlushDestroyQueue() @@ -46,9 +60,17 @@ namespace Engine::ECS { if (m_PendingDestroy.empty()) return; for (Entity e : m_PendingDestroy) { - if (m_Registry.valid(e)) { - m_Registry.destroy(e); // entt 自动回收槽位并递增版本号 + if (!m_Registry.valid(e)) continue; // 级联重复入队 / 已销毁 + // 摘链:从(仍存活的)父的 children 表移除,防悬空句柄残留 + if (const auto* h = m_Registry.try_get(e); + h && h->parent != entt::null && m_Registry.valid(h->parent)) { + auto& siblings = + m_Registry.get(h->parent).children; + siblings.erase( + std::remove(siblings.begin(), siblings.end(), e), + siblings.end()); } + m_Registry.destroy(e); // entt 自动回收槽位并递增版本号 } m_PendingDestroy.clear(); } @@ -69,13 +91,178 @@ namespace Engine::ECS { void Scene::UpdateTransforms() { ZoneScoped; - // 只重算脏的:UE transform-update 阶段模式。 - // 静止实体(绝大多数)零成本;层级传播在此扩展 - m_Registry.view().each([](TransformComponent& t) { - if (t.IsDirty()) t.Compose(); + // 层级级联:从根 DFS(父先于子)。"自身脏 || 祖先脏" 才重算 + // ——父 world 变化传播给整棵子树,静止子树零成本(UE + // transform-update 阶段模式)。迭代栈防爆栈(深层链安全) + struct Frame { Entity e; bool ancestorDirty; }; + std::vector stack; + + m_Registry.view().each([&](Entity e, TransformComponent&) { + // 根 = 无有效父链接的实体 + const auto* h = m_Registry.try_get(e); + if (h && h->parent != entt::null && m_Registry.valid(h->parent)) { + return; + } + stack.push_back({e, false}); + while (!stack.empty()) { + const Frame f = stack.back(); + stack.pop_back(); + auto& t = m_Registry.get(f.e); + const bool recompose = f.ancestorDirty || t.IsDirty(); + const auto* hc = m_Registry.try_get(f.e); + if (recompose) { + const Entity p = (hc && hc->parent != entt::null && + m_Registry.valid(hc->parent)) + ? hc->parent : entt::null; + if (p != entt::null) { + t.ComposeFromParent( + m_Registry.get(p)); + } else { + t.Compose(); + } + } + // 无论本节点是否重算都下探:子自身可能脏(新建实体), + // ancestorDirty = 本节点是否重算(父 world 变了才传播) + if (hc) { + for (Entity c : hc->children) { + if (m_Registry.valid(c) && + m_Registry.all_of(c)) { + stack.push_back({c, recompose}); + } + } + } + } }); } + // ---- 层级(UE Actor attach 语义)---- + + Entity Scene::GetParent(Entity e) const + { + if (!m_Registry.valid(e)) return entt::null; + const auto* h = m_Registry.try_get(e); + return (h && h->parent != entt::null && m_Registry.valid(h->parent)) + ? h->parent : entt::null; + } + + bool Scene::IsDescendantOf(Entity e, Entity ancestor) const + { + if (!m_Registry.valid(e) || !m_Registry.valid(ancestor)) return false; + Entity cur = e; + while (cur != entt::null) { + if (cur == ancestor) return true; + cur = GetParent(cur); + } + return false; + } + + bool Scene::SetParent(Entity child, Entity parent, bool keepWorld) + { + if (!m_Registry.valid(child)) return false; + if (parent != entt::null && !m_Registry.valid(parent)) return false; + if (child == parent) return false; + + // 防环:新父的祖先链不得包含 child(否则成环,DFS 死循环) + if (parent != entt::null) { + Entity a = parent; + while (a != entt::null) { + if (a == child) { + ENGINE_WARN("Scene::SetParent: rejected (would create " + "cycle: parent is descendant of child)"); + return false; + } + a = GetParent(a); + } + } + if (GetParent(child) == parent) return true; // 幂等:已在位 + + // keepWorld 换算基准 = 改挂前 world(补算脏节点,缓存新鲜) + EnsureWorldFresh(child); + const glm::vec3 wp = m_Registry.get(child) + .GetWorldPosition(); + const glm::quat wr = m_Registry.get(child) + .GetWorldRotationQuat(); + const glm::vec3 ws = m_Registry.get(child) + .GetWorldScale(); + + // 从旧父摘链 + auto& h = m_Registry.get_or_emplace(child); + if (h.parent != entt::null && m_Registry.valid(h.parent)) { + auto& siblings = + m_Registry.get(h.parent).children; + siblings.erase( + std::remove(siblings.begin(), siblings.end(), child), + siblings.end()); + } + + // keepWorld:world SRT → 沿新父反解 local(UE Attach 语义: + // 挂载前后世界位姿不变) + h.parent = parent; + if (keepWorld) { + SetWorldPose(child, wp, wr, ws); + } else { + // 序列化加载:local 已从文件就位,只需标脏触发重算 + m_Registry.get(child).MarkDirty(); + } + if (parent != entt::null) { + m_Registry.get_or_emplace(parent) + .children.push_back(child); + } + return true; + } + + void Scene::SetWorldPose(Entity e, const glm::vec3& worldPos, + const glm::quat& worldRot, + const glm::vec3& worldScale) + { + if (!m_Registry.valid(e)) return; + const Entity p = GetParent(e); + auto& t = m_Registry.get(e); + if (p == entt::null) { + // 根:local 即 world + t.SetPosition(worldPos).SetRotationQuat(worldRot) + .SetScale(worldScale); + return; + } + EnsureWorldFresh(p); + const auto& pt = m_Registry.get(p); + // world SRT → local:除父缩放 → 反旋父旋转 → 平移差 + //(ComposeFromParent 的严格逆运算)。父缩放零保护:clamp 避免除零 + const glm::vec3 invS{ + std::abs(pt.GetWorldScale().x) > 1e-8f ? 1.0f / pt.GetWorldScale().x : 0.0f, + std::abs(pt.GetWorldScale().y) > 1e-8f ? 1.0f / pt.GetWorldScale().y : 0.0f, + std::abs(pt.GetWorldScale().z) > 1e-8f ? 1.0f / pt.GetWorldScale().z : 0.0f}; + const glm::quat invR = glm::conjugate(pt.GetWorldRotationQuat()); + t.SetPosition(invR * ((worldPos - pt.GetWorldPosition()) * invS)) + .SetRotationQuat(invR * worldRot) + .SetScale(worldScale * invS); + } + + void Scene::EnsureWorldFresh(Entity e) + { + if (!m_Registry.valid(e)) return; + // 收集父链到根,自顶向下补算。一旦某节点重算,其下全部后代 + // 的 world 缓存都基于旧父 → 必须级联重算(latch) + std::vector chain; + Entity cur = e; + while (cur != entt::null) { + chain.push_back(cur); + cur = GetParent(cur); + } + bool dirty = false; + for (auto it = chain.rbegin(); it != chain.rend(); ++it) { + auto& t = m_Registry.get(*it); + if (!dirty && !t.IsDirty()) continue; // 该节点照旧 + const Entity p = GetParent(*it); + if (p != entt::null) { + t.ComposeFromParent(m_Registry.get(p)); + } else { + t.Compose(); + } + dirty = true; // 子孙缓存已过时 + } + } + CameraComponent* Scene::GetPrimaryCamera(TransformComponent** outTransform) { CameraComponent* found = nullptr; diff --git a/Engine/src/Physics/PhysicsWorld.cpp b/Engine/src/Physics/PhysicsWorld.cpp index 458a866..c5f585a 100644 --- a/Engine/src/Physics/PhysicsWorld.cpp +++ b/Engine/src/Physics/PhysicsWorld.cpp @@ -417,28 +417,29 @@ namespace Engine { // 编辑器拖动 Dynamic 体(暂停期/物理未步进的帧)→ 传送 + // 清速度(UE 编辑器语义:编辑器操作权威于物理 body,拖动 = // 重新放置)。物理模拟中 Writeback 每步同步 lastPos/lastRot - // → 本比对只在真正拖动帧为真,不会打断自由模拟 - if (rec.lastPos != tr.GetPosition() || - rec.lastRot != tr.GetRotationQuat()) { + // → 本比对只在真正拖动帧为真,不会打断自由模拟。 + // body 是世界系:读 world 缓存(层级实体取父链组合结果) + if (rec.lastPos != tr.GetWorldPosition() || + rec.lastRot != tr.GetWorldRotationQuat()) { bi.SetPositionAndRotation( id, - JPH::RVec3(tr.GetPosition().x, tr.GetPosition().y, tr.GetPosition().z), - ToJolt(tr.GetRotationQuat()), + JPH::RVec3(tr.GetWorldPosition().x, tr.GetWorldPosition().y, tr.GetWorldPosition().z), + ToJolt(tr.GetWorldRotationQuat()), JPH::EActivation::Activate); bi.SetLinearVelocity(id, JPH::Vec3(0.0f, 0.0f, 0.0f)); bi.SetAngularVelocity(id, JPH::Vec3(0.0f, 0.0f, 0.0f)); - rec.lastPos = tr.GetPosition(); - rec.lastRot = tr.GetRotationQuat(); + rec.lastPos = tr.GetWorldPosition(); + rec.lastRot = tr.GetWorldRotationQuat(); } - } else if (rec.lastPos != tr.GetPosition() || - rec.lastRot != tr.GetRotationQuat()) { + } else if (rec.lastPos != tr.GetWorldPosition() || + rec.lastRot != tr.GetWorldRotationQuat()) { // 编辑器拖动静态/运动学体 → 传送 body bi.SetPositionAndRotation(id, - JPH::RVec3(tr.GetPosition().x, tr.GetPosition().y, tr.GetPosition().z), - ToJolt(tr.GetRotationQuat()), + JPH::RVec3(tr.GetWorldPosition().x, tr.GetWorldPosition().y, tr.GetWorldPosition().z), + ToJolt(tr.GetWorldRotationQuat()), JPH::EActivation::DontActivate); - rec.lastPos = tr.GetPosition(); - rec.lastRot = tr.GetRotationQuat(); + rec.lastPos = tr.GetWorldPosition(); + rec.lastRot = tr.GetWorldRotationQuat(); } } @@ -519,8 +520,8 @@ namespace Engine { if (tr1 == nullptr || tr2 == nullptr) { return; } - const glm::vec3 s1 = tr1->GetScale(); - const glm::vec3 s2 = tr2->GetScale(); + const glm::vec3 s1 = tr1->GetWorldScale(); + const glm::vec3 s2 = tr2->GetWorldScale(); // 双写锁读位姿/COM + 创建(SyncScene 在主线程,与 Update 不并发) JPH::BodyLockWrite lock1(m_System->GetBodyLockInterface(), b1.id); @@ -642,22 +643,22 @@ namespace Engine { if (found == m_Characters.end()) { CharacterEntry entry; entry.cc = std::make_unique( - *m_System, p, tr.GetPosition()); - entry.lastPos = tr.GetPosition(); + *m_System, p, tr.GetWorldPosition()); + entry.lastPos = tr.GetWorldPosition(); m_Characters.emplace(bits, std::move(entry)); continue; } if (found->second.cc->GetParams() != p) { // 参数变更 → 重建(位置沿用当前 Transform) found->second.cc = std::make_unique( - *m_System, p, tr.GetPosition()); - found->second.lastPos = tr.GetPosition(); + *m_System, p, tr.GetWorldPosition()); + found->second.lastPos = tr.GetWorldPosition(); continue; } - if (found->second.lastPos != tr.GetPosition()) { + if (found->second.lastPos != tr.GetWorldPosition()) { // 编辑器拖动实体 → 传送角色(角色是唯一位置源,反向覆盖) - found->second.cc->Teleport(tr.GetPosition()); - found->second.lastPos = tr.GetPosition(); + found->second.cc->Teleport(tr.GetWorldPosition()); + found->second.lastPos = tr.GetWorldPosition(); } } } @@ -672,8 +673,12 @@ namespace Engine { continue; } entry.cc->Update(dt, gravity, *m_TempAllocator); - const glm::vec3 p = entry.cc->GetPosition(); // 原点 = 脚底 - tr->SetPosition(p); // 标脏 → UpdateTransforms 重算 world + const glm::vec3 p = entry.cc->GetPosition(); // 原点 = 脚底(世界系) + // 写回走 world→local 换算(层级实体挂父下仍正确;标脏 → + // UpdateTransforms 重算 world) + m_Scene.SetWorldPose( + static_cast(bits), p, + tr->GetWorldRotationQuat(), tr->GetWorldScale()); entry.lastPos = p; } } @@ -693,8 +698,9 @@ namespace Engine { return nullptr; } - // 缩放烘进顶点:Jolt shape 无缩放语义,body 只吃 pos/rot - const glm::vec3 s = tr.GetScale(); + // 缩放烘进顶点:Jolt shape 无缩放语义,body 只吃 pos/rot。 + // 取 world 缩放(层级实体 = 父链累计) + const glm::vec3 s = tr.GetWorldScale(); // 动态/运动体 → ConvexHull(MeshShape 三角网格太贵,Jolt 不允许动) if (rb.type != ECS::RigidBodyComponent::BodyType::Static) { @@ -833,23 +839,23 @@ namespace Engine { shape = BuildMeshShape(entityBits, rb, tr); if (shape == nullptr) { shape = new JPH::BoxShape(JPH::Vec3( - std::max(rb.boxHalfExtents.x * tr.GetScale().x, 0.01f), - std::max(rb.boxHalfExtents.y * tr.GetScale().y, 0.01f), - std::max(rb.boxHalfExtents.z * tr.GetScale().z, 0.01f))); + std::max(rb.boxHalfExtents.x * tr.GetWorldScale().x, 0.01f), + std::max(rb.boxHalfExtents.y * tr.GetWorldScale().y, 0.01f), + std::max(rb.boxHalfExtents.z * tr.GetWorldScale().z, 0.01f))); } } else { shape = new JPH::BoxShape(JPH::Vec3( - std::max(rb.boxHalfExtents.x * tr.GetScale().x, 0.01f), - std::max(rb.boxHalfExtents.y * tr.GetScale().y, 0.01f), - std::max(rb.boxHalfExtents.z * tr.GetScale().z, 0.01f))); + std::max(rb.boxHalfExtents.x * tr.GetWorldScale().x, 0.01f), + std::max(rb.boxHalfExtents.y * tr.GetWorldScale().y, 0.01f), + std::max(rb.boxHalfExtents.z * tr.GetWorldScale().z, 0.01f))); } const bool dynamic = rb.type == ECS::RigidBodyComponent::BodyType::Dynamic; const bool kinematic = rb.type == ECS::RigidBodyComponent::BodyType::Kinematic; JPH::BodyCreationSettings settings( shape, - JPH::RVec3(tr.GetPosition().x, tr.GetPosition().y, tr.GetPosition().z), - ToJolt(tr.GetRotationQuat()), + JPH::RVec3(tr.GetWorldPosition().x, tr.GetWorldPosition().y, tr.GetWorldPosition().z), + ToJolt(tr.GetWorldRotationQuat()), dynamic ? JPH::EMotionType::Dynamic : (kinematic @@ -882,8 +888,8 @@ namespace Engine { rec.compound = rb.compound; rec.mass = rb.mass; rec.isTrigger = rb.isTrigger; - rec.lastPos = tr.GetPosition(); - rec.lastRot = tr.GetRotationQuat(); + rec.lastPos = tr.GetWorldPosition(); + rec.lastRot = tr.GetWorldRotationQuat(); m_Bodies[entityBits] = rec; rb.bodyId = id.GetIndexAndSequenceNumber(); @@ -1053,8 +1059,13 @@ namespace Engine { const JPH::BodyID id(rb.bodyId); const JPH::RVec3 pos = bi.GetPosition(id); const JPH::Quat rot = bi.GetRotation(id); - tr.SetPosition(glm::vec3(pos.GetX(), pos.GetY(), pos.GetZ())); - tr.SetRotationQuat(ToGlm(rot)); + // body 姿态是世界系:写回经 world→local 换算(挂父层级实体 + // 不破坏相对位姿;根实体 = 直写)。标脏 → UpdateTransforms 重算 + m_Scene.SetWorldPose( + e, + glm::vec3(pos.GetX(), pos.GetY(), pos.GetZ()), + ToGlm(rot), + tr.GetWorldScale()); const JPH::Vec3 vel = bi.GetLinearVelocity(id); rb.velocity = glm::vec3(vel.GetX(), vel.GetY(), vel.GetZ()); @@ -1063,8 +1074,8 @@ namespace Engine { // 位移误判为编辑器拖动 → teleport 回旧位置死循环 const u32 bits = static_cast(entt::to_integral(e)); if (auto it = m_Bodies.find(bits); it != m_Bodies.end()) { - it->second.lastPos = tr.GetPosition(); - it->second.lastRot = tr.GetRotationQuat(); + it->second.lastPos = tr.GetWorldPosition(); + it->second.lastRot = tr.GetWorldRotationQuat(); } } } diff --git a/Engine/src/Renderer/SceneRenderer.cpp b/Engine/src/Renderer/SceneRenderer.cpp index 7dce399..9e3409c 100644 --- a/Engine/src/Renderer/SceneRenderer.cpp +++ b/Engine/src/Renderer/SceneRenderer.cpp @@ -354,7 +354,7 @@ namespace Engine : 2.0f); if (light.type == LightComponent::Type::Point) { - dst.position = glm::vec4(t.GetPosition(), 1.0f); + dst.position = glm::vec4(t.GetWorldPosition(), 1.0f); dst.direction = glm::vec4(0.0f); } else { glm::vec3 dir = glm::length(light.direction) > 0.0f @@ -391,7 +391,7 @@ namespace Engine shadowDir = glm::vec3(collected[0].direction); } ComputeCascades(shadowDir); - ubo.cameraPos = glm::vec4(camTransform->GetPosition(), 1.0f); + ubo.cameraPos = glm::vec4(camTransform->GetWorldPosition(), 1.0f); // ---- 2.5 骨骼动画推进 + 姿态采样(与可见性无关:播放头恒走, // 否则被剔除的实体回屏时动画跳变)---- @@ -579,7 +579,7 @@ namespace Engine // LOD 选级:屏占比近似 = radius/距离(主视图按实例距离; // 阈值 10/20 倍半径,可调)。开关关闭 = 恒 LOD0 const f32 dist = - glm::length(worldCenter - camTransform->GetPosition()); + glm::length(worldCenter - camTransform->GetWorldPosition()); u32 mainLod = 0; if (m_LodEnabled) { if (dist >= worldRadius * 20.0f) { diff --git a/Engine/src/Serialization/SceneSerializer.cpp b/Engine/src/Serialization/SceneSerializer.cpp index 002eb24..83d8066 100644 --- a/Engine/src/Serialization/SceneSerializer.cpp +++ b/Engine/src/Serialization/SceneSerializer.cpp @@ -34,6 +34,7 @@ #include #include #include +#include #include #include @@ -176,6 +177,7 @@ namespace Engine::Serialization { { u64 uuid = 0; std::string name = "entity"; + u64 parentUuid = 0; // 0 = 根实体(层级键缺省) YAML::Node comps; // 缺省 = Null(无组件) }; @@ -190,6 +192,13 @@ namespace Engine::Serialization { if (const auto* name = reg.try_get(e)) { out << YAML::Key << "Name" << YAML::Value << name->name; } + // 层级(UE Actor attach):父存 UUID(跨存档稳定); + // 悬空父(不该发生)按根实体写 + if (const auto* h = reg.try_get(e); + h && h->parent != entt::null && reg.valid(h->parent)) { + out << YAML::Key << "Parent" << YAML::Value + << reg.get(h->parent).uuid.Value(); + } out << YAML::Key << "Components" << YAML::Value << YAML::BeginMap; @@ -524,6 +533,7 @@ namespace Engine::Serialization { ParsedEntity pe; pe.uuid = e["Entity"].as(0); pe.name = e["Name"].as(pe.name); + pe.parentUuid = e["Parent"].as(0); if (const YAML::Node comps = e["Components"]; comps && comps.IsMap()) { // 仅在 rhs 有效时赋值(僵尸节点 operator= 会抛) @@ -543,6 +553,7 @@ namespace Engine::Serialization { scene.Clear(); std::unordered_set seenUuids; + std::unordered_map byUuid; // 第二遍重链用 for (ParsedEntity& pe : parsed) { auto handle = scene.CreateEntity(pe.name); @@ -552,6 +563,8 @@ namespace Engine::Serialization { uuid = UUID::New().Value(); } handle.GetComponent().uuid = UUID{uuid}; + pe.uuid = uuid; // 回写最终 uuid(重生成后父引用仍可命中) + byUuid.emplace(uuid, static_cast(handle)); const YAML::Node comps = pe.comps; // 拷贝初始化,缺省 Null @@ -734,6 +747,22 @@ namespace Engine::Serialization { } } + // ---- 阶段 3:重链父子(两遍解析:父可能出现在子之后)。 + // keepWorld=false:local TRS 已从文件就位(相对父语义), + // 只建链标脏。防环由 SetParent 兜底(手编档成环 → 告警 + 挂根) + for (const ParsedEntity& pe : parsed) { + if (pe.parentUuid == 0) continue; + const auto c = byUuid.find(pe.uuid); + const auto p = byUuid.find(pe.parentUuid); + if (c == byUuid.end() || p == byUuid.end()) { + ENGINE_WARN("SceneSerializer: parent {} missing for entity " + "{}, kept as root", + pe.parentUuid, pe.uuid); + continue; + } + scene.SetParent(c->second, p->second, /*keepWorld=*/false); + } + // 加载在 OnImGuiRender 段,错过本帧 UpdateTransforms: // 显式补算,否则首帧 World = 单位阵 scene.UpdateTransforms();