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
@@ -34,6 +34,7 @@
#include <fstream>
#include <string>
#include <string_view>
#include <unordered_map>
#include <unordered_set>
#include <vector>
@@ -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<ECS::NameComponent>(e)) {
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;
@@ -524,6 +533,7 @@ namespace Engine::Serialization {
ParsedEntity pe;
pe.uuid = e["Entity"].as<u64>(0);
pe.name = e["Name"].as<std::string>(pe.name);
pe.parentUuid = e["Parent"].as<u64>(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<u64> seenUuids;
std::unordered_map<u64, entt::entity> byUuid; // 第二遍重链用
for (ParsedEntity& pe : parsed) {
auto handle = scene.CreateEntity(pe.name);
@@ -552,6 +563,8 @@ namespace Engine::Serialization {
uuid = UUID::New().Value();
}
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
@@ -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();