From dde2c7daf93e63b0a3c0bb5672e830ea9e11d1bb Mon Sep 17 00:00:00 2001 From: Bram Verhulst Date: Thu, 22 Oct 2026 18:10:23 +0200 Subject: [PATCH] feat: add pauzing of the running game --- destrum/include/destrum/App.h | 5 +- destrum/include/destrum/Scene/SceneManager.h | 6 +++ destrum/src/App.cpp | 50 ++++++++++++++++++-- destrum/src/Scene/SceneManager.cpp | 6 +-- lightkeeper/src/Lightkeeper.cpp | 4 +- tests/core_tests.cpp | 8 ++++ 6 files changed, 69 insertions(+), 10 deletions(-) diff --git a/destrum/include/destrum/App.h b/destrum/include/destrum/App.h index d1158f8..7cc0660 100644 --- a/destrum/include/destrum/App.h +++ b/destrum/include/destrum/App.h @@ -55,7 +55,9 @@ public: virtual void onPlayModeChanged(bool) {} [[nodiscard]] bool isPlaying() const { return playing; } + [[nodiscard]] bool isPaused() const { return paused; } void requestPlayModeToggle() { playModeToggleRequested = true; } + void setPaused(bool pause); virtual void customWindowsMenu() {} @@ -75,7 +77,6 @@ protected: bool cleanedUp{false}; bool cleaningUp{false}; bool customInitStarted{false}; - bool gamePaused{false}; bool frameLimit{false}; float frameTime{0.f}; @@ -110,6 +111,8 @@ private: void exitPlayMode(); bool playing{true}; + bool paused{false}; + bool mouseCapturedBeforePause{false}; bool playModeToggleRequested{false}; std::optional editSnapshot; std::weak_ptr editScene; diff --git a/destrum/include/destrum/Scene/SceneManager.h b/destrum/include/destrum/Scene/SceneManager.h index 44340f9..2123f13 100644 --- a/destrum/include/destrum/Scene/SceneManager.h +++ b/destrum/include/destrum/Scene/SceneManager.h @@ -20,6 +20,8 @@ public: void SetPlaying(bool playing) { m_playing = playing; } [[nodiscard]] bool IsPlaying() const { return m_playing; } + void SetPaused(bool paused) { m_paused = paused; } + [[nodiscard]] bool IsPaused() const { return m_paused; } void Update(float dt); void FixedUpdate(float dt); @@ -55,7 +57,11 @@ private: std::vector> m_activeScenes; std::vector> m_scenes; + + [[nodiscard]] bool IsTicking() const { return m_playing && !m_paused; } + bool m_playing{true}; + bool m_paused{false}; }; #endif //SCENEMANAGER_H diff --git a/destrum/src/App.cpp b/destrum/src/App.cpp index abb5365..1dcd729 100644 --- a/destrum/src/App.cpp +++ b/destrum/src/App.cpp @@ -188,6 +188,10 @@ if (!isRunning) break; { playModeToggleRequested = true; } + if (playing && InputManager::GetInstance().WasKeyPressed(SDL_SCANCODE_F7)) + { + setPaused(!paused); + } // Consume SDL events before updating the base camera so held and // newly pressed inputs are applied in the same frame. @@ -431,21 +435,57 @@ void App::drawWindowsMenu() void App::drawPlayControls() { - const char* state = playing ? "PLAYING" : "EDIT"; + const char* state = !playing ? "EDIT" : paused ? "PAUSED" : "PLAYING"; const char* action = playing ? "Stop (F5)" : "Play (F5)"; + const char* pauseAction = paused ? "Resume (F7)" : "Pause (F7)"; const ImGuiStyle& style = ImGui::GetStyle(); - const float width = ImGui::CalcTextSize(state).x + ImGui::CalcTextSize(action).x + - style.ItemSpacing.x * 2.0f + style.FramePadding.x * 2.0f; + float width = ImGui::CalcTextSize(state).x + ImGui::CalcTextSize(action).x + + style.ItemSpacing.x * 2.0f + style.FramePadding.x * 2.0f; + if (playing) + { + width += ImGui::CalcTextSize(pauseAction).x + style.ItemSpacing.x + style.FramePadding.x * 2.0f; + } // Centered like Godot's run buttons, unless the menus already reach past the middle ImGui::SetCursorPosX(std::max(ImGui::GetCursorPosX(), (ImGui::GetWindowWidth() - width) * 0.5f)); - ImGui::TextColored(playing ? ImVec4{0.4f, 0.9f, 0.4f, 1.0f} : ImVec4{0.7f, 0.7f, 0.7f, 1.0f}, "%s", state); + const ImVec4 stateColor = !playing ? ImVec4{0.7f, 0.7f, 0.7f, 1.0f} + : paused ? ImVec4{0.95f, 0.75f, 0.3f, 1.0f} + : ImVec4{0.4f, 0.9f, 0.4f, 1.0f}; + ImGui::TextColored(stateColor, "%s", state); + if (playing && ImGui::MenuItem(pauseAction)) + { + setPaused(!paused); + } if (ImGui::MenuItem(action)) { playModeToggleRequested = true; } } +void App::setPaused(bool pause) +{ + if (!playing || pause == paused) + { + return; + } + + paused = pause; + SceneManager::GetInstance().SetPaused(paused); + + // Hand the mouse to the editor while paused, give it back to the game on resume + auto& input = InputManager::GetInstance(); + if (paused) + { + mouseCapturedBeforePause = input.IsMouseCaptured(); + input.SetMouseCaptured(false); + } + else + { + input.SetMouseCaptured(mouseCapturedBeforePause); + } + spdlog::info(paused ? "Paused play mode" : "Resumed play mode"); +} + void App::applyPlayModeToggle() { if (playing) @@ -500,7 +540,9 @@ void App::exitPlayMode() { auto& sceneManager = SceneManager::GetInstance(); playing = false; + paused = false; sceneManager.SetPlaying(false); + sceneManager.SetPaused(false); InputManager::GetInstance().SetMouseCaptured(false); const std::shared_ptr scene = editScene.lock(); diff --git a/destrum/src/Scene/SceneManager.cpp b/destrum/src/Scene/SceneManager.cpp index 1940167..cb81ef2 100644 --- a/destrum/src/Scene/SceneManager.cpp +++ b/destrum/src/Scene/SceneManager.cpp @@ -16,7 +16,7 @@ Scene& SceneManager::GetCurrentScene() const { void SceneManager::Update(float dt) { for (const auto& scene : m_activeScenes) { - if (m_playing) { + if (IsTicking()) { scene->Update(dt); } else { scene->CommitPendingAdditions(); @@ -25,7 +25,7 @@ void SceneManager::Update(float dt) { } void SceneManager::FixedUpdate(float dt) { - if (!m_playing) { + if (!IsTicking()) { return; } for (const auto& scene : m_activeScenes) { @@ -34,7 +34,7 @@ void SceneManager::FixedUpdate(float dt) { } void SceneManager::LateUpdate(float dt) { - if (!m_playing) { + if (!IsTicking()) { return; } for (const auto& scene : m_activeScenes) { diff --git a/lightkeeper/src/Lightkeeper.cpp b/lightkeeper/src/Lightkeeper.cpp index e6c6e94..f9a82f9 100644 --- a/lightkeeper/src/Lightkeeper.cpp +++ b/lightkeeper/src/Lightkeeper.cpp @@ -83,7 +83,7 @@ void LightKeeper::updateViewMode() auto& input = InputManager::GetInstance(); FirstPersonController* player = mainScene.playerController; - if (isPlaying() && player != nullptr && input.WasKeyPressed(SDL_SCANCODE_F6)) + if (isPlaying() && !isPaused() && player != nullptr && input.WasKeyPressed(SDL_SCANCODE_F6)) { player->SetPossessed(!player->IsPossessed()); } @@ -100,7 +100,7 @@ void LightKeeper::updateViewMode() bool LightKeeper::isPlayerViewActive() const { - return isPlaying() && mainScene.playerController != nullptr && mainScene.playerController->IsPossessed(); + return isPlaying() && !isPaused() && mainScene.playerController != nullptr && mainScene.playerController->IsPossessed(); } CameraComponent* LightKeeper::getSceneCamera() const diff --git a/tests/core_tests.cpp b/tests/core_tests.cpp index 6d99721..760082f 100644 --- a/tests/core_tests.cpp +++ b/tests/core_tests.cpp @@ -304,6 +304,14 @@ namespace { check(counter->updates == 1 && counter->fixedUpdates == 1 && counter->lateUpdates == 1, "play mode must tick components"); + sceneManager.SetPaused(true); + sceneManager.Update(0.1f); + sceneManager.FixedUpdate(0.1f); + sceneManager.LateUpdate(0.1f); + sceneManager.SetPaused(false); + check(counter->updates == 1 && counter->fixedUpdates == 1 && counter->lateUpdates == 1, + "a paused game must not tick components"); + sceneManager.Destroy(); } }