serialization: scene save/load (Unity-style YAML + UE-style GUID registry)
Engine-layer SceneSerializer (yaml-cpp submodule): entities/components
to versioned YAML, atomic clear-and-rebuild on load with per-entity
fault tolerance, missing-field defaults, unknown-component skip and
UUID dedup. Asset references are {Guid, Path} pairs resolved through
the new AssetManager GUID registry (assets/asset_registry.yaml) with
path fallback; procedural:// pseudo-paths rebuild from parameters.
Editor File menu gets Save/Load Scene modals; app re-resolves stale
entity handles via onSceneLoaded callback.
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -15,12 +15,17 @@
|
||||
#include "ViewportPanel.h"
|
||||
#include "StatsPanel.h"
|
||||
|
||||
#include <Engine/Core/Log.h>
|
||||
#include <Engine/Serialization/SceneSerializer.h>
|
||||
|
||||
#include <imgui.h>
|
||||
#include <imgui_internal.h> // DockBuilder*(内部 API)
|
||||
#include <ImGuizmo.h>
|
||||
|
||||
#include <GLFW/glfw3.h>
|
||||
|
||||
#include <filesystem>
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -40,6 +40,9 @@ struct EditorContext {
|
||||
|
||||
std::function<int()> getEnvIndex; // 当前环境表下标
|
||||
std::function<void(int)> setEnvIndex;
|
||||
// 场景加载成功后由 SandboxApp 重解析悬空句柄(registry 清场重建后
|
||||
// 应用层持有的 EntityHandle 全部悬空,见 Scene::Clear 注释)
|
||||
std::function<void()> onSceneLoaded;
|
||||
};
|
||||
|
||||
#endif //HELLOVULKAN_EDITORCONTEXT_H
|
||||
|
||||
@@ -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<CameraComponent>([&](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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -73,6 +73,7 @@ target_link_libraries(Engine PUBLIC
|
||||
assimp::assimp
|
||||
meshoptimizer
|
||||
Jolt::Jolt
|
||||
yaml-cpp::yaml-cpp
|
||||
imgui
|
||||
ImGuizmo
|
||||
tracy)
|
||||
|
||||
@@ -172,14 +172,31 @@ namespace Engine
|
||||
|
||||
AssetCache<Model>& 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<T> + 便捷方法,模式与 Model 完全一致。
|
||||
|
||||
private:
|
||||
void LoadGUIDRegistry();
|
||||
void SaveGUIDRegistry() const;
|
||||
|
||||
IAssetUploader& m_Uploader;
|
||||
|
||||
AssetCache<Model> m_Models;
|
||||
AssetCache<Cubemap> m_Environments;
|
||||
|
||||
// GUID 注册表(双向);dirty 即时保存,无析构落盘依赖
|
||||
std::unordered_map<u64, std::string> m_GuidToPath;
|
||||
std::unordered_map<std::string, u64> m_PathToGuid;
|
||||
};
|
||||
} // namespace Assets
|
||||
} // namespace Engine
|
||||
|
||||
@@ -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)----
|
||||
|
||||
|
||||
@@ -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 <string>
|
||||
|
||||
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
|
||||
@@ -4,13 +4,106 @@
|
||||
|
||||
#include <Engine/Assets/AssetManager.h>
|
||||
#include <Engine/Core/Log.h>
|
||||
#include <Engine/Core/UUID.h>
|
||||
|
||||
#include <yaml-cpp/yaml.h>
|
||||
|
||||
#include <fstream>
|
||||
|
||||
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<u64>(0);
|
||||
auto path = a["Path"].as<std::string>();
|
||||
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)
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -0,0 +1,467 @@
|
||||
//
|
||||
// Created by atdunbg on 2026/10/3.
|
||||
//
|
||||
// SceneSerializer:场景 ↔ YAML。结构见头文件注释与计划 v3。
|
||||
//
|
||||
// 原子性:Deserialize 先把整个文件解析进中间结构(逐实体容错),
|
||||
// 全部成功才 scene.Clear() 换血重建——解析失败不动现场。
|
||||
//
|
||||
|
||||
#include <Engine/Serialization/SceneSerializer.h>
|
||||
#include <Engine/ECS/Scene.h>
|
||||
#include <Engine/ECS/EntityHandle.h>
|
||||
#include <Engine/ECS/Components.h>
|
||||
#include <Engine/Assets/AssetManager.h>
|
||||
#include <Engine/Core/Log.h>
|
||||
#include <Engine/Core/UUID.h>
|
||||
|
||||
#include <yaml-cpp/yaml.h>
|
||||
|
||||
#include <glm/glm.hpp>
|
||||
#include <glm/gtc/quaternion.hpp>
|
||||
|
||||
#include <cmath>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <unordered_set>
|
||||
#include <vector>
|
||||
|
||||
namespace Engine::Serialization {
|
||||
|
||||
namespace {
|
||||
|
||||
// ---- 写出辅助:glm → 数组 ----
|
||||
|
||||
std::vector<f32> Vec3Arr(const glm::vec3& v) { return {v.x, v.y, v.z}; }
|
||||
std::vector<f32> Vec4Arr(const glm::vec4& v) { return {v.x, v.y, v.z, v.w}; }
|
||||
std::vector<f32> QuatArr(const glm::quat& q) { return {q.w, q.x, q.y, q.z}; }
|
||||
|
||||
// ---- 读入辅助:缺字段/类型错 → 默认值(as<T>(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<f32>(def.x), n[1].as<f32>(def.y), n[2].as<f32>(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<f32>(def.x), n[1].as<f32>(def.y),
|
||||
n[2].as<f32>(def.z), n[3].as<f32>(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<f32>(1.0f), x = n[1].as<f32>(0.0f);
|
||||
const f32 y = n[2].as<f32>(0.0f), z = n[3].as<f32>(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 <size_t N>
|
||||
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<u8>(c[0]), static_cast<u8>(c[1]),
|
||||
static_cast<u8>(c[2]), static_cast<u8>(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<u8>(c[0]), static_cast<u8>(c[1]),
|
||||
static_cast<u8>(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<ECS::IdComponent>(e);
|
||||
|
||||
YAML::Node ent;
|
||||
ent["Entity"] = id.uuid.Value();
|
||||
if (const auto* name = reg.try_get<ECS::NameComponent>(e)) {
|
||||
ent["Name"] = name->name;
|
||||
}
|
||||
|
||||
YAML::Node comps;
|
||||
|
||||
if (const auto* t = reg.try_get<ECS::TransformComponent>(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<ECS::MeshRendererComponent>(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<ECS::AnimationComponent>(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<ECS::CameraComponent>(e)) {
|
||||
YAML::Node n;
|
||||
n["Primary"] = c->primary; // 相机不存投影参数(调用方传入)
|
||||
comps["Camera"] = n;
|
||||
}
|
||||
|
||||
if (const auto* l = reg.try_get<ECS::LightComponent>(e)) {
|
||||
YAML::Node n;
|
||||
n["Type"] = static_cast<u8>(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<ECS::RigidBodyComponent>(e)) {
|
||||
YAML::Node n;
|
||||
n["Type"] = static_cast<u8>(rb->type);
|
||||
n["Collider"] = static_cast<u8>(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<ECS::IdComponent>().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<int>(0);
|
||||
version > kSceneVersion) {
|
||||
ENGINE_ERROR("SceneSerializer: '{}' version {} > supported {}",
|
||||
inPath, version, kSceneVersion);
|
||||
return false;
|
||||
}
|
||||
|
||||
outEnvironmentPath = root["Environment"].as<std::string>("");
|
||||
|
||||
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<bool>(s.DrawSkybox);
|
||||
}
|
||||
|
||||
// ---- 阶段 1:解析到中间结构(逐实体容错,不动 registry)----
|
||||
std::vector<ParsedEntity> 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<u64>(0);
|
||||
pe.name = e["Name"].as<std::string>(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<u64> 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<ECS::IdComponent>().uuid = UUID{uuid};
|
||||
|
||||
if (pe.transform) {
|
||||
const YAML::Node n = pe.transform;
|
||||
auto& t = handle.GetComponent<ECS::TransformComponent>();
|
||||
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<u64>(0);
|
||||
const auto path = n["Model"]["Path"].as<std::string>("");
|
||||
if (ModelRef ref = ResolveModel(assets, guid, path);
|
||||
ref.Valid()) {
|
||||
handle.AddComponent<ECS::MeshRendererComponent>(
|
||||
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<ECS::AnimationComponent>();
|
||||
a.clipIndex = n["ClipIndex"].as<int>(a.clipIndex);
|
||||
a.playing = n["Playing"].as<bool>(a.playing);
|
||||
a.loop = n["Loop"].as<bool>(a.loop);
|
||||
a.speed = n["Speed"].as<f32>(a.speed);
|
||||
}
|
||||
|
||||
if (pe.camera) {
|
||||
const YAML::Node n = pe.camera;
|
||||
auto& c = handle.AddComponent<ECS::CameraComponent>();
|
||||
c.primary = n["Primary"].as<bool>(false);
|
||||
}
|
||||
|
||||
if (pe.light) {
|
||||
const YAML::Node n = pe.light;
|
||||
auto& l = handle.AddComponent<ECS::LightComponent>();
|
||||
if (const u8 type = n["Type"].as<u8>(0); type <= 1) {
|
||||
l.type = static_cast<ECS::LightComponent::Type>(type);
|
||||
}
|
||||
l.direction = ReadVec3(n["Direction"], l.direction);
|
||||
l.color = ReadVec4(n["Color"], l.color);
|
||||
l.radius = n["Radius"].as<f32>(l.radius);
|
||||
l.castShadow = n["CastShadow"].as<bool>(l.castShadow);
|
||||
}
|
||||
|
||||
if (pe.rigidBody) {
|
||||
const YAML::Node n = pe.rigidBody;
|
||||
auto& rb = handle.AddComponent<ECS::RigidBodyComponent>();
|
||||
if (const u8 type = n["Type"].as<u8>(0); type <= 2) {
|
||||
rb.type = static_cast<ECS::RigidBodyComponent::BodyType>(
|
||||
type);
|
||||
}
|
||||
if (const u8 col = n["Collider"].as<u8>(0); col <= 2) {
|
||||
rb.collider =
|
||||
static_cast<ECS::RigidBodyComponent::ColliderType>(
|
||||
col);
|
||||
}
|
||||
rb.boxHalfExtents = ReadVec3(n["BoxHalfExtents"],
|
||||
rb.boxHalfExtents);
|
||||
rb.sphereRadius = n["SphereRadius"].as<f32>(rb.sphereRadius);
|
||||
rb.mass = n["Mass"].as<f32>(rb.mass);
|
||||
rb.gravityScale = n["GravityScale"].as<f32>(rb.gravityScale);
|
||||
rb.friction = n["Friction"].as<f32>(rb.friction);
|
||||
rb.restitution = n["Restitution"].as<f32>(rb.restitution);
|
||||
rb.isSensor = n["IsSensor"].as<bool>(rb.isSensor);
|
||||
// bodyId/bodyValid/velocity 保持默认(对账重建)
|
||||
}
|
||||
}
|
||||
|
||||
// 加载在 OnImGuiRender 段,错过本帧 UpdateTransforms:
|
||||
// 显式补算,否则首帧 World = 单位阵
|
||||
scene.UpdateTransforms();
|
||||
|
||||
ENGINE_INFO("SceneSerializer: loaded {} entities from '{}'",
|
||||
parsed.size(), inPath);
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace Engine::Serialization
|
||||
Submodule
+1
Submodule Lib/yaml-cpp added at 1e0876c671
Reference in New Issue
Block a user