#include #include #include #include #include #include #include #include using namespace XCEngine::Components; using namespace XCEngine::Scripting; namespace { std::string LifecycleMethodToString(ScriptLifecycleMethod method) { switch (method) { case ScriptLifecycleMethod::Awake: return "Awake"; case ScriptLifecycleMethod::OnEnable: return "OnEnable"; case ScriptLifecycleMethod::Start: return "Start"; case ScriptLifecycleMethod::FixedUpdate: return "FixedUpdate"; case ScriptLifecycleMethod::Update: return "Update"; case ScriptLifecycleMethod::LateUpdate: return "LateUpdate"; case ScriptLifecycleMethod::OnDisable: return "OnDisable"; case ScriptLifecycleMethod::OnDestroy: return "OnDestroy"; } return "Unknown"; } class OrderedObserverComponent : public Component { public: explicit OrderedObserverComponent(std::vector* events) : m_events(events) { } std::string GetName() const override { return "OrderedObserver"; } void Start() override { if (m_events) { m_events->push_back("NativeStart:" + GetGameObject()->GetName()); } } void FixedUpdate() override { if (m_events) { m_events->push_back("NativeFixedUpdate:" + GetGameObject()->GetName()); } } void Update(float deltaTime) override { (void)deltaTime; if (m_events) { m_events->push_back("NativeUpdate:" + GetGameObject()->GetName()); } } void LateUpdate(float deltaTime) override { (void)deltaTime; if (m_events) { m_events->push_back("NativeLateUpdate:" + GetGameObject()->GetName()); } } private: std::vector* m_events = nullptr; }; class RecordingScriptRuntime : public IScriptRuntime { public: explicit RecordingScriptRuntime(std::vector* events) : m_events(events) { } void OnRuntimeStart(Scene* scene) override { if (m_events) { m_events->push_back("RuntimeStart:" + (scene ? scene->GetName() : std::string("null"))); } } void OnRuntimeStop(Scene* scene) override { if (m_events) { m_events->push_back("RuntimeStop:" + (scene ? scene->GetName() : std::string("null"))); } } bool CreateScriptInstance(const ScriptRuntimeContext& context) override { if (m_events) { m_events->push_back("Create:" + Describe(context)); } return true; } void DestroyScriptInstance(const ScriptRuntimeContext& context) override { if (m_events) { m_events->push_back("Destroy:" + Describe(context)); } } void InvokeMethod( const ScriptRuntimeContext& context, ScriptLifecycleMethod method, float deltaTime) override { (void)deltaTime; if (m_events) { m_events->push_back(LifecycleMethodToString(method) + ":" + Describe(context)); } } private: static std::string Describe(const ScriptRuntimeContext& context) { const std::string gameObjectName = context.gameObject ? context.gameObject->GetName() : "null"; const std::string className = context.component ? context.component->GetFullClassName() : "null"; return gameObjectName + ":" + className; } std::vector* m_events = nullptr; }; class SceneRuntimeTest : public ::testing::Test { protected: void SetUp() override { scriptEngine = &ScriptEngine::Get(); scriptEngine->OnRuntimeStop(); runtimeImpl = std::make_unique(&events); scriptEngine->SetRuntime(runtimeImpl.get()); } void TearDown() override { runtime.Stop(); scriptEngine->OnRuntimeStop(); scriptEngine->SetRuntime(nullptr); runtimeImpl.reset(); scene.reset(); } Scene* CreateScene(const std::string& sceneName) { scene = std::make_unique(sceneName); return scene.get(); } ScriptComponent* AddScript(GameObject* gameObject, const std::string& namespaceName, const std::string& className) { ScriptComponent* component = gameObject->AddComponent(); component->SetScriptClass("GameScripts", namespaceName, className); return component; } std::vector events; std::unique_ptr scene; std::unique_ptr runtimeImpl; ScriptEngine* scriptEngine = nullptr; SceneRuntime runtime; }; TEST_F(SceneRuntimeTest, StartAndStopForwardToScriptEngine) { Scene* runtimeScene = CreateScene("RuntimeScene"); GameObject* host = runtimeScene->CreateGameObject("Host"); AddScript(host, "Gameplay", "Bootstrap"); runtime.Start(runtimeScene); runtime.Stop(); const std::vector expected = { "RuntimeStart:RuntimeScene", "Create:Host:Gameplay.Bootstrap", "Awake:Host:Gameplay.Bootstrap", "OnEnable:Host:Gameplay.Bootstrap", "OnDisable:Host:Gameplay.Bootstrap", "OnDestroy:Host:Gameplay.Bootstrap", "Destroy:Host:Gameplay.Bootstrap", "RuntimeStop:RuntimeScene" }; EXPECT_EQ(events, expected); EXPECT_FALSE(runtime.IsRunning()); EXPECT_EQ(runtime.GetScene(), nullptr); } TEST_F(SceneRuntimeTest, FrameOrderRunsScriptLifecycleBeforeNativeComponents) { Scene* runtimeScene = CreateScene("RuntimeScene"); GameObject* host = runtimeScene->CreateGameObject("Host"); host->AddComponent(&events); AddScript(host, "Gameplay", "Mover"); runtime.Start(runtimeScene); events.clear(); runtime.FixedUpdate(0.02f); runtime.Update(0.016f); runtime.LateUpdate(0.016f); const std::vector expected = { "FixedUpdate:Host:Gameplay.Mover", "NativeFixedUpdate:Host", "Start:Host:Gameplay.Mover", "Update:Host:Gameplay.Mover", "NativeStart:Host", "NativeUpdate:Host", "LateUpdate:Host:Gameplay.Mover", "NativeLateUpdate:Host" }; EXPECT_EQ(events, expected); } TEST_F(SceneRuntimeTest, InactiveSceneSkipsFrameExecution) { Scene* runtimeScene = CreateScene("RuntimeScene"); GameObject* host = runtimeScene->CreateGameObject("Host"); host->AddComponent(&events); AddScript(host, "Gameplay", "PausedScript"); runtimeScene->SetActive(false); runtime.Start(runtimeScene); events.clear(); runtime.FixedUpdate(0.02f); runtime.Update(0.016f); runtime.LateUpdate(0.016f); EXPECT_TRUE(events.empty()); } TEST_F(SceneRuntimeTest, StartingNewSceneStopsPreviousRuntimeFirst) { Scene* firstScene = CreateScene("FirstScene"); GameObject* firstHost = firstScene->CreateGameObject("FirstHost"); AddScript(firstHost, "Gameplay", "FirstScript"); runtime.Start(firstScene); std::unique_ptr secondScene = std::make_unique("SecondScene"); GameObject* secondHost = secondScene->CreateGameObject("SecondHost"); ScriptComponent* secondScript = AddScript(secondHost, "Gameplay", "SecondScript"); events.clear(); runtime.Start(secondScene.get()); const std::vector expected = { "OnDisable:FirstHost:Gameplay.FirstScript", "OnDestroy:FirstHost:Gameplay.FirstScript", "Destroy:FirstHost:Gameplay.FirstScript", "RuntimeStop:FirstScene", "RuntimeStart:SecondScene", "Create:SecondHost:Gameplay.SecondScript", "Awake:SecondHost:Gameplay.SecondScript", "OnEnable:SecondHost:Gameplay.SecondScript" }; EXPECT_EQ(events, expected); EXPECT_EQ(runtime.GetScene(), secondScene.get()); EXPECT_TRUE(scriptEngine->HasRuntimeInstance(secondScript)); runtime.Stop(); } } // namespace