diff --git a/App/editor/Editor.cpp b/App/editor/Editor.cpp index da74d09..70602e8 100644 --- a/App/editor/Editor.cpp +++ b/App/editor/Editor.cpp @@ -40,8 +40,9 @@ Editor::~Editor() = default; void Editor::Init(const EditorContext& ctx) { m_Ctx = ctx; - // PIE 运行态指针:指向本对象成员(ctx 拷贝后补接,面板只读) + // PIE 运行/暂停态指针:指向本对象成员(ctx 拷贝后补接,面板只读) m_Ctx.playState = &m_Playing; + m_Ctx.pausedState = &m_Paused; m_Hierarchy = std::make_unique(); m_Inspector = std::make_unique(); @@ -78,7 +79,9 @@ void Editor::Init(const EditorContext& ctx) // ---- PIE(Play-In-Editor)---- -// Play:当前场景 → 内存 YAML 快照 → 切游戏渲染(primary 相机实体) +// Play:当前场景 → 内存 YAML 快照 → 切 gameView(Viewport 面板内嵌 +// primary 相机渲染,UE PIE 语义——编辑器 UI 保持可用,不切全屏; +// kDisplayDirect 全屏预览是后期发布形态,手动经 Stats 切换) // → 解冻时间(物理/脚本/动画开始跑)。快照失败不进 Play(现场不动) void Editor::DoPlay() { @@ -93,7 +96,9 @@ void Editor::DoPlay() } m_Playing = true; - m_Ctx.renderer->SetDisplayMode(kDisplayDirect); + 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()); @@ -117,7 +122,8 @@ void Editor::DoStop() m_PlaySnapshot.clear(); m_Playing = false; - m_Ctx.renderer->SetDisplayMode(kDisplayViewport); + m_Paused = false; + m_Ctx.renderer->SetGameView(false); Time::SetTimeScale(0.0f); SelectEntity(entt::null); m_Gizmo.dragging = false; @@ -129,6 +135,14 @@ 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 等 @@ -154,6 +168,7 @@ void Editor::Draw() ImGui::PopStyleVar(3); DrawMenuBar(); + DrawToolbar(); DrawDockSpace(); ImGui::End(); @@ -227,42 +242,6 @@ void Editor::DrawMenuBar() return; } - // ---- PIE 工具栏(UE 主工具栏语义):Play/Stop 切换按钮 + - // Ctrl+P 全局热键。运行态按钮变 Stop(黄底提示在视口内)---- - { - const ImGuiIO& io = ImGui::GetIO(); - // Ctrl+P:文本输入中(改名/路径框)不触发,防打字误切 PIE - if (!m_Playing && io.KeyCtrl && !io.WantTextInput && - ImGui::IsKeyPressed(ImGuiKey_P, false)) - { - TogglePlay(); - } - - if (m_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::SmallButton("Stop (Ctrl+P)")) - { - TogglePlay(); - } - ImGui::PopStyleColor(3); - ImGui::SameLine(); - ImGui::TextColored(ImVec4(1.0f, 0.72f, 0.25f, 1.0f), - "PLAYING - editor input disabled"); - } - else if (ImGui::SmallButton("Play (Ctrl+P)")) - { - TogglePlay(); - } - ImGui::SameLine(); - ImGui::Separator(); - } - if (ImGui::BeginMenu("File")) { // 运行态禁存取:Save 会把"玩出来的状态"当编辑结果落盘, @@ -298,6 +277,98 @@ void Editor::DrawMenuBar() 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"); diff --git a/App/editor/Editor.h b/App/editor/Editor.h index 77cad87..0b5bd5f 100644 --- a/App/editor/Editor.h +++ b/App/editor/Editor.h @@ -57,12 +57,16 @@ public: // 请求退出(弹确认框;窗口关闭事件走这里) void RequestQuit() { m_ShowQuitConfirm = true; } - // ---- PIE(Play-In-Editor,UE 工具栏语义)---- + // ---- PIE(Play-In-Editor,UE 主工具栏语义)---- bool IsPlaying() const { return m_Playing; } - // Play:内存快照当前场景 → 切游戏渲染(primary 相机)→ 解冻时间; + bool IsPaused() const { return m_Paused; } + // Play:内存快照当前场景 → 切 gameView(Viewport 内嵌 primary 相机 + // 渲染,UE PIE 语义;不切全屏)→ 解冻时间; // Stop:快照原子换血恢复 → 回编辑器渲染 → 冻结时间。 // 编辑器态默认 timeScale=0(物理冻结 = UE 语义),Play 解冻 void TogglePlay(); + // PIE 暂停切换(UE Pause 语义):timeScale 0 ↔ 1 + void TogglePause(); private: void DoPlay(); // TogglePlay 的 Play 半程(快照 + 切渲染 + 解冻) @@ -86,7 +90,8 @@ private: entt::entity GetSelected() const { return m_Selected; } private: - void DrawMenuBar(); // host 窗口内顶栏 + void DrawMenuBar(); // host 窗口内顶栏(File/View 菜单) + void DrawToolbar(); // 菜单栏下方主工具栏(Play/Pause/Stop 居中) void DrawDockSpace(); // 全屏 host + dockspace + 默认布局 void DrawQuitModal(); // 退出确认模态框 void DrawSceneModals(); // 保存/加载路径模态框(共用一个路径输入) @@ -127,6 +132,7 @@ private: // PIE 状态:Play 时场景 YAML 内存快照,Stop 时原子换血恢复 bool m_Playing = false; + bool m_Paused = false; // PIE 暂停(timeScale=0;Stop 时复位) std::string m_PlaySnapshot; }; diff --git a/App/editor/EditorContext.h b/App/editor/EditorContext.h index 95a618a..fbe518e 100644 --- a/App/editor/EditorContext.h +++ b/App/editor/EditorContext.h @@ -29,6 +29,8 @@ struct EditorContext { // PIE 运行态(Play-In-Editor):Editor 持有,只读指针。 // 面板门控用——运行态禁 Gizmo/编辑器热键,渲染源已是 primary 相机 const bool *playState = nullptr; + // PIE 暂停态(timeScale=0):Editor 持有,只读指针(视口 PAUSED 提示) + const bool *pausedState = nullptr; }; #endif //HELLOVULKAN_EDITORCONTEXT_H diff --git a/App/editor/StatsPanel.cpp b/App/editor/StatsPanel.cpp index eb365d8..d7ed23d 100644 --- a/App/editor/StatsPanel.cpp +++ b/App/editor/StatsPanel.cpp @@ -25,6 +25,9 @@ void StatsPanel::Draw(const EditorContext& ctx) // ---- View:看哪里(渲染目标)+ 相机位姿(跟随渲染源分派)---- if (ImGui::CollapsingHeader("View", ImGuiTreeNodeFlags_DefaultOpen)) { + // 渲染源分派与 SceneRenderer::BuildFrame 同规则:PIE 内嵌播放 + // 或 kDisplayDirect 全屏预览 = primary 相机,否则编辑器相机 + const bool playing = ctx.playState && *ctx.playState; static const char* displayTarget[] = { "Fullscreen", "Viewport", @@ -35,13 +38,7 @@ void StatsPanel::Draw(const EditorContext& ctx) { renderer.SetDisplayMode(mode); } - if (renderer.GetDisplayMode() == kDisplayViewport && ctx.editorCamera) - { - const glm::vec3& pos = ctx.editorCamera->GetTransform().GetPosition(); - ImGui::Text("Editor Camera Pos"); - ImGui::Text("(%.3f, %.3f, %.3f)", pos.x, pos.y, pos.z); - } - else + if ((playing || renderer.GetDisplayMode() == kDisplayDirect)) { Engine::ECS::TransformComponent* camT = nullptr; if (ctx.scene->GetPrimaryCamera(&camT) != nullptr && camT != nullptr) @@ -56,17 +53,21 @@ void StatsPanel::Draw(const EditorContext& ctx) ImGui::TextDisabled("(no primary camera)"); } } + else if (ctx.editorCamera) + { + const glm::vec3& pos = ctx.editorCamera->GetTransform().GetPosition(); + ImGui::Text("Editor Camera Pos"); + ImGui::Text("(%.3f, %.3f, %.3f)", pos.x, pos.y, pos.z); + } + else + { + ImGui::TextDisabled("(no camera)"); + } } - // ---- Scene:模拟控制 + 场景级设置 ---- + // ---- Scene:模拟控制 + 场景级设置。PIE 暂停在主工具栏(UE 语义)---- if (ImGui::CollapsingHeader("Scene", ImGuiTreeNodeFlags_DefaultOpen)) { - // 暂停模拟(引擎 Time 门面:动画/物理统一冻结) - bool paused = Time::TimeScale() == 0.0f; - if (ImGui::Checkbox("Pause Simulation", &paused)) - { - Time::SetTimeScale(paused ? 0.0f : 1.0f); - } ImGui::ColorEdit4("Clear Color", glm::value_ptr(ctx.scene->GetSettings().ClearColor)); } diff --git a/App/editor/ViewportPanel.cpp b/App/editor/ViewportPanel.cpp index 69334e5..ac846c1 100644 --- a/App/editor/ViewportPanel.cpp +++ b/App/editor/ViewportPanel.cpp @@ -69,12 +69,14 @@ namespace // 视图相机分派(必须与 SceneRenderer::BuildFrame 渲染源同一规则, // 否则 Gizmo 杆与画面错位):编辑器形态 = EditorCamera(Scene view - // 专用,与相机实体无关);游戏形态 = primary 相机实体。 + // 专用,与相机实体无关);游戏形态(kDisplayDirect 全屏预览或 PIE + // 内嵌播放)= primary 相机实体。 // 返回 nullptr = 游戏形态且无 primary 相机(本帧无 Gizmo) Engine::Camera* ViewCamera(const EditorContext& ctx, ECS::TransformComponent*& outT) { - if (ctx.renderer->GetDisplayMode() == kDisplayViewport) + const bool playing = ctx.playState && *ctx.playState; + if (!playing && ctx.renderer->GetDisplayMode() == kDisplayViewport) { outT = &ctx.editorCamera->GetTransform(); return &ctx.editorCamera->GetController(); @@ -281,12 +283,14 @@ std::optional ViewportPanel::Draw(const EditorContext &ctx, } // Gizmo 状态提示(左上角 drawlist 叠加,原因同下:不占布局行) - // PIE 运行态 = 黄色 PLAYING 提示替代编辑状态 + // PIE 运行态 = 黄色 PLAYING/PAUSED 提示替代编辑状态 { char tip[64]; if (playing) { + const bool paused = ctx.pausedState && *ctx.pausedState; std::snprintf(tip, sizeof(tip), - "PLAYING - Ctrl+P to stop"); + paused ? "PAUSED - Ctrl+P to stop" + : "PLAYING - Ctrl+P to stop"); } else { std::snprintf(tip, sizeof(tip), "Space: %s [T] Snap: Ctrl", @@ -335,8 +339,9 @@ std::optional ViewportPanel::Draw(const EditorContext &ctx, // 点击拾取:单击 Image 且未命中 UI 遮挡(IsItemHovered 已含)。 // 抓杆时不出 pick(IsOver 悬停杆/IsUsing 拖拽中)——否则按在 - // 操纵杆上的点击会穿透发拾取请求 - if (ImGui::IsItemHovered() && ImGui::IsMouseClicked(0) && + // 操纵杆上的点击会穿透发拾取请求。PIE 运行态不出拾取: + // 点击 = 游戏输入(UE Play 语义,编辑操作冻结) + if (!playing && ImGui::IsItemHovered() && ImGui::IsMouseClicked(0) && !ImGuizmo::IsOver() && !ImGuizmo::IsUsing()) { RequestPickAt(ctx, size); } diff --git a/App/main.cpp b/App/main.cpp index 19af26a..923b184 100644 --- a/App/main.cpp +++ b/App/main.cpp @@ -65,8 +65,11 @@ namespace { // 编辑器形态:驱动编辑器相机(RMB 飞行 / MMB 焦点环绕 / // 滚轮推拉)。暂停(timeScale=0)时仍可飞行——unscaled dt。 + // PIE 内嵌播放:渲染源已切 primary 相机,编辑器相机停驱 + //(否则与游戏脚本抢 WASD,Stop 后视角也被偷跑)。 // 游戏形态:相机实体由游戏逻辑驱动(本沙盒无逻辑 = 静止) - if (GetRenderer().GetDisplayMode() == kDisplayViewport) + if (GetRenderer().GetDisplayMode() == kDisplayViewport && + !m_Editor->IsPlaying()) { GetRenderer().GetEditorCamera().Update(GetWindow(), frameTime.UnscaledDt()); diff --git a/Engine/include/Engine/Renderer/Renderer.h b/Engine/include/Engine/Renderer/Renderer.h index e04fed2..f845774 100644 --- a/Engine/include/Engine/Renderer/Renderer.h +++ b/Engine/include/Engine/Renderer/Renderer.h @@ -142,6 +142,7 @@ namespace Engine { // ---- 显示模式(Editor 视口 ↔ 全屏直出;帧首同步生效)---- int GetDisplayMode() const { return m_DesiredDisplayMode; } void SetDisplayMode(int mode); // cpp 内定义(SceneRenderer 前置声明) + void SetGameView(bool gameView); // PIE 内嵌播放(cpp 转发 SceneRenderer) // 编辑器相机(Scene view 恒用,与场景相机实体无关)。转发 // SceneRenderer 持有的实例:Application 驱动 Update,面板读写位姿 EditorCamera& GetEditorCamera(); // cpp 内定义(SceneRenderer 完整类型) diff --git a/Engine/include/Engine/Renderer/SceneRenderer.h b/Engine/include/Engine/Renderer/SceneRenderer.h index 841b414..4c7fc5e 100644 --- a/Engine/include/Engine/Renderer/SceneRenderer.h +++ b/Engine/include/Engine/Renderer/SceneRenderer.h @@ -66,6 +66,10 @@ namespace Engine { void SetLodEnabled(bool b) { m_LodEnabled = b; } void SetDebugView(int v) { m_DebugView = v; } void SetDisplayMode(int m) { m_DisplayMode = m; } // kDisplayDirect / kDisplayViewport + // PIE 内嵌播放(UE Play-In-Editor 语义):viewport 模式下也用 + // primary 相机渲染进 sceneTex(Play 后 Scene view 显示游戏画面, + // 编辑器 UI 保持可用)。kDisplayDirect 恒为游戏形态不受此影响 + void SetGameView(bool g) { m_GameView = g; } // 编辑器相机(视口 UI 状态,SceneRenderer 持有 = 渲染源在此分派); // Application 驱动 Update,ViewportPanel/Inspector 经 Renderer 门面访问 EditorCamera& GetEditorCamera() { return m_EditorCamera; } @@ -171,6 +175,7 @@ namespace Engine { bool m_LodEnabled = true; // 关 = 全部强制 LOD0(选级跳过) int m_DebugView = 0; int m_DisplayMode = kDisplayViewport; // Editor 视口为默认形态 + bool m_GameView = false; // PIE 内嵌播放(Editor Play/Stop 驱动) }; } // namespace Engine diff --git a/Engine/src/Renderer/Renderer.cpp b/Engine/src/Renderer/Renderer.cpp index ffb000a..a9354bb 100644 --- a/Engine/src/Renderer/Renderer.cpp +++ b/Engine/src/Renderer/Renderer.cpp @@ -234,6 +234,11 @@ namespace Engine m_SceneRenderer->SetDisplayMode(mode); } + void Renderer::SetGameView(bool gameView) + { + m_SceneRenderer->SetGameView(gameView); + } + EditorCamera& Renderer::GetEditorCamera() { return m_SceneRenderer->GetEditorCamera(); diff --git a/Engine/src/Renderer/SceneRenderer.cpp b/Engine/src/Renderer/SceneRenderer.cpp index abeef82..e36a994 100644 --- a/Engine/src/Renderer/SceneRenderer.cpp +++ b/Engine/src/Renderer/SceneRenderer.cpp @@ -269,12 +269,13 @@ namespace Engine ubo.prevView = m_LastView; ubo.prevProj = m_LastProjNoJitter; - // ---- 1. 相机:渲染源按显示模式分派(UE 语义)。编辑器形态 + // ---- 1. 相机:渲染源按形态分派(UE 语义)。编辑器形态 // Scene view 恒用编辑器相机(视口 UI 状态,空场景可用); - // 游戏形态用场景 primary 相机实体(游戏逻辑所有)---- + // 游戏形态(kDisplayDirect 全屏预览或 PIE 内嵌播放)用 + // 场景 primary 相机实体(游戏逻辑所有)---- ECS::TransformComponent* camTransform = nullptr; const Camera* camera = nullptr; - if (m_DisplayMode == kDisplayViewport) { + if (m_DisplayMode == kDisplayViewport && !m_GameView) { camTransform = &m_EditorCamera.GetTransform(); camera = &m_EditorCamera.GetController(); } else if (auto* cameraComp = scene.GetPrimaryCamera(&camTransform)) {