From ed7cb417bd8815be1c7ac7f9e64b1685530084a9 Mon Sep 17 00:00:00 2001 From: atdunbg Date: Thu, 8 Oct 2026 22:01:26 +0800 Subject: [PATCH] =?UTF-8?q?=E6=96=B0=E5=A2=9E=20Prefab=20=E7=B3=BB?= =?UTF-8?q?=E7=BB=9F=EF=BC=9A=E5=AE=9E=E4=BD=93=E5=AD=90=E6=A0=91=E5=AD=98?= =?UTF-8?q?=E5=8F=96=20+=20=E7=BC=96=E8=BE=91=E5=99=A8=20Save=20As=20Prefa?= =?UTF-8?q?b/=E6=8B=96=E5=85=A5=E5=AE=9E=E4=BE=8B=E5=8C=96=20+=20=E8=84=9A?= =?UTF-8?q?=E6=9C=AC=20Instantiate?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- App/editor/panels/AssetBrowserPanel.cpp | 40 +- App/editor/panels/AssetBrowserPanel.h | 5 + App/editor/panels/HierarchyPanel.cpp | 71 ++- App/scripts/PrefabTest.cpp | 88 +++ Engine/include/Engine/Scripting/Script.h | 10 + .../Engine/Serialization/SceneSerializer.h | 27 + Engine/src/Scripting/ScriptRuntime.cpp | 10 + Engine/src/Serialization/SceneSerializer.cpp | 600 ++++++++++++------ 8 files changed, 643 insertions(+), 208 deletions(-) create mode 100644 App/scripts/PrefabTest.cpp diff --git a/App/editor/panels/AssetBrowserPanel.cpp b/App/editor/panels/AssetBrowserPanel.cpp index 9945318..b23ab96 100644 --- a/App/editor/panels/AssetBrowserPanel.cpp +++ b/App/editor/panels/AssetBrowserPanel.cpp @@ -7,6 +7,7 @@ #include "ControllerEditorPanel.h" // kControllerDragPayload + 双击打开控制器编辑器 #include +#include #include @@ -27,10 +28,12 @@ namespace { // 类型过滤(下标即身份):0 = All constexpr const char* kTypeNames[] = {"All", "Models", "Textures", "Scenes", - "Materials", "Controllers"}; + "Materials", "Controllers", + "Prefabs"}; // 资产类别:按扩展名分派(过滤用) - enum class Kind { Model, Texture, Scene, Material, AnimClip, Other }; + enum class Kind { Model, Texture, Scene, Material, AnimClip, Prefab, + Other }; Kind KindOf(const std::filesystem::path& p) { @@ -59,6 +62,10 @@ namespace { { return Kind::AnimClip; } + if (ext == ".prefab") + { + return Kind::Prefab; + } return Kind::Other; } @@ -71,6 +78,7 @@ namespace { case 3: return kind == Kind::Scene; case 4: return kind == Kind::Material; case 5: return kind == Kind::AnimClip; + case 6: return kind == Kind::Prefab; default: return true; } } @@ -85,6 +93,7 @@ namespace { case Kind::Scene: return "[S]"; case Kind::Material: return "[Ma]"; case Kind::AnimClip: return "[An]"; + case Kind::Prefab: return "[Pf]"; default: return "[ ]"; } } @@ -121,7 +130,7 @@ void AssetBrowserPanel::Draw(const EditorContext& ctx) } ImGui::SameLine(); ImGui::SetNextItemWidth(110.0f); - ImGui::Combo("##type", &m_TypeFilter, kTypeNames, 6); + ImGui::Combo("##type", &m_TypeFilter, kTypeNames, 7); ImGui::SameLine(); ImGui::SetNextItemWidth(140.0f); ImGui::InputTextWithHint("##name", "filter", m_NameFilter, @@ -296,6 +305,16 @@ void AssetBrowserPanel::DrawFileList(const EditorContext& ctx, ImGui::EndDragDropSource(); } + // .prefab 拖拽源:拖到 Outliner(节点=挂其下 / 空白区=挂根) + // 实例化整棵子树(kPrefabDragPayload) + if (item.kind == Kind::Prefab && ImGui::BeginDragDropSource()) + { + ImGui::SetDragDropPayload(kPrefabDragPayload, + assetPath.c_str(), assetPath.size() + 1); + ImGui::TextUnformatted(name.c_str()); + ImGui::EndDragDropSource(); + } + // 双击:.material 打开 Material Editor;.animctrl 打开 // Controller Editor;model/HDR 进缓存预热(路径去重,重复 // 双击零成本);场景加载含环境恢复/选中清理流程,提示走 @@ -328,6 +347,16 @@ void AssetBrowserPanel::DrawFileList(const EditorContext& ctx, ENGINE_INFO("AssetBrowser: load scenes via File menu ({})", assetPath); } + else if (item.kind == Kind::Prefab && ctx.scene && ctx.assets) + { + // 双击 = 快捷实例化(授权位姿落点,选中交给 Outliner + // 手动改);拖拽实例化见 Outliner 落点 + const auto root = Engine::Serialization::SceneSerializer:: + InstantiatePrefab(*ctx.scene, *ctx.assets, assetPath); + ENGINE_INFO("AssetBrowser: prefab '{}' instantiated " + "(valid={})", + assetPath, root != entt::null); + } } // hover 信息卡(UE Content Browser 式):类型/大小/修改时间/加载状态 @@ -336,11 +365,12 @@ void AssetBrowserPanel::DrawFileList(const EditorContext& ctx, ImGui::BeginTooltip(); ImGui::TextUnformatted(name.c_str()); ImGui::Separator(); - // 与 Kind 枚举逐位对齐(Kind::Other = 5)——漏项 = tooltip + // 与 Kind 枚举逐位对齐(Kind::Other = 6)——漏项 = tooltip // 数组越界读 static const char* kKindNames[] = {"Model", "Texture/Environment", "Scene", "Material", - "Controller", "File"}; + "Controller", "Prefab", + "File"}; ImGui::Text("type: %s", kKindNames[static_cast(item.kind)]); namespace fs = std::filesystem; std::error_code ec; diff --git a/App/editor/panels/AssetBrowserPanel.h b/App/editor/panels/AssetBrowserPanel.h index c10c447..802dc22 100644 --- a/App/editor/panels/AssetBrowserPanel.h +++ b/App/editor/panels/AssetBrowserPanel.h @@ -18,6 +18,11 @@ #include #include +// Prefab 资产拖拽 payload(Content Browser 拖 .prefab → Outliner 落点 +// 实例化;字符串 = 资产相对路径)。对齐 kMaterialDragPayload 模式: +// 常量住源面板头 +inline constexpr const char* kPrefabDragPayload = "PREFAB_PATH"; + class AssetBrowserPanel { public: void Draw(const EditorContext& ctx); diff --git a/App/editor/panels/HierarchyPanel.cpp b/App/editor/panels/HierarchyPanel.cpp index 2fdbef3..fd41e1e 100644 --- a/App/editor/panels/HierarchyPanel.cpp +++ b/App/editor/panels/HierarchyPanel.cpp @@ -7,12 +7,16 @@ // #include "HierarchyPanel.h" +#include "AssetBrowserPanel.h" // kPrefabDragPayload(Content Browser 源) #include "Engine/ECS/Components.h" #include "Engine/ECS/EntityHandle.h" // CreateEntity 返回完整 EntityHandle +#include "Engine/Serialization/SceneSerializer.h" +#include "Engine/Core/Log.h" #include #include +#include #include #include @@ -33,6 +37,34 @@ namespace { return "(unnamed)"; } + // Prefab 拖入实例化(树节点/空白区落点共用)。根实体挂在场景根、 + // 保留授权位姿;返回根实体(失败 entt::null) + entt::entity InstantiatePrefabDrop(const EditorContext& ctx, + const char* path) + { + if (!ctx.scene || !ctx.assets || !path) { + return entt::null; + } + return Engine::Serialization::SceneSerializer::InstantiatePrefab( + *ctx.scene, *ctx.assets, path); + } + + // 实体名 → 合法文件名(路径分隔符/保留符净化;空名兜底) + std::string PrefabFileName(std::string name) + { + for (char& c : name) { + if (c == '/' || c == '\\' || c == ':' || c == '*' || + c == '?' || c == '"' || c == '<' || c == '>' || + c == '|') { + c = '_'; + } + } + if (name.empty() || name == "(unnamed)") { + name = "prefab"; + } + return name; + } + // 递归绘制单节点(含子树)。返回值无意义,选中写 selected void DrawEntityNode(const EditorContext& ctx, entt::entity e, entt::entity& selected) @@ -78,7 +110,9 @@ namespace { ImGui::EndDragDropSource(); } - // ---- 拖放目标:挂到本实体下(keepWorld,世界位姿不变)---- + // ---- 拖放目标:挂到本实体下(keepWorld,世界位姿不变); + // Prefab 拖入 = 实例化子树后挂到本实体下(keepWorld 保持 + // 授权世界位姿)---- if (ImGui::BeginDragDropTarget()) { if (const ImGuiPayload* pl = ImGui::AcceptDragDropPayload(kHierPayload)) { @@ -91,6 +125,17 @@ namespace { ctx.scene->SetParent(dragged, e); } } + if (const ImGuiPayload* pl = + ImGui::AcceptDragDropPayload(kPrefabDragPayload)) { + if (const entt::entity root = + InstantiatePrefabDrop(ctx, + static_cast( + pl->Data)); + root != entt::null) { + ctx.scene->SetParent(root, e); // keepWorld 默认 true + selected = root; + } + } ImGui::EndDragDropTarget(); } @@ -102,6 +147,19 @@ namespace { ctx.scene->SetParent(child, e); selected = child; } + if (ImGui::MenuItem("Save As Prefab")) { + // 固定目录 + 实体名派生文件名(无文件对话框库,与场景 + // File 菜单同策略;重名覆盖,改名走文件系统) + const std::string path = + "assets/prefabs/" + + PrefabFileName(DisplayName(reg, e)) + ".prefab"; + std::error_code ec; + std::filesystem::create_directories("assets/prefabs", ec); + if (Serialization::SceneSerializer::SerializePrefab( + *ctx.scene, *ctx.assets, e, path)) { + ENGINE_INFO("Hierarchy: prefab saved to '{}'", path); + } + } if (ctx.scene->GetParent(e) != entt::null && ImGui::MenuItem("Detach to Root")) { ctx.scene->SetParent(e, entt::null); @@ -194,7 +252,8 @@ void HierarchyPanel::Draw(const EditorContext& ctx, entt::entity& selected) DrawEntityNode(ctx, e, selected); }); - // ---- 空白区 = 挂根落点(UE Outliner 拖到空白 = 顶层)---- + // ---- 空白区 = 挂根落点(UE Outliner 拖到空白 = 顶层); + // Prefab 拖入 = 实例化到场景根(授权位姿)---- ImVec2 rem = ImGui::GetContentRegionAvail(); if (rem.y > 4.0f) { ImGui::InvisibleButton("##hier_drop_root", rem); @@ -206,6 +265,14 @@ void HierarchyPanel::Draw(const EditorContext& ctx, entt::entity& selected) ctx.scene->SetParent(static_cast(bits), entt::null); } + if (const ImGuiPayload* pl = + ImGui::AcceptDragDropPayload(kPrefabDragPayload)) { + if (const entt::entity root = InstantiatePrefabDrop( + ctx, static_cast(pl->Data)); + root != entt::null) { + selected = root; + } + } ImGui::EndDragDropTarget(); } } diff --git a/App/scripts/PrefabTest.cpp b/App/scripts/PrefabTest.cpp new file mode 100644 index 0000000..7b80f9d --- /dev/null +++ b/App/scripts/PrefabTest.cpp @@ -0,0 +1,88 @@ +// +// PrefabTest.cpp:阶段2 验收脚本——脚本侧 Instantiate(prefab) 运行时 +// 实例化链路。 +// +// PrefabTest(挂任意空实体,作为实例化锚点): +// OnStart 按参数路径实例化一次 prefab;Interval > 0 时周期性再实例化 +// (多实例验收:每次 UUID 全量再生、物理体独立、沿 X 向排开下落)。 +// +// 覆盖的引擎设施: +// - ScriptBehavior::Instantiate(path, pos, rot)(Unity Object. +// Instantiate 同位)→ SceneSerializer::InstantiatePrefab +// - prefab 子树含 RigidBody/ScriptComponent 时由 PhysicsWorld/ +// ScriptRuntime 下一帧对账消化(与 Spawn 同语义) +// +// 验收步骤: +// 1. 编辑器搭一个实体子树(如 Dynamic 盒刚体 + MeshRenderer 的箱子, +// 可再挂个子实体验证层级一起走)→ Outliner 右键 Save As Prefab +// (落盘 assets/prefabs/<名字>.prefab) +// 2. 建空实体挂 PrefabTest,参数 PrefabPath 填该路径 → Play: +// 锚点处出现实例;Interval 设 1.0 可看连续多实例物理下落互不串扰 +// 3. 编辑态也可从 Content Browser 把 .prefab 拖进 Outliner(节点= +// 挂其下,空白区=挂根)或双击直接实例化——UUID 独立、资产引用有效 +// + +#include + +#include + +using namespace Engine; // f32 / ScriptBehavior 等免前缀(对齐 TestShoot) + +namespace { + + using Engine::Scripting::ParamBuilder; + + class PrefabTest final : public Engine::Scripting::ScriptBehavior { + public: + const char* Name() const override { return "PrefabTest"; } + + void DeclareParams(ParamBuilder& pb) override { + // prefab 资产路径(工作目录相对,同场景/资产管理约定) + pb.Field("PrefabPath", &m_PrefabPath, + std::string{"assets/prefabs/crate.prefab"}); + // 周期实例化间隔(秒;<=0 = 仅 OnStart 实例化一次) + pb.Field("Interval", &m_Interval, 0.0f); + // 相邻实例的 X 向排开间距(多实例不叠在一起) + pb.Field("SpreadX", &m_SpreadX, 2.0f); + } + + void OnStart() override { + m_Timer = m_Interval; + SpawnInstance(); + } + + void OnUpdate(f32 dt) override { + if (m_Interval <= 0.0f) { + return; + } + m_Timer -= dt; + if (m_Timer <= 0.0f) { + m_Timer = m_Interval; + SpawnInstance(); + } + } + + private: + void SpawnInstance() { + // 锚点实体位姿 + 排开偏移;旋转 = 锚点自身旋转 + const auto& tr = Transform(); + const glm::vec3 pos = + tr.GetPosition() + glm::vec3{m_SpreadX * f32(m_Count), + 0.0f, 0.0f}; + const glm::quat rot = tr.GetRotationQuat(); + if (Instantiate(m_PrefabPath, pos, rot) == entt::null) { + return; // 路径无效/解析失败:日志已在序列化器里打过 + } + ++m_Count; + } + + std::string m_PrefabPath; + f32 m_Interval = 0.0f; + f32 m_SpreadX = 2.0f; + f32 m_Timer = 0.0f; + int m_Count = 0; // 已实例化份数(排开偏移基准) + }; + + ENGINE_REGISTER_SCRIPT(PrefabTest) + +} // namespace diff --git a/Engine/include/Engine/Scripting/Script.h b/Engine/include/Engine/Scripting/Script.h index 8279cb7..2500dfb 100644 --- a/Engine/include/Engine/Scripting/Script.h +++ b/Engine/include/Engine/Scripting/Script.h @@ -123,6 +123,16 @@ namespace Engine { //(遍历安全);调用方持有的句柄此后悬空 void Destroy(ECS::Entity e); + // ---- Prefab 实例化(Unity Object.Instantiate 同位)---- + // .prefab 文件 → 整棵子树追加进场景(UUID 全量再生,子树内 + // Joint.target 自动重映射)。pos/rot = 根实体 local 位姿 + //(根挂场景根时即世界位姿)。失败返回 entt::null; + // RigidBody/ScriptComponent 由 PhysicsWorld/ScriptRuntime + // 下一帧对账消化(与 Spawn 同语义) + ECS::Entity Instantiate(const std::string& prefabPath, + const glm::vec3& pos, + const glm::quat& rot = glm::quat{}); + // ---- 组件启停(Unity SetActive / Behaviour.enabled 语义)---- // 自身组件总开关(整个脚本组件停 tick,实例保留) void SetActive(bool on) { diff --git a/Engine/include/Engine/Serialization/SceneSerializer.h b/Engine/include/Engine/Serialization/SceneSerializer.h index 673b3d2..f6f15b6 100644 --- a/Engine/include/Engine/Serialization/SceneSerializer.h +++ b/Engine/include/Engine/Serialization/SceneSerializer.h @@ -23,6 +23,8 @@ #ifndef HELLOVULKAN_SCENESERIALIZER_H #define HELLOVULKAN_SCENESERIALIZER_H +#include // Entity + glm vec3/quat(Prefab API 签名用) + #include namespace Engine::ECS { class Scene; } @@ -66,6 +68,31 @@ namespace Engine::Serialization { static bool DeserializeFromString(ECS::Scene& scene, Assets::AssetManager& assets, const std::string& yamlText); + + // ---- Prefab(实体子树存取;与场景共用实体段读写,实现见 .cpp)---- + + // 保存 root 子树(根 + 全部后代,DFS 序:父先于子)到 outPath。 + // 格式 = 场景实体段子集:"Prefab:" 文档头 + Version + Entities + // 列表(组件段与场景逐键一致)。首实体 = 根(实例化约定)。 + // 失败(根无效/无法写文件)返回 false + static bool SerializePrefab(const ECS::Scene& scene, + Assets::AssetManager& assets, + ECS::Entity root, + const std::string& outPath); + + // 实例化 Prefab 到 scene(追加式,不换血)。UUID 全量再生—— + // 同一 prefab 多次实例化、与场景既有实体均不冲突;子树内 + // Joint.target 按旧→新重映射,指向子树外的引用保持原值。 + // overridePos/overrideRot 非空时覆写根实体 local 位姿(根挂 + // 场景根时即世界位姿);空指针 = 保留 prefab 授权位姿。 + // 返回根实体(= 文件首实体);失败(解析失败/无实体)返回 + // entt::null——逐实体容错与场景加载同语义,失败前可能已部分 + // 实例化(单实体损坏只跳过该项) + static ECS::Entity InstantiatePrefab( + ECS::Scene& scene, Assets::AssetManager& assets, + const std::string& inPath, + const glm::vec3* overridePos = nullptr, + const glm::quat* overrideRot = nullptr); }; } // namespace Engine::Serialization diff --git a/Engine/src/Scripting/ScriptRuntime.cpp b/Engine/src/Scripting/ScriptRuntime.cpp index 119f46f..e189f7c 100644 --- a/Engine/src/Scripting/ScriptRuntime.cpp +++ b/Engine/src/Scripting/ScriptRuntime.cpp @@ -10,6 +10,7 @@ #include #include #include +#include #include @@ -32,6 +33,15 @@ namespace Engine { App().GetScene().DestroyEntity(e); } + // Prefab 实例化(声明在 Script.h,实现下沉此处——同 Spawn/ + // Destroy 模式;序列化器与 Application 门面都在引擎内可达) + ECS::Entity ScriptBehavior::Instantiate(const std::string& prefabPath, + const glm::vec3& pos, + const glm::quat& rot) { + return Serialization::SceneSerializer::InstantiatePrefab( + App().GetScene(), App().GetAssets(), prefabPath, &pos, &rot); + } + // ---- 参数按名读写(ScriptBehavior 声明,经绑定表;写穿 // Item.params 由运行时 SetParam 完成)---- bool ScriptBehavior::WriteParam(const std::string& name, diff --git a/Engine/src/Serialization/SceneSerializer.cpp b/Engine/src/Serialization/SceneSerializer.cpp index 9076c1d..9aa45a9 100644 --- a/Engine/src/Serialization/SceneSerializer.cpp +++ b/Engine/src/Serialization/SceneSerializer.cpp @@ -271,6 +271,36 @@ namespace Engine::Serialization { YAML::Node comps; // 缺省 = Null(无组件) }; + // 实体列表解析(场景加载 / Prefab 实例化共用):逐实体容错, + // 单实体损坏跳过继续,不动 registry + void ParseEntityList(const YAML::Node& entities, + std::vector& out) + { + if (!entities || !entities.IsSequence()) { + return; + } + out.reserve(entities.size()); + for (const auto& e : entities) { + try { + ParsedEntity pe; + pe.uuid = e["Entity"].as(0); + pe.name = e["Name"].as(pe.name); + pe.parentUuid = e["Parent"].as(0); + if (const YAML::Node comps = e["Components"]; + comps && comps.IsMap()) { + // 仅在 rhs 有效时赋值(僵尸节点 operator= 会抛) + pe.comps = comps; + } + out.push_back(std::move(pe)); + } + catch (const std::exception& err) { + // 单实体损坏不炸全场:跳过继续 + ENGINE_WARN("SceneSerializer: entity skipped ({})", + err.what()); + } + } + } + // 写端:单实体 → Emitter(调用方已进入 Entities 序列) void EmitEntity(YAML::Emitter& out, Assets::AssetManager& assets, const ECS::Registry& reg, entt::entity e) @@ -310,8 +340,8 @@ namespace Engine::Serialization { if (!path.empty()) { out << YAML::Key << "MeshRenderer" << YAML::Value << YAML::BeginMap; - out << YAML::Key << "Model" << YAML::Value << YAML::Flow - << YAML::BeginMap; + out << YAML::Key << "Model" << YAML::Value + << YAML::BeginMap; if (const u64 guid = assets.GetOrCreateGUID(path); guid != 0) { out << YAML::Key << "Guid" << YAML::Value << guid; @@ -347,12 +377,14 @@ namespace Engine::Serialization { } if (const auto* a = reg.try_get(e)) { - out << YAML::Key << "Animation" << YAML::Value << YAML::Flow - << YAML::BeginMap; + out << YAML::Key << "Animation" << YAML::Value + << YAML::BeginMap; // block 风格:键多,单行不可读 out << YAML::Key << "ClipIndex" << YAML::Value << a->clipIndex; out << YAML::Key << "Playing" << YAML::Value << a->playing; out << YAML::Key << "Loop" << YAML::Value << a->loop; out << YAML::Key << "Speed" << YAML::Value << a->speed; + out << YAML::Key << "RootMotion" << YAML::Value + << a->applyRootMotion; out << YAML::Key << "FadeDuration" << YAML::Value << a->fadeDuration; out << YAML::Key << "Active" << YAML::Value << a->active; @@ -379,8 +411,9 @@ namespace Engine::Serialization { out << YAML::EndMap; } // 外部动画源(编辑器 attach 列表):clip 本体运行时并入 - // Model 资产不落盘,加载端按此列表幂等重放 - out << YAML::Key << "Attached" << YAML::Value << YAML::Flow + // Model 资产不落盘,加载端按此列表幂等重放。 + // block 风格:路径长且条目多,flow 单行不可读 + out << YAML::Key << "Attached" << YAML::Value << YAML::BeginSeq; for (const auto& p : a->attachedAnimPaths) { out << p; @@ -390,7 +423,7 @@ namespace Engine::Serialization { } if (const auto* c = reg.try_get(e)) { - out << YAML::Key << "Camera" << YAML::Value << YAML::Flow + out << YAML::Key << "Attached" << YAML::Value << YAML::BeginMap; out << YAML::Key << "Primary" << YAML::Value << c->primary; out << YAML::Key << "Active" << YAML::Value << c->active; @@ -583,185 +616,14 @@ namespace Engine::Serialization { out << YAML::EndMap; // Entity } - } // namespace - - // ---- 保存 ---- - - // Emitter 构建段(文件版与内存版共用):documentName 空则不写 - // 文档头 "Scene" 字段(快照无文件名,该字段是注释性的) - static std::string SerializeDoc(const ECS::Scene& scene, - Assets::AssetManager& assets, - const std::string& documentName) - { - // float32 无损往返需 9 位有效数字(std::numeric_limits - // ::max_digits10) - YAML::Emitter out; - out.SetFloatPrecision(9); - - out << YAML::BeginMap; - if (!documentName.empty()) { - out << YAML::Key << "Scene" << YAML::Value << documentName; - } - out << YAML::Key << "Version" << YAML::Value << kSceneVersion; - - const auto& s = scene.GetSettings(); - out << YAML::Key << "Settings" << YAML::Value << YAML::BeginMap; - out << YAML::Key << "ClearColor" << YAML::Value; - EmitVec4(out, s.ClearColor); - out << YAML::EndMap; - - out << YAML::Key << "Entities" << YAML::Value << YAML::BeginSeq; - const ECS::Registry& reg = scene.GetRegistry(); - reg.view().each( - [&](entt::entity e, const ECS::IdComponent&) { - EmitEntity(out, assets, reg, e); - }); - out << YAML::EndSeq; - out << YAML::EndMap; - - if (!out.good()) { - ENGINE_ERROR("SceneSerializer: emit failed: {}", out.GetLastError()); - return {}; - } - return {out.c_str(), out.size()}; - } - - bool SceneSerializer::Serialize(const ECS::Scene& scene, - Assets::AssetManager& assets, - const std::string& outPath) - { - try { - const std::string doc = SerializeDoc( - scene, assets, - std::filesystem::path(outPath).stem().string()); - if (doc.empty()) { - return false; // emit 失败,日志已在 SerializeDoc 打过 - } - - std::ofstream file(outPath, std::ios::trunc); - if (!file) { - ENGINE_ERROR("SceneSerializer: cannot open '{}' for write", - outPath); - return false; - } - file << doc; - assets.FlushGUIDRegistry(); - return true; - } - catch (const YAML::Exception& e) { - ENGINE_ERROR("SceneSerializer: save failed ({})", e.what()); - return false; - } - catch (const std::exception& e) { - ENGINE_ERROR("SceneSerializer: save failed ({})", e.what()); - return false; - } - } - - std::string SceneSerializer::SerializeToString( - const ECS::Scene& scene, - Assets::AssetManager& assets, - const std::string& documentName) - { - try { - return SerializeDoc(scene, assets, documentName); - } - catch (const YAML::Exception& e) { - ENGINE_ERROR("SceneSerializer: snapshot failed ({})", e.what()); - return {}; - } - catch (const std::exception& e) { - ENGINE_ERROR("SceneSerializer: snapshot failed ({})", e.what()); - return {}; - } - } - - // ---- 加载 ---- - - // 解析段(文件版与内存快照共用):root → 原子换血重建。 - // sourceName 仅用于日志(文件路径或 "snapshot") - static bool ParseRoot(ECS::Scene& scene, - Assets::AssetManager& assets, - const YAML::Node& root, - const std::string& sourceName) - { - // Version 闸:高版本拒载(结构未知),缺失按 0 宽容旧档 - if (const int version = root["Version"].as(0); - version > kSceneVersion) { - ENGINE_ERROR("SceneSerializer: '{}' version {} > supported {}", - sourceName, version, kSceneVersion); - return false; - } - - // ---- 旧档 v1 环境语义兼容读(v2 起环境 = 实体组件)---- - // 僵尸节点安全:as(fallback) 对缺失键返回 fallback;环境 - // 键仅在真实存在时置位(避免新档误触发兼容建实体) - const std::string legacyEnv = - root["Environment"].as(""); - bool legacyEnvKey = !legacyEnv.empty(); - glm::vec4 legacyAmbient{0.07f, 0.07f, 0.09f, 0.0f}; - bool legacyAmbientKey = false; - bool legacyDrawSkybox = true; - bool legacySkyboxKey = false; - if (const YAML::Node st = root["Settings"]; st && st.IsMap()) { - auto& s = scene.GetSettings(); - s.ClearColor = ReadVec4(st["ClearColor"], s.ClearColor); - if (const YAML::Node amb = st["AmbientColor"]) { - legacyAmbient = ReadVec4(amb, legacyAmbient); - legacyAmbientKey = true; - } - if (const YAML::Node ds = st["DrawSkybox"]) { - legacyDrawSkybox = ds.as(legacyDrawSkybox); - legacySkyboxKey = true; - } - } - - // ---- 阶段 1:解析到中间结构(逐实体容错,不动 registry)---- - std::vector parsed; - - const YAML::Node entities = root["Entities"]; - if (entities && entities.IsSequence()) { - parsed.reserve(entities.size()); - for (const auto& e : entities) { - try { - ParsedEntity pe; - pe.uuid = e["Entity"].as(0); - pe.name = e["Name"].as(pe.name); - pe.parentUuid = e["Parent"].as(0); - if (const YAML::Node comps = e["Components"]; - comps && comps.IsMap()) { - // 仅在 rhs 有效时赋值(僵尸节点 operator= 会抛) - pe.comps = comps; - } - parsed.push_back(std::move(pe)); - } - catch (const std::exception& err) { - // 单实体损坏不炸全场:跳过继续 - ENGINE_WARN("SceneSerializer: entity skipped ({})", - err.what()); - } - } - } - - // ---- 阶段 2:原子换血重建 ---- - scene.Clear(); - - std::unordered_set seenUuids; - std::unordered_map byUuid; // 第二遍重链用 - for (ParsedEntity& pe : parsed) { - auto handle = scene.CreateEntity(pe.name); - - // UUID 重复/无效 → 重新生成(Prefab 实例化的 ID 重映射语义) - u64 uuid = pe.uuid; - if (uuid == 0 || !seenUuids.insert(uuid).second) { - uuid = UUID::New().Value(); - } - handle.GetComponent().uuid = UUID{uuid}; - pe.uuid = uuid; // 回写最终 uuid(重生成后父引用仍可命中) - byUuid.emplace(uuid, static_cast(handle)); - - const YAML::Node comps = pe.comps; // 拷贝初始化,缺省 Null - + // 组件应用(场景加载 / Prefab 实例化共用):Components 节点 → + // 逐组件解析建池。handle 须为 CreateEntity 出厂态(IdComponent/ + // Transform 出生即有,其余组件按需 AddComponent)。资产引用 + // 解析与预载语义见各 Resolve* 辅助 + void ApplyComponents(ECS::EntityHandle handle, + const YAML::Node& comps, + Assets::AssetManager& assets) + { if (const YAML::Node n = comps["Transform"]) { auto& t = handle.GetComponent(); t.SetPosition(ReadVec3(n["Position"], t.GetPosition())); @@ -812,6 +674,8 @@ namespace Engine::Serialization { a.playing = n["Playing"].as(a.playing); a.loop = n["Loop"].as(a.loop); a.speed = n["Speed"].as(a.speed); + a.applyRootMotion = n["RootMotion"].as( + a.applyRootMotion); // 缺键 = 旧行为(保留根位移) a.fadeDuration = n["FadeDuration"].as(a.fadeDuration); // 缺键回退 0.3 a.active = n["Active"].as(true); @@ -1030,6 +894,200 @@ namespace Engine::Serialization { } } + // 父子重链(场景加载 / Prefab 实例化共用;两遍解析:父可能 + // 出现在子之后)。keepWorld=false:local TRS 已从文件就位, + // 只建链标脏。防环由 SetParent 兜底(手编档成环 → 告警 + 挂根)。 + // byUuid = 创建期构建的最终 uuid → entity 映射 + void RelinkParents( + ECS::Scene& scene, const std::vector& parsed, + const std::unordered_map& byUuid) + { + 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); + } + } + + // 子树收集(Prefab 写端):DFS 根 → 后代(父先于子,可读性 + + // 实例化"首实体 = 根"约定) + void CollectSubtree(const ECS::Registry& reg, ECS::Entity root, + std::vector& out) + { + out.push_back(root); + if (const auto* h = reg.try_get(root)) { + for (const ECS::Entity c : h->children) { + if (reg.valid(c)) { + CollectSubtree(reg, c, out); + } + } + } + } + + } // namespace + + // ---- 保存 ---- + + // Emitter 构建段(文件版与内存版共用):documentName 空则不写 + // 文档头 "Scene" 字段(快照无文件名,该字段是注释性的) + static std::string SerializeDoc(const ECS::Scene& scene, + Assets::AssetManager& assets, + const std::string& documentName) + { + // float32 无损往返需 9 位有效数字(std::numeric_limits + // ::max_digits10) + YAML::Emitter out; + out.SetFloatPrecision(9); + + out << YAML::BeginMap; + if (!documentName.empty()) { + out << YAML::Key << "Scene" << YAML::Value << documentName; + } + out << YAML::Key << "Version" << YAML::Value << kSceneVersion; + + const auto& s = scene.GetSettings(); + out << YAML::Key << "Settings" << YAML::Value << YAML::BeginMap; + out << YAML::Key << "ClearColor" << YAML::Value; + EmitVec4(out, s.ClearColor); + out << YAML::EndMap; + + out << YAML::Key << "Entities" << YAML::Value << YAML::BeginSeq; + const ECS::Registry& reg = scene.GetRegistry(); + reg.view().each( + [&](entt::entity e, const ECS::IdComponent&) { + EmitEntity(out, assets, reg, e); + }); + out << YAML::EndSeq; + out << YAML::EndMap; + + if (!out.good()) { + ENGINE_ERROR("SceneSerializer: emit failed: {}", out.GetLastError()); + return {}; + } + return {out.c_str(), out.size()}; + } + + bool SceneSerializer::Serialize(const ECS::Scene& scene, + Assets::AssetManager& assets, + const std::string& outPath) + { + try { + const std::string doc = SerializeDoc( + scene, assets, + std::filesystem::path(outPath).stem().string()); + if (doc.empty()) { + return false; // emit 失败,日志已在 SerializeDoc 打过 + } + + std::ofstream file(outPath, std::ios::trunc); + if (!file) { + ENGINE_ERROR("SceneSerializer: cannot open '{}' for write", + outPath); + return false; + } + file << doc; + assets.FlushGUIDRegistry(); + return true; + } + catch (const YAML::Exception& e) { + ENGINE_ERROR("SceneSerializer: save failed ({})", e.what()); + return false; + } + catch (const std::exception& e) { + ENGINE_ERROR("SceneSerializer: save failed ({})", e.what()); + return false; + } + } + + std::string SceneSerializer::SerializeToString( + const ECS::Scene& scene, + Assets::AssetManager& assets, + const std::string& documentName) + { + try { + return SerializeDoc(scene, assets, documentName); + } + catch (const YAML::Exception& e) { + ENGINE_ERROR("SceneSerializer: snapshot failed ({})", e.what()); + return {}; + } + catch (const std::exception& e) { + ENGINE_ERROR("SceneSerializer: snapshot failed ({})", e.what()); + return {}; + } + } + + // ---- 加载 ---- + + // 解析段(文件版与内存快照共用):root → 原子换血重建。 + // sourceName 仅用于日志(文件路径或 "snapshot") + static bool ParseRoot(ECS::Scene& scene, + Assets::AssetManager& assets, + const YAML::Node& root, + const std::string& sourceName) + { + // Version 闸:高版本拒载(结构未知),缺失按 0 宽容旧档 + if (const int version = root["Version"].as(0); + version > kSceneVersion) { + ENGINE_ERROR("SceneSerializer: '{}' version {} > supported {}", + sourceName, version, kSceneVersion); + return false; + } + + // ---- 旧档 v1 环境语义兼容读(v2 起环境 = 实体组件)---- + // 僵尸节点安全:as(fallback) 对缺失键返回 fallback;环境 + // 键仅在真实存在时置位(避免新档误触发兼容建实体) + const std::string legacyEnv = + root["Environment"].as(""); + bool legacyEnvKey = !legacyEnv.empty(); + glm::vec4 legacyAmbient{0.07f, 0.07f, 0.09f, 0.0f}; + bool legacyAmbientKey = false; + bool legacyDrawSkybox = true; + bool legacySkyboxKey = false; + if (const YAML::Node st = root["Settings"]; st && st.IsMap()) { + auto& s = scene.GetSettings(); + s.ClearColor = ReadVec4(st["ClearColor"], s.ClearColor); + if (const YAML::Node amb = st["AmbientColor"]) { + legacyAmbient = ReadVec4(amb, legacyAmbient); + legacyAmbientKey = true; + } + if (const YAML::Node ds = st["DrawSkybox"]) { + legacyDrawSkybox = ds.as(legacyDrawSkybox); + legacySkyboxKey = true; + } + } + + // ---- 阶段 1:解析到中间结构(逐实体容错,不动 registry)---- + std::vector parsed; + ParseEntityList(root["Entities"], parsed); + + // ---- 阶段 2:原子换血重建 ---- + scene.Clear(); + + std::unordered_set seenUuids; + std::unordered_map byUuid; // 第二遍重链用 + for (ParsedEntity& pe : parsed) { + auto handle = scene.CreateEntity(pe.name); + + // UUID 重复/无效 → 重新生成(Prefab 实例化的 ID 重映射语义) + u64 uuid = pe.uuid; + if (uuid == 0 || !seenUuids.insert(uuid).second) { + uuid = UUID::New().Value(); + } + handle.GetComponent().uuid = UUID{uuid}; + pe.uuid = uuid; // 回写最终 uuid(重生成后父引用仍可命中) + byUuid.emplace(uuid, static_cast(handle)); + + ApplyComponents(handle, pe.comps, assets); + } + // ---- 旧档环境兼容迁移:v1 的 Environment/Settings.AmbientColor/ // DrawSkybox → 补建一个 Environment 实体(视觉等价)。仅当旧键 // 真实存在且实体列表没有环境组件时触发(新档缺省键不触发; @@ -1051,21 +1109,8 @@ namespace Engine::Serialization { legacyEnv); } - // ---- 阶段 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); - } + // ---- 阶段 3:重链父子(两遍解析:父可能出现在子之后)---- + RelinkParents(scene, parsed, byUuid); // 加载在 OnImGuiRender 段,错过本帧 UpdateTransforms: // 显式补算,否则首帧 World = 单位阵 @@ -1123,4 +1168,157 @@ namespace Engine::Serialization { return ParseRoot(scene, assets, root, "snapshot"); } + // ---- Prefab(实体子树存取;与场景共用实体段读写)---- + + bool SceneSerializer::SerializePrefab(const ECS::Scene& scene, + Assets::AssetManager& assets, + ECS::Entity root, + const std::string& outPath) + { + const ECS::Registry& reg = scene.GetRegistry(); + if (!scene.IsAlive(root)) { + ENGINE_ERROR("SceneSerializer: prefab root invalid, '{}' not " + "written", outPath); + return false; + } + + try { + YAML::Emitter out; + out.SetFloatPrecision(9); + + out << YAML::BeginMap; + out << YAML::Key << "Prefab" << YAML::Value + << std::filesystem::path(outPath).stem().string(); + out << YAML::Key << "Version" << YAML::Value << kSceneVersion; + out << YAML::Key << "Entities" << YAML::Value << YAML::BeginSeq; + std::vector subtree; + CollectSubtree(reg, root, subtree); + for (const ECS::Entity e : subtree) { + EmitEntity(out, assets, reg, e); + } + out << YAML::EndSeq; + out << YAML::EndMap; + + if (!out.good()) { + ENGINE_ERROR("SceneSerializer: prefab emit failed: {}", + out.GetLastError()); + return false; + } + + std::ofstream file(outPath, std::ios::trunc); + if (!file) { + ENGINE_ERROR("SceneSerializer: cannot open '{}' for write", + outPath); + return false; + } + file.write(out.c_str(), static_cast(out.size())); + assets.FlushGUIDRegistry(); + return true; + } + catch (const YAML::Exception& e) { + ENGINE_ERROR("SceneSerializer: prefab save failed ({})", e.what()); + return false; + } + catch (const std::exception& e) { + ENGINE_ERROR("SceneSerializer: prefab save failed ({})", e.what()); + return false; + } + } + + ECS::Entity SceneSerializer::InstantiatePrefab( + ECS::Scene& scene, Assets::AssetManager& assets, + const std::string& inPath, + const glm::vec3* overridePos, const glm::quat* overrideRot) + { + YAML::Node root; + try { + root = YAML::LoadFile(inPath); + } + catch (const YAML::Exception& e) { + ENGINE_ERROR("SceneSerializer: cannot parse prefab '{}' ({})", + inPath, e.what()); + return entt::null; + } + catch (const std::exception& e) { + ENGINE_ERROR("SceneSerializer: cannot read prefab '{}' ({})", + inPath, e.what()); + return entt::null; + } + + // 版本闸同场景(实体段同构:高版本拒载,缺失按 0 宽容) + if (const int version = root["Version"].as(0); + version > kSceneVersion) { + ENGINE_ERROR("SceneSerializer: prefab '{}' version {} > " + "supported {}", inPath, version, kSceneVersion); + return entt::null; + } + + // ---- 解析到中间结构(逐实体容错,同场景加载)---- + std::vector parsed; + ParseEntityList(root["Entities"], parsed); + if (parsed.empty()) { + ENGINE_ERROR("SceneSerializer: prefab '{}' has no entities", + inPath); + return entt::null; + } + + // ---- 追加式实例化(不换血)。UUID 全量再生:同一 prefab 多次 + // 实例化、与场景既有实体均天然不冲突。old2new 供子树内引用 + // (Joint.target)重映射;父引用由 RelinkParents 走 byUuid 消化 + std::unordered_map byUuid; + std::unordered_map old2new; + ECS::Entity rootEntity = entt::null; // 约定 = 文件首实体 + for (ParsedEntity& pe : parsed) { + auto handle = scene.CreateEntity(pe.name); + if (rootEntity == entt::null) { + rootEntity = handle; + } + const u64 uuid = UUID::New().Value(); + old2new.emplace(pe.uuid, uuid); + pe.uuid = uuid; + handle.GetComponent().uuid = UUID{uuid}; + byUuid.emplace(uuid, static_cast(handle)); + ApplyComponents(handle, pe.comps, assets); + } + + // 子树内 Joint.target 重映射(旧 → 新);指向子树外的引用保持 + // 原值(悬空语义与场景加载一致,PhysicsWorld 惰性反查落空) + auto& reg = scene.GetRegistry(); + for (const ParsedEntity& pe : parsed) { + const auto self = byUuid.find(pe.uuid); + if (self == byUuid.end()) { + continue; + } + if (auto* jc = reg.try_get(self->second)) { + if (const auto it = old2new.find(jc->target.Value()); + it != old2new.end()) { + jc->target = UUID{it->second}; + } + } + } + + RelinkParents(scene, parsed, byUuid); + + // 根位姿覆写(local 语义:实例根挂场景根时即世界位姿) + if (rootEntity != entt::null && (overridePos || overrideRot)) { + auto& t = reg.get(rootEntity); + if (overridePos) { + t.SetPosition(*overridePos); + } + if (overrideRot) { + t.SetRotationQuat(*overrideRot); + } + } + + // 实例化常发生在 OnImGuiRender/脚本回调(错过本帧 + // UpdateTransforms):显式补算 world;uuid 被 CreateEntity 后 + // 覆写,全量重建索引(同场景加载语义) + scene.UpdateTransforms(); + scene.RebuildUuidIndex(); + + ENGINE_INFO("SceneSerializer: prefab '{}' instantiated " + "({} entities)", inPath, parsed.size()); + return rootEntity; + } + } // namespace Engine::Serialization