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:
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user