feat: parent-child transform hierarchy (UE Actor attach semantics)

- TransformComponent TRS = local (relative to parent); cached world SRT
  composed hierarchically (parent pos/rot/scale, no matrix decomposition
  shear under non-uniform parent scale)
- HierarchyComponent (parent + children, lazily attached); Scene::
  SetParent (keepWorld, cycle-safe) / GetParent / IsDescendantOf /
  SetWorldPose (world->local inverse, used by physics writeback & gizmo)
- UpdateTransforms: DFS from roots, recompose only dirty-or-dirty-
  ancestor subtrees (parents before children)
- DestroyEntity: descendants buried with parent (UE cascade)
- Serialization v2: optional Parent uuid key, two-pass relink on load
- Hierarchy panel: tree view, drag-drop reparent / drop-to-root detach,
  Create Child / Detach context menu; Inspector shows parent name
- Consumers moved to world-space reads: lights, camera pos, physics
  bodies/characters/joints/collider baking, gizmo, F-focus, AlignTo
This commit is contained in:
2026-10-04 15:22:25 +08:00
parent 97408e506c
commit ee5ab8e94e
13 changed files with 550 additions and 135 deletions
+151 -52
View File
@@ -1,5 +1,9 @@
// //
// HierarchyPanel.cpp:实体列表实现。 // HierarchyPanel.cpp:实体层级树实现(UE Outliner 语义)。
//
// 树形展示(根 = 无有效父链接的实体,子 = HierarchyComponent 挂载序)、
// 点击选中、拖拽改父/拖到空白区挂根(keepWorld = 挂载前后世界位姿
// 不变)、右键 Create Child / Detach / Delete(级联随葬,延迟销毁)。
// //
#include "HierarchyPanel.h" #include "HierarchyPanel.h"
@@ -8,17 +12,138 @@
#include <imgui.h> #include <imgui.h>
#include <cstring>
#include <string>
#include <vector>
using namespace Engine; using namespace Engine;
namespace { namespace {
struct ScopedID {
explicit ScopedID(const u64 uuid) { // 拖拽 payload 类型名(Editor 内部约定;实体位模式 4 字节)
ImGui::PushID(reinterpret_cast<const void*>(uuid)); constexpr const char* kHierPayload = "HIER_ENTITY";
// 实体显示名
std::string DisplayName(const entt::registry& reg, entt::entity e)
{
if (const auto* n = reg.try_get<Engine::ECS::NameComponent>(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<u64>(static_cast<u32>(e));
if (const auto* id = reg.try_get<Engine::ECS::IdComponent>(e)) {
rowId = id->uuid.Value();
}
ImGui::PushID(reinterpret_cast<const void*>(rowId));
// 子表(过滤悬空 + 无 Name 的防御性兜底)
const auto* h = reg.try_get<Engine::ECS::HierarchyComponent>(e);
std::vector<entt::entity> kids;
if (h) {
for (entt::entity c : h->children) {
if (reg.valid(c) && reg.all_of<Engine::ECS::NameComponent>(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<u32>(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<entt::entity>(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<Engine::ECS::MeshRendererComponent>(e)) badge += "[M]";
if (reg.all_of<Engine::ECS::AnimationComponent>(e)) badge += "[A]";
if (reg.all_of<Engine::ECS::CameraComponent>(e)) badge += "[C]";
if (reg.all_of<Engine::ECS::LightComponent>(e)) badge += "[L]";
if (reg.all_of<Engine::ECS::RigidBodyComponent>(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) void HierarchyPanel::Draw(const EditorContext& ctx, entt::entity& selected)
{ {
ImGui::Begin("Hierarchy"); ImGui::Begin("Hierarchy");
@@ -33,55 +158,29 @@ void HierarchyPanel::Draw(const EditorContext& ctx, entt::entity& selected)
auto& reg = ctx.scene->GetRegistry(); auto& reg = ctx.scene->GetRegistry();
// 树形遍历:根 = 无有效父链接的实体(creation 序),子按挂载序递归
reg.view<Engine::ECS::NameComponent>().each( reg.view<Engine::ECS::NameComponent>().each(
[&](const entt::entity e, const Engine::ECS::NameComponent& name) [&](const entt::entity e, const Engine::ECS::NameComponent&)
{ {
const bool isSelected = (selected == e); if (ctx.scene->GetParent(e) != entt::null) return; // 非根
DrawEntityNode(ctx, e, selected);
u64 rowId = static_cast < uint64_t >( static_cast < uint32_t >(e));
if (const auto * id = reg.try_get<Engine::ECS::IdComponent>(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<Engine::ECS::MeshRendererComponent>(e)) badge += "[M]";
if (reg.all_of<Engine::ECS::AnimationComponent>(e)) badge += "[A]";
if (reg.all_of<Engine::ECS::CameraComponent>(e)) badge += "[C]";
if (reg.all_of<Engine::ECS::LightComponent>(e)) badge += "[L]";
if (reg.all_of<Engine::ECS::RigidBodyComponent>(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");
}
}
}); });
// ---- 空白区 = 挂根落点(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<entt::entity>(bits),
entt::null);
}
ImGui::EndDragDropTarget();
}
}
ImGui::End(); ImGui::End();
} }
+4 -3
View File
@@ -1,8 +1,9 @@
// //
// HierarchyPanel:实体列表面板。 // HierarchyPanel:实体层级树面板。
// //
// 点击选中(写 Editor 的选中状态)、工具栏创建实体、右键删除 // 树形展示父子层级(UE Outliner)、点击选中、拖拽改父/拖到空白挂根
//(延迟销毁,帧尾 flush,遍历安全)。每项尾缀显示组件构成徽标。 //(keepWorld)、右键 Create Child / Detach / Delete(级联随葬,帧尾
// flush,遍历安全)。每项尾缀显示组件构成徽标。
// //
#ifndef HELLOVULKAN_HIERARCHYPANEL_H #ifndef HELLOVULKAN_HIERARCHYPANEL_H
+11
View File
@@ -215,6 +215,17 @@ void InspectorPanel::Draw(const EditorContext& ctx, entt::entity selected)
{ {
if (ComponentHeader("Transform", nullptr, false, nullptr)) 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<Engine::ECS::NameComponent>(p)) {
pname = n->name;
}
ImGui::TextDisabled("parent: %s", pname.c_str());
}
}
glm::vec3 pos = t->GetPosition(); glm::vec3 pos = t->GetPosition();
if (DragVec3Row("position", pos, 0.05f)) if (DragVec3Row("position", pos, 0.05f))
{ {
+2 -1
View File
@@ -32,7 +32,8 @@ void StatsPanel::Draw(const EditorContext& ctx)
Engine::ECS::TransformComponent* camT = nullptr; Engine::ECS::TransformComponent* camT = nullptr;
if (ctx.scene->GetPrimaryCamera(&camT) != nullptr && 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("Game camera pos");
ImGui::Text("(%.3f, %.3f, %.3f)", pos.x, pos.y, pos.z); ImGui::Text("(%.3f, %.3f, %.3f)", pos.x, pos.y, pos.z);
} }
+10 -10
View File
@@ -164,14 +164,15 @@ std::optional<entt::entity> ViewportPanel::Draw(const EditorContext &ctx,
if (ImGui::IsKeyPressed(ImGuiKey_F) && tr != nullptr) { if (ImGui::IsKeyPressed(ImGuiKey_F) && tr != nullptr) {
// F = 焦点选中实体(frame-selection):编辑器相机 // F = 焦点选中实体(frame-selection):编辑器相机
// 环绕 pivot 设为选中实体位置,沿视线平移对准(环 // 环绕 pivot 设为选中实体位置,沿视线平移对准(环
// 绕距离沿用当前值,滚轮可调) // 绕距离沿用当前值,滚轮可调)。世界位姿(层级
ctx.editorCamera->FocusOn(tr->GetPosition()); // 实体 = 父链组合结果)
ctx.editorCamera->FocusOn(tr->GetWorldPosition());
} }
} }
// ---- Gizmo 操纵杆(ImGui drawlist 叠加,无 GPU pass)---- // ---- Gizmo 操纵杆(ImGui drawlist 叠加,无 GPU pass)----
// 选中实体 + 有 Transform 才出杆;矩阵分解直接写回 // 选中实体 + 有 Transform 才出杆;分解 world 矩阵经
// TransformComponent(与 Inspector 同源,下一帧数值自动跟随) // SetWorldPose 写回(与 Inspector 同源,下一帧数值自动跟随)
// PIE 运行态不出杆(编辑操作冻结) // PIE 运行态不出杆(编辑操作冻结)
if (tr != nullptr && !playing) { if (tr != nullptr && !playing) {
// 相机矩阵:跟随渲染源(编辑器形态 = EditorCamera,游戏 // 相机矩阵:跟随渲染源(编辑器形态 = EditorCamera,游戏
@@ -263,8 +264,8 @@ std::optional<entt::entity> ViewportPanel::Draw(const EditorContext &ctx,
snapping ? snap : nullptr); snapping ? snap : nullptr);
gizmo.dragging = ImGuizmo::IsUsing(); gizmo.dragging = ImGuizmo::IsUsing();
// 拖拽生效:分解 world 矩阵写回 SRT(Set* 标脏 → // 拖拽生效:分解 world 矩阵 → world SRT → 沿父链反解
// UpdateTransforms 重算 → 渲染下帧生效)。旋转用 // local 写回(SetWorldPose;根实体 = 直写)。旋转用
// decompose 出的 quat 直写(UE FTransform 语义)—— // decompose 出的 quat 直写(UE FTransform 语义)——
// 不经欧拉往返,拖拽零漂移、无万向锁 // 不经欧拉往返,拖拽零漂移、无万向锁
if (ImGuizmo::IsUsing()) { if (ImGuizmo::IsUsing()) {
@@ -272,9 +273,8 @@ std::optional<entt::entity> ViewportPanel::Draw(const EditorContext &ctx,
glm::quat rot; glm::quat rot;
glm::vec4 persp; glm::vec4 persp;
if (glm::decompose(m, scale, rot, transl, skew, persp)) { if (glm::decompose(m, scale, rot, transl, skew, persp)) {
tr->SetPosition(transl) ctx.scene->SetWorldPose(selected, transl, rot,
.SetRotationQuat(rot) scale);
.SetScale(scale);
} }
} }
} }
@@ -382,7 +382,7 @@ void ViewportPanel::LogPick(const EditorContext &ctx, entt::entity e) {
? "static" : "dynamic", ? "static" : "dynamic",
rb->boxHalfExtents.x, rb->boxHalfExtents.y, rb->boxHalfExtents.x, rb->boxHalfExtents.y,
rb->boxHalfExtents.z, rb->sphereRadius, rb->boxHalfExtents.z, rb->sphereRadius,
tr.GetPosition().x, tr.GetPosition().y, tr.GetPosition().z); tr.GetWorldPosition().x, tr.GetWorldPosition().y, tr.GetWorldPosition().z);
} else { } else {
ENGINE_INFO("[pick] '{}' (hitproxy) | (no collider)", name); ENGINE_INFO("[pick] '{}' (hitproxy) | (no collider)", name);
} }
+61 -11
View File
@@ -13,6 +13,8 @@
#include <Engine/Core/AssetRef.h> #include <Engine/Core/AssetRef.h>
#include <Engine/Renderer/Camera.h> #include <Engine/Renderer/Camera.h>
#include <entt/entt.hpp>
#include <glm/glm.hpp> #include <glm/glm.hpp>
#include <glm/gtc/matrix_transform.hpp> #include <glm/gtc/matrix_transform.hpp>
#include <glm/gtc/quaternion.hpp> #include <glm/gtc/quaternion.hpp>
@@ -40,22 +42,29 @@ namespace Engine::ECS {
std::string name; std::string name;
}; };
// 世界变换:UE USceneComponent 的 encapsulation 模式—— // 变换(UE FTransform 语义):TRS 分量 = 相对父的 **local** 值
// SRT 分量私有,所有写入口标脏;world 矩阵由帧循环的 //(无父实体 local 即 world,旧场景零迁移)。分量私有,所有写入口
// Scene::UpdateTransforms() 只对脏的实体重算(transform-update // 标脏;world 缓存(SRT 三分量 + 矩阵)由帧循环的
// 阶段),并自动向未来的子层级传播。渲染热路径只读 World()。 // Scene::UpdateTransforms() 从根 DFS 只对脏子树重算(transform-
// 层级(父子变换)留给后续的层级同步系统。 // update 阶段)。子 world = 父 world SRT 组合而非矩阵乘法——
// 非均匀父缩放下组合式仍是干净 SRT(矩阵分解会引入剪切)。
// 渲染热路径只读 World();灯光/相机/物理/Gizmo 读 world 三分量。
// //
// 旋转内部存储 = 四元数(UE FTransform 语义,不存欧拉角): // 旋转内部存储 = 四元数(UE FTransform 语义,不存欧拉角):
// 欧拉往返是漂移/万向锁之源(Gizmo 拖拽乱跳的根因)。欧拉只在 // 欧拉往返是漂移/万向锁之源(Gizmo 拖拽乱跳的根因)。欧拉只在
// 表皮出现——UI 显示(GetRotation 派生)与 UI 输入(SetRotation // 表皮出现——UI 显示(GetRotation 派生)与 UI 输入(SetRotation
// 转存);Gizmo/物理热路径走 quat 直写(SetRotationQuat)零往返。 // 转存);Gizmo/物理热路径走 quat 直写(SetRotationQuat)零往返。
struct TransformComponent { struct TransformComponent {
// ---- 读取 ---- // ---- local 读取(相对父;UI/游戏逻辑用)----
const glm::vec3& GetPosition() const { return m_Position; } const glm::vec3& GetPosition() const { return m_Position; }
const glm::quat& GetRotationQuat() const { return m_Rotation; } const glm::quat& GetRotationQuat() const { return m_Rotation; }
const glm::vec3& GetScale() const { return m_Scale; } 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 显示/编辑用,勿做存储)。 // 欧拉角(度)视图:从 quat 派生(仅 UI 显示/编辑用,勿做存储)。
// 按引擎 YXZ 序(R = Ry·Rx·Rz)从旋转矩阵反解;glm 列主序索引: // 按引擎 YXZ 序(R = Ry·Rx·Rz)从旋转矩阵反解;glm 列主序索引:
@@ -120,25 +129,66 @@ namespace Engine::ECS {
bool IsDirty() const { return m_Dirty; } bool IsDirty() const { return m_Dirty; }
// 显式标脏:层级系统改父/挂载后触发 world 重算用
//(local 值未变但 world 缓存已失效)
TransformComponent& MarkDirty() { m_Dirty = true; return *this; }
// SRT 分量 → world 矩阵(T * R * S),清除脏标记。 // SRT 分量 → world 矩阵(T * R * S),清除脏标记。
// 由 Scene::UpdateTransforms 调用(或层级系统),勿手动 // 根实体版(world = local);由 Scene::UpdateTransforms 调用,
// 勿手动
TransformComponent& Compose() TransformComponent& Compose()
{ {
const glm::mat4 t = glm::translate(glm::mat4(1.0f), m_Position); m_WorldPosition = m_Position;
const glm::mat4 s = glm::scale(glm::mat4(1.0f), m_Scale); m_WorldRotation = m_Rotation;
m_World = t * glm::mat4_cast(m_Rotation) * s; 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; m_Dirty = false;
return *this; return *this;
} }
private: 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::vec3 m_Position{0.0f};
glm::quat m_Rotation{1.0f, 0.0f, 0.0f, 0.0f}; // 单位四元数(w,x,y,z) glm::quat m_Rotation{1.0f, 0.0f, 0.0f, 0.0f}; // 单位四元数(w,x,y,z)
glm::vec3 m_Scale{1.0f}; 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}; glm::mat4 m_World{1.0f};
bool m_Dirty = true; // 出生即脏:首次 UpdateTransforms 计算 world bool m_Dirty = true; // 出生即脏:首次 UpdateTransforms 计算 world
}; };
// 层级链接(UE Actor attach):父句柄 + 子表。惰性创建——
// 只有参与父子关系的实体才有本组件,独立实体零存储成本。
// entt 句柄仅在 registry 存续期有效;序列化换 UUID,加载后重链
struct HierarchyComponent {
entt::entity parent = entt::null; // entt::null = 根实体
std::vector<entt::entity> children; // 挂载顺序
};
// 网格渲染体:引用共享的 Model 资产。 // 网格渲染体:引用共享的 Model 资产。
// 存中性 AssetRef(Core 层纯值类型,Unity InstanceID 思路)—— // 存中性 AssetRef(Core 层纯值类型,Unity InstanceID 思路)——
// ECS 不感知资产系统内部;解析归 AssetManager,悬空由 generation 拦截。 // ECS 不感知资产系统内部;解析归 AssetManager,悬空由 generation 拦截。
+26 -2
View File
@@ -52,7 +52,8 @@ namespace Engine::ECS {
// 返回句柄表皮:e.AddComponent<T>() / e.GetComponent<T>() / e.Destroy() // 返回句柄表皮:e.AddComponent<T>() / e.GetComponent<T>() / e.Destroy()
EntityHandle CreateEntity(std::string name = "new entity"); EntityHandle CreateEntity(std::string name = "new entity");
void DestroyEntity(Entity e); // 延迟:帧尾统一销毁 // 延迟:帧尾统一销毁。层级级联(UE 语义):销毁父 → 子树随葬
void DestroyEntity(Entity e);
void FlushDestroyQueue(); // 每帧末尾由 Application 调用 void FlushDestroyQueue(); // 每帧末尾由 Application 调用
// 整场景换血(序列化加载):立即销毁全部实体并清空延迟队列。 // 整场景换血(序列化加载):立即销毁全部实体并清空延迟队列。
@@ -66,10 +67,33 @@ namespace Engine::ECS {
// 帧循环统一重算脏 transform 的 world 矩阵(UE transform-update // 帧循环统一重算脏 transform 的 world 矩阵(UE transform-update
// 阶段模式)——由 Application 在 OnUpdate 之后、渲染之前调用, // 阶段模式)——由 Application 在 OnUpdate 之后、渲染之前调用,
// 用户代码永远不需要手动 Compose // 用户代码永远不需要手动 Compose。层级语义:从根 DFS,脏实体
// 或脏祖先的后代才重算(父先于子,ComposeFromParent 组合)
void UpdateTransforms(); void UpdateTransforms();
bool IsAlive(Entity e) const { return m_Registry.valid(e); } 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 // 按稳定 UUID 查实体(序列化/跨引用场景);找不到返回 entt::null
Entity FindEntityByUUID(UUID uuid); Entity FindEntityByUUID(UUID uuid);
@@ -51,8 +51,9 @@ namespace Engine {
// 一次性对齐版)。环绕 pivot 已失效,清除待 MMB 重建 // 一次性对齐版)。环绕 pivot 已失效,清除待 MMB 重建
void AlignTo(const ECS::TransformComponent& target) void AlignTo(const ECS::TransformComponent& target)
{ {
m_Transform.SetPosition(target.GetPosition()); // world 位姿复制(目标实体可能在层级子树下)
m_Transform.SetRotationQuat(target.GetRotationQuat()); m_Transform.SetPosition(target.GetWorldPosition());
m_Transform.SetRotationQuat(target.GetWorldRotationQuat());
m_Transform.Compose(); m_Transform.Compose();
m_Controller.ResetOrbit(); m_Controller.ResetOrbit();
} }
@@ -30,8 +30,9 @@ namespace Engine::Assets { class AssetManager; }
namespace Engine::Serialization { namespace Engine::Serialization {
// 当前场景文件格式版本(字段/结构变更时递增) // 当前场景文件格式版本(字段/结构变更时递增)。
inline constexpr int kSceneVersion = 1; // 2 = 层级(可选键 Parent: <uuid>,缺省 = 根实体,旧档零迁移)
inline constexpr int kSceneVersion = 2;
class SceneSerializer { class SceneSerializer {
public: public:
+195 -8
View File
@@ -8,6 +8,9 @@
#include <tracy/Tracy.hpp> #include <tracy/Tracy.hpp>
#include <algorithm>
#include <cmath>
namespace Engine::ECS { namespace Engine::ECS {
EntityHandle Scene::CreateEntity(std::string name) EntityHandle Scene::CreateEntity(std::string name)
@@ -37,8 +40,19 @@ namespace Engine::ECS {
ENGINE_FATAL("Scene::DestroyEntity: stale entity ({})", ENGINE_FATAL("Scene::DestroyEntity: stale entity ({})",
static_cast<u32>(e)); static_cast<u32>(e));
} }
// 延迟到帧尾,保证遍历期间池结构不变 // 层级级联(UE 语义):销毁父 → 附着子树随葬。整棵子树入队,
m_PendingDestroy.push_back(e); // 重复入队由 FlushDestroyQueue 的 valid 检查消化
std::vector<Entity> 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<HierarchyComponent>(cur)) {
for (Entity c : h->children) {
if (m_Registry.valid(c)) stack.push_back(c);
}
}
}
} }
void Scene::FlushDestroyQueue() void Scene::FlushDestroyQueue()
@@ -46,9 +60,17 @@ namespace Engine::ECS {
if (m_PendingDestroy.empty()) return; if (m_PendingDestroy.empty()) return;
for (Entity e : m_PendingDestroy) { for (Entity e : m_PendingDestroy) {
if (m_Registry.valid(e)) { if (!m_Registry.valid(e)) continue; // 级联重复入队 / 已销毁
m_Registry.destroy(e); // entt 自动回收槽位并递增版本号 // 摘链:从(仍存活的)父的 children 表移除,防悬空句柄残留
if (const auto* h = m_Registry.try_get<HierarchyComponent>(e);
h && h->parent != entt::null && m_Registry.valid(h->parent)) {
auto& siblings =
m_Registry.get<HierarchyComponent>(h->parent).children;
siblings.erase(
std::remove(siblings.begin(), siblings.end(), e),
siblings.end());
} }
m_Registry.destroy(e); // entt 自动回收槽位并递增版本号
} }
m_PendingDestroy.clear(); m_PendingDestroy.clear();
} }
@@ -69,13 +91,178 @@ namespace Engine::ECS {
void Scene::UpdateTransforms() void Scene::UpdateTransforms()
{ {
ZoneScoped; ZoneScoped;
// 只重算脏的:UE transform-update 阶段模式。 // 层级级联:从根 DFS(父先于子)。"自身脏 || 祖先脏" 才重算
// 静止实体(绝大多数)零成本;层级传播在此扩展 // ——父 world 变化传播给整棵子树,静止子树零成本(UE
m_Registry.view<TransformComponent>().each([](TransformComponent& t) { // transform-update 阶段模式)。迭代栈防爆栈(深层链安全)
if (t.IsDirty()) t.Compose(); struct Frame { Entity e; bool ancestorDirty; };
std::vector<Frame> stack;
m_Registry.view<TransformComponent>().each([&](Entity e, TransformComponent&) {
// 根 = 无有效父链接的实体
const auto* h = m_Registry.try_get<HierarchyComponent>(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<TransformComponent>(f.e);
const bool recompose = f.ancestorDirty || t.IsDirty();
const auto* hc = m_Registry.try_get<HierarchyComponent>(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<TransformComponent>(p));
} else {
t.Compose();
}
}
// 无论本节点是否重算都下探:子自身可能脏(新建实体),
// ancestorDirty = 本节点是否重算(父 world 变了才传播)
if (hc) {
for (Entity c : hc->children) {
if (m_Registry.valid(c) &&
m_Registry.all_of<TransformComponent>(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<HierarchyComponent>(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<TransformComponent>(child)
.GetWorldPosition();
const glm::quat wr = m_Registry.get<TransformComponent>(child)
.GetWorldRotationQuat();
const glm::vec3 ws = m_Registry.get<TransformComponent>(child)
.GetWorldScale();
// 从旧父摘链
auto& h = m_Registry.get_or_emplace<HierarchyComponent>(child);
if (h.parent != entt::null && m_Registry.valid(h.parent)) {
auto& siblings =
m_Registry.get<HierarchyComponent>(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<TransformComponent>(child).MarkDirty();
}
if (parent != entt::null) {
m_Registry.get_or_emplace<HierarchyComponent>(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<TransformComponent>(e);
if (p == entt::null) {
// 根:local 即 world
t.SetPosition(worldPos).SetRotationQuat(worldRot)
.SetScale(worldScale);
return;
}
EnsureWorldFresh(p);
const auto& pt = m_Registry.get<TransformComponent>(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<Entity> 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<TransformComponent>(*it);
if (!dirty && !t.IsDirty()) continue; // 该节点照旧
const Entity p = GetParent(*it);
if (p != entt::null) {
t.ComposeFromParent(m_Registry.get<TransformComponent>(p));
} else {
t.Compose();
}
dirty = true; // 子孙缓存已过时
}
}
CameraComponent* Scene::GetPrimaryCamera(TransformComponent** outTransform) CameraComponent* Scene::GetPrimaryCamera(TransformComponent** outTransform)
{ {
CameraComponent* found = nullptr; CameraComponent* found = nullptr;
+51 -40
View File
@@ -417,28 +417,29 @@ namespace Engine {
// 编辑器拖动 Dynamic 体(暂停期/物理未步进的帧)→ 传送 + // 编辑器拖动 Dynamic 体(暂停期/物理未步进的帧)→ 传送 +
// 清速度(UE 编辑器语义:编辑器操作权威于物理 body,拖动 = // 清速度(UE 编辑器语义:编辑器操作权威于物理 body,拖动 =
// 重新放置)。物理模拟中 Writeback 每步同步 lastPos/lastRot // 重新放置)。物理模拟中 Writeback 每步同步 lastPos/lastRot
// → 本比对只在真正拖动帧为真,不会打断自由模拟 // → 本比对只在真正拖动帧为真,不会打断自由模拟。
if (rec.lastPos != tr.GetPosition() || // body 是世界系:读 world 缓存(层级实体取父链组合结果)
rec.lastRot != tr.GetRotationQuat()) { if (rec.lastPos != tr.GetWorldPosition() ||
rec.lastRot != tr.GetWorldRotationQuat()) {
bi.SetPositionAndRotation( bi.SetPositionAndRotation(
id, id,
JPH::RVec3(tr.GetPosition().x, tr.GetPosition().y, tr.GetPosition().z), JPH::RVec3(tr.GetWorldPosition().x, tr.GetWorldPosition().y, tr.GetWorldPosition().z),
ToJolt(tr.GetRotationQuat()), ToJolt(tr.GetWorldRotationQuat()),
JPH::EActivation::Activate); JPH::EActivation::Activate);
bi.SetLinearVelocity(id, JPH::Vec3(0.0f, 0.0f, 0.0f)); bi.SetLinearVelocity(id, JPH::Vec3(0.0f, 0.0f, 0.0f));
bi.SetAngularVelocity(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.lastPos = tr.GetWorldPosition();
rec.lastRot = tr.GetRotationQuat(); rec.lastRot = tr.GetWorldRotationQuat();
} }
} else if (rec.lastPos != tr.GetPosition() || } else if (rec.lastPos != tr.GetWorldPosition() ||
rec.lastRot != tr.GetRotationQuat()) { rec.lastRot != tr.GetWorldRotationQuat()) {
// 编辑器拖动静态/运动学体 → 传送 body // 编辑器拖动静态/运动学体 → 传送 body
bi.SetPositionAndRotation(id, bi.SetPositionAndRotation(id,
JPH::RVec3(tr.GetPosition().x, tr.GetPosition().y, tr.GetPosition().z), JPH::RVec3(tr.GetWorldPosition().x, tr.GetWorldPosition().y, tr.GetWorldPosition().z),
ToJolt(tr.GetRotationQuat()), ToJolt(tr.GetWorldRotationQuat()),
JPH::EActivation::DontActivate); JPH::EActivation::DontActivate);
rec.lastPos = tr.GetPosition(); rec.lastPos = tr.GetWorldPosition();
rec.lastRot = tr.GetRotationQuat(); rec.lastRot = tr.GetWorldRotationQuat();
} }
} }
@@ -519,8 +520,8 @@ namespace Engine {
if (tr1 == nullptr || tr2 == nullptr) { if (tr1 == nullptr || tr2 == nullptr) {
return; return;
} }
const glm::vec3 s1 = tr1->GetScale(); const glm::vec3 s1 = tr1->GetWorldScale();
const glm::vec3 s2 = tr2->GetScale(); const glm::vec3 s2 = tr2->GetWorldScale();
// 双写锁读位姿/COM + 创建(SyncScene 在主线程,与 Update 不并发) // 双写锁读位姿/COM + 创建(SyncScene 在主线程,与 Update 不并发)
JPH::BodyLockWrite lock1(m_System->GetBodyLockInterface(), b1.id); JPH::BodyLockWrite lock1(m_System->GetBodyLockInterface(), b1.id);
@@ -642,22 +643,22 @@ namespace Engine {
if (found == m_Characters.end()) { if (found == m_Characters.end()) {
CharacterEntry entry; CharacterEntry entry;
entry.cc = std::make_unique<CharacterController>( entry.cc = std::make_unique<CharacterController>(
*m_System, p, tr.GetPosition()); *m_System, p, tr.GetWorldPosition());
entry.lastPos = tr.GetPosition(); entry.lastPos = tr.GetWorldPosition();
m_Characters.emplace(bits, std::move(entry)); m_Characters.emplace(bits, std::move(entry));
continue; continue;
} }
if (found->second.cc->GetParams() != p) { if (found->second.cc->GetParams() != p) {
// 参数变更 → 重建(位置沿用当前 Transform) // 参数变更 → 重建(位置沿用当前 Transform)
found->second.cc = std::make_unique<CharacterController>( found->second.cc = std::make_unique<CharacterController>(
*m_System, p, tr.GetPosition()); *m_System, p, tr.GetWorldPosition());
found->second.lastPos = tr.GetPosition(); found->second.lastPos = tr.GetWorldPosition();
continue; continue;
} }
if (found->second.lastPos != tr.GetPosition()) { if (found->second.lastPos != tr.GetWorldPosition()) {
// 编辑器拖动实体 → 传送角色(角色是唯一位置源,反向覆盖) // 编辑器拖动实体 → 传送角色(角色是唯一位置源,反向覆盖)
found->second.cc->Teleport(tr.GetPosition()); found->second.cc->Teleport(tr.GetWorldPosition());
found->second.lastPos = tr.GetPosition(); found->second.lastPos = tr.GetWorldPosition();
} }
} }
} }
@@ -672,8 +673,12 @@ namespace Engine {
continue; continue;
} }
entry.cc->Update(dt, gravity, *m_TempAllocator); entry.cc->Update(dt, gravity, *m_TempAllocator);
const glm::vec3 p = entry.cc->GetPosition(); // 原点 = 脚底 const glm::vec3 p = entry.cc->GetPosition(); // 原点 = 脚底(世界系)
tr->SetPosition(p); // 标脏 → UpdateTransforms 重算 world // 写回走 world→local 换算(层级实体挂父下仍正确;标脏 →
// UpdateTransforms 重算 world)
m_Scene.SetWorldPose(
static_cast<entt::entity>(bits), p,
tr->GetWorldRotationQuat(), tr->GetWorldScale());
entry.lastPos = p; entry.lastPos = p;
} }
} }
@@ -693,8 +698,9 @@ namespace Engine {
return nullptr; return nullptr;
} }
// 缩放烘进顶点:Jolt shape 无缩放语义,body 只吃 pos/rot // 缩放烘进顶点:Jolt shape 无缩放语义,body 只吃 pos/rot。
const glm::vec3 s = tr.GetScale(); // 取 world 缩放(层级实体 = 父链累计)
const glm::vec3 s = tr.GetWorldScale();
// 动态/运动体 → ConvexHull(MeshShape 三角网格太贵,Jolt 不允许动) // 动态/运动体 → ConvexHull(MeshShape 三角网格太贵,Jolt 不允许动)
if (rb.type != ECS::RigidBodyComponent::BodyType::Static) { if (rb.type != ECS::RigidBodyComponent::BodyType::Static) {
@@ -833,23 +839,23 @@ namespace Engine {
shape = BuildMeshShape(entityBits, rb, tr); shape = BuildMeshShape(entityBits, rb, tr);
if (shape == nullptr) { if (shape == nullptr) {
shape = new JPH::BoxShape(JPH::Vec3( shape = new JPH::BoxShape(JPH::Vec3(
std::max(rb.boxHalfExtents.x * tr.GetScale().x, 0.01f), std::max(rb.boxHalfExtents.x * tr.GetWorldScale().x, 0.01f),
std::max(rb.boxHalfExtents.y * tr.GetScale().y, 0.01f), std::max(rb.boxHalfExtents.y * tr.GetWorldScale().y, 0.01f),
std::max(rb.boxHalfExtents.z * tr.GetScale().z, 0.01f))); std::max(rb.boxHalfExtents.z * tr.GetWorldScale().z, 0.01f)));
} }
} else { } else {
shape = new JPH::BoxShape(JPH::Vec3( shape = new JPH::BoxShape(JPH::Vec3(
std::max(rb.boxHalfExtents.x * tr.GetScale().x, 0.01f), std::max(rb.boxHalfExtents.x * tr.GetWorldScale().x, 0.01f),
std::max(rb.boxHalfExtents.y * tr.GetScale().y, 0.01f), std::max(rb.boxHalfExtents.y * tr.GetWorldScale().y, 0.01f),
std::max(rb.boxHalfExtents.z * tr.GetScale().z, 0.01f))); std::max(rb.boxHalfExtents.z * tr.GetWorldScale().z, 0.01f)));
} }
const bool dynamic = rb.type == ECS::RigidBodyComponent::BodyType::Dynamic; const bool dynamic = rb.type == ECS::RigidBodyComponent::BodyType::Dynamic;
const bool kinematic = rb.type == ECS::RigidBodyComponent::BodyType::Kinematic; const bool kinematic = rb.type == ECS::RigidBodyComponent::BodyType::Kinematic;
JPH::BodyCreationSettings settings( JPH::BodyCreationSettings settings(
shape, shape,
JPH::RVec3(tr.GetPosition().x, tr.GetPosition().y, tr.GetPosition().z), JPH::RVec3(tr.GetWorldPosition().x, tr.GetWorldPosition().y, tr.GetWorldPosition().z),
ToJolt(tr.GetRotationQuat()), ToJolt(tr.GetWorldRotationQuat()),
dynamic dynamic
? JPH::EMotionType::Dynamic ? JPH::EMotionType::Dynamic
: (kinematic : (kinematic
@@ -882,8 +888,8 @@ namespace Engine {
rec.compound = rb.compound; rec.compound = rb.compound;
rec.mass = rb.mass; rec.mass = rb.mass;
rec.isTrigger = rb.isTrigger; rec.isTrigger = rb.isTrigger;
rec.lastPos = tr.GetPosition(); rec.lastPos = tr.GetWorldPosition();
rec.lastRot = tr.GetRotationQuat(); rec.lastRot = tr.GetWorldRotationQuat();
m_Bodies[entityBits] = rec; m_Bodies[entityBits] = rec;
rb.bodyId = id.GetIndexAndSequenceNumber(); rb.bodyId = id.GetIndexAndSequenceNumber();
@@ -1053,8 +1059,13 @@ namespace Engine {
const JPH::BodyID id(rb.bodyId); const JPH::BodyID id(rb.bodyId);
const JPH::RVec3 pos = bi.GetPosition(id); const JPH::RVec3 pos = bi.GetPosition(id);
const JPH::Quat rot = bi.GetRotation(id); const JPH::Quat rot = bi.GetRotation(id);
tr.SetPosition(glm::vec3(pos.GetX(), pos.GetY(), pos.GetZ())); // body 姿态是世界系:写回经 world→local 换算(挂父层级实体
tr.SetRotationQuat(ToGlm(rot)); // 不破坏相对位姿;根实体 = 直写)。标脏 → UpdateTransforms 重算
m_Scene.SetWorldPose(
e,
glm::vec3(pos.GetX(), pos.GetY(), pos.GetZ()),
ToGlm(rot),
tr.GetWorldScale());
const JPH::Vec3 vel = bi.GetLinearVelocity(id); const JPH::Vec3 vel = bi.GetLinearVelocity(id);
rb.velocity = glm::vec3(vel.GetX(), vel.GetY(), vel.GetZ()); rb.velocity = glm::vec3(vel.GetX(), vel.GetY(), vel.GetZ());
@@ -1063,8 +1074,8 @@ namespace Engine {
// 位移误判为编辑器拖动 → teleport 回旧位置死循环 // 位移误判为编辑器拖动 → teleport 回旧位置死循环
const u32 bits = static_cast<u32>(entt::to_integral(e)); const u32 bits = static_cast<u32>(entt::to_integral(e));
if (auto it = m_Bodies.find(bits); it != m_Bodies.end()) { if (auto it = m_Bodies.find(bits); it != m_Bodies.end()) {
it->second.lastPos = tr.GetPosition(); it->second.lastPos = tr.GetWorldPosition();
it->second.lastRot = tr.GetRotationQuat(); it->second.lastRot = tr.GetWorldRotationQuat();
} }
} }
} }
+3 -3
View File
@@ -354,7 +354,7 @@ namespace Engine
: 2.0f); : 2.0f);
if (light.type == LightComponent::Type::Point) { 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); dst.direction = glm::vec4(0.0f);
} else { } else {
glm::vec3 dir = glm::length(light.direction) > 0.0f glm::vec3 dir = glm::length(light.direction) > 0.0f
@@ -391,7 +391,7 @@ namespace Engine
shadowDir = glm::vec3(collected[0].direction); shadowDir = glm::vec3(collected[0].direction);
} }
ComputeCascades(shadowDir); ComputeCascades(shadowDir);
ubo.cameraPos = glm::vec4(camTransform->GetPosition(), 1.0f); ubo.cameraPos = glm::vec4(camTransform->GetWorldPosition(), 1.0f);
// ---- 2.5 骨骼动画推进 + 姿态采样(与可见性无关:播放头恒走, // ---- 2.5 骨骼动画推进 + 姿态采样(与可见性无关:播放头恒走,
// 否则被剔除的实体回屏时动画跳变)---- // 否则被剔除的实体回屏时动画跳变)----
@@ -579,7 +579,7 @@ namespace Engine
// LOD 选级:屏占比近似 = radius/距离(主视图按实例距离; // LOD 选级:屏占比近似 = radius/距离(主视图按实例距离;
// 阈值 10/20 倍半径,可调)。开关关闭 = 恒 LOD0 // 阈值 10/20 倍半径,可调)。开关关闭 = 恒 LOD0
const f32 dist = const f32 dist =
glm::length(worldCenter - camTransform->GetPosition()); glm::length(worldCenter - camTransform->GetWorldPosition());
u32 mainLod = 0; u32 mainLod = 0;
if (m_LodEnabled) { if (m_LodEnabled) {
if (dist >= worldRadius * 20.0f) { if (dist >= worldRadius * 20.0f) {
@@ -34,6 +34,7 @@
#include <fstream> #include <fstream>
#include <string> #include <string>
#include <string_view> #include <string_view>
#include <unordered_map>
#include <unordered_set> #include <unordered_set>
#include <vector> #include <vector>
@@ -176,6 +177,7 @@ namespace Engine::Serialization {
{ {
u64 uuid = 0; u64 uuid = 0;
std::string name = "entity"; std::string name = "entity";
u64 parentUuid = 0; // 0 = 根实体(层级键缺省)
YAML::Node comps; // 缺省 = Null(无组件) YAML::Node comps; // 缺省 = Null(无组件)
}; };
@@ -190,6 +192,13 @@ namespace Engine::Serialization {
if (const auto* name = reg.try_get<ECS::NameComponent>(e)) { if (const auto* name = reg.try_get<ECS::NameComponent>(e)) {
out << YAML::Key << "Name" << YAML::Value << name->name; out << YAML::Key << "Name" << YAML::Value << name->name;
} }
// 层级(UE Actor attach):父存 UUID(跨存档稳定);
// 悬空父(不该发生)按根实体写
if (const auto* h = reg.try_get<ECS::HierarchyComponent>(e);
h && h->parent != entt::null && reg.valid(h->parent)) {
out << YAML::Key << "Parent" << YAML::Value
<< reg.get<ECS::IdComponent>(h->parent).uuid.Value();
}
out << YAML::Key << "Components" << YAML::Value << YAML::BeginMap; out << YAML::Key << "Components" << YAML::Value << YAML::BeginMap;
@@ -524,6 +533,7 @@ namespace Engine::Serialization {
ParsedEntity pe; ParsedEntity pe;
pe.uuid = e["Entity"].as<u64>(0); pe.uuid = e["Entity"].as<u64>(0);
pe.name = e["Name"].as<std::string>(pe.name); pe.name = e["Name"].as<std::string>(pe.name);
pe.parentUuid = e["Parent"].as<u64>(0);
if (const YAML::Node comps = e["Components"]; if (const YAML::Node comps = e["Components"];
comps && comps.IsMap()) { comps && comps.IsMap()) {
// 仅在 rhs 有效时赋值(僵尸节点 operator= 会抛) // 仅在 rhs 有效时赋值(僵尸节点 operator= 会抛)
@@ -543,6 +553,7 @@ namespace Engine::Serialization {
scene.Clear(); scene.Clear();
std::unordered_set<u64> seenUuids; std::unordered_set<u64> seenUuids;
std::unordered_map<u64, entt::entity> byUuid; // 第二遍重链用
for (ParsedEntity& pe : parsed) { for (ParsedEntity& pe : parsed) {
auto handle = scene.CreateEntity(pe.name); auto handle = scene.CreateEntity(pe.name);
@@ -552,6 +563,8 @@ namespace Engine::Serialization {
uuid = UUID::New().Value(); uuid = UUID::New().Value();
} }
handle.GetComponent<ECS::IdComponent>().uuid = UUID{uuid}; handle.GetComponent<ECS::IdComponent>().uuid = UUID{uuid};
pe.uuid = uuid; // 回写最终 uuid(重生成后父引用仍可命中)
byUuid.emplace(uuid, static_cast<entt::entity>(handle));
const YAML::Node comps = pe.comps; // 拷贝初始化,缺省 Null 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: // 加载在 OnImGuiRender 段,错过本帧 UpdateTransforms:
// 显式补算,否则首帧 World = 单位阵 // 显式补算,否则首帧 World = 单位阵
scene.UpdateTransforms(); scene.UpdateTransforms();