diff --git a/.gitmodules b/.gitmodules index 4bcc889..e710c3e 100644 --- a/.gitmodules +++ b/.gitmodules @@ -32,3 +32,6 @@ [submodule "Lib/ImGuizmo"] path = Lib/ImGuizmo url = https://github.com/CedricGuillemet/ImGuizmo.git +[submodule "Lib/yaml-cpp"] + path = Lib/yaml-cpp + url = https://github.com/jbeder/yaml-cpp.git diff --git a/App/editor/Editor.cpp b/App/editor/Editor.cpp index 1de78bc..174418c 100644 --- a/App/editor/Editor.cpp +++ b/App/editor/Editor.cpp @@ -15,12 +15,17 @@ #include "ViewportPanel.h" #include "StatsPanel.h" +#include +#include + #include #include // DockBuilder*(内部 API) #include #include +#include + using namespace Engine; // kDisplayViewport / kDisplayDirect(SceneTypes.h) Editor::Editor() = default; @@ -79,6 +84,7 @@ void Editor::Draw() if (m_ShowStats) m_Stats->Draw(m_Ctx); DrawQuitModal(); + DrawSceneModals(); } // 输入归属判定:ImGui 侧经自身后端链式回调永远正常收输入,这里 @@ -126,6 +132,15 @@ void Editor::DrawMenuBar() if (ImGui::BeginMenu("File")) { + if (ImGui::MenuItem("Save Scene...", nullptr)) + { + m_ShowSaveModal = true; + } + if (ImGui::MenuItem("Load Scene...", nullptr)) + { + m_ShowLoadModal = true; + } + ImGui::Separator(); if (ImGui::MenuItem("Quit", "Alt+F4")) { RequestQuit(); @@ -215,3 +230,147 @@ void Editor::DrawQuitModal() m_Ctx.app->Close(); } } + +void Editor::DrawSceneModals() +{ + // 保存/加载共用一个路径输入(UE 同款:默认路径记住上次输入) + if (m_ShowSaveModal) + { + ImGui::OpenPopup("Save Scene"); + m_ShowSaveModal = false; + } + if (m_ShowLoadModal) + { + ImGui::OpenPopup("Load Scene"); + m_ShowLoadModal = false; + } + + ImGui::SetNextWindowPos( + ImGui::GetMainViewport()->GetCenter(), + ImGuiCond_Appearing, ImVec2(0.5f, 0.5f)); + + if (ImGui::BeginPopupModal("Save Scene", nullptr, + ImGuiWindowFlags_AlwaysAutoResize)) + { + ImGui::TextUnformatted("Scene file path (relative to working dir):"); + ImGui::SetNextItemWidth(400.0f); + ImGui::InputText("##SavePath", m_ScenePathBuf, sizeof(m_ScenePathBuf)); + ImGui::Separator(); + if (ImGui::Button("Save", ImVec2(120, 0))) + { + DoSaveScene(); + ImGui::CloseCurrentPopup(); + } + ImGui::SameLine(); + if (ImGui::Button("Cancel", ImVec2(120, 0))) + { + ImGui::CloseCurrentPopup(); + } + ImGui::EndPopup(); + } + + if (ImGui::BeginPopupModal("Load Scene", nullptr, + ImGuiWindowFlags_AlwaysAutoResize)) + { + ImGui::TextUnformatted("Scene file path (relative to working dir):"); + ImGui::SetNextItemWidth(400.0f); + ImGui::InputText("##LoadPath", m_ScenePathBuf, sizeof(m_ScenePathBuf)); + ImGui::Separator(); + if (ImGui::Button("Load", ImVec2(120, 0))) + { + DoLoadScene(); + ImGui::CloseCurrentPopup(); + } + ImGui::SameLine(); + if (ImGui::Button("Cancel", ImVec2(120, 0))) + { + ImGui::CloseCurrentPopup(); + } + ImGui::EndPopup(); + } +} + +void Editor::DoSaveScene() +{ + const std::string path = m_ScenePathBuf; + if (path.empty() || !m_Ctx.scene || !m_Ctx.assets) + { + return; + } + + // scenes/ 目录不存在时自动创建(空父目录/已存在均合法) + std::error_code ec; + std::filesystem::create_directories( + std::filesystem::path(path).parent_path(), ec); + + // 环境路径:引擎序列化器只认字符串,查表由编辑器侧负责 + std::string envPath; + if (m_Ctx.getEnvIndex) + { + const int idx = m_Ctx.getEnvIndex(); + if (idx >= 0 && idx < kEnvCount) envPath = kEnvPaths[idx]; + } + + if (Serialization::SceneSerializer::Serialize(*m_Ctx.scene, *m_Ctx.assets, + path, envPath)) + { + ENGINE_INFO("Editor: scene saved to '{}'", path); + } + else + { + ENGINE_ERROR("Editor: failed to save scene to '{}'", path); + } +} + +void Editor::DoLoadScene() +{ + const std::string path = m_ScenePathBuf; + if (path.empty() || !m_Ctx.scene || !m_Ctx.assets) + { + return; + } + + std::string envPath; + if (!Serialization::SceneSerializer::Deserialize(*m_Ctx.scene, + *m_Ctx.assets, path, + envPath)) + { + ENGINE_ERROR("Editor: failed to load scene '{}'", path); + return; + } + ENGINE_INFO("Editor: scene loaded from '{}'", path); + + // 旧实体已销毁:清选中(连带清描边,见 SelectEntity) + SelectEntity(entt::null); + + // 环境恢复:场景带环境且命中环境表才切换(镜像 StatsPanel 同步路径) + if (!envPath.empty() && m_Ctx.getEnvIndex && m_Ctx.setEnvIndex) + { + int found = -1; + for (int i = 0; i < kEnvCount; ++i) + { + if (envPath == kEnvPaths[i]) { found = i; break; } + } + if (found >= 0) + { + m_Ctx.setEnvIndex(found); + const auto env = m_Ctx.assets->LoadEnvironment(kEnvPaths[found]); + if (env.Valid()) + { + m_Ctx.renderer->SetEnvironment( + m_Ctx.assets->GetEnvironment(env)); + } + } + else + { + ENGINE_WARN("Editor: scene env '{}' not in env table, " + "keeping current", envPath); + } + } + + // 应用层重解析悬空句柄(相机等沙盒状态) + if (m_Ctx.onSceneLoaded) + { + m_Ctx.onSceneLoaded(); + } +} diff --git a/App/editor/Editor.h b/App/editor/Editor.h index 0e4171c..8a953ee 100644 --- a/App/editor/Editor.h +++ b/App/editor/Editor.h @@ -73,6 +73,9 @@ private: void DrawMenuBar(); // host 窗口内顶栏 void DrawDockSpace(); // 全屏 host + dockspace + 默认布局 void DrawQuitModal(); // 退出确认模态框 + void DrawSceneModals(); // 保存/加载路径模态框(共用一个路径输入) + void DoSaveScene(); // 模态确认动作:目录创建 + 序列化落盘 + void DoLoadScene(); // 模态确认动作:反序列化 + 选中清空 + 环境恢复 + 回调 EditorContext m_Ctx{}; @@ -98,6 +101,11 @@ private: // 退出确认链 bool m_ShowQuitConfirm = false; bool m_QuitConfirmed = false; + + // 场景保存/加载(File 菜单 → 路径模态框;无文件对话框库,InputText 手输) + bool m_ShowSaveModal = false; + bool m_ShowLoadModal = false; + char m_ScenePathBuf[256] = "scenes/sandbox.scene"; }; #endif //HELLOVULKAN_EDITOR_H diff --git a/App/editor/EditorContext.h b/App/editor/EditorContext.h index 8e47825..f7dd0db 100644 --- a/App/editor/EditorContext.h +++ b/App/editor/EditorContext.h @@ -40,6 +40,9 @@ struct EditorContext { std::function getEnvIndex; // 当前环境表下标 std::function setEnvIndex; + // 场景加载成功后由 SandboxApp 重解析悬空句柄(registry 清场重建后 + // 应用层持有的 EntityHandle 全部悬空,见 Scene::Clear 注释) + std::function onSceneLoaded; }; #endif //HELLOVULKAN_EDITORCONTEXT_H diff --git a/App/main.cpp b/App/main.cpp index d663896..53d3ca8 100644 --- a/App/main.cpp +++ b/App/main.cpp @@ -201,6 +201,10 @@ namespace void OnUpdate(const FrameTime& frameTime) override { + // 加载过无相机实体的场景(或场景被清空):相机句柄悬空, + // 直接跳过本帧游戏逻辑(渲染仍以编辑器视角进行) + if (!m_Camera.IsAlive()) return; + // 相机(RMB 飞行 / MMB 焦点环绕 / 滚轮推拉;位姿读写实体 // Transform)。编辑器相机走真实轨:暂停(timeScale=0)时仍 // 可飞行观察。编辑器模式移动键需 RMB 按住(UE 语义:飞行时 @@ -268,6 +272,23 @@ namespace ctx.app = this; ctx.getEnvIndex = [this] { return m_EnvIndex; }; ctx.setEnvIndex = [this](int index) { m_EnvIndex = index; }; + // 场景加载成功 → 沙盒侧悬空句柄重解析(registry 清场重建后 + // m_Camera 等成员句柄全部失效,不重解析则下一帧必崩) + ctx.onSceneLoaded = [this] { + Scene& scene = GetScene(); + m_Spinners.clear(); + m_Gun = {}; + m_Ground = {}; + m_KeyLight = {}; + entt::entity camEnt = entt::null; + scene.Each([&](entt::entity e, + CameraComponent& cc) { + if (cc.primary) camEnt = e; + }); + m_Camera = scene.GetEntityHandle(camEnt); // 无效 → 空句柄 + ENGINE_INFO("SandboxApp: scene state re-resolved (camera {})", + m_Camera.IsAlive() ? "found" : "missing"); + }; // 继承原沙盒默认:初始暂停(StatsPanel 的 Pause Simulation 开关恢复) Time::SetTimeScale(0.0f); m_Editor->Init(ctx); diff --git a/CMakeLists.txt b/CMakeLists.txt index ec56aa7..ffbfba2 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -59,6 +59,13 @@ set(ASSIMP_BUILD_STL_IMPORTER ON CACHE BOOL "" FORCE) set(ASSIMP_BUILD_PLY_IMPORTER ON CACHE BOOL "" FORCE) add_subdirectory(Lib/assimp) +# ---- yaml-cpp(submodule,场景序列化;只编静态库本体)---- +set(YAML_CPP_BUILD_CONTRIB OFF CACHE BOOL "" FORCE) +set(YAML_CPP_BUILD_TOOLS OFF CACHE BOOL "" FORCE) +set(YAML_CPP_BUILD_TESTS OFF CACHE BOOL "" FORCE) +set(YAML_CPP_INSTALL OFF CACHE BOOL "" FORCE) +add_subdirectory(Lib/yaml-cpp) + # Vulkan diff --git a/Engine/CMakeLists.txt b/Engine/CMakeLists.txt index 7371ce6..8ebeb86 100644 --- a/Engine/CMakeLists.txt +++ b/Engine/CMakeLists.txt @@ -73,6 +73,7 @@ target_link_libraries(Engine PUBLIC assimp::assimp meshoptimizer Jolt::Jolt + yaml-cpp::yaml-cpp imgui ImGuizmo tracy) diff --git a/Engine/include/Engine/Assets/AssetManager.h b/Engine/include/Engine/Assets/AssetManager.h index 4cd6fc0..eff380f 100644 --- a/Engine/include/Engine/Assets/AssetManager.h +++ b/Engine/include/Engine/Assets/AssetManager.h @@ -172,14 +172,31 @@ namespace Engine AssetCache& Models() { return m_Models; } + // ---- 资产 GUID 注册表(UE AssetRegistry 模式:中央注册表)---- + // 磁盘路径资产持久映射 GUID→路径(存 assets/asset_registry.yaml), + // 场景文件以 GUID 为主键引用资产——重命名/移动资产不断引用, + // Path 仅作人工可读提示与解析兜底。procedural:// 伪路径(参数 + // 确定性生成,无需稳定身份)不参与 + // 查询/分配(新路径分配 GUID 并立即持久化,崩溃安全) + u64 GetOrCreateGUID(const std::string& path); + // GUID → 路径;未登记返回空串 + std::string ResolveGUID(u64 guid) const; + // 后续扩展点:TextureHandle / MaterialHandle / ShaderHandle // 各自一个 AssetCache + 便捷方法,模式与 Model 完全一致。 private: + void LoadGUIDRegistry(); + void SaveGUIDRegistry() const; + IAssetUploader& m_Uploader; AssetCache m_Models; AssetCache m_Environments; + + // GUID 注册表(双向);dirty 即时保存,无析构落盘依赖 + std::unordered_map m_GuidToPath; + std::unordered_map m_PathToGuid; }; } // namespace Assets } // namespace Engine diff --git a/Engine/include/Engine/ECS/Scene.h b/Engine/include/Engine/ECS/Scene.h index a20b6ef..03ef101 100644 --- a/Engine/include/Engine/ECS/Scene.h +++ b/Engine/include/Engine/ECS/Scene.h @@ -46,6 +46,7 @@ namespace Engine::ECS { bool DrawSkybox = true; }; Settings& GetSettings() { return m_Settings; } + const Settings& GetSettings() const { return m_Settings; } // 序列化只读 // ---- 实体 ---- @@ -54,6 +55,15 @@ namespace Engine::ECS { void DestroyEntity(Entity e); // 延迟:帧尾统一销毁 void FlushDestroyQueue(); // 每帧末尾由 Application 调用 + // 整场景换血(序列化加载):立即销毁全部实体并清空延迟队列。 + // 仅用于"校验通过后的原子替换"——调用方持有的 Entity/EntityHandle + // 全部悬空,必须自行重解析(GetEntityHandle 可从裸 entity 重建) + void Clear(); + + // 裸 entity → 句柄表皮(EntityHandle 构造私有 + friend Scene; + // 序列化加载后应用层重解析悬空句柄用)。无效 entity 返回空句柄 + EntityHandle GetEntityHandle(Entity e); + // 帧循环统一重算脏 transform 的 world 矩阵(UE transform-update // 阶段模式)——由 Application 在 OnUpdate 之后、渲染之前调用, // 用户代码永远不需要手动 Compose @@ -64,6 +74,7 @@ namespace Engine::ECS { Entity FindEntityByUUID(UUID uuid); Registry& GetRegistry() { return m_Registry; } + const Registry& GetRegistry() const { return m_Registry; } // 序列化等只读遍历 // ---- 组件便捷接口(转发 entt)---- diff --git a/Engine/include/Engine/Serialization/SceneSerializer.h b/Engine/include/Engine/Serialization/SceneSerializer.h new file mode 100644 index 0000000..53d08d9 --- /dev/null +++ b/Engine/include/Engine/Serialization/SceneSerializer.h @@ -0,0 +1,62 @@ +// +// Created by atdunbg on 2026/10/3. +// +// SceneSerializer:场景 → YAML 文件的双向转换(Unity 场景 YAML 同构简化)。 +// +// 参照系(定案):Unity 的场景 YAML 结构 + UE AssetRegistry 式中央 +// GUID 注册表(AssetManager 维护,存 assets/asset_registry.yaml)。 +// 场景内资产引用双写 {Guid, Path}:GUID 是主键(重命名/移动资产不断 +// 引用),Path 是人工可读提示 + 注册表缺失时的解析兜底。 +// +// 引擎层身份:本类不感知编辑器(kEnvPaths 表)与渲染器—— +// environmentPath 以普通字符串经参数进出,消费方(编辑器/游戏运行时) +// 自行解释。一套 API 服务两个消费者:编辑器 File 菜单 与 +// 游戏运行时动态加载,不存在"另一套序列化"。 +// +// 前向兼容(Prefab 路线预留): +// - 场景 = 实体节点列表,Prefab 复用同一套实体读写函数 +// - Version 闸:高版本文件拒载并 WARN,低版本宽容 +// - 未知组件名跳过(Unity 语义:新版场景在旧版引擎打开可玩) +// - UUID 重复 → 重新生成(即 Prefab 实例化的 ID 重映射) +// + +#ifndef HELLOVULKAN_SCENESERIALIZER_H +#define HELLOVULKAN_SCENESERIALIZER_H + +#include + +namespace Engine::ECS { class Scene; } +namespace Engine::Assets { class AssetManager; } + +namespace Engine::Serialization { + + // 当前场景文件格式版本(字段/结构变更时递增) + inline constexpr int kSceneVersion = 1; + + class SceneSerializer { + public: + // ---- 保存 ---- + // scene 序列化到 outPath(YAML)。environmentPath 可空(不写 + // Environment 字段)。失败(无法写文件)返回 false。 + static bool Serialize(const ECS::Scene& scene, + Assets::AssetManager& assets, + const std::string& outPath, + const std::string& environmentPath = {}); + + // ---- 加载 ---- + // 原子换血:先解析校验到中间结构,全部成功才 scene.Clear() 重建 + //(解析失败不动现场)。成功时 outEnvironmentPath 带回场景记录的 + // 环境路径(空 = 场景未带环境,消费方保持当前环境即可)。 + // 健壮性:单实体损坏跳过继续、缺字段取默认值、未知组件跳过、 + // UUID 重复重新生成、YAML 异常优雅报错。 + // 注意:调用方持有的 Entity/EntityHandle 在成功后全部悬空, + // 必须重解析(Scene::GetEntityHandle / 按 UUID 查询)。 + static bool Deserialize(ECS::Scene& scene, + Assets::AssetManager& assets, + const std::string& inPath, + std::string& outEnvironmentPath); + }; + +} // namespace Engine::Serialization + +#endif //HELLOVULKAN_SCENESERIALIZER_H diff --git a/Engine/src/Assets/AssetManager.cpp b/Engine/src/Assets/AssetManager.cpp index a3b151d..c21b65a 100644 --- a/Engine/src/Assets/AssetManager.cpp +++ b/Engine/src/Assets/AssetManager.cpp @@ -4,13 +4,106 @@ #include #include +#include + +#include + +#include namespace Engine::Assets { + namespace { + // GUID 注册表落盘位置(相对 CWD,与 assets/ 约定一致) + constexpr const char* kGuidRegistryPath = "assets/asset_registry.yaml"; + // 伪路径前缀:参数确定性生成,无稳定身份需求,不进 GUID 注册表 + constexpr std::string_view kProceduralPrefix = "procedural://"; + bool IsProceduralPath(const std::string& path) + { + return path.rfind(kProceduralPrefix, 0) == 0; + } + } + AssetManager::AssetManager(IAssetUploader& uploader) : m_Uploader(uploader) { - ENGINE_INFO("AssetManager created"); + LoadGUIDRegistry(); + ENGINE_INFO("AssetManager created ({} guids registered)", m_GuidToPath.size()); + } + + // ---- 资产 GUID 注册表 ---- + + u64 AssetManager::GetOrCreateGUID(const std::string& path) + { + if (path.empty() || IsProceduralPath(path)) return 0; + + if (auto it = m_PathToGuid.find(path); it != m_PathToGuid.end()) { + return it->second; + } + + // 64 位随机 + 双 map 双向查重(UUID::New 撞已有 GUID 概率可忽略, + // 但注册表是持久身份,防御性重试) + u64 guid = 0; + do { guid = UUID::New().Value(); } while (m_GuidToPath.contains(guid)); + + m_PathToGuid.emplace(path, guid); + m_GuidToPath.emplace(guid, path); + SaveGUIDRegistry(); + return guid; + } + + std::string AssetManager::ResolveGUID(const u64 guid) const + { + if (auto it = m_GuidToPath.find(guid); it != m_GuidToPath.end()) { + return it->second; + } + return {}; + } + + void AssetManager::LoadGUIDRegistry() + { + try { + YAML::Node root = YAML::LoadFile(kGuidRegistryPath); + if (const YAML::Node assets = root["Assets"]; assets.IsSequence()) { + for (const auto& a : assets) { + const u64 guid = a["Guid"].as(0); + auto path = a["Path"].as(); + if (guid != 0 && !path.empty() && + !m_GuidToPath.contains(guid)) { + m_GuidToPath.emplace(guid, path); + m_PathToGuid.emplace(std::move(path), guid); + } + } + } + } + catch (const YAML::Exception& e) { + // 注册表缺失/损坏不致命:场景里同时存了 Path 兜底 + ENGINE_WARN("AssetManager: guid registry unavailable ({})", e.what()); + } + catch (const std::exception& e) { + ENGINE_WARN("AssetManager: guid registry unreadable ({})", e.what()); + } + } + + void AssetManager::SaveGUIDRegistry() const + { + try { + YAML::Node root; + for (const auto& [guid, path] : m_GuidToPath) { + YAML::Node a; + a["Guid"] = guid; + a["Path"] = path; + root["Assets"].push_back(a); + } + std::ofstream out(kGuidRegistryPath, std::ios::trunc); + if (!out) { + ENGINE_WARN("AssetManager: cannot write {}", kGuidRegistryPath); + return; + } + out << root; + } + catch (const YAML::Exception& e) { + ENGINE_ERROR("AssetManager: guid registry save failed ({})", e.what()); + } } ModelRef AssetManager::LoadModel(const std::string& path) diff --git a/Engine/src/ECS/Scene.cpp b/Engine/src/ECS/Scene.cpp index 2940715..d1116be 100644 --- a/Engine/src/ECS/Scene.cpp +++ b/Engine/src/ECS/Scene.cpp @@ -53,6 +53,19 @@ namespace Engine::ECS { m_PendingDestroy.clear(); } + void Scene::Clear() + { + // 先清延迟队列再销毁全部:避免 FlushDestroyQueue 语义混淆 + //(registry.clear() 后队列里的句柄全部失效,直接丢弃即可) + m_PendingDestroy.clear(); + m_Registry.clear(); + } + + EntityHandle Scene::GetEntityHandle(Entity e) + { + return m_Registry.valid(e) ? EntityHandle{this, e} : EntityHandle{}; + } + void Scene::UpdateTransforms() { ZoneScoped; diff --git a/Engine/src/Serialization/SceneSerializer.cpp b/Engine/src/Serialization/SceneSerializer.cpp new file mode 100644 index 0000000..9e06772 --- /dev/null +++ b/Engine/src/Serialization/SceneSerializer.cpp @@ -0,0 +1,467 @@ +// +// Created by atdunbg on 2026/10/3. +// +// SceneSerializer:场景 ↔ YAML。结构见头文件注释与计划 v3。 +// +// 原子性:Deserialize 先把整个文件解析进中间结构(逐实体容错), +// 全部成功才 scene.Clear() 换血重建——解析失败不动现场。 +// + +#include +#include +#include +#include +#include +#include +#include + +#include + +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +namespace Engine::Serialization { + + namespace { + + // ---- 写出辅助:glm → 数组 ---- + + std::vector Vec3Arr(const glm::vec3& v) { return {v.x, v.y, v.z}; } + std::vector Vec4Arr(const glm::vec4& v) { return {v.x, v.y, v.z, v.w}; } + std::vector QuatArr(const glm::quat& q) { return {q.w, q.x, q.y, q.z}; } + + // ---- 读入辅助:缺字段/类型错 → 默认值(as(fallback) 语义)---- + + glm::vec3 ReadVec3(const YAML::Node& n, const glm::vec3& def) + { + if (!n || !n.IsSequence() || n.size() < 3) return def; + return {n[0].as(def.x), n[1].as(def.y), n[2].as(def.z)}; + } + + glm::vec4 ReadVec4(const YAML::Node& n, const glm::vec4& def) + { + if (!n || !n.IsSequence() || n.size() < 4) return def; + return {n[0].as(def.x), n[1].as(def.y), + n[2].as(def.z), n[3].as(def.w)}; + } + + // [w,x,y,z] → 归一化 quat;非法/缺失回退单位四元数(防手改档出 NaN) + glm::quat ReadQuat(const YAML::Node& n) + { + if (!n || !n.IsSequence() || n.size() < 4) return {}; + const f32 w = n[0].as(1.0f), x = n[1].as(0.0f); + const f32 y = n[2].as(0.0f), z = n[3].as(0.0f); + const f32 len = std::sqrt(x * x + y * y + z * z + w * w); + return len > 1e-8f ? glm::quat(w, x, y, z) / len : glm::quat{}; + } + + // "a,b,c[,d]" → N 个 int(procedural 伪路径参数解析) + template + bool ParseCsvInts(const std::string& s, int (&out)[N]) + { + size_t pos = 0; + for (size_t i = 0; i < N; ++i) { + const size_t next = + (i + 1 < N) ? s.find(',', pos) : std::string::npos; + if (i + 1 < N && next == std::string::npos) return false; + try { + out[i] = std::stoi(s.substr(pos, next - pos)); + } + catch (const std::exception&) { return false; } + pos = (next == std::string::npos) ? s.size() : next + 1; + } + return true; + } + + // procedural:// 伪路径 → 参数反解重建(确定性键必然命中缓存; + // 绝不对伪路径调 LoadModel——那会走 assimp 必炸) + ModelRef ResolveProcedural(Assets::AssetManager& assets, + const std::string& path) + { + constexpr std::string_view kPlane = "procedural://plane/"; + constexpr std::string_view kTPlane = + "procedural://transparent-plane/"; + + if (path.rfind(kTPlane, 0) == 0) { + // "{halfSize}/{r},{g},{b},{a}" + const std::string rest = path.substr(kTPlane.size()); + const auto slash = rest.find('/'); + if (slash == std::string::npos) return {}; + int c[4]; + if (!ParseCsvInts(rest.substr(slash + 1), c)) return {}; + try { + return assets.CreateTransparentPlane( + std::stof(rest.substr(0, slash)), + static_cast(c[0]), static_cast(c[1]), + static_cast(c[2]), static_cast(c[3])); + } + catch (const std::exception&) { return {}; } + } + + if (path.rfind(kPlane, 0) == 0) { + // "{halfSize}/{r},{g},{b}" + const std::string rest = path.substr(kPlane.size()); + const auto slash = rest.find('/'); + if (slash == std::string::npos) return {}; + int c[3]; + if (!ParseCsvInts(rest.substr(slash + 1), c)) return {}; + try { + return assets.CreatePlane( + std::stof(rest.substr(0, slash)), + static_cast(c[0]), static_cast(c[1]), + static_cast(c[2])); + } + catch (const std::exception&) { return {}; } + } + + return {}; // 未知伪路径 + } + + // 模型引用解析降级链:procedural 反解 → GUID 主键 → Path 兜底。 + // 加载成功后 GetOrCreateGUID 回写注册表(注册表丢档自愈) + ModelRef ResolveModel(Assets::AssetManager& assets, u64 guid, + const std::string& path) + { + if (ModelRef ref = ResolveProcedural(assets, path); ref.Valid()) { + return ref; + } + + std::string resolved; + if (guid != 0) resolved = assets.ResolveGUID(guid); + if (resolved.empty()) resolved = path; + if (resolved.empty()) return {}; + + ModelRef ref = assets.LoadModel(resolved); + if (ref.Valid()) assets.GetOrCreateGUID(resolved); + return ref; + } + + // 资产引用双写:{Guid, Path}(procedural 只有 Path,Guid=0 不写) + void WriteModelRef(YAML::Node& node, Assets::AssetManager& assets, + ModelRef ref) + { + const std::string& path = assets.Models().Path(ref); + if (path.empty()) { + ENGINE_WARN("SceneSerializer: entity has invalid model ref, " + "MeshRenderer skipped"); + return; + } + YAML::Node m; + if (const u64 guid = assets.GetOrCreateGUID(path); guid != 0) { + m["Guid"] = guid; + } + m["Path"] = path; + node["Model"] = m; + } + + // ---- 中间结构:解析全部成功才碰 registry ---- + + struct ParsedEntity + { + u64 uuid = 0; + std::string name = "entity"; + YAML::Node transform, meshRenderer, animation, camera, light, + rigidBody; // 未出现 = undefined + }; + + void SerializeEntity(YAML::Node& out, Assets::AssetManager& assets, + const ECS::Registry& reg, entt::entity e) + { + const auto& id = reg.get(e); + + YAML::Node ent; + ent["Entity"] = id.uuid.Value(); + if (const auto* name = reg.try_get(e)) { + ent["Name"] = name->name; + } + + YAML::Node comps; + + if (const auto* t = reg.try_get(e)) { + YAML::Node n; + n["Position"] = Vec3Arr(t->GetPosition()); + n["Rotation"] = QuatArr(t->GetRotationQuat()); // (w,x,y,z) 无损 + n["Scale"] = Vec3Arr(t->GetScale()); + comps["Transform"] = n; + } + + if (const auto* mr = reg.try_get(e)) { + YAML::Node n; + WriteModelRef(n, assets, mr->model); + if (const YAML::Node m = n["Model"]) comps["MeshRenderer"] = n; + } + + if (const auto* a = reg.try_get(e)) { + YAML::Node n; + n["ClipIndex"] = a->clipIndex; + n["Playing"] = a->playing; + n["Loop"] = a->loop; + n["Speed"] = a->speed; // fade*/time 是运行态,不存 + comps["Animation"] = n; + } + + if (const auto* c = reg.try_get(e)) { + YAML::Node n; + n["Primary"] = c->primary; // 相机不存投影参数(调用方传入) + comps["Camera"] = n; + } + + if (const auto* l = reg.try_get(e)) { + YAML::Node n; + n["Type"] = static_cast(l->type); + n["Direction"] = Vec3Arr(l->direction); + n["Color"] = Vec4Arr(l->color); + n["Radius"] = l->radius; + n["CastShadow"] = l->castShadow; + comps["Light"] = n; + } + + if (const auto* rb = reg.try_get(e)) { + YAML::Node n; + n["Type"] = static_cast(rb->type); + n["Collider"] = static_cast(rb->collider); + n["BoxHalfExtents"] = Vec3Arr(rb->boxHalfExtents); + n["SphereRadius"] = rb->sphereRadius; + n["Mass"] = rb->mass; + n["GravityScale"] = rb->gravityScale; + n["Friction"] = rb->friction; + n["Restitution"] = rb->restitution; + n["IsSensor"] = rb->isSensor; + // bodyId/bodyValid/velocity 由 PhysicsWorld 对账重建,不存 + comps["RigidBody"] = n; + } + + if (comps.size() > 0) ent["Components"] = comps; + out.push_back(ent); + } + + } // namespace + + // ---- 保存 ---- + + bool SceneSerializer::Serialize(const ECS::Scene& scene, + Assets::AssetManager& assets, + const std::string& outPath, + const std::string& environmentPath) + { + try { + YAML::Node root; + root["Scene"] = std::filesystem::path(outPath).stem().string(); + root["Version"] = kSceneVersion; + if (!environmentPath.empty()) { + root["Environment"] = environmentPath; + } + + const auto& s = scene.GetSettings(); + YAML::Node st; + st["ClearColor"] = Vec4Arr(s.ClearColor); + st["AmbientColor"] = Vec4Arr(s.AmbientColor); + st["DrawSkybox"] = s.DrawSkybox; + root["Settings"] = st; + + YAML::Node entities(YAML::NodeType::Sequence); + const ECS::Registry& reg = scene.GetRegistry(); + reg.view().each( + [&](entt::entity e, const ECS::IdComponent&) { + SerializeEntity(entities, assets, reg, e); + }); + root["Entities"] = entities; + + std::ofstream out(outPath, std::ios::trunc); + if (!out) { + ENGINE_ERROR("SceneSerializer: cannot open '{}' for write", + outPath); + return false; + } + out << root; + 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; + } + } + + // ---- 加载 ---- + + bool SceneSerializer::Deserialize(ECS::Scene& scene, + Assets::AssetManager& assets, + const std::string& inPath, + std::string& outEnvironmentPath) + { + outEnvironmentPath.clear(); + + YAML::Node root; + try { + root = YAML::LoadFile(inPath); + } + catch (const YAML::Exception& e) { + ENGINE_ERROR("SceneSerializer: cannot parse '{}' ({})", inPath, + e.what()); + return false; + } + catch (const std::exception& e) { + ENGINE_ERROR("SceneSerializer: cannot read '{}' ({})", inPath, + e.what()); + return false; + } + + // Version 闸:高版本拒载(结构未知),缺失按 0 宽容旧档 + if (const int version = root["Version"].as(0); + version > kSceneVersion) { + ENGINE_ERROR("SceneSerializer: '{}' version {} > supported {}", + inPath, version, kSceneVersion); + return false; + } + + outEnvironmentPath = root["Environment"].as(""); + + if (const YAML::Node st = root["Settings"]; st && st.IsMap()) { + auto& s = scene.GetSettings(); + s.ClearColor = ReadVec4(st["ClearColor"], s.ClearColor); + s.AmbientColor = ReadVec4(st["AmbientColor"], s.AmbientColor); + s.DrawSkybox = st["DrawSkybox"].as(s.DrawSkybox); + } + + // ---- 阶段 1:解析到中间结构(逐实体容错,不动 registry)---- + std::vector parsed; + + const YAML::Node entities = root["Entities"]; + if (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); + if (const YAML::Node comps = e["Components"]; + comps && comps.IsMap()) { + pe.transform = comps["Transform"]; + pe.meshRenderer = comps["MeshRenderer"]; + pe.animation = comps["Animation"]; + pe.camera = comps["Camera"]; + pe.light = comps["Light"]; + pe.rigidBody = comps["RigidBody"]; + // 未识别的组件键自然跳过(前向兼容) + } + 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; + 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}; + + if (pe.transform) { + const YAML::Node n = pe.transform; + auto& t = handle.GetComponent(); + t.SetPosition(ReadVec3(n["Position"], t.GetPosition())); + t.SetRotationQuat(ReadQuat(n["Rotation"])); + t.SetScale(ReadVec3(n["Scale"], t.GetScale())); + } + + if (pe.meshRenderer) { + const YAML::Node n = pe.meshRenderer; + const u64 guid = n["Model"]["Guid"].as(0); + const auto path = n["Model"]["Path"].as(""); + if (ModelRef ref = ResolveModel(assets, guid, path); + ref.Valid()) { + handle.AddComponent( + ECS::MeshRendererComponent{ref}); + } + else { + ENGINE_WARN("SceneSerializer: model unresolved " + "(guid={}, path='{}'), MeshRenderer skipped", + guid, path); + } + } + + if (pe.animation) { + const YAML::Node n = pe.animation; + auto& a = handle.AddComponent(); + a.clipIndex = n["ClipIndex"].as(a.clipIndex); + a.playing = n["Playing"].as(a.playing); + a.loop = n["Loop"].as(a.loop); + a.speed = n["Speed"].as(a.speed); + } + + if (pe.camera) { + const YAML::Node n = pe.camera; + auto& c = handle.AddComponent(); + c.primary = n["Primary"].as(false); + } + + if (pe.light) { + const YAML::Node n = pe.light; + auto& l = handle.AddComponent(); + if (const u8 type = n["Type"].as(0); type <= 1) { + l.type = static_cast(type); + } + l.direction = ReadVec3(n["Direction"], l.direction); + l.color = ReadVec4(n["Color"], l.color); + l.radius = n["Radius"].as(l.radius); + l.castShadow = n["CastShadow"].as(l.castShadow); + } + + if (pe.rigidBody) { + const YAML::Node n = pe.rigidBody; + auto& rb = handle.AddComponent(); + if (const u8 type = n["Type"].as(0); type <= 2) { + rb.type = static_cast( + type); + } + if (const u8 col = n["Collider"].as(0); col <= 2) { + rb.collider = + static_cast( + col); + } + rb.boxHalfExtents = ReadVec3(n["BoxHalfExtents"], + rb.boxHalfExtents); + rb.sphereRadius = n["SphereRadius"].as(rb.sphereRadius); + rb.mass = n["Mass"].as(rb.mass); + rb.gravityScale = n["GravityScale"].as(rb.gravityScale); + rb.friction = n["Friction"].as(rb.friction); + rb.restitution = n["Restitution"].as(rb.restitution); + rb.isSensor = n["IsSensor"].as(rb.isSensor); + // bodyId/bodyValid/velocity 保持默认(对账重建) + } + } + + // 加载在 OnImGuiRender 段,错过本帧 UpdateTransforms: + // 显式补算,否则首帧 World = 单位阵 + scene.UpdateTransforms(); + + ENGINE_INFO("SceneSerializer: loaded {} entities from '{}'", + parsed.size(), inPath); + return true; + } + +} // namespace Engine::Serialization diff --git a/Lib/yaml-cpp b/Lib/yaml-cpp new file mode 160000 index 0000000..1e0876c --- /dev/null +++ b/Lib/yaml-cpp @@ -0,0 +1 @@ +Subproject commit 1e0876c671268661deb2628040e3959e1e9d6e69