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:
2026-10-03 14:47:02 +08:00
parent ffe33e9967
commit 007d175314
14 changed files with 867 additions and 1 deletions
+159
View File
@@ -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();
}
}