diff --git a/editor/src/Core/PlaySessionController.cpp b/editor/src/Core/PlaySessionController.cpp index 4bc97ed0..454293a1 100644 --- a/editor/src/Core/PlaySessionController.cpp +++ b/editor/src/Core/PlaySessionController.cpp @@ -72,6 +72,7 @@ void PlaySessionController::Attach(IEditorContext& context) { void PlaySessionController::Detach(IEditorContext& context) { StopPlay(context); + ClearRuntimeSceneSyncSubscription(context); if (m_playStartRequestedHandlerId != 0) { context.GetEventBus().Unsubscribe(m_playStartRequestedHandlerId); @@ -141,6 +142,7 @@ bool PlaySessionController::StartPlay(IEditorContext& context) { XCEngine::Input::InputManager::Get().Shutdown(); XCEngine::Input::InputManager::Get().Initialize(nullptr); m_runtimeLoop.Start(sceneManager.GetScene()); + EnsureRuntimeSceneSyncSubscription(context); context.GetUndoManager().ClearHistory(); context.SetRuntimeMode(EditorRuntimeMode::Play); context.GetEventBus().Publish(PlayModeStartedEvent{}); @@ -152,6 +154,7 @@ bool PlaySessionController::StopPlay(IEditorContext& context) { return false; } + ClearRuntimeSceneSyncSubscription(context); auto& sceneManager = context.GetSceneManager(); m_runtimeLoop.Stop(); ResetRuntimeInputBridge(); @@ -206,6 +209,39 @@ void PlaySessionController::ResetRuntimeInputBridge() { m_hasPendingGameViewInput = false; } +void PlaySessionController::EnsureRuntimeSceneSyncSubscription(IEditorContext& context) { + if (m_runtimeSceneChangedHandlerId != 0) { + return; + } + + m_runtimeSceneChangedHandlerId = context.GetEventBus().Subscribe( + [this, &context](const SceneChangedEvent&) { + SyncRuntimeSceneIfNeeded(context); + }); +} + +void PlaySessionController::ClearRuntimeSceneSyncSubscription(IEditorContext& context) { + if (m_runtimeSceneChangedHandlerId == 0) { + return; + } + + context.GetEventBus().Unsubscribe(m_runtimeSceneChangedHandlerId); + m_runtimeSceneChangedHandlerId = 0; +} + +void PlaySessionController::SyncRuntimeSceneIfNeeded(IEditorContext& context) { + if (!IsEditorRuntimeActive(context.GetRuntimeMode())) { + return; + } + + auto* currentScene = context.GetSceneManager().GetScene(); + if (currentScene == m_runtimeLoop.GetScene()) { + return; + } + + m_runtimeLoop.ReplaceScene(currentScene); +} + void PlaySessionController::ApplyGameViewInputFrame(float deltaTime) { using XCEngine::Input::InputManager; using XCEngine::Input::KeyCode; diff --git a/editor/src/Core/PlaySessionController.h b/editor/src/Core/PlaySessionController.h index b986b7ca..ced093ad 100644 --- a/editor/src/Core/PlaySessionController.h +++ b/editor/src/Core/PlaySessionController.h @@ -28,9 +28,14 @@ public: bool ResumePlay(IEditorContext& context); bool StepPlay(IEditorContext& context); + ::XCEngine::Components::Scene* GetRuntimeScene() const { return m_runtimeLoop.GetScene(); } + private: void ResetRuntimeInputBridge(); void ApplyGameViewInputFrame(float deltaTime); + void EnsureRuntimeSceneSyncSubscription(IEditorContext& context); + void ClearRuntimeSceneSyncSubscription(IEditorContext& context); + void SyncRuntimeSceneIfNeeded(IEditorContext& context); uint64_t m_playStartRequestedHandlerId = 0; uint64_t m_playStopRequestedHandlerId = 0; @@ -38,6 +43,7 @@ private: uint64_t m_playResumeRequestedHandlerId = 0; uint64_t m_playStepRequestedHandlerId = 0; uint64_t m_gameViewInputFrameHandlerId = 0; + uint64_t m_runtimeSceneChangedHandlerId = 0; SceneSnapshot m_editorSnapshot = {}; GameViewInputFrameEvent m_pendingGameViewInput = {}; GameViewInputFrameEvent m_appliedGameViewInput = {}; diff --git a/engine/include/XCEngine/Scene/RuntimeLoop.h b/engine/include/XCEngine/Scene/RuntimeLoop.h index 6f8fd387..20a6f88c 100644 --- a/engine/include/XCEngine/Scene/RuntimeLoop.h +++ b/engine/include/XCEngine/Scene/RuntimeLoop.h @@ -22,6 +22,7 @@ public: void Start(Scene* scene); void Stop(); + void ReplaceScene(Scene* scene); void Tick(float deltaTime); void Pause(); diff --git a/engine/include/XCEngine/Scene/SceneRuntime.h b/engine/include/XCEngine/Scene/SceneRuntime.h index a5c04740..526b2f8c 100644 --- a/engine/include/XCEngine/Scene/SceneRuntime.h +++ b/engine/include/XCEngine/Scene/SceneRuntime.h @@ -34,6 +34,7 @@ public: void Start(Scene* scene); void Stop(); + void ReplaceScene(Scene* scene); void FixedUpdate(float fixedDeltaTime); void Update(float deltaTime); diff --git a/engine/include/XCEngine/Scripting/ScriptEngine.h b/engine/include/XCEngine/Scripting/ScriptEngine.h index 4ed8b2be..488bcb19 100644 --- a/engine/include/XCEngine/Scripting/ScriptEngine.h +++ b/engine/include/XCEngine/Scripting/ScriptEngine.h @@ -28,6 +28,7 @@ public: void OnRuntimeStart(Components::Scene* scene); void OnRuntimeStop(); + void OnRuntimeSceneReplaced(Components::Scene* scene); void OnFixedUpdate(float fixedDeltaTime); void OnUpdate(float deltaTime); diff --git a/engine/src/Scene/RuntimeLoop.cpp b/engine/src/Scene/RuntimeLoop.cpp index 95ca960a..81a9b5b4 100644 --- a/engine/src/Scene/RuntimeLoop.cpp +++ b/engine/src/Scene/RuntimeLoop.cpp @@ -44,6 +44,12 @@ void RuntimeLoop::Stop() { m_stepRequested = false; } +void RuntimeLoop::ReplaceScene(Scene* scene) { + m_sceneRuntime.ReplaceScene(scene); + m_fixedAccumulator = 0.0f; + m_stepRequested = false; +} + void RuntimeLoop::Tick(float deltaTime) { if (!IsRunning()) { return; diff --git a/engine/src/Scene/SceneRuntime.cpp b/engine/src/Scene/SceneRuntime.cpp index 774da1cf..b5bf8d13 100644 --- a/engine/src/Scene/SceneRuntime.cpp +++ b/engine/src/Scene/SceneRuntime.cpp @@ -46,6 +46,18 @@ void SceneRuntime::Stop() { m_scene = nullptr; } +void SceneRuntime::ReplaceScene(Scene* scene) { + if (!m_running) { + m_scene = scene; + m_uiRuntime->Reset(); + return; + } + + m_scene = scene; + m_uiRuntime->Reset(); + Scripting::ScriptEngine::Get().OnRuntimeSceneReplaced(scene); +} + void SceneRuntime::FixedUpdate(float fixedDeltaTime) { if (!m_running || !m_scene || !m_scene->IsActive()) { return; diff --git a/engine/src/Scripting/ScriptEngine.cpp b/engine/src/Scripting/ScriptEngine.cpp index 3cd18571..e157d1e7 100644 --- a/engine/src/Scripting/ScriptEngine.cpp +++ b/engine/src/Scripting/ScriptEngine.cpp @@ -141,6 +141,54 @@ void ScriptEngine::OnRuntimeStop() { m_runtime->OnRuntimeStop(stoppedScene); } +void ScriptEngine::OnRuntimeSceneReplaced(Components::Scene* scene) { + if (!m_runtimeRunning) { + m_runtimeScene = nullptr; + m_runtimeSceneCreatedSubscription = 0; + m_scriptStates.clear(); + m_scriptOrder.clear(); + return; + } + + const float configuredFixedDeltaTime = m_runtimeFixedDeltaTime; + m_runtime->OnRuntimeStop(nullptr); + m_runtimeFixedDeltaTime = configuredFixedDeltaTime; + + m_runtimeScene = scene; + m_runtimeSceneCreatedSubscription = 0; + m_scriptStates.clear(); + m_scriptOrder.clear(); + + if (!m_runtimeScene) { + return; + } + + m_runtime->OnRuntimeStart(m_runtimeScene); + m_runtimeSceneCreatedSubscription = m_runtimeScene->OnGameObjectCreated().Subscribe( + [this](Components::GameObject* gameObject) { + HandleGameObjectCreated(gameObject); + }); + + for (Components::GameObject* root : m_runtimeScene->GetRootGameObjects()) { + CollectScriptComponents(root); + } + + const std::vector startupKeys = m_scriptOrder; + for (const ScriptInstanceKey& key : startupKeys) { + auto it = m_scriptStates.find(key); + if (it == m_scriptStates.end()) { + continue; + } + + ScriptInstanceState& state = it->second; + if (!ShouldScriptRun(state)) { + continue; + } + + EnsureScriptReady(state, true); + } +} + void ScriptEngine::OnFixedUpdate(float fixedDeltaTime) { if (!m_runtimeRunning) { return; diff --git a/tests/editor/test_play_session_controller.cpp b/tests/editor/test_play_session_controller.cpp index 99d2f214..e7db9a77 100644 --- a/tests/editor/test_play_session_controller.cpp +++ b/tests/editor/test_play_session_controller.cpp @@ -3,6 +3,7 @@ #include "Core/EditorContext.h" #include "Core/EditorEvents.h" #include "Core/PlaySessionController.h" +#include "Commands/EntityCommands.h" #include #include @@ -236,5 +237,42 @@ TEST_F(PlaySessionControllerTest, GameViewInputFramesDriveAndReleaseRuntimeInput m_controller.Detach(m_context); } +TEST_F(PlaySessionControllerTest, RuntimeSceneUndoRedoRebindsPlaySessionBeforeStop) { + auto* editorEntity = m_context.GetSceneManager().CreateEntity("Persistent"); + ASSERT_NE(editorEntity, nullptr); + const uint64_t editorEntityId = editorEntity->GetID(); + + ASSERT_TRUE(m_controller.StartPlay(m_context)); + ASSERT_EQ(m_context.GetRuntimeMode(), EditorRuntimeMode::Play); + + const auto* runtimeSceneBeforeUndo = m_context.GetSceneManager().GetScene(); + ASSERT_NE(runtimeSceneBeforeUndo, nullptr); + ASSERT_EQ(m_controller.GetRuntimeScene(), runtimeSceneBeforeUndo); + + auto* runtimeEntity = Commands::CreateEmptyEntity(m_context, nullptr, "Create Runtime Entity", "RuntimeOnly"); + ASSERT_NE(runtimeEntity, nullptr); + const uint64_t runtimeEntityId = runtimeEntity->GetID(); + ASSERT_EQ(m_controller.GetRuntimeScene(), m_context.GetSceneManager().GetScene()); + + m_context.GetUndoManager().Undo(); + const auto* runtimeSceneAfterUndo = m_context.GetSceneManager().GetScene(); + ASSERT_NE(runtimeSceneAfterUndo, nullptr); + EXPECT_NE(runtimeSceneAfterUndo, runtimeSceneBeforeUndo); + EXPECT_EQ(m_controller.GetRuntimeScene(), runtimeSceneAfterUndo); + EXPECT_EQ(m_context.GetSceneManager().GetEntity(runtimeEntityId), nullptr); + + m_context.GetUndoManager().Redo(); + const auto* runtimeSceneAfterRedo = m_context.GetSceneManager().GetScene(); + ASSERT_NE(runtimeSceneAfterRedo, nullptr); + EXPECT_NE(runtimeSceneAfterRedo, runtimeSceneAfterUndo); + EXPECT_EQ(m_controller.GetRuntimeScene(), runtimeSceneAfterRedo); + EXPECT_NE(m_context.GetSceneManager().GetEntity(runtimeEntityId), nullptr); + + ASSERT_TRUE(m_controller.StopPlay(m_context)); + EXPECT_EQ(m_context.GetRuntimeMode(), EditorRuntimeMode::Edit); + ASSERT_NE(m_context.GetSceneManager().GetEntity(editorEntityId), nullptr); + EXPECT_EQ(m_context.GetSceneManager().GetEntity(runtimeEntityId), nullptr); +} + } // namespace } // namespace XCEngine::Editor