feat: add play mode pause resume and step controls
This commit is contained in:
@@ -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 语义
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -58,6 +58,15 @@ struct PlayModePausedEvent {
|
||||
struct PlayModePauseRequestedEvent {
|
||||
};
|
||||
|
||||
struct PlayModeResumedEvent {
|
||||
};
|
||||
|
||||
struct PlayModeResumeRequestedEvent {
|
||||
};
|
||||
|
||||
struct PlayModeStepRequestedEvent {
|
||||
};
|
||||
|
||||
struct EditorModeChangedEvent {
|
||||
EditorRuntimeMode oldMode = EditorRuntimeMode::Edit;
|
||||
EditorRuntimeMode newMode = EditorRuntimeMode::Edit;
|
||||
|
||||
@@ -30,6 +30,20 @@ void PlaySessionController::Attach(IEditorContext& context) {
|
||||
PausePlay(context);
|
||||
});
|
||||
}
|
||||
|
||||
if (m_playResumeRequestedHandlerId == 0) {
|
||||
m_playResumeRequestedHandlerId = context.GetEventBus().Subscribe<PlayModeResumeRequestedEvent>(
|
||||
[this, &context](const PlayModeResumeRequestedEvent&) {
|
||||
ResumePlay(context);
|
||||
});
|
||||
}
|
||||
|
||||
if (m_playStepRequestedHandlerId == 0) {
|
||||
m_playStepRequestedHandlerId = context.GetEventBus().Subscribe<PlayModeStepRequestedEvent>(
|
||||
[this, &context](const PlayModeStepRequestedEvent&) {
|
||||
StepPlay(context);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void PlaySessionController::Detach(IEditorContext& context) {
|
||||
@@ -49,6 +63,16 @@ void PlaySessionController::Detach(IEditorContext& context) {
|
||||
context.GetEventBus().Unsubscribe<PlayModePauseRequestedEvent>(m_playPauseRequestedHandlerId);
|
||||
m_playPauseRequestedHandlerId = 0;
|
||||
}
|
||||
|
||||
if (m_playResumeRequestedHandlerId != 0) {
|
||||
context.GetEventBus().Unsubscribe<PlayModeResumeRequestedEvent>(m_playResumeRequestedHandlerId);
|
||||
m_playResumeRequestedHandlerId = 0;
|
||||
}
|
||||
|
||||
if (m_playStepRequestedHandlerId != 0) {
|
||||
context.GetEventBus().Unsubscribe<PlayModeStepRequestedEvent>(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
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -889,6 +889,12 @@ void ConsolePanel::OnAttach() {
|
||||
HandlePlayModePaused();
|
||||
});
|
||||
}
|
||||
if (!m_playModeResumedHandlerId) {
|
||||
m_playModeResumedHandlerId = m_context->GetEventBus().Subscribe<PlayModeResumedEvent>(
|
||||
[this](const PlayModeResumedEvent&) {
|
||||
HandlePlayModeResumed();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void ConsolePanel::OnDetach() {
|
||||
@@ -908,6 +914,10 @@ void ConsolePanel::OnDetach() {
|
||||
m_context->GetEventBus().Unsubscribe<PlayModePausedEvent>(m_playModePausedHandlerId);
|
||||
m_playModePausedHandlerId = 0;
|
||||
}
|
||||
if (m_playModeResumedHandlerId) {
|
||||
m_context->GetEventBus().Unsubscribe<PlayModeResumedEvent>(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()) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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<RuntimeLoopObserverComponent>(&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
|
||||
|
||||
@@ -284,6 +284,65 @@ TEST_F(EditorActionRoutingTest, MainMenuRouterRequestsExitResetAndAboutPopup) {
|
||||
m_context.GetEventBus().Unsubscribe<DockLayoutResetRequestedEvent>(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<PlayModeStartRequestedEvent>(
|
||||
[&](const PlayModeStartRequestedEvent&) {
|
||||
++playStartRequestCount;
|
||||
});
|
||||
const uint64_t playStopSubscription = m_context.GetEventBus().Subscribe<PlayModeStopRequestedEvent>(
|
||||
[&](const PlayModeStopRequestedEvent&) {
|
||||
++playStopRequestCount;
|
||||
});
|
||||
const uint64_t playPauseSubscription = m_context.GetEventBus().Subscribe<PlayModePauseRequestedEvent>(
|
||||
[&](const PlayModePauseRequestedEvent&) {
|
||||
++playPauseRequestCount;
|
||||
});
|
||||
const uint64_t playResumeSubscription = m_context.GetEventBus().Subscribe<PlayModeResumeRequestedEvent>(
|
||||
[&](const PlayModeResumeRequestedEvent&) {
|
||||
++playResumeRequestCount;
|
||||
});
|
||||
const uint64_t playStepSubscription = m_context.GetEventBus().Subscribe<PlayModeStepRequestedEvent>(
|
||||
[&](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<PlayModeStartRequestedEvent>(playStartSubscription);
|
||||
m_context.GetEventBus().Unsubscribe<PlayModeStopRequestedEvent>(playStopSubscription);
|
||||
m_context.GetEventBus().Unsubscribe<PlayModePauseRequestedEvent>(playPauseSubscription);
|
||||
m_context.GetEventBus().Unsubscribe<PlayModeResumeRequestedEvent>(playResumeSubscription);
|
||||
m_context.GetEventBus().Unsubscribe<PlayModeStepRequestedEvent>(playStepSubscription);
|
||||
}
|
||||
|
||||
TEST_F(EditorActionRoutingTest, PlayModeAllowsRuntimeSceneUndoRedoButKeepsSceneDocumentCommandsBlocked) {
|
||||
const fs::path savedScenePath = m_projectRoot / "Assets" / "Scenes" / "PlayModeRuntimeEditing.xc";
|
||||
ASSERT_TRUE(m_context.GetSceneManager().SaveSceneAs(savedScenePath.string()));
|
||||
|
||||
@@ -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<PlayModePausedEvent>(
|
||||
[&](const PlayModePausedEvent&) {
|
||||
++pausedCount;
|
||||
});
|
||||
const uint64_t resumedSubscription = m_context.GetEventBus().Subscribe<PlayModeResumedEvent>(
|
||||
[&](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<PlayModePausedEvent>(pausedSubscription);
|
||||
m_context.GetEventBus().Unsubscribe<PlayModeResumedEvent>(resumedSubscription);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
} // namespace XCEngine::Editor
|
||||
|
||||
Reference in New Issue
Block a user