From 5225faff1d57ef408702ffc72b746759de1923c5 Mon Sep 17 00:00:00 2001 From: ssdfasd <2156608475@qq.com> Date: Fri, 3 Apr 2026 14:31:07 +0800 Subject: [PATCH] Align Unity-style object and hierarchy scripting APIs --- docs/plan/C#脚本模块下一阶段计划.md | 265 +++++++++++++++++ .../Scripting/Mono/MonoScriptRuntime.h | 1 + .../src/Scripting/Mono/MonoScriptRuntime.cpp | 273 ++++++++++++++++-- managed/CMakeLists.txt | 2 + managed/GameScripts/ObjectApiProbe.cs | 111 +++++++ managed/XCEngine.ScriptCore/Component.cs | 12 +- managed/XCEngine.ScriptCore/GameObject.cs | 12 +- managed/XCEngine.ScriptCore/InternalCalls.cs | 9 + managed/XCEngine.ScriptCore/Object.cs | 19 ++ tests/scripting/test_mono_script_runtime.cpp | 83 ++++++ 10 files changed, 765 insertions(+), 22 deletions(-) create mode 100644 docs/plan/C#脚本模块下一阶段计划.md create mode 100644 managed/GameScripts/ObjectApiProbe.cs create mode 100644 managed/XCEngine.ScriptCore/Object.cs diff --git a/docs/plan/C#脚本模块下一阶段计划.md b/docs/plan/C#脚本模块下一阶段计划.md new file mode 100644 index 00000000..4e122121 --- /dev/null +++ b/docs/plan/C#脚本模块下一阶段计划.md @@ -0,0 +1,265 @@ +# C#脚本模块下一阶段计划 + +日期:2026-04-03 + +## 1. 当前阶段判断 + +C# 脚本模块已经完成了第一阶段的核心闭环,不再是“从 0 到 1 的原型验证”状态,而是进入了“从可运行走向可长期使用”的第二阶段。 + +当前已经完成的关键能力包括: + +1. `ScriptComponent + MonoBehaviour` 基本运行时模型已经建立 +2. `MonoScriptRuntime` 已经能加载程序集、发现脚本类、创建托管实例、调用生命周期 +3. `Awake / OnEnable / Start / FixedUpdate / Update / LateUpdate / OnDisable / OnDestroy` 已经打通 +4. `Debug.Log / LogWarning / LogError` 已经打通到 native logger +5. `Time`、`Input`、`GameObject`、`Transform`、`Behaviour.enabled`、部分内建组件包装已经可用 +6. 脚本字段存储、默认值、覆盖值、运行时值三层模型已经建立 +7. Play / Pause / Resume / Step 已经能驱动脚本生命周期 +8. editor 已经具备脚本类选择、字段 Inspector 编辑、项目脚本程序集重建与重载入口 + +这意味着下一阶段的重点,不再是“脚本能不能跑”,而是: + +1. Unity 风格 API 是否足够严格一致 +2. 项目级脚本工作流是否足够稳定 +3. 字段系统是否能支撑真实项目使用 + +--- + +## 2. 下一阶段总目标 + +下一阶段的总目标是: + +把当前“可运行的 C# 脚本运行时”收口成“命名、语义、工作流都更接近 Unity 的第一版可用脚本模块”。 + +具体聚焦三件事: + +1. **Unity API 严格对齐** +2. **项目脚本程序集链路收口** +3. **脚本字段系统扩展** + +--- + +## 3. 第一优先级:Unity API 严格对齐 + +这是当前最高优先级。 + +原因: + +1. 你已经明确要求 API 命名必须和 Unity C# API 保持严格一致 +2. 运行时主链路已经可用,当前最需要尽快收口的是 API 契约 +3. 如果 API 名字、属性名、行为语义先发散,后面越做越难回收 + +本阶段需要重点检查和补齐以下几类接口: + +### 3.1 基础对象层 + +优先补齐 Unity 风格的基础对象模型: + +1. `Object` +2. `Component` +3. `Behaviour` +4. `MonoBehaviour` + +重点不是“再做一套功能”,而是统一 API 入口和语义边界。 + +### 3.2 GameObject / Component 常用 API + +优先补齐最常用、最影响脚本编写体验的 Unity 接口: + +1. `GetComponent()` +2. `TryGetComponent(out T)` +3. `AddComponent()` +4. `GetComponents()` +5. `GetComponentInChildren()` +6. `GetComponentInParent()` +7. `CompareTag` +8. `tag` +9. `layer` + +其中前四项优先级最高。 + +### 3.3 Object 静态入口 + +优先补齐 Unity 风格最核心的对象静态方法: + +1. `Object.Destroy` +2. `Object.Instantiate` + +这一层非常关键,因为 Unity 用户的很多使用习惯都是围绕 `Object` 静态 API 建立的。 + +### 3.4 Transform 常用缺口补齐 + +当前 `Transform` 已经有较完整的空间变换接口,但还需要继续检查 Unity 一致性,重点看: + +1. 属性命名 +2. 重载形态 +3. `childCount` / `GetChild` +4. `parent` / `SetParent` +5. `Translate` / `Rotate` / `LookAt` +6. 常用便捷属性是否缺失 + +原则是: + +1. 已有 API 尽量不发散出自定义命名 +2. 优先补 Unity 常见重载 + +--- + +## 4. 第二优先级:项目脚本程序集链路收口 + +当前 editor 已经具备: + +1. `project/Assets` 扫描 +2. `GameScripts.dll` 编译 +3. `Rebuild Scripts` +4. `Reload Scripts` +5. Inspector 脚本类发现 + +但这一层还没有完全稳定,尤其是: + +1. 项目程序集输出目录依赖当前工作区状态 +2. 测试环境下 `project/Library/ScriptAssemblies` 还不稳定 +3. editor / 测试 / 项目目录三者之间还缺少更彻底的收口 + +这一阶段要完成的目标是: + +1. `project/Assets/*.cs -> Library/ScriptAssemblies/*.dll` 变成稳定链路 +2. editor 重建后总能看到最新脚本类 +3. 对应测试不依赖手工残留文件 +4. 项目程序集相关测试可以稳定进入常规回归集 + +这部分的目标不是继续扩 API,而是把工程化链路做稳。 + +--- + +## 5. 第三优先级:脚本字段系统扩展 + +当前字段系统已经支持: + +1. `float` +2. `double` +3. `bool` +4. `int32` +5. `uint64` +6. `string` +7. `Vector2` +8. `Vector3` +9. `Vector4` +10. `GameObject` 引用 + +下一阶段应继续向 Unity 常用字段模型推进,优先顺序建议如下: + +1. `enum` +2. `SerializeField` 支持 +3. private 可序列化字段 +4. 组件引用字段 +5. 数组 / List +6. 资源引用字段 + +其中最优先的是: + +1. `enum` +2. `[SerializeField]` +3. 组件引用字段 + +因为这三项最直接决定脚本是否能进入“真实可写”阶段。 + +--- + +## 6. editor 集成边界 + +这一阶段 editor 不再是完全不碰,但仍然不是主战场。 + +原则是: + +1. 只做支撑脚本模块使用所必需的 editor 收口 +2. 不把主要精力放在复杂编辑器体验优化上 +3. 继续优先保证 runtime、程序集链路、字段模型稳定 + +本阶段 editor 侧只建议继续做以下范围: + +1. 脚本类发现与重建流程稳定性 +2. Inspector 字段编辑最小闭环 +3. 类型新增后对应字段控件补齐 + +以下内容暂时不进入主线: + +1. 热重载 +2. 托管调试器 +3. 完整 Console/Exception 调试体验 +4. 高级脚本模板与自动生成工具 + +--- + +## 7. 本阶段不做的内容 + +为避免范围继续膨胀,下一阶段先明确不做: + +1. 域重载 / 热重载 +2. CoreCLR 切换 +3. IL2CPP / AOT +4. 完整 Unity 级别资源 API +5. 复杂序列化对象图 +6. 完整调试器接入 + +这些都可以进入后续阶段,但不应抢占当前优先级。 + +--- + +## 8. 建议执行顺序 + +建议严格按下面顺序推进: + +1. 先做 **Unity API 命名与契约收口** +2. 再做 **项目脚本程序集链路稳定化** +3. 再做 **字段系统扩展** +4. 再做 **新增字段类型对应的 Inspector 支持** +5. 最后再评估热重载、调试器、异常体验 + +原因很简单: + +1. API 契约不先收口,后面所有脚本都会建立在不稳定接口上 +2. 程序集链路不稳定,项目脚本工作流就不可靠 +3. 字段系统是把脚本从“可跑”推进到“可用”的关键 + +--- + +## 9. 验收标准 + +本阶段完成后,至少应满足: + +1. 核心托管 API 的命名与 Unity 保持一致,不再出现明显自定义发散 +2. `Object / GameObject / Component / Behaviour / MonoBehaviour / Transform` 的主干接口基本齐全 +3. 项目脚本程序集能稳定重建、重载、发现 +4. 项目级脚本程序集测试可以稳定跑通 +5. Inspector 能编辑扩展后的核心字段类型 +6. 脚本模块可以支撑真实项目写出第一批常规 gameplay 脚本 + +--- + +## 10. 当前结论 + +脚本模块现在已经完成了“第一阶段:核心运行时落地”。 + +下一阶段不应该再围绕“能不能跑”展开,而应该围绕: + +1. **Unity API 严格一致** +2. **项目工作流稳定** +3. **字段系统可用于真实开发** + +后续执行时,默认按这个优先级推进。 +## 阶段进展 2026-04-03 + +- 已完成第一步 Unity API 对齐收口: +- 新增 `XCEngine.Object` 基类。 +- 新增 `Object.Destroy(Object)` 静态销毁入口。 +- 新增 `GameObject / Component.GetComponentInChildren()`。 +- 新增 `GameObject / Component.GetComponentInParent()`。 +- 已补齐对应 Mono internal call 与 native 销毁路径。 +- 已新增运行时回归用例,验证层级查找、自身命中、组件销毁、GameObject 销毁。 +- 已通过 `MonoScriptRuntimeTest.*` 与 `ProjectScriptAssemblyTest.*` 相关整组验证。 + +- 下一步建议继续做第二批 Unity API 对齐: +- `GetComponents()` +- `Object.Instantiate` +- `tag / CompareTag / layer` diff --git a/engine/include/XCEngine/Scripting/Mono/MonoScriptRuntime.h b/engine/include/XCEngine/Scripting/Mono/MonoScriptRuntime.h index bce44086..0678fb43 100644 --- a/engine/include/XCEngine/Scripting/Mono/MonoScriptRuntime.h +++ b/engine/include/XCEngine/Scripting/Mono/MonoScriptRuntime.h @@ -67,6 +67,7 @@ public: size_t GetManagedInstanceCount() const { return m_instances.size(); } MonoObject* GetManagedInstanceObject(const ScriptComponent* component) const; MonoObject* CreateManagedComponentWrapper(MonoClass* componentClass, uint64_t gameObjectUUID); + bool DestroyManagedObject(MonoObject* managedObject); bool TryGetFieldValue( const ScriptComponent* component, diff --git a/engine/src/Scripting/Mono/MonoScriptRuntime.cpp b/engine/src/Scripting/Mono/MonoScriptRuntime.cpp index 69ede79a..48571d78 100644 --- a/engine/src/Scripting/Mono/MonoScriptRuntime.cpp +++ b/engine/src/Scripting/Mono/MonoScriptRuntime.cpp @@ -109,18 +109,8 @@ MonoScriptRuntime* GetActiveMonoScriptRuntime() { return dynamic_cast(ScriptEngine::Get().GetRuntime()); } -ManagedComponentTypeInfo ResolveManagedComponentTypeInfo(MonoReflectionType* reflectionType) { +ManagedComponentTypeInfo ResolveManagedComponentTypeInfo(MonoClass* monoClass) { ManagedComponentTypeInfo typeInfo; - if (!reflectionType) { - return typeInfo; - } - - MonoType* monoType = mono_reflection_type_get_type(reflectionType); - if (!monoType) { - return typeInfo; - } - - MonoClass* monoClass = mono_class_from_mono_type(monoType); if (!monoClass) { return typeInfo; } @@ -163,6 +153,19 @@ ManagedComponentTypeInfo ResolveManagedComponentTypeInfo(MonoReflectionType* ref return typeInfo; } +ManagedComponentTypeInfo ResolveManagedComponentTypeInfo(MonoReflectionType* reflectionType) { + if (!reflectionType) { + return {}; + } + + MonoType* monoType = mono_reflection_type_get_type(reflectionType); + if (!monoType) { + return {}; + } + + return ResolveManagedComponentTypeInfo(mono_class_from_mono_type(monoType)); +} + Components::GameObject* FindGameObjectByUUIDRecursive(Components::GameObject* gameObject, uint64_t uuid) { if (!gameObject) { return nullptr; @@ -231,28 +234,32 @@ ScriptComponent* FindScriptComponentByUUID(uint64_t scriptComponentUUID) { return nullptr; } -bool HasNativeComponent(Components::GameObject* gameObject, ManagedComponentKind componentKind) { +Components::Component* FindNativeComponent(Components::GameObject* gameObject, ManagedComponentKind componentKind) { if (!gameObject) { - return false; + return nullptr; } switch (componentKind) { case ManagedComponentKind::Transform: - return gameObject->GetTransform() != nullptr; + return gameObject->GetTransform(); case ManagedComponentKind::Camera: - return gameObject->GetComponent() != nullptr; + return gameObject->GetComponent(); case ManagedComponentKind::Light: - return gameObject->GetComponent() != nullptr; + return gameObject->GetComponent(); case ManagedComponentKind::MeshFilter: - return gameObject->GetComponent() != nullptr; + return gameObject->GetComponent(); case ManagedComponentKind::MeshRenderer: - return gameObject->GetComponent() != nullptr; + return gameObject->GetComponent(); case ManagedComponentKind::Script: case ManagedComponentKind::Unknown: - return false; + return nullptr; } - return false; + return nullptr; +} + +bool HasNativeComponent(Components::GameObject* gameObject, ManagedComponentKind componentKind) { + return FindNativeComponent(gameObject, componentKind) != nullptr; } Components::Component* AddOrGetNativeComponent(Components::GameObject* gameObject, ManagedComponentKind componentKind) { @@ -309,6 +316,87 @@ ScriptComponent* FindMatchingScriptComponent( return nullptr; } +ScriptComponent* FindMatchingScriptComponentInChildren( + Components::GameObject* gameObject, + const ManagedComponentTypeInfo& typeInfo) { + if (!gameObject) { + return nullptr; + } + + if (ScriptComponent* component = FindMatchingScriptComponent(gameObject, typeInfo)) { + return component; + } + + for (Components::GameObject* child : gameObject->GetChildren()) { + if (ScriptComponent* component = FindMatchingScriptComponentInChildren(child, typeInfo)) { + return component; + } + } + + return nullptr; +} + +ScriptComponent* FindMatchingScriptComponentInParent( + Components::GameObject* gameObject, + const ManagedComponentTypeInfo& typeInfo) { + while (gameObject) { + if (ScriptComponent* component = FindMatchingScriptComponent(gameObject, typeInfo)) { + return component; + } + + gameObject = gameObject->GetParent(); + } + + return nullptr; +} + +Components::Component* FindNativeComponentInChildren( + Components::GameObject* gameObject, + ManagedComponentKind componentKind) { + if (!gameObject) { + return nullptr; + } + + if (Components::Component* component = FindNativeComponent(gameObject, componentKind)) { + return component; + } + + for (Components::GameObject* child : gameObject->GetChildren()) { + if (Components::Component* component = FindNativeComponentInChildren(child, componentKind)) { + return component; + } + } + + return nullptr; +} + +Components::Component* FindNativeComponentInParent( + Components::GameObject* gameObject, + ManagedComponentKind componentKind) { + while (gameObject) { + if (Components::Component* component = FindNativeComponent(gameObject, componentKind)) { + return component; + } + + gameObject = gameObject->GetParent(); + } + + return nullptr; +} + +bool DestroyNativeComponentInstance(Components::GameObject* gameObject, Components::Component* component) { + if (!gameObject || !component || component == gameObject->GetTransform()) { + return false; + } + + if (component->IsEnabled() && gameObject->IsActiveInHierarchy()) { + component->OnDisable(); + } + + component->OnDestroy(); + return gameObject->RemoveComponent(component); +} + Components::CameraComponent* FindCameraComponent(uint64_t gameObjectUUID) { Components::GameObject* gameObject = FindGameObjectByUUID(gameObjectUUID); return gameObject ? gameObject->GetComponent() : nullptr; @@ -520,6 +608,72 @@ MonoObject* InternalCall_GameObject_GetComponent(uint64_t gameObjectUUID, MonoRe return runtime->CreateManagedComponentWrapper(typeInfo.monoClass, gameObjectUUID); } +MonoObject* InternalCall_GameObject_GetComponentInChildren(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.kind == ManagedComponentKind::Script) { + ScriptComponent* component = FindMatchingScriptComponentInChildren(gameObject, typeInfo); + if (!component) { + return nullptr; + } + + if (!runtime->HasManagedInstance(component)) { + ScriptEngine::Get().OnScriptComponentEnabled(component); + } + + return runtime->GetManagedInstanceObject(component); + } + + Components::Component* component = FindNativeComponentInChildren(gameObject, typeInfo.kind); + if (!component || !typeInfo.monoClass) { + return nullptr; + } + + return runtime->CreateManagedComponentWrapper(typeInfo.monoClass, component->GetGameObject()->GetUUID()); +} + +MonoObject* InternalCall_GameObject_GetComponentInParent(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.kind == ManagedComponentKind::Script) { + ScriptComponent* component = FindMatchingScriptComponentInParent(gameObject, typeInfo); + if (!component) { + return nullptr; + } + + if (!runtime->HasManagedInstance(component)) { + ScriptEngine::Get().OnScriptComponentEnabled(component); + } + + return runtime->GetManagedInstanceObject(component); + } + + Components::Component* component = FindNativeComponentInParent(gameObject, typeInfo.kind); + if (!component || !typeInfo.monoClass) { + return nullptr; + } + + return runtime->CreateManagedComponentWrapper(typeInfo.monoClass, component->GetGameObject()->GetUUID()); +} + MonoObject* InternalCall_GameObject_AddComponent(uint64_t gameObjectUUID, MonoReflectionType* componentType) { Components::GameObject* gameObject = FindGameObjectByUUID(gameObjectUUID); if (!gameObject) { @@ -588,6 +742,15 @@ void InternalCall_GameObject_Destroy(uint64_t gameObjectUUID) { scene->DestroyGameObject(gameObject); } +void InternalCall_Object_Destroy(MonoObject* object) { + MonoScriptRuntime* runtime = GetActiveMonoScriptRuntime(); + if (!runtime || !object) { + return; + } + + runtime->DestroyManagedObject(object); +} + mono_bool InternalCall_Behaviour_GetEnabled(uint64_t scriptComponentUUID) { ScriptComponent* component = FindScriptComponentByUUID(scriptComponentUUID); return (component && component->IsEnabled()) ? 1 : 0; @@ -1241,10 +1404,13 @@ 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_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)); 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::Object_Destroy", reinterpret_cast(&InternalCall_Object_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)); @@ -2224,6 +2390,73 @@ MonoObject* MonoScriptRuntime::CreateManagedGameObject(uint64_t gameObjectUUID) return managedObject; } +bool MonoScriptRuntime::DestroyManagedObject(MonoObject* managedObject) { + if (!m_initialized || !managedObject) { + return false; + } + + SetCurrentDomain(); + + MonoClass* monoClass = mono_object_get_class(managedObject); + if (!monoClass) { + return false; + } + + if (monoClass == m_gameObjectClass) { + uint64_t gameObjectUUID = 0; + mono_field_get_value(managedObject, m_managedGameObjectUUIDField, &gameObjectUUID); + + Components::Scene* scene = GetInternalCallScene(); + Components::GameObject* gameObject = FindGameObjectByUUID(gameObjectUUID); + if (!scene || !gameObject || gameObject->GetScene() != scene) { + return false; + } + + scene->DestroyGameObject(gameObject); + return true; + } + + if (monoClass != m_componentClass && !mono_class_is_subclass_of(monoClass, m_componentClass, false)) { + return false; + } + + uint64_t gameObjectUUID = 0; + mono_field_get_value(managedObject, m_gameObjectUUIDField, &gameObjectUUID); + + Components::GameObject* gameObject = FindGameObjectByUUID(gameObjectUUID); + if (!gameObject) { + return false; + } + + if (monoClass == m_behaviourClass || mono_class_is_subclass_of(monoClass, m_behaviourClass, false)) { + uint64_t scriptComponentUUID = 0; + mono_field_get_value(managedObject, m_scriptComponentUUIDField, &scriptComponentUUID); + + if (scriptComponentUUID != 0) { + ScriptComponent* component = FindScriptComponentByUUID(scriptComponentUUID); + return DestroyNativeComponentInstance(gameObject, component); + } + } + + const ManagedComponentTypeInfo typeInfo = ResolveManagedComponentTypeInfo(monoClass); + switch (typeInfo.kind) { + case ManagedComponentKind::Camera: + return DestroyNativeComponentInstance(gameObject, gameObject->GetComponent()); + case ManagedComponentKind::Light: + return DestroyNativeComponentInstance(gameObject, gameObject->GetComponent()); + case ManagedComponentKind::MeshFilter: + return DestroyNativeComponentInstance(gameObject, gameObject->GetComponent()); + case ManagedComponentKind::MeshRenderer: + return DestroyNativeComponentInstance(gameObject, gameObject->GetComponent()); + case ManagedComponentKind::Transform: + case ManagedComponentKind::Script: + case ManagedComponentKind::Unknown: + return false; + } + + return false; +} + bool MonoScriptRuntime::TryExtractGameObjectReference( MonoObject* managedObject, GameObjectReference& outReference) const { diff --git a/managed/CMakeLists.txt b/managed/CMakeLists.txt index 16a2c99c..bee362d0 100644 --- a/managed/CMakeLists.txt +++ b/managed/CMakeLists.txt @@ -87,6 +87,7 @@ set(XCENGINE_SCRIPT_CORE_SOURCES ${CMAKE_CURRENT_SOURCE_DIR}/XCEngine.ScriptCore/MeshFilter.cs ${CMAKE_CURRENT_SOURCE_DIR}/XCEngine.ScriptCore/MeshRenderer.cs ${CMAKE_CURRENT_SOURCE_DIR}/XCEngine.ScriptCore/MonoBehaviour.cs + ${CMAKE_CURRENT_SOURCE_DIR}/XCEngine.ScriptCore/Object.cs ${CMAKE_CURRENT_SOURCE_DIR}/XCEngine.ScriptCore/Quaternion.cs ${CMAKE_CURRENT_SOURCE_DIR}/XCEngine.ScriptCore/Space.cs ${CMAKE_CURRENT_SOURCE_DIR}/XCEngine.ScriptCore/Time.cs @@ -107,6 +108,7 @@ set(XCENGINE_GAME_SCRIPT_SOURCES ${CMAKE_CURRENT_SOURCE_DIR}/GameScripts/LifecycleProbe.cs ${CMAKE_CURRENT_SOURCE_DIR}/GameScripts/MeshComponentProbe.cs ${CMAKE_CURRENT_SOURCE_DIR}/GameScripts/MeshRendererEdgeCaseProbe.cs + ${CMAKE_CURRENT_SOURCE_DIR}/GameScripts/ObjectApiProbe.cs ${CMAKE_CURRENT_SOURCE_DIR}/GameScripts/TickLogProbe.cs ${CMAKE_CURRENT_SOURCE_DIR}/GameScripts/TransformConversionProbe.cs ${CMAKE_CURRENT_SOURCE_DIR}/GameScripts/TransformMotionProbe.cs diff --git a/managed/GameScripts/ObjectApiProbe.cs b/managed/GameScripts/ObjectApiProbe.cs new file mode 100644 index 00000000..3b89f622 --- /dev/null +++ b/managed/GameScripts/ObjectApiProbe.cs @@ -0,0 +1,111 @@ +using XCEngine; + +namespace Gameplay +{ + public sealed class ObjectApiMarkerProbe : MonoBehaviour + { + } + + public sealed class ObjectApiDestroyTargetProbe : MonoBehaviour + { + public int DisableCount; + + public void OnDisable() + { + DisableCount++; + } + } + + public sealed class ObjectApiProbe : MonoBehaviour + { + public bool FoundSelfScriptViaParentLookup; + public bool FoundSelfScriptViaGameObjectParentLookup; + public bool FoundSelfTransformViaChildrenLookup; + public bool FoundSelfTransformViaGameObjectChildrenLookup; + public bool FoundCameraInParent; + public bool FoundCameraInParentViaGameObject; + public bool FoundMarkerInParent; + public bool FoundMeshRendererInChildren; + public bool FoundMeshRendererInChildrenViaGameObject; + public bool FoundTargetScriptInChildren; + public string ObservedParentCameraHostName = string.Empty; + public string ObservedChildMeshRendererHostName = string.Empty; + public string ObservedChildScriptHostName = string.Empty; + public bool ChildCameraMissingAfterDestroy; + public bool ChildScriptMissingAfterDestroy; + public bool ChildGameObjectMissingAfterDestroy; + public int ObservedChildScriptDisableCount = -1; + public int ObservedChildCountAfterDestroy = -1; + + public void Start() + { + ObjectApiProbe selfViaParent = GetComponentInParent(); + ObjectApiProbe selfViaGameObjectParent = gameObject.GetComponentInParent(); + Transform selfViaChildren = GetComponentInChildren(); + Transform selfViaGameObjectChildren = gameObject.GetComponentInChildren(); + + FoundSelfScriptViaParentLookup = selfViaParent == this; + FoundSelfScriptViaGameObjectParentLookup = selfViaGameObjectParent == this; + FoundSelfTransformViaChildrenLookup = selfViaChildren != null + && selfViaChildren.GameObjectUUID == GameObjectUUID; + FoundSelfTransformViaGameObjectChildrenLookup = selfViaGameObjectChildren != null + && selfViaGameObjectChildren.GameObjectUUID == GameObjectUUID; + + Camera parentCamera = GetComponentInParent(); + Camera parentCameraViaGameObject = gameObject.GetComponentInParent(); + ObjectApiMarkerProbe parentMarker = GetComponentInParent(); + MeshRenderer childMeshRenderer = GetComponentInChildren(); + MeshRenderer childMeshRendererViaGameObject = gameObject.GetComponentInChildren(); + ObjectApiDestroyTargetProbe childScript = GetComponentInChildren(); + + FoundCameraInParent = parentCamera != null; + FoundCameraInParentViaGameObject = parentCameraViaGameObject != null; + FoundMarkerInParent = parentMarker != null; + FoundMeshRendererInChildren = childMeshRenderer != null; + FoundMeshRendererInChildrenViaGameObject = childMeshRendererViaGameObject != null; + FoundTargetScriptInChildren = childScript != null; + + if (parentCamera != null) + { + ObservedParentCameraHostName = parentCamera.gameObject.name; + } + + if (childMeshRenderer != null) + { + ObservedChildMeshRendererHostName = childMeshRenderer.gameObject.name; + } + + if (childScript != null) + { + ObservedChildScriptHostName = childScript.gameObject.name; + } + + Transform firstChild = transform.childCount > 0 ? transform.GetChild(0) : null; + GameObject child = firstChild != null ? firstChild.gameObject : null; + if (child == null) + { + return; + } + + Camera childCamera = child.GetComponent(); + if (childCamera != null) + { + Destroy(childCamera); + } + + ChildCameraMissingAfterDestroy = child.GetComponent() == null; + + if (childScript != null) + { + Destroy(childScript); + ObservedChildScriptDisableCount = childScript.DisableCount; + } + + ChildScriptMissingAfterDestroy = child.GetComponent() == null; + + Destroy(child); + ChildGameObjectMissingAfterDestroy = GameObject.Find("Child") == null; + ObservedChildCountAfterDestroy = transform.childCount; + } + } +} diff --git a/managed/XCEngine.ScriptCore/Component.cs b/managed/XCEngine.ScriptCore/Component.cs index 5585dbb2..0102cb69 100644 --- a/managed/XCEngine.ScriptCore/Component.cs +++ b/managed/XCEngine.ScriptCore/Component.cs @@ -3,7 +3,7 @@ using System.Reflection; namespace XCEngine { - public abstract class Component + public abstract class Component : Object { internal ulong m_gameObjectUUID; @@ -34,6 +34,16 @@ namespace XCEngine return GameObject.GetComponent(); } + public T GetComponentInChildren() where T : Component + { + return GameObject.GetComponentInChildren(); + } + + public T GetComponentInParent() where T : Component + { + return GameObject.GetComponentInParent(); + } + public T AddComponent() where T : Component { return GameObject.AddComponent(); diff --git a/managed/XCEngine.ScriptCore/GameObject.cs b/managed/XCEngine.ScriptCore/GameObject.cs index 269baa54..9cf49288 100644 --- a/managed/XCEngine.ScriptCore/GameObject.cs +++ b/managed/XCEngine.ScriptCore/GameObject.cs @@ -1,6 +1,6 @@ namespace XCEngine { - public sealed class GameObject + public sealed class GameObject : Object { private readonly ulong m_uuid; @@ -67,6 +67,16 @@ namespace XCEngine return InternalCalls.GameObject_GetComponent(UUID, typeof(T)) as T; } + public T GetComponentInChildren() where T : Component + { + return InternalCalls.GameObject_GetComponentInChildren(UUID, typeof(T)) as T; + } + + public T GetComponentInParent() where T : Component + { + return InternalCalls.GameObject_GetComponentInParent(UUID, typeof(T)) as T; + } + public T AddComponent() where T : Component { return InternalCalls.GameObject_AddComponent(UUID, typeof(T)) as T; diff --git a/managed/XCEngine.ScriptCore/InternalCalls.cs b/managed/XCEngine.ScriptCore/InternalCalls.cs index 112e602a..d7b93b7d 100644 --- a/managed/XCEngine.ScriptCore/InternalCalls.cs +++ b/managed/XCEngine.ScriptCore/InternalCalls.cs @@ -86,6 +86,12 @@ namespace XCEngine [MethodImpl(MethodImplOptions.InternalCall)] internal static extern Component GameObject_GetComponent(ulong gameObjectUUID, Type componentType); + [MethodImpl(MethodImplOptions.InternalCall)] + internal static extern Component GameObject_GetComponentInChildren(ulong gameObjectUUID, Type componentType); + + [MethodImpl(MethodImplOptions.InternalCall)] + internal static extern Component GameObject_GetComponentInParent(ulong gameObjectUUID, Type componentType); + [MethodImpl(MethodImplOptions.InternalCall)] internal static extern Component GameObject_AddComponent(ulong gameObjectUUID, Type componentType); @@ -98,6 +104,9 @@ namespace XCEngine [MethodImpl(MethodImplOptions.InternalCall)] internal static extern void GameObject_Destroy(ulong gameObjectUUID); + [MethodImpl(MethodImplOptions.InternalCall)] + internal static extern void Object_Destroy(global::XCEngine.Object obj); + [MethodImpl(MethodImplOptions.InternalCall)] internal static extern bool Behaviour_GetEnabled(ulong scriptComponentUUID); diff --git a/managed/XCEngine.ScriptCore/Object.cs b/managed/XCEngine.ScriptCore/Object.cs new file mode 100644 index 00000000..ca5e1d74 --- /dev/null +++ b/managed/XCEngine.ScriptCore/Object.cs @@ -0,0 +1,19 @@ +namespace XCEngine +{ + public abstract class Object + { + protected Object() + { + } + + public static void Destroy(Object obj) + { + if (obj == null) + { + return; + } + + InternalCalls.Object_Destroy(obj); + } + } +} diff --git a/tests/scripting/test_mono_script_runtime.cpp b/tests/scripting/test_mono_script_runtime.cpp index 43db1513..d6e1f179 100644 --- a/tests/scripting/test_mono_script_runtime.cpp +++ b/tests/scripting/test_mono_script_runtime.cpp @@ -1920,6 +1920,89 @@ TEST_F(MonoScriptRuntimeTest, TransformHierarchyApiExposesParentChildAndReparent EXPECT_EQ(reparentTarget->GetChildCount(), 1u); } +TEST_F(MonoScriptRuntimeTest, UnityObjectApiSupportsHierarchyLookupAndDestroy) { + Scene* runtimeScene = CreateScene("MonoRuntimeScene"); + GameObject* root = runtimeScene->CreateGameObject("Root"); + GameObject* host = runtimeScene->CreateGameObject("Host", root); + GameObject* child = runtimeScene->CreateGameObject("Child", host); + + root->AddComponent(); + child->AddComponent(); + child->AddComponent(); + + AddScript(root, "Gameplay", "ObjectApiMarkerProbe"); + AddScript(child, "Gameplay", "ObjectApiDestroyTargetProbe"); + ScriptComponent* hostProbe = AddScript(host, "Gameplay", "ObjectApiProbe"); + + engine->OnRuntimeStart(runtimeScene); + engine->OnUpdate(0.016f); + + bool foundSelfScriptViaParentLookup = false; + bool foundSelfScriptViaGameObjectParentLookup = false; + bool foundSelfTransformViaChildrenLookup = false; + bool foundSelfTransformViaGameObjectChildrenLookup = false; + bool foundCameraInParent = false; + bool foundCameraInParentViaGameObject = false; + bool foundMarkerInParent = false; + bool foundMeshRendererInChildren = false; + bool foundMeshRendererInChildrenViaGameObject = false; + bool foundTargetScriptInChildren = false; + bool childCameraMissingAfterDestroy = false; + bool childScriptMissingAfterDestroy = false; + bool childGameObjectMissingAfterDestroy = false; + int32_t observedChildScriptDisableCount = -1; + int32_t observedChildCountAfterDestroy = -1; + std::string observedParentCameraHostName; + std::string observedChildMeshRendererHostName; + std::string observedChildScriptHostName; + + EXPECT_TRUE(runtime->TryGetFieldValue(hostProbe, "FoundSelfScriptViaParentLookup", foundSelfScriptViaParentLookup)); + EXPECT_TRUE(runtime->TryGetFieldValue(hostProbe, "FoundSelfScriptViaGameObjectParentLookup", foundSelfScriptViaGameObjectParentLookup)); + EXPECT_TRUE(runtime->TryGetFieldValue(hostProbe, "FoundSelfTransformViaChildrenLookup", foundSelfTransformViaChildrenLookup)); + EXPECT_TRUE(runtime->TryGetFieldValue(hostProbe, "FoundSelfTransformViaGameObjectChildrenLookup", foundSelfTransformViaGameObjectChildrenLookup)); + EXPECT_TRUE(runtime->TryGetFieldValue(hostProbe, "FoundCameraInParent", foundCameraInParent)); + EXPECT_TRUE(runtime->TryGetFieldValue(hostProbe, "FoundCameraInParentViaGameObject", foundCameraInParentViaGameObject)); + EXPECT_TRUE(runtime->TryGetFieldValue(hostProbe, "FoundMarkerInParent", foundMarkerInParent)); + EXPECT_TRUE(runtime->TryGetFieldValue(hostProbe, "FoundMeshRendererInChildren", foundMeshRendererInChildren)); + EXPECT_TRUE(runtime->TryGetFieldValue(hostProbe, "FoundMeshRendererInChildrenViaGameObject", foundMeshRendererInChildrenViaGameObject)); + EXPECT_TRUE(runtime->TryGetFieldValue(hostProbe, "FoundTargetScriptInChildren", foundTargetScriptInChildren)); + EXPECT_TRUE(runtime->TryGetFieldValue(hostProbe, "ChildCameraMissingAfterDestroy", childCameraMissingAfterDestroy)); + EXPECT_TRUE(runtime->TryGetFieldValue(hostProbe, "ChildScriptMissingAfterDestroy", childScriptMissingAfterDestroy)); + EXPECT_TRUE(runtime->TryGetFieldValue(hostProbe, "ChildGameObjectMissingAfterDestroy", childGameObjectMissingAfterDestroy)); + EXPECT_TRUE(runtime->TryGetFieldValue(hostProbe, "ObservedChildScriptDisableCount", observedChildScriptDisableCount)); + EXPECT_TRUE(runtime->TryGetFieldValue(hostProbe, "ObservedChildCountAfterDestroy", observedChildCountAfterDestroy)); + EXPECT_TRUE(runtime->TryGetFieldValue(hostProbe, "ObservedParentCameraHostName", observedParentCameraHostName)); + EXPECT_TRUE(runtime->TryGetFieldValue(hostProbe, "ObservedChildMeshRendererHostName", observedChildMeshRendererHostName)); + EXPECT_TRUE(runtime->TryGetFieldValue(hostProbe, "ObservedChildScriptHostName", observedChildScriptHostName)); + + EXPECT_TRUE(foundSelfScriptViaParentLookup); + EXPECT_TRUE(foundSelfScriptViaGameObjectParentLookup); + EXPECT_TRUE(foundSelfTransformViaChildrenLookup); + EXPECT_TRUE(foundSelfTransformViaGameObjectChildrenLookup); + EXPECT_TRUE(foundCameraInParent); + EXPECT_TRUE(foundCameraInParentViaGameObject); + EXPECT_TRUE(foundMarkerInParent); + EXPECT_TRUE(foundMeshRendererInChildren); + EXPECT_TRUE(foundMeshRendererInChildrenViaGameObject); + EXPECT_TRUE(foundTargetScriptInChildren); + EXPECT_TRUE(childCameraMissingAfterDestroy); + EXPECT_TRUE(childScriptMissingAfterDestroy); + EXPECT_TRUE(childGameObjectMissingAfterDestroy); + EXPECT_EQ(observedChildScriptDisableCount, 1); + EXPECT_EQ(observedChildCountAfterDestroy, 0); + EXPECT_EQ(observedParentCameraHostName, "Root"); + EXPECT_EQ(observedChildMeshRendererHostName, "Child"); + EXPECT_EQ(observedChildScriptHostName, "Child"); + + EXPECT_EQ(runtimeScene->Find("Child"), nullptr); + EXPECT_EQ(host->GetChildCount(), 0u); + EXPECT_EQ(host->GetComponentInChildren(), nullptr); + EXPECT_EQ(host->GetComponentInChildren(), nullptr); + EXPECT_EQ(host->GetComponents().size(), 1u); + EXPECT_EQ(root->GetComponents().size(), 1u); + EXPECT_EQ(runtime->GetManagedInstanceCount(), 2u); +} + TEST_F(MonoScriptRuntimeTest, TransformSpaceApiReadsAndWritesWorldAndLocalValues) { Scene* runtimeScene = CreateScene("MonoRuntimeScene"); GameObject* parent = runtimeScene->CreateGameObject("Parent");