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);
|
||||
}
|
||||
|
||||
@@ -13,6 +13,8 @@
|
||||
#include <Engine/Core/AssetRef.h>
|
||||
#include <Engine/Renderer/Camera.h>
|
||||
|
||||
#include <entt/entt.hpp>
|
||||
|
||||
#include <glm/glm.hpp>
|
||||
#include <glm/gtc/matrix_transform.hpp>
|
||||
#include <glm/gtc/quaternion.hpp>
|
||||
@@ -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<entt::entity> children; // 挂载顺序
|
||||
};
|
||||
|
||||
// 网格渲染体:引用共享的 Model 资产。
|
||||
// 存中性 AssetRef(Core 层纯值类型,Unity InstanceID 思路)——
|
||||
// ECS 不感知资产系统内部;解析归 AssetManager,悬空由 generation 拦截。
|
||||
|
||||
@@ -52,7 +52,8 @@ namespace Engine::ECS {
|
||||
|
||||
// 返回句柄表皮:e.AddComponent<T>() / e.GetComponent<T>() / 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);
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -30,8 +30,9 @@ namespace Engine::Assets { class AssetManager; }
|
||||
|
||||
namespace Engine::Serialization {
|
||||
|
||||
// 当前场景文件格式版本(字段/结构变更时递增)
|
||||
inline constexpr int kSceneVersion = 1;
|
||||
// 当前场景文件格式版本(字段/结构变更时递增)。
|
||||
// 2 = 层级(可选键 Parent: <uuid>,缺省 = 根实体,旧档零迁移)
|
||||
inline constexpr int kSceneVersion = 2;
|
||||
|
||||
class SceneSerializer {
|
||||
public:
|
||||
|
||||
+195
-8
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user