feat: add runtime play tick and play-mode scene editing semantics

This commit is contained in:
2026-04-02 19:37:35 +08:00
parent e30f5d5ffa
commit fb15d60be9
28 changed files with 2016 additions and 45 deletions

View File

@@ -4,6 +4,7 @@ project(XCEngine_EditorTests)
set(EDITOR_TEST_SOURCES
test_action_routing.cpp
test_play_session_controller.cpp
test_scene_viewport_camera_controller.cpp
test_scene_viewport_move_gizmo.cpp
test_scene_viewport_rotate_gizmo.cpp
@@ -16,6 +17,7 @@ set(EDITOR_TEST_SOURCES
test_viewport_render_flow_utils.cpp
test_builtin_icon_layout_utils.cpp
${CMAKE_SOURCE_DIR}/editor/src/Core/UndoManager.cpp
${CMAKE_SOURCE_DIR}/editor/src/Core/PlaySessionController.cpp
${CMAKE_SOURCE_DIR}/editor/src/Managers/SceneManager.cpp
${CMAKE_SOURCE_DIR}/editor/src/Managers/ProjectManager.cpp
${CMAKE_SOURCE_DIR}/editor/src/Viewport/SceneViewportPicker.cpp
@@ -30,6 +32,7 @@ if(MSVC)
set_target_properties(editor_tests PROPERTIES
LINK_FLAGS "/NODEFAULTLIB:libcpmt.lib /NODEFAULTLIB:libcmt.lib"
)
target_compile_options(editor_tests PRIVATE /FS)
endif()
target_link_libraries(editor_tests PRIVATE

View File

@@ -7,6 +7,7 @@
#include "Commands/EntityCommands.h"
#include "Commands/SceneCommands.h"
#include "Core/EditorContext.h"
#include "Core/PlaySessionController.h"
#include <XCEngine/Core/Math/Quaternion.h>
#include <XCEngine/Core/Math/Vector3.h>
@@ -283,6 +284,45 @@ TEST_F(EditorActionRoutingTest, MainMenuRouterRequestsExitResetAndAboutPopup) {
m_context.GetEventBus().Unsubscribe<DockLayoutResetRequestedEvent>(resetSubscription);
}
TEST_F(EditorActionRoutingTest, PlayModeAllowsRuntimeSceneUndoRedoButKeepsSceneDocumentCommandsBlocked) {
const fs::path savedScenePath = m_projectRoot / "Assets" / "Scenes" / "PlayModeRuntimeEditing.xc";
ASSERT_TRUE(m_context.GetSceneManager().SaveSceneAs(savedScenePath.string()));
ASSERT_FALSE(m_context.GetSceneManager().IsSceneDirty());
PlaySessionController controller;
ASSERT_TRUE(controller.StartPlay(m_context));
EXPECT_EQ(m_context.GetRuntimeMode(), EditorRuntimeMode::Play);
EXPECT_FALSE(m_context.GetSceneManager().IsSceneDocumentDirtyTrackingEnabled());
EXPECT_FALSE(Commands::NewScene(m_context, "Blocked During Play"));
EXPECT_FALSE(Commands::SaveCurrentScene(m_context));
const size_t entityCountBeforeCreate = CountHierarchyEntities(m_context.GetSceneManager());
auto* runtimeEntity = Commands::CreateEmptyEntity(m_context, nullptr, "Create Runtime Entity", "RuntimeOnly");
ASSERT_NE(runtimeEntity, nullptr);
const uint64_t runtimeEntityId = runtimeEntity->GetID();
EXPECT_EQ(CountHierarchyEntities(m_context.GetSceneManager()), entityCountBeforeCreate + 1);
EXPECT_FALSE(m_context.GetSceneManager().IsSceneDirty());
const Actions::ActionBinding undoAction = Actions::MakeUndoAction(m_context);
EXPECT_TRUE(undoAction.enabled);
Actions::ExecuteUndo(m_context);
EXPECT_EQ(CountHierarchyEntities(m_context.GetSceneManager()), entityCountBeforeCreate);
EXPECT_EQ(m_context.GetSceneManager().GetEntity(runtimeEntityId), nullptr);
EXPECT_FALSE(m_context.GetSceneManager().IsSceneDirty());
const Actions::ActionBinding redoAction = Actions::MakeRedoAction(m_context);
EXPECT_TRUE(redoAction.enabled);
Actions::ExecuteRedo(m_context);
EXPECT_EQ(CountHierarchyEntities(m_context.GetSceneManager()), entityCountBeforeCreate + 1);
EXPECT_FALSE(m_context.GetSceneManager().IsSceneDirty());
ASSERT_TRUE(controller.StopPlay(m_context));
EXPECT_EQ(m_context.GetRuntimeMode(), EditorRuntimeMode::Edit);
EXPECT_TRUE(m_context.GetSceneManager().IsSceneDocumentDirtyTrackingEnabled());
EXPECT_FALSE(m_context.GetSceneManager().IsSceneDirty());
}
TEST_F(EditorActionRoutingTest, HierarchyRouterRenameHelpersPublishAndCommit) {
auto* entity = Commands::CreateEmptyEntity(m_context, nullptr, "Create Entity", "BeforeRename");
ASSERT_NE(entity, nullptr);

View File

@@ -0,0 +1,83 @@
#include <gtest/gtest.h>
#include "Core/EditorContext.h"
#include "Core/EditorEvents.h"
#include "Core/PlaySessionController.h"
#include <XCEngine/Core/Math/Vector3.h>
namespace XCEngine::Editor {
namespace {
class PlaySessionControllerTest : public ::testing::Test {
protected:
void SetUp() override {
m_context.GetSceneManager().NewScene("Play Session Scene");
}
EditorContext m_context;
PlaySessionController m_controller;
};
TEST_F(PlaySessionControllerTest, StartPlayClonesCurrentSceneAndStopRestoresEditorScene) {
auto* editorEntity = m_context.GetSceneManager().CreateEntity("Persistent");
ASSERT_NE(editorEntity, nullptr);
const uint64_t editorEntityId = editorEntity->GetID();
editorEntity->GetTransform()->SetLocalPosition(Math::Vector3(1.0f, 2.0f, 3.0f));
int startedCount = 0;
int stoppedCount = 0;
const uint64_t startedSubscription = m_context.GetEventBus().Subscribe<PlayModeStartedEvent>(
[&](const PlayModeStartedEvent&) {
++startedCount;
});
const uint64_t stoppedSubscription = m_context.GetEventBus().Subscribe<PlayModeStoppedEvent>(
[&](const PlayModeStoppedEvent&) {
++stoppedCount;
});
ASSERT_TRUE(m_controller.StartPlay(m_context));
EXPECT_EQ(m_context.GetRuntimeMode(), EditorRuntimeMode::Play);
EXPECT_EQ(startedCount, 1);
auto* runtimeEntity = m_context.GetSceneManager().GetEntity(editorEntityId);
ASSERT_NE(runtimeEntity, nullptr);
runtimeEntity->GetTransform()->SetLocalPosition(Math::Vector3(8.0f, 9.0f, 10.0f));
auto* runtimeOnlyEntity = m_context.GetSceneManager().CreateEntity("RuntimeOnly");
ASSERT_NE(runtimeOnlyEntity, nullptr);
const uint64_t runtimeOnlyId = runtimeOnlyEntity->GetID();
ASSERT_TRUE(m_controller.StopPlay(m_context));
EXPECT_EQ(m_context.GetRuntimeMode(), EditorRuntimeMode::Edit);
EXPECT_EQ(stoppedCount, 1);
auto* restoredEntity = m_context.GetSceneManager().GetEntity(editorEntityId);
ASSERT_NE(restoredEntity, nullptr);
EXPECT_EQ(m_context.GetSceneManager().GetEntity(runtimeOnlyId), nullptr);
const Math::Vector3 restoredPosition = restoredEntity->GetTransform()->GetLocalPosition();
EXPECT_NEAR(restoredPosition.x, 1.0f, 1e-4f);
EXPECT_NEAR(restoredPosition.y, 2.0f, 1e-4f);
EXPECT_NEAR(restoredPosition.z, 3.0f, 1e-4f);
m_context.GetEventBus().Unsubscribe<PlayModeStartedEvent>(startedSubscription);
m_context.GetEventBus().Unsubscribe<PlayModeStoppedEvent>(stoppedSubscription);
}
TEST_F(PlaySessionControllerTest, StartAndStopRequestsRouteThroughEventBus) {
auto* editorEntity = m_context.GetSceneManager().CreateEntity("Persistent");
ASSERT_NE(editorEntity, nullptr);
m_controller.Attach(m_context);
m_context.GetEventBus().Publish(PlayModeStartRequestedEvent{});
EXPECT_EQ(m_context.GetRuntimeMode(), EditorRuntimeMode::Play);
m_context.GetEventBus().Publish(PlayModeStopRequestedEvent{});
EXPECT_EQ(m_context.GetRuntimeMode(), EditorRuntimeMode::Edit);
m_controller.Detach(m_context);
}
} // namespace
} // namespace XCEngine::Editor