diff --git a/engine/CMakeLists.txt b/engine/CMakeLists.txt index 65c8debd..741f8a2c 100644 --- a/engine/CMakeLists.txt +++ b/engine/CMakeLists.txt @@ -241,12 +241,17 @@ add_library(XCEngine STATIC ${CMAKE_CURRENT_SOURCE_DIR}/src/Resources/AudioClip/AudioLoader.cpp # Scripting + ${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/Scripting/IScriptRuntime.h + ${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/Scripting/NullScriptRuntime.h ${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/Scripting/ScriptField.h ${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/Scripting/ScriptFieldStorage.h ${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/Scripting/ScriptComponent.h + ${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/Scripting/ScriptEngine.h + ${CMAKE_CURRENT_SOURCE_DIR}/src/Scripting/NullScriptRuntime.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/Scripting/ScriptField.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/Scripting/ScriptFieldStorage.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/Scripting/ScriptComponent.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/src/Scripting/ScriptEngine.cpp # Components ${CMAKE_CURRENT_SOURCE_DIR}/include/XCEngine/Components/Component.h diff --git a/engine/include/XCEngine/Scripting/IScriptRuntime.h b/engine/include/XCEngine/Scripting/IScriptRuntime.h new file mode 100644 index 00000000..e27462dc --- /dev/null +++ b/engine/include/XCEngine/Scripting/IScriptRuntime.h @@ -0,0 +1,52 @@ +#pragma once + +#include + +namespace XCEngine { + +namespace Components { +class GameObject; +class Scene; +} + +namespace Scripting { + +class ScriptComponent; + +enum class ScriptLifecycleMethod { + Awake = 0, + OnEnable, + Start, + FixedUpdate, + Update, + LateUpdate, + OnDisable, + OnDestroy +}; + +struct ScriptRuntimeContext { + Components::Scene* scene = nullptr; + Components::GameObject* gameObject = nullptr; + ScriptComponent* component = nullptr; + uint64_t gameObjectUUID = 0; + uint64_t scriptComponentUUID = 0; +}; + +class IScriptRuntime { +public: + virtual ~IScriptRuntime() = default; + + virtual void OnRuntimeStart(Components::Scene* scene) = 0; + virtual void OnRuntimeStop(Components::Scene* scene) = 0; + + virtual bool CreateScriptInstance(const ScriptRuntimeContext& context) = 0; + virtual void DestroyScriptInstance(const ScriptRuntimeContext& context) = 0; + + virtual void InvokeMethod( + const ScriptRuntimeContext& context, + ScriptLifecycleMethod method, + float deltaTime) = 0; +}; + +} // namespace Scripting +} // namespace XCEngine diff --git a/engine/include/XCEngine/Scripting/NullScriptRuntime.h b/engine/include/XCEngine/Scripting/NullScriptRuntime.h new file mode 100644 index 00000000..07c47ad0 --- /dev/null +++ b/engine/include/XCEngine/Scripting/NullScriptRuntime.h @@ -0,0 +1,23 @@ +#pragma once + +#include + +namespace XCEngine { +namespace Scripting { + +class NullScriptRuntime : public IScriptRuntime { +public: + void OnRuntimeStart(Components::Scene* scene) override; + void OnRuntimeStop(Components::Scene* scene) override; + + bool CreateScriptInstance(const ScriptRuntimeContext& context) override; + void DestroyScriptInstance(const ScriptRuntimeContext& context) override; + + void InvokeMethod( + const ScriptRuntimeContext& context, + ScriptLifecycleMethod method, + float deltaTime) override; +}; + +} // namespace Scripting +} // namespace XCEngine diff --git a/engine/include/XCEngine/Scripting/ScriptComponent.h b/engine/include/XCEngine/Scripting/ScriptComponent.h index 9b5e3c5d..d1ec2f0e 100644 --- a/engine/include/XCEngine/Scripting/ScriptComponent.h +++ b/engine/include/XCEngine/Scripting/ScriptComponent.h @@ -36,6 +36,10 @@ public: const ScriptFieldStorage& GetFieldStorage() const { return m_fieldStorage; } void SetFieldStorage(const ScriptFieldStorage& fieldStorage) { m_fieldStorage = fieldStorage; } + void OnEnable() override; + void OnDisable() override; + void OnDestroy() override; + void Serialize(std::ostream& os) const override; void Deserialize(std::istream& is) override; diff --git a/engine/include/XCEngine/Scripting/ScriptEngine.h b/engine/include/XCEngine/Scripting/ScriptEngine.h new file mode 100644 index 00000000..5143e014 --- /dev/null +++ b/engine/include/XCEngine/Scripting/ScriptEngine.h @@ -0,0 +1,88 @@ +#pragma once + +#include +#include + +#include +#include +#include +#include + +namespace XCEngine { +namespace Scripting { + +class ScriptComponent; + +class ScriptEngine { +public: + static ScriptEngine& Get(); + + void SetRuntime(IScriptRuntime* runtime); + IScriptRuntime* GetRuntime() const { return m_runtime; } + + void OnRuntimeStart(Components::Scene* scene); + void OnRuntimeStop(); + + void OnFixedUpdate(float fixedDeltaTime); + void OnUpdate(float deltaTime); + void OnLateUpdate(float deltaTime); + + void OnScriptComponentEnabled(ScriptComponent* component); + void OnScriptComponentDisabled(ScriptComponent* component); + void OnScriptComponentDestroyed(ScriptComponent* component); + + bool IsRuntimeRunning() const { return m_runtimeRunning; } + Components::Scene* GetRuntimeScene() const { return m_runtimeScene; } + + bool HasTrackedScriptComponent(const ScriptComponent* component) const; + bool HasRuntimeInstance(const ScriptComponent* component) const; + size_t GetTrackedScriptCount() const { return m_scriptOrder.size(); } + +private: + struct ScriptInstanceKey { + uint64_t gameObjectUUID = 0; + uint64_t scriptComponentUUID = 0; + + bool operator==(const ScriptInstanceKey& other) const { + return gameObjectUUID == other.gameObjectUUID + && scriptComponentUUID == other.scriptComponentUUID; + } + }; + + struct ScriptInstanceKeyHasher { + size_t operator()(const ScriptInstanceKey& key) const; + }; + + struct ScriptInstanceState { + ScriptRuntimeContext context; + bool instanceCreated = false; + bool awakeCalled = false; + bool enabled = false; + bool startCalled = false; + bool startPending = false; + }; + + ScriptEngine() = default; + + void CollectScriptComponents(Components::GameObject* gameObject); + ScriptInstanceState* TrackScriptComponent(ScriptComponent* component); + ScriptInstanceState* FindState(const ScriptComponent* component); + const ScriptInstanceState* FindState(const ScriptComponent* component) const; + void RemoveTrackedScriptComponent(const ScriptComponent* component); + + bool ShouldScriptRun(const ScriptInstanceState& state) const; + bool EnsureScriptReady(ScriptInstanceState& state, bool invokeEnableIfNeeded); + void InvokeLifecycleMethod(ScriptInstanceState& state, ScriptLifecycleMethod method, float deltaTime = 0.0f); + void StopTrackingScript(ScriptInstanceState& state, bool runtimeStopping); + + NullScriptRuntime m_nullRuntime; + IScriptRuntime* m_runtime = &m_nullRuntime; + Components::Scene* m_runtimeScene = nullptr; + bool m_runtimeRunning = false; + + std::unordered_map m_scriptStates; + std::vector m_scriptOrder; +}; + +} // namespace Scripting +} // namespace XCEngine diff --git a/engine/src/Scripting/NullScriptRuntime.cpp b/engine/src/Scripting/NullScriptRuntime.cpp new file mode 100644 index 00000000..f946eed5 --- /dev/null +++ b/engine/src/Scripting/NullScriptRuntime.cpp @@ -0,0 +1,32 @@ +#include "Scripting/NullScriptRuntime.h" + +namespace XCEngine { +namespace Scripting { + +void NullScriptRuntime::OnRuntimeStart(Components::Scene* scene) { + (void)scene; +} + +void NullScriptRuntime::OnRuntimeStop(Components::Scene* scene) { + (void)scene; +} + +bool NullScriptRuntime::CreateScriptInstance(const ScriptRuntimeContext& context) { + return context.component != nullptr; +} + +void NullScriptRuntime::DestroyScriptInstance(const ScriptRuntimeContext& context) { + (void)context; +} + +void NullScriptRuntime::InvokeMethod( + const ScriptRuntimeContext& context, + ScriptLifecycleMethod method, + float deltaTime) { + (void)context; + (void)method; + (void)deltaTime; +} + +} // namespace Scripting +} // namespace XCEngine diff --git a/engine/src/Scripting/ScriptComponent.cpp b/engine/src/Scripting/ScriptComponent.cpp index 8c76f9ce..24c2cd2e 100644 --- a/engine/src/Scripting/ScriptComponent.cpp +++ b/engine/src/Scripting/ScriptComponent.cpp @@ -1,5 +1,7 @@ #include "Scripting/ScriptComponent.h" +#include "Scripting/ScriptEngine.h" + #include #include @@ -44,6 +46,18 @@ std::string ScriptComponent::GetFullClassName() const { return m_namespaceName + "." + m_className; } +void ScriptComponent::OnEnable() { + ScriptEngine::Get().OnScriptComponentEnabled(this); +} + +void ScriptComponent::OnDisable() { + ScriptEngine::Get().OnScriptComponentDisabled(this); +} + +void ScriptComponent::OnDestroy() { + ScriptEngine::Get().OnScriptComponentDestroyed(this); +} + void ScriptComponent::Serialize(std::ostream& os) const { os << "scriptComponentUUID=" << m_scriptComponentUUID << ";"; os << "assembly=" << EscapeScriptString(m_assemblyName) << ";"; diff --git a/engine/src/Scripting/ScriptEngine.cpp b/engine/src/Scripting/ScriptEngine.cpp new file mode 100644 index 00000000..7173e069 --- /dev/null +++ b/engine/src/Scripting/ScriptEngine.cpp @@ -0,0 +1,355 @@ +#include "Scripting/ScriptEngine.h" + +#include "Components/GameObject.h" +#include "Scene/Scene.h" +#include "Scripting/ScriptComponent.h" + +#include + +namespace XCEngine { +namespace Scripting { + +ScriptEngine& ScriptEngine::Get() { + static ScriptEngine instance; + return instance; +} + +void ScriptEngine::SetRuntime(IScriptRuntime* runtime) { + m_runtime = runtime ? runtime : &m_nullRuntime; +} + +void ScriptEngine::OnRuntimeStart(Components::Scene* scene) { + OnRuntimeStop(); + + if (!scene) { + return; + } + + m_runtimeScene = scene; + m_runtimeRunning = true; + m_runtime->OnRuntimeStart(scene); + + for (Components::GameObject* root : scene->GetRootGameObjects()) { + CollectScriptComponents(root); + } + + for (const ScriptInstanceKey& key : m_scriptOrder) { + 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::OnRuntimeStop() { + if (!m_runtimeRunning) { + m_runtimeScene = nullptr; + m_scriptStates.clear(); + m_scriptOrder.clear(); + return; + } + + std::vector keys = m_scriptOrder; + for (const ScriptInstanceKey& key : keys) { + auto it = m_scriptStates.find(key); + if (it != m_scriptStates.end()) { + StopTrackingScript(it->second, true); + } + } + + Components::Scene* stoppedScene = m_runtimeScene; + m_scriptStates.clear(); + m_scriptOrder.clear(); + m_runtimeRunning = false; + m_runtimeScene = nullptr; + m_runtime->OnRuntimeStop(stoppedScene); +} + +void ScriptEngine::OnFixedUpdate(float fixedDeltaTime) { + if (!m_runtimeRunning) { + return; + } + + for (const ScriptInstanceKey& key : m_scriptOrder) { + auto it = m_scriptStates.find(key); + if (it == m_scriptStates.end()) { + continue; + } + + ScriptInstanceState& state = it->second; + if (!ShouldScriptRun(state) || !EnsureScriptReady(state, true) || !state.enabled) { + continue; + } + + InvokeLifecycleMethod(state, ScriptLifecycleMethod::FixedUpdate, fixedDeltaTime); + } +} + +void ScriptEngine::OnUpdate(float deltaTime) { + if (!m_runtimeRunning) { + return; + } + + for (const ScriptInstanceKey& key : m_scriptOrder) { + auto it = m_scriptStates.find(key); + if (it == m_scriptStates.end()) { + continue; + } + + ScriptInstanceState& state = it->second; + if (!ShouldScriptRun(state) || !EnsureScriptReady(state, true) || !state.enabled) { + continue; + } + + if (state.startPending && !state.startCalled) { + InvokeLifecycleMethod(state, ScriptLifecycleMethod::Start); + state.startCalled = true; + state.startPending = false; + } + + InvokeLifecycleMethod(state, ScriptLifecycleMethod::Update, deltaTime); + } +} + +void ScriptEngine::OnLateUpdate(float deltaTime) { + if (!m_runtimeRunning) { + return; + } + + for (const ScriptInstanceKey& key : m_scriptOrder) { + auto it = m_scriptStates.find(key); + if (it == m_scriptStates.end()) { + continue; + } + + ScriptInstanceState& state = it->second; + if (!ShouldScriptRun(state) || !EnsureScriptReady(state, true) || !state.enabled) { + continue; + } + + InvokeLifecycleMethod(state, ScriptLifecycleMethod::LateUpdate, deltaTime); + } +} + +void ScriptEngine::OnScriptComponentEnabled(ScriptComponent* component) { + if (!m_runtimeRunning || !component) { + return; + } + + ScriptInstanceState* state = TrackScriptComponent(component); + if (!state) { + return; + } + + if (ShouldScriptRun(*state)) { + EnsureScriptReady(*state, true); + } +} + +void ScriptEngine::OnScriptComponentDisabled(ScriptComponent* component) { + if (!m_runtimeRunning || !component) { + return; + } + + ScriptInstanceState* state = FindState(component); + if (!state || !state->enabled) { + return; + } + + InvokeLifecycleMethod(*state, ScriptLifecycleMethod::OnDisable); + state->enabled = false; +} + +void ScriptEngine::OnScriptComponentDestroyed(ScriptComponent* component) { + if (!m_runtimeRunning || !component) { + return; + } + + ScriptInstanceState* state = FindState(component); + if (!state) { + return; + } + + StopTrackingScript(*state, false); +} + +bool ScriptEngine::HasTrackedScriptComponent(const ScriptComponent* component) const { + return FindState(component) != nullptr; +} + +bool ScriptEngine::HasRuntimeInstance(const ScriptComponent* component) const { + const ScriptInstanceState* state = FindState(component); + return state && state->instanceCreated; +} + +size_t ScriptEngine::ScriptInstanceKeyHasher::operator()(const ScriptInstanceKey& key) const { + const size_t h1 = std::hash{}(key.gameObjectUUID); + const size_t h2 = std::hash{}(key.scriptComponentUUID); + return h1 ^ (h2 + 0x9e3779b97f4a7c15ULL + (h1 << 6) + (h1 >> 2)); +} + +void ScriptEngine::CollectScriptComponents(Components::GameObject* gameObject) { + if (!gameObject) { + return; + } + + for (ScriptComponent* component : gameObject->GetComponents()) { + TrackScriptComponent(component); + } + + for (Components::GameObject* child : gameObject->GetChildren()) { + CollectScriptComponents(child); + } +} + +ScriptEngine::ScriptInstanceState* ScriptEngine::TrackScriptComponent(ScriptComponent* component) { + if (!component || !component->GetGameObject()) { + return nullptr; + } + + const ScriptInstanceKey key{ + component->GetGameObject()->GetUUID(), + component->GetScriptComponentUUID() + }; + + auto it = m_scriptStates.find(key); + if (it != m_scriptStates.end()) { + it->second.context.scene = component->GetScene(); + it->second.context.gameObject = component->GetGameObject(); + it->second.context.component = component; + it->second.context.gameObjectUUID = component->GetGameObject()->GetUUID(); + it->second.context.scriptComponentUUID = component->GetScriptComponentUUID(); + return &it->second; + } + + ScriptInstanceState state; + state.context.scene = component->GetScene(); + state.context.gameObject = component->GetGameObject(); + state.context.component = component; + state.context.gameObjectUUID = component->GetGameObject()->GetUUID(); + state.context.scriptComponentUUID = component->GetScriptComponentUUID(); + + auto [insertedIt, inserted] = m_scriptStates.emplace(key, std::move(state)); + if (inserted) { + m_scriptOrder.push_back(key); + } + + return &insertedIt->second; +} + +ScriptEngine::ScriptInstanceState* ScriptEngine::FindState(const ScriptComponent* component) { + if (!component || !component->GetGameObject()) { + return nullptr; + } + + const ScriptInstanceKey key{ + component->GetGameObject()->GetUUID(), + component->GetScriptComponentUUID() + }; + + auto it = m_scriptStates.find(key); + return it != m_scriptStates.end() ? &it->second : nullptr; +} + +const ScriptEngine::ScriptInstanceState* ScriptEngine::FindState(const ScriptComponent* component) const { + if (!component || !component->GetGameObject()) { + return nullptr; + } + + const ScriptInstanceKey key{ + component->GetGameObject()->GetUUID(), + component->GetScriptComponentUUID() + }; + + auto it = m_scriptStates.find(key); + return it != m_scriptStates.end() ? &it->second : nullptr; +} + +void ScriptEngine::RemoveTrackedScriptComponent(const ScriptComponent* component) { + if (!component || !component->GetGameObject()) { + return; + } + + const ScriptInstanceKey key{ + component->GetGameObject()->GetUUID(), + component->GetScriptComponentUUID() + }; + + m_scriptStates.erase(key); + m_scriptOrder.erase( + std::remove(m_scriptOrder.begin(), m_scriptOrder.end(), key), + m_scriptOrder.end()); +} + +bool ScriptEngine::ShouldScriptRun(const ScriptInstanceState& state) const { + return m_runtimeRunning + && state.context.scene == m_runtimeScene + && state.context.scene != nullptr + && state.context.scene->IsActive() + && state.context.gameObject != nullptr + && state.context.gameObject->IsActiveInHierarchy() + && state.context.component != nullptr + && state.context.component->IsEnabled() + && state.context.component->HasScriptClass(); +} + +bool ScriptEngine::EnsureScriptReady(ScriptInstanceState& state, bool invokeEnableIfNeeded) { + if (!state.context.component || !state.context.gameObject || !state.context.scene || !state.context.component->HasScriptClass()) { + return false; + } + + if (!state.instanceCreated) { + if (!m_runtime->CreateScriptInstance(state.context)) { + return false; + } + state.instanceCreated = true; + } + + if (!state.awakeCalled) { + InvokeLifecycleMethod(state, ScriptLifecycleMethod::Awake); + state.awakeCalled = true; + } + + if (invokeEnableIfNeeded && !state.enabled && ShouldScriptRun(state)) { + InvokeLifecycleMethod(state, ScriptLifecycleMethod::OnEnable); + state.enabled = true; + if (!state.startCalled) { + state.startPending = true; + } + } + + return true; +} + +void ScriptEngine::InvokeLifecycleMethod(ScriptInstanceState& state, ScriptLifecycleMethod method, float deltaTime) { + m_runtime->InvokeMethod(state.context, method, deltaTime); +} + +void ScriptEngine::StopTrackingScript(ScriptInstanceState& state, bool runtimeStopping) { + if (state.enabled) { + InvokeLifecycleMethod(state, ScriptLifecycleMethod::OnDisable); + state.enabled = false; + } + + if (state.instanceCreated) { + InvokeLifecycleMethod(state, ScriptLifecycleMethod::OnDestroy); + m_runtime->DestroyScriptInstance(state.context); + state.instanceCreated = false; + } + + state.startPending = false; + + if (!runtimeStopping) { + RemoveTrackedScriptComponent(state.context.component); + } +} + +} // namespace Scripting +} // namespace XCEngine diff --git a/tests/scripting/CMakeLists.txt b/tests/scripting/CMakeLists.txt index 966d559e..c3d9ba6a 100644 --- a/tests/scripting/CMakeLists.txt +++ b/tests/scripting/CMakeLists.txt @@ -5,6 +5,7 @@ project(XCEngine_ScriptingTests) set(SCRIPTING_TEST_SOURCES test_script_field_storage.cpp test_script_component.cpp + test_script_engine.cpp ) add_executable(scripting_tests ${SCRIPTING_TEST_SOURCES}) diff --git a/tests/scripting/test_script_engine.cpp b/tests/scripting/test_script_engine.cpp new file mode 100644 index 00000000..797a452b --- /dev/null +++ b/tests/scripting/test_script_engine.cpp @@ -0,0 +1,239 @@ +#include + +#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 FakeScriptRuntime : public IScriptRuntime { +public: + void OnRuntimeStart(Scene* scene) override { + events.push_back("RuntimeStart:" + (scene ? scene->GetName() : std::string("null"))); + } + + void OnRuntimeStop(Scene* scene) override { + events.push_back("RuntimeStop:" + (scene ? scene->GetName() : std::string("null"))); + } + + bool CreateScriptInstance(const ScriptRuntimeContext& context) override { + events.push_back("Create:" + Describe(context)); + return context.component != nullptr; + } + + void DestroyScriptInstance(const ScriptRuntimeContext& context) override { + events.push_back("Destroy:" + Describe(context)); + } + + void InvokeMethod( + const ScriptRuntimeContext& context, + ScriptLifecycleMethod method, + float deltaTime) override { + (void)deltaTime; + events.push_back(LifecycleMethodToString(method) + ":" + Describe(context)); + } + + void Clear() { + events.clear(); + } + + std::vector events; + +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; + } +}; + +class ScriptEngineTest : public ::testing::Test { +protected: + void SetUp() override { + engine = &ScriptEngine::Get(); + engine->OnRuntimeStop(); + engine->SetRuntime(&runtime); + } + + void TearDown() override { + engine->OnRuntimeStop(); + engine->SetRuntime(nullptr); + scene.reset(); + } + + ScriptComponent* AddScriptComponent(GameObject* gameObject, const std::string& namespaceName, const std::string& className) { + ScriptComponent* component = gameObject->AddComponent(); + component->SetScriptClass("GameScripts", namespaceName, className); + return component; + } + + Scene* CreateScene(const std::string& sceneName) { + scene = std::make_unique(sceneName); + return scene.get(); + } + + ScriptEngine* engine = nullptr; + FakeScriptRuntime runtime; + std::unique_ptr scene; +}; + +TEST_F(ScriptEngineTest, RuntimeStartTracksActiveScriptsAndInvokesAwakeThenOnEnable) { + Scene* runtimeScene = CreateScene("RuntimeScene"); + GameObject* host = runtimeScene->CreateGameObject("Host"); + ScriptComponent* component = AddScriptComponent(host, "Gameplay", "PlayerController"); + + engine->OnRuntimeStart(runtimeScene); + + EXPECT_TRUE(engine->IsRuntimeRunning()); + EXPECT_EQ(engine->GetRuntimeScene(), runtimeScene); + EXPECT_EQ(engine->GetTrackedScriptCount(), 1u); + EXPECT_TRUE(engine->HasTrackedScriptComponent(component)); + EXPECT_TRUE(engine->HasRuntimeInstance(component)); + + const std::vector expected = { + "RuntimeStart:RuntimeScene", + "Create:Host:Gameplay.PlayerController", + "Awake:Host:Gameplay.PlayerController", + "OnEnable:Host:Gameplay.PlayerController" + }; + EXPECT_EQ(runtime.events, expected); +} + +TEST_F(ScriptEngineTest, UpdateInvokesStartOnlyOnceAndRunsPerFrameMethods) { + Scene* runtimeScene = CreateScene("RuntimeScene"); + GameObject* host = runtimeScene->CreateGameObject("Host"); + AddScriptComponent(host, "Gameplay", "Mover"); + + engine->OnRuntimeStart(runtimeScene); + runtime.Clear(); + + engine->OnUpdate(0.016f); + engine->OnFixedUpdate(0.02f); + engine->OnUpdate(0.016f); + engine->OnLateUpdate(0.016f); + + const std::vector expected = { + "Start:Host:Gameplay.Mover", + "Update:Host:Gameplay.Mover", + "FixedUpdate:Host:Gameplay.Mover", + "Update:Host:Gameplay.Mover", + "LateUpdate:Host:Gameplay.Mover" + }; + EXPECT_EQ(runtime.events, expected); +} + +TEST_F(ScriptEngineTest, InactiveObjectDefersCreationUntilActivated) { + Scene* runtimeScene = CreateScene("RuntimeScene"); + GameObject* host = runtimeScene->CreateGameObject("Host"); + ScriptComponent* component = AddScriptComponent(host, "Gameplay", "SpawnedLater"); + host->SetActive(false); + + engine->OnRuntimeStart(runtimeScene); + + ASSERT_EQ(engine->GetTrackedScriptCount(), 1u); + EXPECT_TRUE(engine->HasTrackedScriptComponent(component)); + EXPECT_FALSE(engine->HasRuntimeInstance(component)); + ASSERT_EQ(runtime.events.size(), 1u); + EXPECT_EQ(runtime.events[0], "RuntimeStart:RuntimeScene"); + + runtime.Clear(); + host->SetActive(true); + engine->OnUpdate(0.016f); + + const std::vector expected = { + "Create:Host:Gameplay.SpawnedLater", + "Awake:Host:Gameplay.SpawnedLater", + "OnEnable:Host:Gameplay.SpawnedLater", + "Start:Host:Gameplay.SpawnedLater", + "Update:Host:Gameplay.SpawnedLater" + }; + EXPECT_EQ(runtime.events, expected); +} + +TEST_F(ScriptEngineTest, RuntimeStopInvokesDisableDestroyAndRuntimeStop) { + Scene* runtimeScene = CreateScene("RuntimeScene"); + GameObject* host = runtimeScene->CreateGameObject("Host"); + AddScriptComponent(host, "Gameplay", "ShutdownWatcher"); + + engine->OnRuntimeStart(runtimeScene); + runtime.Clear(); + + engine->OnRuntimeStop(); + + const std::vector expected = { + "OnDisable:Host:Gameplay.ShutdownWatcher", + "OnDestroy:Host:Gameplay.ShutdownWatcher", + "Destroy:Host:Gameplay.ShutdownWatcher", + "RuntimeStop:RuntimeScene" + }; + EXPECT_EQ(runtime.events, expected); + EXPECT_FALSE(engine->IsRuntimeRunning()); + EXPECT_EQ(engine->GetRuntimeScene(), nullptr); + EXPECT_EQ(engine->GetTrackedScriptCount(), 0u); +} + +TEST_F(ScriptEngineTest, ReEnablingScriptDoesNotInvokeStartTwice) { + Scene* runtimeScene = CreateScene("RuntimeScene"); + GameObject* host = runtimeScene->CreateGameObject("Host"); + ScriptComponent* component = AddScriptComponent(host, "Gameplay", "ToggleWatcher"); + + engine->OnRuntimeStart(runtimeScene); + engine->OnUpdate(0.016f); + runtime.Clear(); + + component->SetEnabled(false); + component->SetEnabled(true); + engine->OnUpdate(0.016f); + + const std::vector expected = { + "OnDisable:Host:Gameplay.ToggleWatcher", + "OnEnable:Host:Gameplay.ToggleWatcher", + "Update:Host:Gameplay.ToggleWatcher" + }; + EXPECT_EQ(runtime.events, expected); +} + +TEST_F(ScriptEngineTest, DestroyingGameObjectWhileRuntimeRunningDestroysTrackedScript) { + Scene* runtimeScene = CreateScene("RuntimeScene"); + GameObject* host = runtimeScene->CreateGameObject("Host"); + AddScriptComponent(host, "Gameplay", "DestroyWatcher"); + + engine->OnRuntimeStart(runtimeScene); + runtime.Clear(); + + runtimeScene->DestroyGameObject(host); + + const std::vector expected = { + "OnDisable:Host:Gameplay.DestroyWatcher", + "OnDestroy:Host:Gameplay.DestroyWatcher", + "Destroy:Host:Gameplay.DestroyWatcher" + }; + EXPECT_EQ(runtime.events, expected); + EXPECT_EQ(engine->GetTrackedScriptCount(), 0u); +} + +} // namespace