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:
+152
-53
@@ -1,5 +1,9 @@
|
||||
//
|
||||
// HierarchyPanel.cpp:实体列表实现。
|
||||
// HierarchyPanel.cpp:实体层级树实现(UE Outliner 语义)。
|
||||
//
|
||||
// 树形展示(根 = 无有效父链接的实体,子 = HierarchyComponent 挂载序)、
|
||||
// 点击选中、拖拽改父/拖到空白区挂根(keepWorld = 挂载前后世界位姿
|
||||
// 不变)、右键 Create Child / Detach / Delete(级联随葬,延迟销毁)。
|
||||
//
|
||||
|
||||
#include "HierarchyPanel.h"
|
||||
@@ -8,16 +12,137 @@
|
||||
|
||||
#include <imgui.h>
|
||||
|
||||
#include <cstring>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
using namespace Engine;
|
||||
|
||||
namespace {
|
||||
struct ScopedID {
|
||||
explicit ScopedID(const u64 uuid) {
|
||||
ImGui::PushID(reinterpret_cast<const void*>(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<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)
|
||||
{
|
||||
@@ -33,55 +158,29 @@ void HierarchyPanel::Draw(const EditorContext& ctx, entt::entity& selected)
|
||||
|
||||
auto& reg = ctx.scene->GetRegistry();
|
||||
|
||||
// 树形遍历:根 = 无有效父链接的实体(creation 序),子按挂载序递归
|
||||
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);
|
||||
|
||||
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");
|
||||
}
|
||||
}
|
||||
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<entt::entity>(bits),
|
||||
entt::null);
|
||||
}
|
||||
ImGui::EndDragDropTarget();
|
||||
}
|
||||
}
|
||||
|
||||
ImGui::End();
|
||||
}
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
//
|
||||
// HierarchyPanel:实体列表面板。
|
||||
// HierarchyPanel:实体层级树面板。
|
||||
//
|
||||
// 点击选中(写 Editor 的选中状态)、工具栏创建实体、右键删除
|
||||
//(延迟销毁,帧尾 flush,遍历安全)。每项尾缀显示组件构成徽标。
|
||||
// 树形展示父子层级(UE Outliner)、点击选中、拖拽改父/拖到空白挂根
|
||||
//(keepWorld)、右键 Create Child / Detach / Delete(级联随葬,帧尾
|
||||
// flush,遍历安全)。每项尾缀显示组件构成徽标。
|
||||
//
|
||||
|
||||
#ifndef HELLOVULKAN_HIERARCHYPANEL_H
|
||||
|
||||
@@ -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<Engine::ECS::NameComponent>(p)) {
|
||||
pname = n->name;
|
||||
}
|
||||
ImGui::TextDisabled("parent: %s", pname.c_str());
|
||||
}
|
||||
}
|
||||
glm::vec3 pos = t->GetPosition();
|
||||
if (DragVec3Row("position", pos, 0.05f))
|
||||
{
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -164,14 +164,15 @@ std::optional<entt::entity> 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<entt::entity> 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<entt::entity> 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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user