From 0f51f553c80179e249e4c75a0d015b20287f7a76 Mon Sep 17 00:00:00 2001 From: ssdfasd <2156608475@qq.com> Date: Fri, 3 Apr 2026 14:51:52 +0800 Subject: [PATCH] Add Unity-style GetComponents scripting API --- docs/plan/C#脚本模块下一阶段计划.md | 6 +- .../src/Scripting/Mono/MonoScriptRuntime.cpp | 108 ++++++++++++++++-- managed/CMakeLists.txt | 1 + managed/GameScripts/GetComponentsProbe.cs | 49 ++++++++ managed/XCEngine.ScriptCore/Component.cs | 5 + managed/XCEngine.ScriptCore/GameObject.cs | 22 ++++ managed/XCEngine.ScriptCore/InternalCalls.cs | 3 + tests/scripting/test_mono_script_runtime.cpp | 56 +++++++++ 8 files changed, 240 insertions(+), 10 deletions(-) create mode 100644 managed/GameScripts/GetComponentsProbe.cs diff --git a/docs/plan/C#脚本模块下一阶段计划.md b/docs/plan/C#脚本模块下一阶段计划.md index 4e122121..2361ee5e 100644 --- a/docs/plan/C#脚本模块下一阶段计划.md +++ b/docs/plan/C#脚本模块下一阶段计划.md @@ -260,6 +260,6 @@ C# 脚本模块已经完成了第一阶段的核心闭环,不再是“从 0 - 已通过 `MonoScriptRuntimeTest.*` 与 `ProjectScriptAssemblyTest.*` 相关整组验证。 - 下一步建议继续做第二批 Unity API 对齐: -- `GetComponents()` -- `Object.Instantiate` -- `tag / CompareTag / layer` +- 已完成第二步第一小项:`GetComponents()` +- 下一步候选:`Object.Instantiate` +- 下一步候选:`tag / CompareTag / layer` diff --git a/engine/src/Scripting/Mono/MonoScriptRuntime.cpp b/engine/src/Scripting/Mono/MonoScriptRuntime.cpp index 48571d78..7e88edc1 100644 --- a/engine/src/Scripting/Mono/MonoScriptRuntime.cpp +++ b/engine/src/Scripting/Mono/MonoScriptRuntime.cpp @@ -262,6 +262,15 @@ bool HasNativeComponent(Components::GameObject* gameObject, ManagedComponentKind return FindNativeComponent(gameObject, componentKind) != nullptr; } +bool IsMatchingScriptComponent(const ScriptComponent* component, const ManagedComponentTypeInfo& typeInfo) { + return component + && component->HasScriptClass() + && typeInfo.kind == ManagedComponentKind::Script + && component->GetAssemblyName() == typeInfo.assemblyName + && component->GetNamespaceName() == typeInfo.namespaceName + && component->GetClassName() == typeInfo.className; +} + Components::Component* AddOrGetNativeComponent(Components::GameObject* gameObject, ManagedComponentKind componentKind) { if (!gameObject) { return nullptr; @@ -302,13 +311,7 @@ ScriptComponent* FindMatchingScriptComponent( } for (ScriptComponent* component : gameObject->GetComponents()) { - if (!component || !component->HasScriptClass()) { - continue; - } - - if (component->GetAssemblyName() == typeInfo.assemblyName - && component->GetNamespaceName() == typeInfo.namespaceName - && component->GetClassName() == typeInfo.className) { + if (IsMatchingScriptComponent(component, typeInfo)) { return component; } } @@ -423,6 +426,28 @@ Components::Space ResolveManagedSpace(int32_t value) { : Components::Space::Self; } +MonoArray* CreateManagedComponentArray(MonoClass* componentClass, const std::vector& components) { + if (!componentClass) { + return nullptr; + } + + MonoDomain* domain = mono_domain_get(); + if (!domain) { + return nullptr; + } + + MonoArray* array = mono_array_new(domain, componentClass, static_cast(components.size())); + if (!array) { + return nullptr; + } + + for (uintptr_t index = 0; index < components.size(); ++index) { + mono_array_setref(array, index, components[index]); + } + + return array; +} + void InternalCall_Debug_Log(MonoString* message) { XCEngine::Debug::Logger::Get().Info( XCEngine::Debug::LogCategory::Scripting, @@ -608,6 +633,74 @@ MonoObject* InternalCall_GameObject_GetComponent(uint64_t gameObjectUUID, MonoRe return runtime->CreateManagedComponentWrapper(typeInfo.monoClass, gameObjectUUID); } +MonoArray* InternalCall_GameObject_GetComponents(uint64_t gameObjectUUID, MonoReflectionType* componentType) { + Components::GameObject* gameObject = FindGameObjectByUUID(gameObjectUUID); + if (!gameObject) { + return nullptr; + } + + MonoScriptRuntime* runtime = GetActiveMonoScriptRuntime(); + if (!runtime) { + return nullptr; + } + + const ManagedComponentTypeInfo typeInfo = ResolveManagedComponentTypeInfo(componentType); + if (!typeInfo.monoClass) { + return nullptr; + } + + std::vector managedComponents; + auto appendNativeComponents = [&](const auto& nativeComponents) { + for (const auto* component : nativeComponents) { + if (!component || !component->GetGameObject()) { + continue; + } + + if (MonoObject* managedObject = + runtime->CreateManagedComponentWrapper(typeInfo.monoClass, component->GetGameObject()->GetUUID())) { + managedComponents.push_back(managedObject); + } + } + }; + + switch (typeInfo.kind) { + case ManagedComponentKind::Script: + for (ScriptComponent* component : gameObject->GetComponents()) { + if (!IsMatchingScriptComponent(component, typeInfo)) { + continue; + } + + if (!runtime->HasManagedInstance(component)) { + ScriptEngine::Get().OnScriptComponentEnabled(component); + } + + if (MonoObject* managedObject = runtime->GetManagedInstanceObject(component)) { + managedComponents.push_back(managedObject); + } + } + break; + case ManagedComponentKind::Transform: + appendNativeComponents(gameObject->GetComponents()); + break; + case ManagedComponentKind::Camera: + appendNativeComponents(gameObject->GetComponents()); + break; + case ManagedComponentKind::Light: + appendNativeComponents(gameObject->GetComponents()); + break; + case ManagedComponentKind::MeshFilter: + appendNativeComponents(gameObject->GetComponents()); + break; + case ManagedComponentKind::MeshRenderer: + appendNativeComponents(gameObject->GetComponents()); + break; + case ManagedComponentKind::Unknown: + return nullptr; + } + + return CreateManagedComponentArray(typeInfo.monoClass, managedComponents); +} + MonoObject* InternalCall_GameObject_GetComponentInChildren(uint64_t gameObjectUUID, MonoReflectionType* componentType) { Components::GameObject* gameObject = FindGameObjectByUUID(gameObjectUUID); if (!gameObject) { @@ -1404,6 +1497,7 @@ void RegisterInternalCalls() { mono_add_internal_call("XCEngine.InternalCalls::GameObject_SetActive", reinterpret_cast(&InternalCall_GameObject_SetActive)); 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_GetComponents", reinterpret_cast(&InternalCall_GameObject_GetComponents)); mono_add_internal_call("XCEngine.InternalCalls::GameObject_GetComponentInChildren", reinterpret_cast(&InternalCall_GameObject_GetComponentInChildren)); mono_add_internal_call("XCEngine.InternalCalls::GameObject_GetComponentInParent", reinterpret_cast(&InternalCall_GameObject_GetComponentInParent)); mono_add_internal_call("XCEngine.InternalCalls::GameObject_AddComponent", reinterpret_cast(&InternalCall_GameObject_AddComponent)); diff --git a/managed/CMakeLists.txt b/managed/CMakeLists.txt index bee362d0..da09bc02 100644 --- a/managed/CMakeLists.txt +++ b/managed/CMakeLists.txt @@ -101,6 +101,7 @@ set(XCENGINE_GAME_SCRIPT_SOURCES ${CMAKE_CURRENT_SOURCE_DIR}/GameScripts/BuiltinComponentProbe.cs ${CMAKE_CURRENT_SOURCE_DIR}/GameScripts/AddComponentProbe.cs ${CMAKE_CURRENT_SOURCE_DIR}/GameScripts/FieldMetadataProbe.cs + ${CMAKE_CURRENT_SOURCE_DIR}/GameScripts/GetComponentsProbe.cs ${CMAKE_CURRENT_SOURCE_DIR}/GameScripts/ScriptComponentApiProbe.cs ${CMAKE_CURRENT_SOURCE_DIR}/GameScripts/RuntimeGameObjectProbe.cs ${CMAKE_CURRENT_SOURCE_DIR}/GameScripts/HierarchyProbe.cs diff --git a/managed/GameScripts/GetComponentsProbe.cs b/managed/GameScripts/GetComponentsProbe.cs new file mode 100644 index 00000000..aedb13ce --- /dev/null +++ b/managed/GameScripts/GetComponentsProbe.cs @@ -0,0 +1,49 @@ +using System; +using XCEngine; + +namespace Gameplay +{ + public sealed class GetComponentsProbe : MonoBehaviour + { + public int ObservedTransformCount; + public int ObservedCameraCount; + public int ObservedCameraCountViaGameObject; + public int ObservedMarkerCount; + public int ObservedMeshRendererCount; + public bool ObservedAllMarkersNonNull; + public bool ObservedFirstMarkerMatchesGetComponent; + public bool ObservedMarkerInstancesAreDistinct; + public bool ObservedTransformBoundToHost; + public string ObservedFirstMarkerHostName = string.Empty; + + public void Start() + { + Transform[] transforms = GetComponents(); + Camera[] cameras = GetComponents(); + Camera[] camerasViaGameObject = gameObject.GetComponents(); + ObjectApiMarkerProbe[] markers = GetComponents(); + MeshRenderer[] meshRenderers = GetComponents(); + ObjectApiMarkerProbe firstMarker = GetComponent(); + + ObservedTransformCount = transforms.Length; + ObservedCameraCount = cameras.Length; + ObservedCameraCountViaGameObject = camerasViaGameObject.Length; + ObservedMarkerCount = markers.Length; + ObservedMeshRendererCount = meshRenderers.Length; + ObservedAllMarkersNonNull = Array.TrueForAll(markers, marker => marker != null); + ObservedFirstMarkerMatchesGetComponent = firstMarker != null + && markers.Length > 0 + && ReferenceEquals(markers[0], firstMarker); + ObservedMarkerInstancesAreDistinct = markers.Length >= 2 + && !ReferenceEquals(markers[0], markers[1]); + ObservedTransformBoundToHost = transforms.Length == 1 + && transforms[0] != null + && transforms[0].GameObjectUUID == GameObjectUUID; + + if (markers.Length > 0 && markers[0] != null) + { + ObservedFirstMarkerHostName = markers[0].gameObject.name; + } + } + } +} diff --git a/managed/XCEngine.ScriptCore/Component.cs b/managed/XCEngine.ScriptCore/Component.cs index 0102cb69..970c951a 100644 --- a/managed/XCEngine.ScriptCore/Component.cs +++ b/managed/XCEngine.ScriptCore/Component.cs @@ -34,6 +34,11 @@ namespace XCEngine return GameObject.GetComponent(); } + public T[] GetComponents() where T : Component + { + return GameObject.GetComponents(); + } + public T GetComponentInChildren() where T : Component { return GameObject.GetComponentInChildren(); diff --git a/managed/XCEngine.ScriptCore/GameObject.cs b/managed/XCEngine.ScriptCore/GameObject.cs index 9cf49288..f2269a0d 100644 --- a/managed/XCEngine.ScriptCore/GameObject.cs +++ b/managed/XCEngine.ScriptCore/GameObject.cs @@ -67,6 +67,28 @@ namespace XCEngine return InternalCalls.GameObject_GetComponent(UUID, typeof(T)) as T; } + public T[] GetComponents() where T : Component + { + Component[] components = InternalCalls.GameObject_GetComponents(UUID, typeof(T)); + if (components == null || components.Length == 0) + { + return System.Array.Empty(); + } + + if (components is T[] typedComponents) + { + return typedComponents; + } + + T[] result = new T[components.Length]; + for (int index = 0; index < components.Length; ++index) + { + result[index] = components[index] as T; + } + + return result; + } + public T GetComponentInChildren() where T : Component { return InternalCalls.GameObject_GetComponentInChildren(UUID, typeof(T)) as T; diff --git a/managed/XCEngine.ScriptCore/InternalCalls.cs b/managed/XCEngine.ScriptCore/InternalCalls.cs index d7b93b7d..b2456791 100644 --- a/managed/XCEngine.ScriptCore/InternalCalls.cs +++ b/managed/XCEngine.ScriptCore/InternalCalls.cs @@ -86,6 +86,9 @@ namespace XCEngine [MethodImpl(MethodImplOptions.InternalCall)] internal static extern Component GameObject_GetComponent(ulong gameObjectUUID, Type componentType); + [MethodImpl(MethodImplOptions.InternalCall)] + internal static extern Component[] GameObject_GetComponents(ulong gameObjectUUID, Type componentType); + [MethodImpl(MethodImplOptions.InternalCall)] internal static extern Component GameObject_GetComponentInChildren(ulong gameObjectUUID, Type componentType); diff --git a/tests/scripting/test_mono_script_runtime.cpp b/tests/scripting/test_mono_script_runtime.cpp index d6e1f179..ff51c6e1 100644 --- a/tests/scripting/test_mono_script_runtime.cpp +++ b/tests/scripting/test_mono_script_runtime.cpp @@ -1920,6 +1920,62 @@ TEST_F(MonoScriptRuntimeTest, TransformHierarchyApiExposesParentChildAndReparent EXPECT_EQ(reparentTarget->GetChildCount(), 1u); } +TEST_F(MonoScriptRuntimeTest, UnityObjectApiGetComponentsReturnsDirectComponentsAndReusesManagedInstances) { + Scene* runtimeScene = CreateScene("MonoRuntimeScene"); + GameObject* host = runtimeScene->CreateGameObject("Host"); + GameObject* child = runtimeScene->CreateGameObject("Child", host); + + host->AddComponent(); + child->AddComponent(); + child->AddComponent(); + + AddScript(host, "Gameplay", "ObjectApiMarkerProbe"); + AddScript(host, "Gameplay", "ObjectApiMarkerProbe"); + ScriptComponent* hostProbe = AddScript(host, "Gameplay", "GetComponentsProbe"); + + engine->OnRuntimeStart(runtimeScene); + engine->OnUpdate(0.016f); + + int32_t observedTransformCount = -1; + int32_t observedCameraCount = -1; + int32_t observedCameraCountViaGameObject = -1; + int32_t observedMarkerCount = -1; + int32_t observedMeshRendererCount = -1; + bool observedAllMarkersNonNull = false; + bool observedFirstMarkerMatchesGetComponent = false; + bool observedMarkerInstancesAreDistinct = false; + bool observedTransformBoundToHost = false; + std::string observedFirstMarkerHostName; + + EXPECT_TRUE(runtime->TryGetFieldValue(hostProbe, "ObservedTransformCount", observedTransformCount)); + EXPECT_TRUE(runtime->TryGetFieldValue(hostProbe, "ObservedCameraCount", observedCameraCount)); + EXPECT_TRUE(runtime->TryGetFieldValue(hostProbe, "ObservedCameraCountViaGameObject", observedCameraCountViaGameObject)); + EXPECT_TRUE(runtime->TryGetFieldValue(hostProbe, "ObservedMarkerCount", observedMarkerCount)); + EXPECT_TRUE(runtime->TryGetFieldValue(hostProbe, "ObservedMeshRendererCount", observedMeshRendererCount)); + EXPECT_TRUE(runtime->TryGetFieldValue(hostProbe, "ObservedAllMarkersNonNull", observedAllMarkersNonNull)); + EXPECT_TRUE(runtime->TryGetFieldValue(hostProbe, "ObservedFirstMarkerMatchesGetComponent", observedFirstMarkerMatchesGetComponent)); + EXPECT_TRUE(runtime->TryGetFieldValue(hostProbe, "ObservedMarkerInstancesAreDistinct", observedMarkerInstancesAreDistinct)); + EXPECT_TRUE(runtime->TryGetFieldValue(hostProbe, "ObservedTransformBoundToHost", observedTransformBoundToHost)); + EXPECT_TRUE(runtime->TryGetFieldValue(hostProbe, "ObservedFirstMarkerHostName", observedFirstMarkerHostName)); + + EXPECT_EQ(observedTransformCount, 1); + EXPECT_EQ(observedCameraCount, 1); + EXPECT_EQ(observedCameraCountViaGameObject, 1); + EXPECT_EQ(observedMarkerCount, 2); + EXPECT_EQ(observedMeshRendererCount, 0); + EXPECT_TRUE(observedAllMarkersNonNull); + EXPECT_TRUE(observedFirstMarkerMatchesGetComponent); + EXPECT_TRUE(observedMarkerInstancesAreDistinct); + EXPECT_TRUE(observedTransformBoundToHost); + EXPECT_EQ(observedFirstMarkerHostName, "Host"); + + EXPECT_EQ(host->GetComponents().size(), 1u); + EXPECT_EQ(host->GetComponents().size(), 3u); + EXPECT_EQ(child->GetComponents().size(), 1u); + EXPECT_EQ(child->GetComponents().size(), 1u); + EXPECT_EQ(runtime->GetManagedInstanceCount(), 3u); +} + TEST_F(MonoScriptRuntimeTest, UnityObjectApiSupportsHierarchyLookupAndDestroy) { Scene* runtimeScene = CreateScene("MonoRuntimeScene"); GameObject* root = runtimeScene->CreateGameObject("Root");