diff --git a/docs/plan/Unity式Tick系统与PlayMode运行时方案-阶段进展.md b/docs/plan/Unity式Tick系统与PlayMode运行时方案-阶段进展.md index 97041bb2..d2595d17 100644 --- a/docs/plan/Unity式Tick系统与PlayMode运行时方案-阶段进展.md +++ b/docs/plan/Unity式Tick系统与PlayMode运行时方案-阶段进展.md @@ -20,6 +20,22 @@ - `Play / Paused` 下允许对 runtime scene 进行对象级编辑与 `Undo / Redo` - runtime scene 的对象改动默认不再污染场景文档 dirty 状态 +### 阶段 C 当前收口 + +- 已补全 `Pause / Resume / Step` 的完整请求与状态切换 +- `Run` 菜单现在区分 `Play/Stop`、`Pause/Resume`、`Step` +- `Error Pause` 已接入正式 Pause 请求通道 +- `Paused` 下维持 runtime world,不回退到 editor scene +- `Step` 现在只在 `Paused` 下有效,并保持 `Paused` 状态不变 + +## 本轮验证 + +- 已重新执行 `cmake -S . -B build` +- 已通过 `cmake --build build --config Debug --target scene_tests` +- 已通过 `cmake --build build --config Debug --target editor_tests -- /m:1 /v:minimal` +- 已通过聚焦测试: + `ctest --test-dir build -C Debug --output-on-failure -j1 -R "RuntimeLoopTest|PlaySessionControllerTest|EditorActionRoutingTest.*PlayMode|EditorActionRoutingTest.*MainMenuRouterRequestsPlayPauseResumeAndStepEvents"` + ## 当前语义 - `editor tick` 负责托管运行时会话 @@ -30,6 +46,6 @@ ## 下一阶段建议 -- 补全 `Pause / Resume / Step` 的完整状态机 -- 明确 `Paused` 下的 `Undo / Redo / Gizmo / Inspector` 交互语义 -- 将 `Error Pause` 完整并入正式状态机 +- 明确 `Paused` 下的 `Undo / Redo / Gizmo / Inspector` 更细粒度交互边界 +- 将 GameView 输入正式接入 runtime input 通道 +- 继续补 `Simulate` 与更完整的 Time 语义 diff --git a/editor/src/Actions/EditorActions.h b/editor/src/Actions/EditorActions.h index 06e59a96..bb500881 100644 --- a/editor/src/Actions/EditorActions.h +++ b/editor/src/Actions/EditorActions.h @@ -176,6 +176,27 @@ inline ActionBinding MakeTogglePlayModeAction(EditorRuntimeMode mode, bool enabl Shortcut(ImGuiKey_F5)); } +inline ActionBinding MakeTogglePauseModeAction(EditorRuntimeMode mode, bool enabled = true) { + const bool paused = mode == EditorRuntimeMode::Paused; + return MakeAction( + paused ? "Resume" : "Pause", + "F6", + paused, + enabled, + false, + Shortcut(ImGuiKey_F6)); +} + +inline ActionBinding MakeStepPlayModeAction(bool enabled = true) { + return MakeAction( + "Step", + "F7", + false, + enabled, + false, + Shortcut(ImGuiKey_F7)); +} + inline ActionBinding MakeNavigateBackAction(bool enabled) { return MakeAction("<", "Alt+Left", false, enabled, false, Shortcut(ImGuiKey_LeftArrow, false, false, true)); } diff --git a/editor/src/Actions/MainMenuActionRouter.h b/editor/src/Actions/MainMenuActionRouter.h index 4c309a23..0b976521 100644 --- a/editor/src/Actions/MainMenuActionRouter.h +++ b/editor/src/Actions/MainMenuActionRouter.h @@ -71,15 +71,42 @@ inline void RequestTogglePlayMode(IEditorContext& context) { context.GetEventBus().Publish(PlayModeStopRequestedEvent{}); } +inline void RequestTogglePauseMode(IEditorContext& context) { + if (context.GetRuntimeMode() == EditorRuntimeMode::Play) { + context.GetEventBus().Publish(PlayModePauseRequestedEvent{}); + return; + } + + if (context.GetRuntimeMode() == EditorRuntimeMode::Paused) { + context.GetEventBus().Publish(PlayModeResumeRequestedEvent{}); + } +} + +inline void RequestStepPlayMode(IEditorContext& context) { + if (context.GetRuntimeMode() != EditorRuntimeMode::Paused) { + return; + } + + context.GetEventBus().Publish(PlayModeStepRequestedEvent{}); +} + inline void RequestAboutPopup(UI::DeferredPopupState& aboutPopup) { aboutPopup.RequestOpen(); } inline void HandleMainMenuShortcuts(IEditorContext& context, const ShortcutContext& shortcutContext) { const bool canEditDocuments = IsDocumentEditingAllowed(context); + const bool canPause = context.GetRuntimeMode() == EditorRuntimeMode::Play || context.GetRuntimeMode() == EditorRuntimeMode::Paused; + const bool canStep = context.GetRuntimeMode() == EditorRuntimeMode::Paused; HandleShortcut(MakeTogglePlayModeAction(context.GetRuntimeMode()), shortcutContext, [&]() { RequestTogglePlayMode(context); }); + HandleShortcut(MakeTogglePauseModeAction(context.GetRuntimeMode(), canPause), shortcutContext, [&]() { + RequestTogglePauseMode(context); + }); + HandleShortcut(MakeStepPlayModeAction(canStep), shortcutContext, [&]() { + RequestStepPlayMode(context); + }); HandleShortcut(MakeNewSceneAction(canEditDocuments), shortcutContext, [&]() { ExecuteNewScene(context); }); HandleShortcut(MakeOpenSceneAction(canEditDocuments), shortcutContext, [&]() { ExecuteOpenScene(context); }); HandleShortcut(MakeSaveSceneAction(canEditDocuments), shortcutContext, [&]() { ExecuteSaveScene(context); }); @@ -104,9 +131,17 @@ inline void DrawFileMenuActions(IEditorContext& context) { } inline void DrawRunMenuActions(IEditorContext& context) { + const bool canPause = context.GetRuntimeMode() == EditorRuntimeMode::Play || context.GetRuntimeMode() == EditorRuntimeMode::Paused; + const bool canStep = context.GetRuntimeMode() == EditorRuntimeMode::Paused; DrawMenuAction(MakeTogglePlayModeAction(context.GetRuntimeMode()), [&]() { RequestTogglePlayMode(context); }); + DrawMenuAction(MakeTogglePauseModeAction(context.GetRuntimeMode(), canPause), [&]() { + RequestTogglePauseMode(context); + }); + DrawMenuAction(MakeStepPlayModeAction(canStep), [&]() { + RequestStepPlayMode(context); + }); } inline void DrawViewMenuActions(IEditorContext& context) { diff --git a/editor/src/Core/EditorEvents.h b/editor/src/Core/EditorEvents.h index 3d959e7d..597a0d6a 100644 --- a/editor/src/Core/EditorEvents.h +++ b/editor/src/Core/EditorEvents.h @@ -58,6 +58,15 @@ struct PlayModePausedEvent { struct PlayModePauseRequestedEvent { }; +struct PlayModeResumedEvent { +}; + +struct PlayModeResumeRequestedEvent { +}; + +struct PlayModeStepRequestedEvent { +}; + struct EditorModeChangedEvent { EditorRuntimeMode oldMode = EditorRuntimeMode::Edit; EditorRuntimeMode newMode = EditorRuntimeMode::Edit; diff --git a/editor/src/Core/PlaySessionController.cpp b/editor/src/Core/PlaySessionController.cpp index 5ee9521e..7213a788 100644 --- a/editor/src/Core/PlaySessionController.cpp +++ b/editor/src/Core/PlaySessionController.cpp @@ -30,6 +30,20 @@ void PlaySessionController::Attach(IEditorContext& context) { PausePlay(context); }); } + + if (m_playResumeRequestedHandlerId == 0) { + m_playResumeRequestedHandlerId = context.GetEventBus().Subscribe( + [this, &context](const PlayModeResumeRequestedEvent&) { + ResumePlay(context); + }); + } + + if (m_playStepRequestedHandlerId == 0) { + m_playStepRequestedHandlerId = context.GetEventBus().Subscribe( + [this, &context](const PlayModeStepRequestedEvent&) { + StepPlay(context); + }); + } } void PlaySessionController::Detach(IEditorContext& context) { @@ -49,6 +63,16 @@ void PlaySessionController::Detach(IEditorContext& context) { context.GetEventBus().Unsubscribe(m_playPauseRequestedHandlerId); m_playPauseRequestedHandlerId = 0; } + + if (m_playResumeRequestedHandlerId != 0) { + context.GetEventBus().Unsubscribe(m_playResumeRequestedHandlerId); + m_playResumeRequestedHandlerId = 0; + } + + if (m_playStepRequestedHandlerId != 0) { + context.GetEventBus().Unsubscribe(m_playStepRequestedHandlerId); + m_playStepRequestedHandlerId = 0; + } } void PlaySessionController::Update(IEditorContext& context, float deltaTime) { @@ -118,5 +142,25 @@ bool PlaySessionController::PausePlay(IEditorContext& context) { return true; } +bool PlaySessionController::ResumePlay(IEditorContext& context) { + if (context.GetRuntimeMode() != EditorRuntimeMode::Paused || !m_runtimeLoop.IsRunning()) { + return false; + } + + m_runtimeLoop.Resume(); + context.SetRuntimeMode(EditorRuntimeMode::Play); + context.GetEventBus().Publish(PlayModeResumedEvent{}); + return true; +} + +bool PlaySessionController::StepPlay(IEditorContext& context) { + if (context.GetRuntimeMode() != EditorRuntimeMode::Paused || !m_runtimeLoop.IsRunning()) { + return false; + } + + m_runtimeLoop.StepFrame(); + return true; +} + } // namespace Editor } // namespace XCEngine diff --git a/editor/src/Core/PlaySessionController.h b/editor/src/Core/PlaySessionController.h index 87779ebf..dbf3caf1 100644 --- a/editor/src/Core/PlaySessionController.h +++ b/editor/src/Core/PlaySessionController.h @@ -22,11 +22,15 @@ public: bool StartPlay(IEditorContext& context); bool StopPlay(IEditorContext& context); bool PausePlay(IEditorContext& context); + bool ResumePlay(IEditorContext& context); + bool StepPlay(IEditorContext& context); private: uint64_t m_playStartRequestedHandlerId = 0; uint64_t m_playStopRequestedHandlerId = 0; uint64_t m_playPauseRequestedHandlerId = 0; + uint64_t m_playResumeRequestedHandlerId = 0; + uint64_t m_playStepRequestedHandlerId = 0; SceneSnapshot m_editorSnapshot = {}; XCEngine::Components::RuntimeLoop m_runtimeLoop; }; diff --git a/editor/src/panels/ConsolePanel.cpp b/editor/src/panels/ConsolePanel.cpp index ce3c2bee..e4354db4 100644 --- a/editor/src/panels/ConsolePanel.cpp +++ b/editor/src/panels/ConsolePanel.cpp @@ -889,6 +889,12 @@ void ConsolePanel::OnAttach() { HandlePlayModePaused(); }); } + if (!m_playModeResumedHandlerId) { + m_playModeResumedHandlerId = m_context->GetEventBus().Subscribe( + [this](const PlayModeResumedEvent&) { + HandlePlayModeResumed(); + }); + } } void ConsolePanel::OnDetach() { @@ -908,6 +914,10 @@ void ConsolePanel::OnDetach() { m_context->GetEventBus().Unsubscribe(m_playModePausedHandlerId); m_playModePausedHandlerId = 0; } + if (m_playModeResumedHandlerId) { + m_context->GetEventBus().Unsubscribe(m_playModeResumedHandlerId); + m_playModeResumedHandlerId = 0; + } } void ConsolePanel::HandlePlayModeStarted() { @@ -941,6 +951,11 @@ void ConsolePanel::HandlePlayModePaused() { m_playModePaused = true; } +void ConsolePanel::HandlePlayModeResumed() { + m_playModeActive = true; + m_playModePaused = false; +} + void ConsolePanel::Render() { UI::PanelWindowScope panel(m_name.c_str()); if (!panel.IsOpen()) { diff --git a/editor/src/panels/ConsolePanel.h b/editor/src/panels/ConsolePanel.h index a23f60f5..1c846d45 100644 --- a/editor/src/panels/ConsolePanel.h +++ b/editor/src/panels/ConsolePanel.h @@ -20,6 +20,7 @@ private: void HandlePlayModeStarted(); void HandlePlayModeStopped(); void HandlePlayModePaused(); + void HandlePlayModeResumed(); UI::ConsoleFilterState m_filterState; char m_searchBuffer[256] = ""; @@ -29,6 +30,7 @@ private: uint64_t m_playModeStartedHandlerId = 0; uint64_t m_playModeStoppedHandlerId = 0; uint64_t m_playModePausedHandlerId = 0; + uint64_t m_playModeResumedHandlerId = 0; std::string m_selectedEntryKey; float m_detailsHeight = 0.0f; bool m_playModeActive = false; diff --git a/tests/Scene/test_runtime_loop.cpp b/tests/Scene/test_runtime_loop.cpp index 4085cccf..e44d62fa 100644 --- a/tests/Scene/test_runtime_loop.cpp +++ b/tests/Scene/test_runtime_loop.cpp @@ -131,4 +131,25 @@ TEST_F(RuntimeLoopTest, PauseSkipsAutomaticTicksUntilStepFrameIsRequested) { EXPECT_TRUE(loop.IsPaused()); } +TEST_F(RuntimeLoopTest, ResumeRestoresAutomaticTickProgressionAfterPause) { + RuntimeLoop loop({0.02f, 0.1f, 4}); + Scene* scene = CreateScene(); + GameObject* host = scene->CreateGameObject("Host"); + host->AddComponent(&counters); + + loop.Start(scene); + loop.Pause(); + loop.Tick(0.025f); + EXPECT_EQ(counters.updateCount, 0); + + loop.Resume(); + EXPECT_FALSE(loop.IsPaused()); + loop.Tick(0.025f); + + EXPECT_EQ(counters.startCount, 1); + EXPECT_EQ(counters.fixedUpdateCount, 1); + EXPECT_EQ(counters.updateCount, 1); + EXPECT_EQ(counters.lateUpdateCount, 1); +} + } // namespace diff --git a/tests/editor/test_action_routing.cpp b/tests/editor/test_action_routing.cpp index 12545e4b..4d5f20cc 100644 --- a/tests/editor/test_action_routing.cpp +++ b/tests/editor/test_action_routing.cpp @@ -284,6 +284,65 @@ TEST_F(EditorActionRoutingTest, MainMenuRouterRequestsExitResetAndAboutPopup) { m_context.GetEventBus().Unsubscribe(resetSubscription); } +TEST_F(EditorActionRoutingTest, MainMenuRouterRequestsPlayPauseResumeAndStepEvents) { + int playStartRequestCount = 0; + int playStopRequestCount = 0; + int playPauseRequestCount = 0; + int playResumeRequestCount = 0; + int playStepRequestCount = 0; + + const uint64_t playStartSubscription = m_context.GetEventBus().Subscribe( + [&](const PlayModeStartRequestedEvent&) { + ++playStartRequestCount; + }); + const uint64_t playStopSubscription = m_context.GetEventBus().Subscribe( + [&](const PlayModeStopRequestedEvent&) { + ++playStopRequestCount; + }); + const uint64_t playPauseSubscription = m_context.GetEventBus().Subscribe( + [&](const PlayModePauseRequestedEvent&) { + ++playPauseRequestCount; + }); + const uint64_t playResumeSubscription = m_context.GetEventBus().Subscribe( + [&](const PlayModeResumeRequestedEvent&) { + ++playResumeRequestCount; + }); + const uint64_t playStepSubscription = m_context.GetEventBus().Subscribe( + [&](const PlayModeStepRequestedEvent&) { + ++playStepRequestCount; + }); + + Actions::RequestTogglePlayMode(m_context); + EXPECT_EQ(playStartRequestCount, 1); + EXPECT_EQ(playStopRequestCount, 0); + + m_context.SetRuntimeMode(EditorRuntimeMode::Play); + Actions::RequestTogglePauseMode(m_context); + EXPECT_EQ(playPauseRequestCount, 1); + EXPECT_EQ(playResumeRequestCount, 0); + Actions::RequestStepPlayMode(m_context); + EXPECT_EQ(playStepRequestCount, 0); + + m_context.SetRuntimeMode(EditorRuntimeMode::Paused); + Actions::RequestTogglePauseMode(m_context); + EXPECT_EQ(playResumeRequestCount, 1); + Actions::RequestStepPlayMode(m_context); + EXPECT_EQ(playStepRequestCount, 1); + + Actions::RequestTogglePlayMode(m_context); + EXPECT_EQ(playStopRequestCount, 1); + + m_context.SetRuntimeMode(EditorRuntimeMode::Edit); + Actions::RequestStepPlayMode(m_context); + EXPECT_EQ(playStepRequestCount, 1); + + m_context.GetEventBus().Unsubscribe(playStartSubscription); + m_context.GetEventBus().Unsubscribe(playStopSubscription); + m_context.GetEventBus().Unsubscribe(playPauseSubscription); + m_context.GetEventBus().Unsubscribe(playResumeSubscription); + m_context.GetEventBus().Unsubscribe(playStepSubscription); +} + TEST_F(EditorActionRoutingTest, PlayModeAllowsRuntimeSceneUndoRedoButKeepsSceneDocumentCommandsBlocked) { const fs::path savedScenePath = m_projectRoot / "Assets" / "Scenes" / "PlayModeRuntimeEditing.xc"; ASSERT_TRUE(m_context.GetSceneManager().SaveSceneAs(savedScenePath.string())); diff --git a/tests/editor/test_play_session_controller.cpp b/tests/editor/test_play_session_controller.cpp index b5f828b7..5497ff13 100644 --- a/tests/editor/test_play_session_controller.cpp +++ b/tests/editor/test_play_session_controller.cpp @@ -79,5 +79,46 @@ TEST_F(PlaySessionControllerTest, StartAndStopRequestsRouteThroughEventBus) { m_controller.Detach(m_context); } +TEST_F(PlaySessionControllerTest, PauseResumeAndStepRequestsDrivePlayStateMachine) { + auto* editorEntity = m_context.GetSceneManager().CreateEntity("Persistent"); + ASSERT_NE(editorEntity, nullptr); + + int pausedCount = 0; + int resumedCount = 0; + const uint64_t pausedSubscription = m_context.GetEventBus().Subscribe( + [&](const PlayModePausedEvent&) { + ++pausedCount; + }); + const uint64_t resumedSubscription = m_context.GetEventBus().Subscribe( + [&](const PlayModeResumedEvent&) { + ++resumedCount; + }); + + m_controller.Attach(m_context); + + m_context.GetEventBus().Publish(PlayModeStartRequestedEvent{}); + EXPECT_EQ(m_context.GetRuntimeMode(), EditorRuntimeMode::Play); + + m_context.GetEventBus().Publish(PlayModePauseRequestedEvent{}); + EXPECT_EQ(m_context.GetRuntimeMode(), EditorRuntimeMode::Paused); + EXPECT_EQ(pausedCount, 1); + + m_context.GetEventBus().Publish(PlayModeStepRequestedEvent{}); + EXPECT_EQ(m_context.GetRuntimeMode(), EditorRuntimeMode::Paused); + EXPECT_EQ(pausedCount, 1); + EXPECT_EQ(resumedCount, 0); + + m_context.GetEventBus().Publish(PlayModeResumeRequestedEvent{}); + EXPECT_EQ(m_context.GetRuntimeMode(), EditorRuntimeMode::Play); + EXPECT_EQ(resumedCount, 1); + + m_context.GetEventBus().Publish(PlayModeStopRequestedEvent{}); + EXPECT_EQ(m_context.GetRuntimeMode(), EditorRuntimeMode::Edit); + + m_controller.Detach(m_context); + m_context.GetEventBus().Unsubscribe(pausedSubscription); + m_context.GetEventBus().Unsubscribe(resumedSubscription); +} + } // namespace } // namespace XCEngine::Editor