Add Unity-style GetComponents scripting API

This commit is contained in:
2026-04-03 14:51:52 +08:00
parent 5225faff1d
commit 0f51f553c8
8 changed files with 240 additions and 10 deletions

View File

@@ -260,6 +260,6 @@ C# 脚本模块已经完成了第一阶段的核心闭环,不再是“从 0
- 已通过 `MonoScriptRuntimeTest.*``ProjectScriptAssemblyTest.*` 相关整组验证。
- 下一步建议继续做第二批 Unity API 对齐:
- `GetComponents<T>()`
- `Object.Instantiate`
- `tag / CompareTag / layer`
- 已完成第二步第一小项:`GetComponents<T>()`
- 下一步候选:`Object.Instantiate`
- 下一步候选:`tag / CompareTag / layer`

View File

@@ -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<ScriptComponent>()) {
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<MonoObject*>& components) {
if (!componentClass) {
return nullptr;
}
MonoDomain* domain = mono_domain_get();
if (!domain) {
return nullptr;
}
MonoArray* array = mono_array_new(domain, componentClass, static_cast<uintptr_t>(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<MonoObject*> 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<ScriptComponent>()) {
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<Components::TransformComponent>());
break;
case ManagedComponentKind::Camera:
appendNativeComponents(gameObject->GetComponents<Components::CameraComponent>());
break;
case ManagedComponentKind::Light:
appendNativeComponents(gameObject->GetComponents<Components::LightComponent>());
break;
case ManagedComponentKind::MeshFilter:
appendNativeComponents(gameObject->GetComponents<Components::MeshFilterComponent>());
break;
case ManagedComponentKind::MeshRenderer:
appendNativeComponents(gameObject->GetComponents<Components::MeshRendererComponent>());
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<const void*>(&InternalCall_GameObject_SetActive));
mono_add_internal_call("XCEngine.InternalCalls::GameObject_HasComponent", reinterpret_cast<const void*>(&InternalCall_GameObject_HasComponent));
mono_add_internal_call("XCEngine.InternalCalls::GameObject_GetComponent", reinterpret_cast<const void*>(&InternalCall_GameObject_GetComponent));
mono_add_internal_call("XCEngine.InternalCalls::GameObject_GetComponents", reinterpret_cast<const void*>(&InternalCall_GameObject_GetComponents));
mono_add_internal_call("XCEngine.InternalCalls::GameObject_GetComponentInChildren", reinterpret_cast<const void*>(&InternalCall_GameObject_GetComponentInChildren));
mono_add_internal_call("XCEngine.InternalCalls::GameObject_GetComponentInParent", reinterpret_cast<const void*>(&InternalCall_GameObject_GetComponentInParent));
mono_add_internal_call("XCEngine.InternalCalls::GameObject_AddComponent", reinterpret_cast<const void*>(&InternalCall_GameObject_AddComponent));

View File

@@ -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

View File

@@ -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<Transform>();
Camera[] cameras = GetComponents<Camera>();
Camera[] camerasViaGameObject = gameObject.GetComponents<Camera>();
ObjectApiMarkerProbe[] markers = GetComponents<ObjectApiMarkerProbe>();
MeshRenderer[] meshRenderers = GetComponents<MeshRenderer>();
ObjectApiMarkerProbe firstMarker = GetComponent<ObjectApiMarkerProbe>();
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;
}
}
}
}

View File

@@ -34,6 +34,11 @@ namespace XCEngine
return GameObject.GetComponent<T>();
}
public T[] GetComponents<T>() where T : Component
{
return GameObject.GetComponents<T>();
}
public T GetComponentInChildren<T>() where T : Component
{
return GameObject.GetComponentInChildren<T>();

View File

@@ -67,6 +67,28 @@ namespace XCEngine
return InternalCalls.GameObject_GetComponent(UUID, typeof(T)) as T;
}
public T[] GetComponents<T>() where T : Component
{
Component[] components = InternalCalls.GameObject_GetComponents(UUID, typeof(T));
if (components == null || components.Length == 0)
{
return System.Array.Empty<T>();
}
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<T>() where T : Component
{
return InternalCalls.GameObject_GetComponentInChildren(UUID, typeof(T)) as T;

View File

@@ -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);

View File

@@ -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<CameraComponent>();
child->AddComponent<CameraComponent>();
child->AddComponent<MeshRendererComponent>();
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<CameraComponent>().size(), 1u);
EXPECT_EQ(host->GetComponents<ScriptComponent>().size(), 3u);
EXPECT_EQ(child->GetComponents<CameraComponent>().size(), 1u);
EXPECT_EQ(child->GetComponents<MeshRendererComponent>().size(), 1u);
EXPECT_EQ(runtime->GetManagedInstanceCount(), 3u);
}
TEST_F(MonoScriptRuntimeTest, UnityObjectApiSupportsHierarchyLookupAndDestroy) {
Scene* runtimeScene = CreateScene("MonoRuntimeScene");
GameObject* root = runtimeScene->CreateGameObject("Root");