diff --git a/engine/include/XCEngine/Scripting/ScriptEngine.h b/engine/include/XCEngine/Scripting/ScriptEngine.h index 5143e014..45df1254 100644 --- a/engine/include/XCEngine/Scripting/ScriptEngine.h +++ b/engine/include/XCEngine/Scripting/ScriptEngine.h @@ -65,6 +65,8 @@ private: ScriptEngine() = default; void CollectScriptComponents(Components::GameObject* gameObject); + void EnsureTrackedScriptsReady(Components::GameObject* gameObject); + void HandleGameObjectCreated(Components::GameObject* gameObject); ScriptInstanceState* TrackScriptComponent(ScriptComponent* component); ScriptInstanceState* FindState(const ScriptComponent* component); const ScriptInstanceState* FindState(const ScriptComponent* component) const; @@ -79,6 +81,7 @@ private: IScriptRuntime* m_runtime = &m_nullRuntime; Components::Scene* m_runtimeScene = nullptr; bool m_runtimeRunning = false; + uint64_t m_runtimeSceneCreatedSubscription = 0; std::unordered_map m_scriptStates; std::vector m_scriptOrder; diff --git a/engine/src/Scripting/Mono/MonoScriptRuntime.cpp b/engine/src/Scripting/Mono/MonoScriptRuntime.cpp index a8a88db9..8ee2b406 100644 --- a/engine/src/Scripting/Mono/MonoScriptRuntime.cpp +++ b/engine/src/Scripting/Mono/MonoScriptRuntime.cpp @@ -354,6 +354,49 @@ uint64_t InternalCall_GameObject_AddComponent(uint64_t gameObjectUUID, MonoRefle return AddOrGetNativeComponent(gameObject, ResolveManagedComponentKind(componentType)) ? gameObjectUUID : 0; } +uint64_t InternalCall_GameObject_Find(MonoString* name) { + Components::Scene* scene = GetInternalCallScene(); + if (!scene) { + return 0; + } + + Components::GameObject* gameObject = scene->Find(MonoStringToUtf8(name)); + return gameObject ? gameObject->GetUUID() : 0; +} + +uint64_t InternalCall_GameObject_Create(MonoString* name, uint64_t parentGameObjectUUID) { + Components::Scene* scene = GetInternalCallScene(); + if (!scene) { + return 0; + } + + Components::GameObject* parent = nullptr; + if (parentGameObjectUUID != 0) { + parent = FindGameObjectByUUID(parentGameObjectUUID); + if (!parent || parent->GetScene() != scene) { + return 0; + } + } + + std::string objectName = MonoStringToUtf8(name); + if (objectName.empty()) { + objectName = "GameObject"; + } + + Components::GameObject* created = scene->CreateGameObject(objectName, parent); + return created ? created->GetUUID() : 0; +} + +void InternalCall_GameObject_Destroy(uint64_t gameObjectUUID) { + Components::Scene* scene = GetInternalCallScene(); + Components::GameObject* gameObject = FindGameObjectByUUID(gameObjectUUID); + if (!scene || !gameObject || gameObject->GetScene() != scene) { + return; + } + + scene->DestroyGameObject(gameObject); +} + mono_bool InternalCall_Behaviour_GetEnabled(uint64_t scriptComponentUUID) { ScriptComponent* component = FindScriptComponentByUUID(scriptComponentUUID); return (component && component->IsEnabled()) ? 1 : 0; @@ -992,6 +1035,9 @@ void RegisterInternalCalls() { mono_add_internal_call("XCEngine.InternalCalls::GameObject_HasComponent", reinterpret_cast(&InternalCall_GameObject_HasComponent)); mono_add_internal_call("XCEngine.InternalCalls::GameObject_GetComponent", reinterpret_cast(&InternalCall_GameObject_GetComponent)); mono_add_internal_call("XCEngine.InternalCalls::GameObject_AddComponent", reinterpret_cast(&InternalCall_GameObject_AddComponent)); + mono_add_internal_call("XCEngine.InternalCalls::GameObject_Find", reinterpret_cast(&InternalCall_GameObject_Find)); + mono_add_internal_call("XCEngine.InternalCalls::GameObject_Create", reinterpret_cast(&InternalCall_GameObject_Create)); + mono_add_internal_call("XCEngine.InternalCalls::GameObject_Destroy", reinterpret_cast(&InternalCall_GameObject_Destroy)); mono_add_internal_call("XCEngine.InternalCalls::Behaviour_GetEnabled", reinterpret_cast(&InternalCall_Behaviour_GetEnabled)); mono_add_internal_call("XCEngine.InternalCalls::Behaviour_SetEnabled", reinterpret_cast(&InternalCall_Behaviour_SetEnabled)); mono_add_internal_call("XCEngine.InternalCalls::Transform_GetLocalPosition", reinterpret_cast(&InternalCall_Transform_GetLocalPosition)); diff --git a/engine/src/Scripting/ScriptComponent.cpp b/engine/src/Scripting/ScriptComponent.cpp index 24c2cd2e..4a728ca2 100644 --- a/engine/src/Scripting/ScriptComponent.cpp +++ b/engine/src/Scripting/ScriptComponent.cpp @@ -24,14 +24,22 @@ ScriptComponent::ScriptComponent() } void ScriptComponent::SetScriptClass(const std::string& namespaceName, const std::string& className) { + const bool hadScriptClass = HasScriptClass(); m_namespaceName = namespaceName; m_className = className; + if (!hadScriptClass && HasScriptClass()) { + ScriptEngine::Get().OnScriptComponentEnabled(this); + } } void ScriptComponent::SetScriptClass(const std::string& assemblyName, const std::string& namespaceName, const std::string& className) { + const bool hadScriptClass = HasScriptClass(); m_assemblyName = assemblyName; m_namespaceName = namespaceName; m_className = className; + if (!hadScriptClass && HasScriptClass()) { + ScriptEngine::Get().OnScriptComponentEnabled(this); + } } std::string ScriptComponent::GetFullClassName() const { diff --git a/engine/src/Scripting/ScriptEngine.cpp b/engine/src/Scripting/ScriptEngine.cpp index 7173e069..6f26bd8e 100644 --- a/engine/src/Scripting/ScriptEngine.cpp +++ b/engine/src/Scripting/ScriptEngine.cpp @@ -28,6 +28,10 @@ void ScriptEngine::OnRuntimeStart(Components::Scene* scene) { m_runtimeScene = scene; m_runtimeRunning = true; m_runtime->OnRuntimeStart(scene); + m_runtimeSceneCreatedSubscription = scene->OnGameObjectCreated().Subscribe( + [this](Components::GameObject* gameObject) { + HandleGameObjectCreated(gameObject); + }); for (Components::GameObject* root : scene->GetRootGameObjects()) { CollectScriptComponents(root); @@ -49,6 +53,12 @@ void ScriptEngine::OnRuntimeStart(Components::Scene* scene) { } void ScriptEngine::OnRuntimeStop() { + if (m_runtimeScene && m_runtimeSceneCreatedSubscription != 0) { + m_runtimeScene->OnGameObjectCreated().Unsubscribe(m_runtimeSceneCreatedSubscription); + m_runtimeScene->OnGameObjectCreated().ProcessUnsubscribes(); + m_runtimeSceneCreatedSubscription = 0; + } + if (!m_runtimeRunning) { m_runtimeScene = nullptr; m_scriptStates.clear(); @@ -209,6 +219,34 @@ void ScriptEngine::CollectScriptComponents(Components::GameObject* gameObject) { } } +void ScriptEngine::EnsureTrackedScriptsReady(Components::GameObject* gameObject) { + if (!gameObject) { + return; + } + + for (ScriptComponent* component : gameObject->GetComponents()) { + ScriptInstanceState* state = FindState(component); + if (!state || !ShouldScriptRun(*state)) { + continue; + } + + EnsureScriptReady(*state, true); + } + + for (Components::GameObject* child : gameObject->GetChildren()) { + EnsureTrackedScriptsReady(child); + } +} + +void ScriptEngine::HandleGameObjectCreated(Components::GameObject* gameObject) { + if (!m_runtimeRunning || !gameObject || gameObject->GetScene() != m_runtimeScene) { + return; + } + + CollectScriptComponents(gameObject); + EnsureTrackedScriptsReady(gameObject); +} + ScriptEngine::ScriptInstanceState* ScriptEngine::TrackScriptComponent(ScriptComponent* component) { if (!component || !component->GetGameObject()) { return nullptr; diff --git a/managed/CMakeLists.txt b/managed/CMakeLists.txt index 54933dd5..6dc7216e 100644 --- a/managed/CMakeLists.txt +++ b/managed/CMakeLists.txt @@ -72,6 +72,7 @@ set(XCENGINE_SCRIPT_CORE_SOURCES set(XCENGINE_GAME_SCRIPT_SOURCES ${CMAKE_CURRENT_SOURCE_DIR}/GameScripts/BuiltinComponentProbe.cs ${CMAKE_CURRENT_SOURCE_DIR}/GameScripts/AddComponentProbe.cs + ${CMAKE_CURRENT_SOURCE_DIR}/GameScripts/RuntimeGameObjectProbe.cs ${CMAKE_CURRENT_SOURCE_DIR}/GameScripts/HierarchyProbe.cs ${CMAKE_CURRENT_SOURCE_DIR}/GameScripts/LifecycleProbe.cs ${CMAKE_CURRENT_SOURCE_DIR}/GameScripts/MeshComponentProbe.cs diff --git a/managed/GameScripts/RuntimeGameObjectProbe.cs b/managed/GameScripts/RuntimeGameObjectProbe.cs new file mode 100644 index 00000000..2f56056a --- /dev/null +++ b/managed/GameScripts/RuntimeGameObjectProbe.cs @@ -0,0 +1,98 @@ +using XCEngine; + +namespace Gameplay +{ + public sealed class RuntimeGameObjectProbe : MonoBehaviour + { + public bool MissingBeforeCreate; + public bool CreatedRootSucceeded; + public bool CreatedChildSucceeded; + public bool FoundRootSucceeded; + public bool FoundChildSucceeded; + public string ObservedFoundRootName = string.Empty; + public string ObservedFoundChildParentName = string.Empty; + public int ObservedRootChildCountBeforeDestroy; + public bool CameraLookupSucceeded; + public bool MeshFilterLookupSucceeded; + public bool MeshRendererLookupSucceeded; + public float ObservedCameraFieldOfView; + public string ObservedMeshPath = string.Empty; + public int ObservedMaterialCount; + public string ObservedMaterial0Path = string.Empty; + public int ObservedRenderLayer; + public bool MissingChildAfterDestroy; + public bool FoundRootAfterDestroySucceeded; + public int ObservedRootChildCountAfterDestroy = -1; + + public void Start() + { + MissingBeforeCreate = GameObject.Find("RuntimeCreatedRoot") == null; + + GameObject root = GameObject.Create("RuntimeCreatedRoot"); + GameObject child = GameObject.Create("RuntimeCreatedChild", root); + + CreatedRootSucceeded = root != null; + CreatedChildSucceeded = child != null; + + GameObject foundRoot = GameObject.Find("RuntimeCreatedRoot"); + GameObject foundChild = GameObject.Find("RuntimeCreatedChild"); + + FoundRootSucceeded = foundRoot != null; + FoundChildSucceeded = foundChild != null; + + if (foundRoot != null) + { + ObservedFoundRootName = foundRoot.name; + ObservedRootChildCountBeforeDestroy = foundRoot.transform.childCount; + + Camera camera = foundRoot.AddComponent(); + MeshFilter meshFilter = foundRoot.AddComponent(); + MeshRenderer meshRenderer = foundRoot.AddComponent(); + + CameraLookupSucceeded = camera != null; + MeshFilterLookupSucceeded = meshFilter != null; + MeshRendererLookupSucceeded = meshRenderer != null; + + if (camera != null) + { + camera.fieldOfView = 68.0f; + ObservedCameraFieldOfView = camera.fieldOfView; + } + + if (meshFilter != null) + { + meshFilter.meshPath = "Meshes/runtime_created.mesh"; + ObservedMeshPath = meshFilter.meshPath; + } + + if (meshRenderer != null) + { + meshRenderer.SetMaterialPath(0, "Materials/runtime_created.mat"); + meshRenderer.renderLayer = 4; + ObservedMaterialCount = meshRenderer.materialCount; + ObservedMaterial0Path = meshRenderer.GetMaterialPath(0); + ObservedRenderLayer = meshRenderer.renderLayer; + } + } + + if (foundChild != null && foundChild.transform.parent != null) + { + ObservedFoundChildParentName = foundChild.transform.parent.gameObject.name; + } + + if (child != null) + { + child.Destroy(); + } + + MissingChildAfterDestroy = GameObject.Find("RuntimeCreatedChild") == null; + + GameObject foundRootAfterDestroy = GameObject.Find("RuntimeCreatedRoot"); + FoundRootAfterDestroySucceeded = foundRootAfterDestroy != null; + if (foundRootAfterDestroy != null) + { + ObservedRootChildCountAfterDestroy = foundRootAfterDestroy.transform.childCount; + } + } + } +} \ No newline at end of file diff --git a/managed/XCEngine.ScriptCore/GameObject.cs b/managed/XCEngine.ScriptCore/GameObject.cs index 41cd4df6..b5d174d2 100644 --- a/managed/XCEngine.ScriptCore/GameObject.cs +++ b/managed/XCEngine.ScriptCore/GameObject.cs @@ -11,6 +11,23 @@ namespace XCEngine public ulong UUID => m_uuid; + public static GameObject Find(string name) + { + ulong uuid = InternalCalls.GameObject_Find(name ?? string.Empty); + return uuid != 0 ? new GameObject(uuid) : null; + } + + public static GameObject Create(string name) + { + return Create(name, null); + } + + public static GameObject Create(string name, GameObject parent) + { + ulong uuid = InternalCalls.GameObject_Create(name ?? string.Empty, parent?.UUID ?? 0); + return uuid != 0 ? new GameObject(uuid) : null; + } + public string Name { get => InternalCalls.GameObject_GetName(UUID) ?? string.Empty; @@ -32,6 +49,11 @@ namespace XCEngine InternalCalls.GameObject_SetActive(UUID, value); } + public void Destroy() + { + InternalCalls.GameObject_Destroy(UUID); + } + public Transform Transform => GetComponent(); public Transform transform => Transform; diff --git a/managed/XCEngine.ScriptCore/InternalCalls.cs b/managed/XCEngine.ScriptCore/InternalCalls.cs index 60fab4b8..43660cb0 100644 --- a/managed/XCEngine.ScriptCore/InternalCalls.cs +++ b/managed/XCEngine.ScriptCore/InternalCalls.cs @@ -41,6 +41,15 @@ namespace XCEngine [MethodImpl(MethodImplOptions.InternalCall)] internal static extern ulong GameObject_AddComponent(ulong gameObjectUUID, Type componentType); + [MethodImpl(MethodImplOptions.InternalCall)] + internal static extern ulong GameObject_Find(string name); + + [MethodImpl(MethodImplOptions.InternalCall)] + internal static extern ulong GameObject_Create(string name, ulong parentGameObjectUUID); + + [MethodImpl(MethodImplOptions.InternalCall)] + internal static extern void GameObject_Destroy(ulong gameObjectUUID); + [MethodImpl(MethodImplOptions.InternalCall)] internal static extern bool Behaviour_GetEnabled(ulong scriptComponentUUID); diff --git a/tests/scripting/test_mono_script_runtime.cpp b/tests/scripting/test_mono_script_runtime.cpp index 8c0988d0..cd9a2af2 100644 --- a/tests/scripting/test_mono_script_runtime.cpp +++ b/tests/scripting/test_mono_script_runtime.cpp @@ -562,6 +562,146 @@ TEST_F(MonoScriptRuntimeTest, GameObjectAddComponentApiCreatesBuiltinComponentsA EXPECT_EQ(meshRenderer->GetRenderLayer(), 6u); } +TEST_F(MonoScriptRuntimeTest, GameObjectRuntimeApiCreatesFindsAndDestroysSceneObjects) { + Scene* runtimeScene = CreateScene("MonoRuntimeScene"); + GameObject* host = runtimeScene->CreateGameObject("Host"); + ScriptComponent* component = AddScript(host, "Gameplay", "RuntimeGameObjectProbe"); + + engine->OnRuntimeStart(runtimeScene); + engine->OnUpdate(0.016f); + + bool missingBeforeCreate = false; + bool createdRootSucceeded = false; + bool createdChildSucceeded = false; + bool foundRootSucceeded = false; + bool foundChildSucceeded = false; + std::string observedFoundRootName; + std::string observedFoundChildParentName; + int32_t observedRootChildCountBeforeDestroy = 0; + bool cameraLookupSucceeded = false; + bool meshFilterLookupSucceeded = false; + bool meshRendererLookupSucceeded = false; + float observedCameraFieldOfView = 0.0f; + std::string observedMeshPath; + int32_t observedMaterialCount = 0; + std::string observedMaterial0Path; + int32_t observedRenderLayer = 0; + bool missingChildAfterDestroy = false; + bool foundRootAfterDestroySucceeded = false; + int32_t observedRootChildCountAfterDestroy = -1; + + EXPECT_TRUE(runtime->TryGetFieldValue(component, "MissingBeforeCreate", missingBeforeCreate)); + EXPECT_TRUE(runtime->TryGetFieldValue(component, "CreatedRootSucceeded", createdRootSucceeded)); + EXPECT_TRUE(runtime->TryGetFieldValue(component, "CreatedChildSucceeded", createdChildSucceeded)); + EXPECT_TRUE(runtime->TryGetFieldValue(component, "FoundRootSucceeded", foundRootSucceeded)); + EXPECT_TRUE(runtime->TryGetFieldValue(component, "FoundChildSucceeded", foundChildSucceeded)); + EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedFoundRootName", observedFoundRootName)); + EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedFoundChildParentName", observedFoundChildParentName)); + EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedRootChildCountBeforeDestroy", observedRootChildCountBeforeDestroy)); + EXPECT_TRUE(runtime->TryGetFieldValue(component, "CameraLookupSucceeded", cameraLookupSucceeded)); + EXPECT_TRUE(runtime->TryGetFieldValue(component, "MeshFilterLookupSucceeded", meshFilterLookupSucceeded)); + EXPECT_TRUE(runtime->TryGetFieldValue(component, "MeshRendererLookupSucceeded", meshRendererLookupSucceeded)); + EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedCameraFieldOfView", observedCameraFieldOfView)); + EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedMeshPath", observedMeshPath)); + EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedMaterialCount", observedMaterialCount)); + EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedMaterial0Path", observedMaterial0Path)); + EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedRenderLayer", observedRenderLayer)); + EXPECT_TRUE(runtime->TryGetFieldValue(component, "MissingChildAfterDestroy", missingChildAfterDestroy)); + EXPECT_TRUE(runtime->TryGetFieldValue(component, "FoundRootAfterDestroySucceeded", foundRootAfterDestroySucceeded)); + EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedRootChildCountAfterDestroy", observedRootChildCountAfterDestroy)); + + EXPECT_TRUE(missingBeforeCreate); + EXPECT_TRUE(createdRootSucceeded); + EXPECT_TRUE(createdChildSucceeded); + EXPECT_TRUE(foundRootSucceeded); + EXPECT_TRUE(foundChildSucceeded); + EXPECT_EQ(observedFoundRootName, "RuntimeCreatedRoot"); + EXPECT_EQ(observedFoundChildParentName, "RuntimeCreatedRoot"); + EXPECT_EQ(observedRootChildCountBeforeDestroy, 1); + EXPECT_TRUE(cameraLookupSucceeded); + EXPECT_TRUE(meshFilterLookupSucceeded); + EXPECT_TRUE(meshRendererLookupSucceeded); + EXPECT_FLOAT_EQ(observedCameraFieldOfView, 68.0f); + EXPECT_EQ(observedMeshPath, "Meshes/runtime_created.mesh"); + EXPECT_EQ(observedMaterialCount, 1); + EXPECT_EQ(observedMaterial0Path, "Materials/runtime_created.mat"); + EXPECT_EQ(observedRenderLayer, 4); + EXPECT_TRUE(missingChildAfterDestroy); + EXPECT_TRUE(foundRootAfterDestroySucceeded); + EXPECT_EQ(observedRootChildCountAfterDestroy, 0); + + GameObject* createdRoot = runtimeScene->Find("RuntimeCreatedRoot"); + ASSERT_NE(createdRoot, nullptr); + EXPECT_EQ(runtimeScene->Find("RuntimeCreatedChild"), nullptr); + EXPECT_EQ(createdRoot->GetParent(), nullptr); + EXPECT_EQ(createdRoot->GetChildCount(), 0u); + + CameraComponent* camera = createdRoot->GetComponent(); + MeshFilterComponent* meshFilter = createdRoot->GetComponent(); + MeshRendererComponent* meshRenderer = createdRoot->GetComponent(); + + ASSERT_NE(camera, nullptr); + ASSERT_NE(meshFilter, nullptr); + ASSERT_NE(meshRenderer, nullptr); + EXPECT_FLOAT_EQ(camera->GetFieldOfView(), 68.0f); + EXPECT_EQ(meshFilter->GetMeshPath(), "Meshes/runtime_created.mesh"); + ASSERT_EQ(meshRenderer->GetMaterialCount(), 1u); + EXPECT_EQ(meshRenderer->GetMaterialPath(0), "Materials/runtime_created.mat"); + EXPECT_EQ(meshRenderer->GetRenderLayer(), 4u); +} + +TEST_F(MonoScriptRuntimeTest, RuntimeCreatedScriptComponentCreatesManagedInstanceAfterClassAssignment) { + Scene* runtimeScene = CreateScene("MonoRuntimeScene"); + + engine->OnRuntimeStart(runtimeScene); + + GameObject* spawned = runtimeScene->CreateGameObject("RuntimeSpawned"); + ScriptComponent* component = spawned->AddComponent(); + component->GetFieldStorage().SetFieldValue("Label", "RuntimeConfigured"); + component->SetScriptClass("GameScripts", "Gameplay", "LifecycleProbe"); + + EXPECT_TRUE(runtime->HasManagedInstance(component)); + + engine->OnUpdate(0.016f); + + int32_t awakeCount = 0; + int32_t enableCount = 0; + int32_t startCount = 0; + int32_t updateCount = 0; + std::string label; + std::string observedGameObjectName; + bool wasAwakened = false; + bool observedEnabled = false; + bool observedActiveSelf = false; + bool observedActiveInHierarchy = false; + bool observedIsActiveAndEnabled = false; + + EXPECT_TRUE(runtime->TryGetFieldValue(component, "AwakeCount", awakeCount)); + EXPECT_TRUE(runtime->TryGetFieldValue(component, "EnableCount", enableCount)); + EXPECT_TRUE(runtime->TryGetFieldValue(component, "StartCount", startCount)); + EXPECT_TRUE(runtime->TryGetFieldValue(component, "UpdateCount", updateCount)); + EXPECT_TRUE(runtime->TryGetFieldValue(component, "Label", label)); + EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedGameObjectName", observedGameObjectName)); + EXPECT_TRUE(runtime->TryGetFieldValue(component, "WasAwakened", wasAwakened)); + EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedEnabled", observedEnabled)); + EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedActiveSelf", observedActiveSelf)); + EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedActiveInHierarchy", observedActiveInHierarchy)); + EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedIsActiveAndEnabled", observedIsActiveAndEnabled)); + + EXPECT_EQ(awakeCount, 1); + EXPECT_EQ(enableCount, 1); + EXPECT_EQ(startCount, 1); + EXPECT_EQ(updateCount, 1); + EXPECT_EQ(label, "RuntimeConfigured|Awake"); + EXPECT_EQ(observedGameObjectName, "RuntimeSpawned_Managed"); + EXPECT_TRUE(wasAwakened); + EXPECT_TRUE(observedEnabled); + EXPECT_TRUE(observedActiveSelf); + EXPECT_TRUE(observedActiveInHierarchy); + EXPECT_TRUE(observedIsActiveAndEnabled); + EXPECT_EQ(spawned->GetName(), "RuntimeSpawned_Managed"); +} + TEST_F(MonoScriptRuntimeTest, TransformHierarchyApiExposesParentChildAndReparenting) { Scene* runtimeScene = CreateScene("MonoRuntimeScene"); GameObject* root = runtimeScene->CreateGameObject("Root"); diff --git a/tests/scripting/test_script_engine.cpp b/tests/scripting/test_script_engine.cpp index 797a452b..58cdd524 100644 --- a/tests/scripting/test_script_engine.cpp +++ b/tests/scripting/test_script_engine.cpp @@ -236,4 +236,34 @@ TEST_F(ScriptEngineTest, DestroyingGameObjectWhileRuntimeRunningDestroysTrackedS EXPECT_EQ(engine->GetTrackedScriptCount(), 0u); } +TEST_F(ScriptEngineTest, RuntimeCreatedScriptComponentIsTrackedImmediatelyAndStartsOnNextUpdate) { + Scene* runtimeScene = CreateScene("RuntimeScene"); + + engine->OnRuntimeStart(runtimeScene); + runtime.Clear(); + + GameObject* spawned = runtimeScene->CreateGameObject("Spawned"); + ScriptComponent* component = AddScriptComponent(spawned, "Gameplay", "RuntimeSpawned"); + + EXPECT_EQ(engine->GetTrackedScriptCount(), 1u); + EXPECT_TRUE(engine->HasTrackedScriptComponent(component)); + EXPECT_TRUE(engine->HasRuntimeInstance(component)); + + const std::vector expectedBeforeUpdate = { + "Create:Spawned:Gameplay.RuntimeSpawned", + "Awake:Spawned:Gameplay.RuntimeSpawned", + "OnEnable:Spawned:Gameplay.RuntimeSpawned" + }; + EXPECT_EQ(runtime.events, expectedBeforeUpdate); + + runtime.Clear(); + engine->OnUpdate(0.016f); + + const std::vector expectedAfterUpdate = { + "Start:Spawned:Gameplay.RuntimeSpawned", + "Update:Spawned:Gameplay.RuntimeSpawned" + }; + EXPECT_EQ(runtime.events, expectedAfterUpdate); +} + } // namespace