feat: add runtime play tick and play-mode scene editing semantics
This commit is contained in:
@@ -5,6 +5,7 @@ project(XCEngine_SceneTests)
|
||||
set(SCENE_TEST_SOURCES
|
||||
test_scene.cpp
|
||||
test_scene_runtime.cpp
|
||||
test_runtime_loop.cpp
|
||||
test_scene_manager.cpp
|
||||
)
|
||||
|
||||
|
||||
134
tests/Scene/test_runtime_loop.cpp
Normal file
134
tests/Scene/test_runtime_loop.cpp
Normal file
@@ -0,0 +1,134 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <XCEngine/Components/Component.h>
|
||||
#include <XCEngine/Components/GameObject.h>
|
||||
#include <XCEngine/Scene/RuntimeLoop.h>
|
||||
#include <XCEngine/Scene/Scene.h>
|
||||
|
||||
#include <memory>
|
||||
#include <string>
|
||||
|
||||
using namespace XCEngine::Components;
|
||||
|
||||
namespace {
|
||||
|
||||
struct RuntimeLoopCounters {
|
||||
int startCount = 0;
|
||||
int fixedUpdateCount = 0;
|
||||
int updateCount = 0;
|
||||
int lateUpdateCount = 0;
|
||||
};
|
||||
|
||||
class RuntimeLoopObserverComponent : public Component {
|
||||
public:
|
||||
explicit RuntimeLoopObserverComponent(RuntimeLoopCounters* counters)
|
||||
: m_counters(counters) {
|
||||
}
|
||||
|
||||
std::string GetName() const override {
|
||||
return "RuntimeLoopObserver";
|
||||
}
|
||||
|
||||
void Start() override {
|
||||
if (m_counters) {
|
||||
++m_counters->startCount;
|
||||
}
|
||||
}
|
||||
|
||||
void FixedUpdate() override {
|
||||
if (m_counters) {
|
||||
++m_counters->fixedUpdateCount;
|
||||
}
|
||||
}
|
||||
|
||||
void Update(float deltaTime) override {
|
||||
(void)deltaTime;
|
||||
if (m_counters) {
|
||||
++m_counters->updateCount;
|
||||
}
|
||||
}
|
||||
|
||||
void LateUpdate(float deltaTime) override {
|
||||
(void)deltaTime;
|
||||
if (m_counters) {
|
||||
++m_counters->lateUpdateCount;
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
RuntimeLoopCounters* m_counters = nullptr;
|
||||
};
|
||||
|
||||
class RuntimeLoopTest : public ::testing::Test {
|
||||
protected:
|
||||
Scene* CreateScene(const std::string& name = "RuntimeLoopScene") {
|
||||
m_scene = std::make_unique<Scene>(name);
|
||||
return m_scene.get();
|
||||
}
|
||||
|
||||
RuntimeLoopCounters counters;
|
||||
std::unique_ptr<Scene> m_scene;
|
||||
};
|
||||
|
||||
TEST_F(RuntimeLoopTest, AccumulatesFixedUpdatesAcrossFramesAndRunsVariableUpdatesEveryTick) {
|
||||
RuntimeLoop loop({0.02f, 0.1f, 4});
|
||||
Scene* scene = CreateScene();
|
||||
GameObject* host = scene->CreateGameObject("Host");
|
||||
host->AddComponent<RuntimeLoopObserverComponent>(&counters);
|
||||
|
||||
loop.Start(scene);
|
||||
|
||||
loop.Tick(0.01f);
|
||||
EXPECT_EQ(counters.fixedUpdateCount, 0);
|
||||
EXPECT_EQ(counters.startCount, 1);
|
||||
EXPECT_EQ(counters.updateCount, 1);
|
||||
EXPECT_EQ(counters.lateUpdateCount, 1);
|
||||
|
||||
loop.Tick(0.01f);
|
||||
EXPECT_EQ(counters.fixedUpdateCount, 1);
|
||||
EXPECT_EQ(counters.startCount, 1);
|
||||
EXPECT_EQ(counters.updateCount, 2);
|
||||
EXPECT_EQ(counters.lateUpdateCount, 2);
|
||||
}
|
||||
|
||||
TEST_F(RuntimeLoopTest, ClampAndFixedStepLimitPreventExcessiveCatchUp) {
|
||||
RuntimeLoop loop({0.02f, 0.05f, 2});
|
||||
Scene* scene = CreateScene();
|
||||
GameObject* host = scene->CreateGameObject("Host");
|
||||
host->AddComponent<RuntimeLoopObserverComponent>(&counters);
|
||||
|
||||
loop.Start(scene);
|
||||
loop.Tick(1.0f);
|
||||
|
||||
EXPECT_EQ(counters.fixedUpdateCount, 2);
|
||||
EXPECT_EQ(counters.startCount, 1);
|
||||
EXPECT_EQ(counters.updateCount, 1);
|
||||
EXPECT_EQ(counters.lateUpdateCount, 1);
|
||||
EXPECT_NEAR(loop.GetFixedAccumulator(), 0.01f, 1e-4f);
|
||||
}
|
||||
|
||||
TEST_F(RuntimeLoopTest, PauseSkipsAutomaticTicksUntilStepFrameIsRequested) {
|
||||
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.fixedUpdateCount, 0);
|
||||
EXPECT_EQ(counters.startCount, 0);
|
||||
EXPECT_EQ(counters.updateCount, 0);
|
||||
EXPECT_EQ(counters.lateUpdateCount, 0);
|
||||
|
||||
loop.StepFrame();
|
||||
loop.Tick(0.025f);
|
||||
EXPECT_EQ(counters.fixedUpdateCount, 1);
|
||||
EXPECT_EQ(counters.startCount, 1);
|
||||
EXPECT_EQ(counters.updateCount, 1);
|
||||
EXPECT_EQ(counters.lateUpdateCount, 1);
|
||||
EXPECT_TRUE(loop.IsPaused());
|
||||
}
|
||||
|
||||
} // namespace
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
83
tests/editor/test_play_session_controller.cpp
Normal file
83
tests/editor/test_play_session_controller.cpp
Normal 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
|
||||
Reference in New Issue
Block a user