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
+195 -8
View File
@@ -8,6 +8,9 @@
#include <tracy/Tracy.hpp>
#include <algorithm>
#include <cmath>
namespace Engine::ECS {
EntityHandle Scene::CreateEntity(std::string name)
@@ -37,8 +40,19 @@ namespace Engine::ECS {
ENGINE_FATAL("Scene::DestroyEntity: stale entity ({})",
static_cast<u32>(e));
}
// 延迟到帧尾,保证遍历期间池结构不变
m_PendingDestroy.push_back(e);
// 层级级联(UE 语义):销毁父 → 附着子树随葬。整棵子树入队,
// 重复入队由 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()
@@ -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<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();
}
@@ -69,13 +91,178 @@ namespace Engine::ECS {
void Scene::UpdateTransforms()
{
ZoneScoped;
// 只重算脏的:UE transform-update 阶段模式。
// 静止实体(绝大多数)零成本;层级传播在此扩展
m_Registry.view<TransformComponent>().each([](TransformComponent& t) {
if (t.IsDirty()) t.Compose();
// 层级级联:从根 DFS(父先于子)。"自身脏 || 祖先脏" 才重算
// ——父 world 变化传播给整棵子树,静止子树零成本(UE
// transform-update 阶段模式)。迭代栈防爆栈(深层链安全)
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* found = nullptr;
+51 -40
View File
@@ -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<CharacterController>(
*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<CharacterController>(
*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<entt::entity>(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<u32>(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();
}
}
}
+3 -3
View File
@@ -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) {
@@ -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();