// // Editor.cpp:docking 骨架实现。 // // 默认布局 = UE5 出厂布局(首帧 DockBuilder 一次,之后 imgui.ini 接管): // ┌──────────┬──────────────────────┬───────────┐ // │ Outliner │ │ │ // │ │ Viewport │ Details │ // │ │ (+Stats tab) │ │ // ├──────────┴──────────────────────┤ │ // │ Content Browser(左+中通栏) │ │ // └─────────────────────────────────┴───────────┘ // #include "Editor.h" #include "AssetBrowserPanel.h" #include "HierarchyPanel.h" #include "InspectorPanel.h" #include "ViewportPanel.h" #include "StatsPanel.h" #include #include #include #include #include // DockBuilder*(内部 API) #include #include #include #include using namespace Engine; // kDisplayViewport / kDisplayDirect(SceneTypes.h) Editor::Editor() = default; Editor::~Editor() = default; void Editor::Init(const EditorContext& ctx) { m_Ctx = ctx; // PIE 运行/暂停态指针:指向本对象成员(ctx 拷贝后补接,面板只读) m_Ctx.playState = &m_Playing; m_Ctx.pausedState = &m_Paused; m_Hierarchy = std::make_unique(); m_Inspector = std::make_unique(); m_Viewport = std::make_unique(); m_Stats = std::make_unique(); m_AssetBrowser = std::make_unique(); // 旧布局一次性迁移:面板改名(Hierarchy→Outliner / Inspector→Details / // Asset Browser→Content Browser)= docking 身份全变,旧 imgui.ini 对 // 新窗口名失效(新窗口首启浮动,而默认布局又因 dock node 非空不再 // 重建)。检测"有旧名无新名" → 删 ini 让 UE 默认布局重建一次。 // 此刻 ImGui 首帧 NewFrame 还未读盘(NewFrame 内才 LoadIniSettings), // 删文件干净无副作用;新式 ini 或无 ini 不动 if (ImGui::GetCurrentContext() && ImGui::GetIO().IniFilename) { const char* path = ImGui::GetIO().IniFilename; std::ifstream file(path); if (file) { const std::string content((std::istreambuf_iterator(file)), std::istreambuf_iterator()); file.close(); if (content.find("[Window][Hierarchy]") != std::string::npos && content.find("[Window][Outliner]") == std::string::npos) { std::error_code ec; std::filesystem::remove(path, ec); ENGINE_INFO("Editor: legacy imgui.ini removed, " "UE5 default layout will be rebuilt"); } } } } // ---- PIE(Play-In-Editor)---- // Play:当前场景 → 内存 YAML 快照 → 切 gameView(Viewport 面板内嵌 // primary 相机渲染,UE PIE 语义——编辑器 UI 保持可用,不切全屏; // kDisplayDirect 全屏预览是后期发布形态,手动经 Stats 切换) // → 解冻时间(物理/脚本/动画开始跑)。快照失败不进 Play(现场不动) void Editor::DoPlay() { if (!m_Ctx.scene || !m_Ctx.assets || !m_Ctx.renderer) return; m_PlaySnapshot = Serialization::SceneSerializer::SerializeToString( *m_Ctx.scene, *m_Ctx.assets, "PlaySnapshot"); if (m_PlaySnapshot.empty()) { ENGINE_ERROR("PIE: snapshot failed, play aborted"); return; } m_Playing = true; m_Paused = false; m_Gizmo.dragging = false; // 编辑态拖杆中点 Play:杆运行态不出,复位 m_Ctx.renderer->SetGameView(true); Time::SetTimeScale(1.0f); ENGINE_INFO("PIE: playing (scene snapshot {} bytes)", m_PlaySnapshot.size()); } // Stop:快照原子换血恢复 → 回编辑器渲染 → 冻结时间。 // 换血后所有 Entity 句柄悬空:清选中(描边同步清)、Gizmo 拖拽态 // 复位。物理 body/约束/角色由对账系统随实体重建/清理,无需干预。 // 恢复失败(快照损坏,理论罕见)也必须回编辑态,现场留给用户 // File→Load 或退出前 Save void Editor::DoStop() { if (!m_Ctx.scene || !m_Ctx.assets || !m_Ctx.renderer) return; if (!Serialization::SceneSerializer::DeserializeFromString( *m_Ctx.scene, *m_Ctx.assets, m_PlaySnapshot)) { ENGINE_ERROR("PIE: snapshot restore failed (scene is now the " "played-out state)"); } m_PlaySnapshot.clear(); m_Playing = false; m_Paused = false; m_Ctx.renderer->SetGameView(false); Time::SetTimeScale(0.0f); SelectEntity(entt::null); m_Gizmo.dragging = false; ENGINE_INFO("PIE: stopped, editor scene restored"); } void Editor::TogglePlay() { m_Playing ? DoStop() : DoPlay(); } // PIE 暂停(UE Pause 语义):timeScale 0 ↔ 1。运行态才有意义 //(编辑器态本来就冻结,Pause 按钮不出现) void Editor::TogglePause() { m_Paused = !m_Paused; Time::SetTimeScale(m_Paused ? 0.0f : 1.0f); } void Editor::Draw() { // ImGuizmo 每帧首调(官方约定):内部会清 mbOverGizmoHotspot 等 // 帧级状态——不调则 hover 过一次杆后 hotspot 永久置位,GetMoveType // 恒返 MT_NONE → 杆可见但永远抓不住(只能看不能拖) ImGuizmo::BeginFrame(); // host 窗口:铺满主视口,承载菜单栏 + DockSpace ImGuiViewport* viewport = ImGui::GetMainViewport(); ImGui::SetNextWindowPos(viewport->Pos); ImGui::SetNextWindowSize(viewport->Size); ImGui::SetNextWindowViewport(viewport->ID); ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 0.0f); ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 0.0f); ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0.0f, 0.0f)); ImGui::Begin("##EditorDockHost", nullptr, ImGuiWindowFlags_MenuBar | ImGuiWindowFlags_NoDocking | ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoBringToFrontOnFocus | ImGuiWindowFlags_NoNavFocus); ImGui::PopStyleVar(3); DrawMenuBar(); DrawToolbar(); DrawDockSpace(); ImGui::End(); // 常规面板窗口(窗口名 = docking 身份) if (m_ShowHierarchy) { const entt::entity prev = m_Selected; m_Hierarchy->Draw(m_Ctx, m_Selected); // 树点选同步描边(面板直写 m_Selected,不走 Viewport 的 // SelectEntity 返回值通道;对比前后变化触发) if (m_Selected != prev) { SelectEntity(m_Selected); } } if (m_ShowInspector) m_Inspector->Draw(m_Ctx, m_Selected); if (m_ShowViewport) { if (auto picked = m_Viewport->Draw(m_Ctx, m_VpInput, m_Gizmo, m_Selected)) { SelectEntity(*picked); // Viewport 点击拾取 → 选中联动 Inspector/Hierarchy } } if (m_ShowStats) m_Stats->Draw(m_Ctx); if (m_ShowAssetBrowser) m_AssetBrowser->Draw(m_Ctx); DrawQuitModal(); DrawSceneModals(); } // 输入归属判定:ImGui 侧经自身后端链式回调永远正常收输入,这里 // 只决定"剩余输入给不给游戏侧"。UI 被隐藏(反引号)时不会走到 // Draw → m_VpInput 停在旧值,谓词调用方(SandboxApp)须先查 // GetImGuiEnabled() 再问这里。 bool Editor::BlockingKeyboard() const { const ImGuiIO& io = ImGui::GetIO(); if (io.WantTextInput) return true; // 打字中(改名等)不让位快捷键 // 游戏形态(Fullscreen):WASD 全窗口生效,键盘只让位文本输入 if (m_Ctx.renderer && m_Ctx.renderer->GetDisplayMode() == kDisplayDirect) { return false; } // 编辑器形态:Viewport 面板持焦才拥有键盘(UE Scene view 语义) return !m_VpInput.focused; } bool Editor::BlockingMouse() const { // Gizmo 拖拽中:鼠标归操纵杆,不给游戏侧相机(松手自动恢复) if (m_Gizmo.dragging) return true; // 游戏形态:沿用 ImGui 全局判定(UI 隐藏时 WantCaptureMouse=false) if (m_Ctx.renderer && m_Ctx.renderer->GetDisplayMode() == kDisplayDirect) { return ImGui::GetIO().WantCaptureMouse; } // 编辑器形态:悬停 Viewport 才放行(模态框遮挡时 hovered=false) return !m_VpInput.hovered; } void Editor::DrawMenuBar() { if (!ImGui::BeginMenuBar()) { return; } if (ImGui::BeginMenu("File")) { // 运行态禁存取:Save 会把"玩出来的状态"当编辑结果落盘, // Load 会撕掉快照恢复链(UE 运行态同样禁) ImGui::BeginDisabled(m_Playing); if (ImGui::MenuItem("Save Scene...", nullptr)) { m_ShowSaveModal = true; } if (ImGui::MenuItem("Load Scene...", nullptr)) { m_ShowLoadModal = true; } ImGui::EndDisabled(); ImGui::Separator(); if (ImGui::MenuItem("Quit", "Alt+F4")) { RequestQuit(); } ImGui::EndMenu(); } if (ImGui::BeginMenu("View")) { ImGui::MenuItem("Outliner", nullptr, &m_ShowHierarchy); ImGui::MenuItem("Details", nullptr, &m_ShowInspector); ImGui::MenuItem("Viewport", nullptr, &m_ShowViewport); ImGui::MenuItem("Stats", nullptr, &m_ShowStats); ImGui::MenuItem("Content Browser", nullptr, &m_ShowAssetBrowser); ImGui::EndMenu(); } ImGui::EndMenuBar(); } // 主工具栏(UE Main Toolbar 语义):菜单栏下方独立一行,Play 组水平 // 居中。编辑态 = [Play](绿);PIE 运行态 = [Pause][Stop](暂停高亮黄 / // 停止橙红)。kDisplayDirect 全屏预览是发布形态预览,不在此处 void Editor::DrawToolbar() { const ImGuiIO& io = ImGui::GetIO(); // Ctrl+P:文本输入中(改名/路径框)不触发,防打字误切 PIE if (!m_Playing && io.KeyCtrl && !io.WantTextInput && ImGui::IsKeyPressed(ImGuiKey_P, false)) { TogglePlay(); } // 按钮加大触感(工具栏行高 = 一个按钮高;DockSpace avail 自适应) ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(12.0f, 4.0f)); const ImGuiStyle& st = ImGui::GetStyle(); // Play 组宽度 → 水平居中(编辑态 1 钮 / 运行态 2 钮) float groupW = 0.0f; if (m_Playing) { groupW = ImGui::CalcTextSize("Pause").x + ImGui::CalcTextSize("Stop").x + 4.0f * st.FramePadding.x + st.ItemSpacing.x; } else { groupW = ImGui::CalcTextSize("Play").x + 2.0f * st.FramePadding.x; } ImGui::SetCursorPosX((ImGui::GetWindowWidth() - groupW) * 0.5f); if (m_Playing) { // Pause:暂停中亮黄常驻(UE 按下态),运行中暗黄 ImGui::PushStyleColor(ImGuiCol_Button, m_Paused ? ImVec4(0.85f, 0.62f, 0.12f, 1.0f) : ImVec4(0.55f, 0.42f, 0.10f, 1.0f)); ImGui::PushStyleColor(ImGuiCol_ButtonHovered, m_Paused ? ImVec4(0.92f, 0.70f, 0.18f, 1.0f) : ImVec4(0.65f, 0.50f, 0.14f, 1.0f)); ImGui::PushStyleColor(ImGuiCol_ButtonActive, ImVec4(0.95f, 0.76f, 0.24f, 1.0f)); if (ImGui::Button("Pause")) { TogglePause(); } ImGui::PopStyleColor(3); if (ImGui::IsItemHovered()) { ImGui::SetTooltip(m_Paused ? "Resume simulation" : "Pause simulation"); } ImGui::SameLine(); // Stop:橙红(沿用视口 PLAYING 提示色系) ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.75f, 0.45f, 0.10f, 1.0f)); ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.85f, 0.55f, 0.15f, 1.0f)); ImGui::PushStyleColor(ImGuiCol_ButtonActive, ImVec4(0.90f, 0.62f, 0.20f, 1.0f)); if (ImGui::Button("Stop")) { TogglePlay(); } ImGui::PopStyleColor(3); if (ImGui::IsItemHovered()) { ImGui::SetTooltip("Stop and restore editor scene (Ctrl+P)"); } } else { // Play:绿(UE Play 按钮语义) ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0.22f, 0.48f, 0.26f, 1.0f)); ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0.28f, 0.58f, 0.32f, 1.0f)); ImGui::PushStyleColor(ImGuiCol_ButtonActive, ImVec4(0.34f, 0.66f, 0.38f, 1.0f)); if (ImGui::Button("Play")) { TogglePlay(); } ImGui::PopStyleColor(3); if (ImGui::IsItemHovered()) { ImGui::SetTooltip("Play in editor (Ctrl+P)"); } } ImGui::PopStyleVar(); } void Editor::DrawDockSpace() { const ImGuiID dockspaceId = ImGui::GetID("EditorDockSpace"); // 默认布局:仅当 dock node 不存在时构建一次 //(imgui.ini 已保存过布局 → DockBuilderGetNode 非空 → 跳过) if (!m_LayoutBuilt && ImGui::DockBuilderGetNode(dockspaceId) == nullptr) { m_LayoutBuilt = true; ImGui::DockBuilderRemoveNode(dockspaceId); ImGui::DockBuilderAddNode(dockspaceId, ImGuiDockNodeFlags_DockSpace); ImGui::DockBuilderSetNodeSize(dockspaceId, ImGui::GetMainViewport()->Size); // UE5 出厂布局:Details 右列通栏 → Content Browser 底部通栏 //(左+中)→ Outliner 左列 → 剩余全给 Viewport(Stats 作其 tab) ImGuiID dockRight = 0, dockBottom = 0, dockLeft = 0, dockCenter = 0; ImGui::DockBuilderSplitNode(dockspaceId, ImGuiDir_Right, 0.26f, &dockRight, &dockCenter); ImGui::DockBuilderSplitNode(dockCenter, ImGuiDir_Down, 0.30f, &dockBottom, &dockCenter); ImGui::DockBuilderSplitNode(dockCenter, ImGuiDir_Left, 0.22f, &dockLeft, &dockCenter); ImGui::DockBuilderDockWindow("Outliner", dockLeft); ImGui::DockBuilderDockWindow("Viewport", dockCenter); ImGui::DockBuilderDockWindow("Stats", dockCenter); // Viewport 旁 tab ImGui::DockBuilderDockWindow("Content Browser", dockBottom); ImGui::DockBuilderDockWindow("Details", dockRight); ImGui::DockBuilderFinish(dockspaceId); } ImGui::DockSpace(dockspaceId, ImVec2(0.0f, 0.0f), ImGuiDockNodeFlags_None); } void Editor::DrawQuitModal() { if (m_ShowQuitConfirm) { ImGui::OpenPopup("Should quit?"); m_ShowQuitConfirm = false; } ImGui::SetNextWindowPos( ImGui::GetMainViewport()->GetCenter(), ImGuiCond_Appearing, ImVec2(0.5f, 0.5f)); if (ImGui::BeginPopupModal("Should quit?", nullptr, ImGuiWindowFlags_AlwaysAutoResize)) { ImGui::TextUnformatted("Quit the application?"); ImGui::Separator(); if (ImGui::Button("Yes", ImVec2(120, 0))) { m_QuitConfirmed = true; ImGui::CloseCurrentPopup(); } ImGui::SameLine(); if (ImGui::Button("No", ImVec2(120, 0))) { ImGui::CloseCurrentPopup(); } ImGui::EndPopup(); } if (m_QuitConfirmed) { m_QuitConfirmed = false; 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); if (Serialization::SceneSerializer::Serialize(*m_Ctx.scene, *m_Ctx.assets, path)) { 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; } if (!Serialization::SceneSerializer::Deserialize(*m_Ctx.scene, *m_Ctx.assets, path)) { ENGINE_ERROR("Editor: failed to load scene '{}'", path); return; } ENGINE_INFO("Editor: scene loaded from '{}'", path); // 旧实体已销毁:清选中(连带清描边,见 SelectEntity) SelectEntity(entt::null); }