feat(scripting): support managed script component api

This commit is contained in:
2026-03-28 00:43:13 +08:00
parent 6bde199393
commit e522bad582
8 changed files with 349 additions and 37 deletions

View File

@@ -53,6 +53,8 @@ public:
bool HasManagedInstance(const ScriptComponent* component) const;
size_t GetManagedInstanceCount() const { return m_instances.size(); }
MonoObject* GetManagedInstanceObject(const ScriptComponent* component) const;
MonoObject* CreateManagedComponentWrapper(MonoClass* componentClass, uint64_t gameObjectUUID);
bool TryGetFieldValue(
const ScriptComponent* component,

View File

@@ -9,6 +9,7 @@
#include "Debug/Logger.h"
#include "Scene/Scene.h"
#include "Scripting/ScriptComponent.h"
#include "Scripting/ScriptEngine.h"
#include <mono/jit/jit.h>
#include <mono/metadata/appdomain.h>
@@ -38,6 +39,7 @@ struct MonoRootState {
enum class ManagedComponentKind {
Unknown,
Script,
Transform,
Camera,
Light,
@@ -45,6 +47,14 @@ enum class ManagedComponentKind {
MeshRenderer,
};
struct ManagedComponentTypeInfo {
ManagedComponentKind kind = ManagedComponentKind::Unknown;
MonoClass* monoClass = nullptr;
std::string assemblyName;
std::string namespaceName;
std::string className;
};
MonoRootState& GetMonoRootState() {
static MonoRootState state;
return state;
@@ -94,40 +104,62 @@ std::string MonoStringToUtf8(MonoString* stringObject) {
return result;
}
ManagedComponentKind ResolveManagedComponentKind(MonoReflectionType* reflectionType) {
MonoScriptRuntime* GetActiveMonoScriptRuntime() {
return dynamic_cast<MonoScriptRuntime*>(ScriptEngine::Get().GetRuntime());
}
ManagedComponentTypeInfo ResolveManagedComponentTypeInfo(MonoReflectionType* reflectionType) {
ManagedComponentTypeInfo typeInfo;
if (!reflectionType) {
return ManagedComponentKind::Unknown;
return typeInfo;
}
MonoType* monoType = mono_reflection_type_get_type(reflectionType);
if (!monoType) {
return ManagedComponentKind::Unknown;
return typeInfo;
}
MonoClass* monoClass = mono_class_from_mono_type(monoType);
if (!monoClass) {
return ManagedComponentKind::Unknown;
return typeInfo;
}
const std::string namespaceName = SafeString(mono_class_get_namespace(monoClass));
const std::string className = SafeString(mono_class_get_name(monoClass));
if (namespaceName == "XCEngine" && className == "Transform") {
return ManagedComponentKind::Transform;
typeInfo.monoClass = monoClass;
typeInfo.namespaceName = SafeString(mono_class_get_namespace(monoClass));
typeInfo.className = SafeString(mono_class_get_name(monoClass));
if (typeInfo.namespaceName == "XCEngine" && typeInfo.className == "Transform") {
typeInfo.kind = ManagedComponentKind::Transform;
return typeInfo;
}
if (namespaceName == "XCEngine" && className == "Camera") {
return ManagedComponentKind::Camera;
if (typeInfo.namespaceName == "XCEngine" && typeInfo.className == "Camera") {
typeInfo.kind = ManagedComponentKind::Camera;
return typeInfo;
}
if (namespaceName == "XCEngine" && className == "Light") {
return ManagedComponentKind::Light;
if (typeInfo.namespaceName == "XCEngine" && typeInfo.className == "Light") {
typeInfo.kind = ManagedComponentKind::Light;
return typeInfo;
}
if (namespaceName == "XCEngine" && className == "MeshFilter") {
return ManagedComponentKind::MeshFilter;
if (typeInfo.namespaceName == "XCEngine" && typeInfo.className == "MeshFilter") {
typeInfo.kind = ManagedComponentKind::MeshFilter;
return typeInfo;
}
if (namespaceName == "XCEngine" && className == "MeshRenderer") {
return ManagedComponentKind::MeshRenderer;
if (typeInfo.namespaceName == "XCEngine" && typeInfo.className == "MeshRenderer") {
typeInfo.kind = ManagedComponentKind::MeshRenderer;
return typeInfo;
}
return ManagedComponentKind::Unknown;
MonoScriptRuntime* runtime = GetActiveMonoScriptRuntime();
if (runtime
&& runtime->IsClassAvailable(
runtime->GetSettings().appAssemblyName,
typeInfo.namespaceName,
typeInfo.className)) {
typeInfo.kind = ManagedComponentKind::Script;
typeInfo.assemblyName = runtime->GetSettings().appAssemblyName;
}
return typeInfo;
}
Components::GameObject* FindGameObjectByUUIDRecursive(Components::GameObject* gameObject, uint64_t uuid) {
@@ -214,6 +246,7 @@ bool HasNativeComponent(Components::GameObject* gameObject, ManagedComponentKind
return gameObject->GetComponent<Components::MeshFilterComponent>() != nullptr;
case ManagedComponentKind::MeshRenderer:
return gameObject->GetComponent<Components::MeshRendererComponent>() != nullptr;
case ManagedComponentKind::Script:
case ManagedComponentKind::Unknown:
return false;
}
@@ -245,6 +278,7 @@ Components::Component* AddOrGetNativeComponent(Components::GameObject* gameObjec
return gameObject->GetComponent<Components::MeshRendererComponent>()
? static_cast<Components::Component*>(gameObject->GetComponent<Components::MeshRendererComponent>())
: static_cast<Components::Component*>(gameObject->AddComponent<Components::MeshRendererComponent>());
case ManagedComponentKind::Script:
case ManagedComponentKind::Unknown:
return nullptr;
}
@@ -252,6 +286,28 @@ Components::Component* AddOrGetNativeComponent(Components::GameObject* gameObjec
return nullptr;
}
ScriptComponent* FindMatchingScriptComponent(
Components::GameObject* gameObject,
const ManagedComponentTypeInfo& typeInfo) {
if (!gameObject || typeInfo.kind != ManagedComponentKind::Script) {
return nullptr;
}
for (ScriptComponent* component : gameObject->GetComponents<ScriptComponent>()) {
if (!component || !component->HasScriptClass()) {
continue;
}
if (component->GetAssemblyName() == typeInfo.assemblyName
&& component->GetNamespaceName() == typeInfo.namespaceName
&& component->GetClassName() == typeInfo.className) {
return component;
}
}
return nullptr;
}
Components::CameraComponent* FindCameraComponent(uint64_t gameObjectUUID) {
Components::GameObject* gameObject = FindGameObjectByUUID(gameObjectUUID);
return gameObject ? gameObject->GetComponent<Components::CameraComponent>() : nullptr;
@@ -337,21 +393,69 @@ void InternalCall_GameObject_SetActive(uint64_t gameObjectUUID, mono_bool active
mono_bool InternalCall_GameObject_HasComponent(uint64_t gameObjectUUID, MonoReflectionType* componentType) {
Components::GameObject* gameObject = FindGameObjectByUUID(gameObjectUUID);
return HasNativeComponent(gameObject, ResolveManagedComponentKind(componentType)) ? 1 : 0;
}
uint64_t InternalCall_GameObject_GetComponent(uint64_t gameObjectUUID, MonoReflectionType* componentType) {
Components::GameObject* gameObject = FindGameObjectByUUID(gameObjectUUID);
if (!HasNativeComponent(gameObject, ResolveManagedComponentKind(componentType))) {
return 0;
const ManagedComponentTypeInfo typeInfo = ResolveManagedComponentTypeInfo(componentType);
if (typeInfo.kind == ManagedComponentKind::Script) {
return FindMatchingScriptComponent(gameObject, typeInfo) ? 1 : 0;
}
return gameObjectUUID;
return HasNativeComponent(gameObject, typeInfo.kind) ? 1 : 0;
}
uint64_t InternalCall_GameObject_AddComponent(uint64_t gameObjectUUID, MonoReflectionType* componentType) {
MonoObject* InternalCall_GameObject_GetComponent(uint64_t gameObjectUUID, MonoReflectionType* componentType) {
Components::GameObject* gameObject = FindGameObjectByUUID(gameObjectUUID);
return AddOrGetNativeComponent(gameObject, ResolveManagedComponentKind(componentType)) ? gameObjectUUID : 0;
if (!gameObject) {
return nullptr;
}
MonoScriptRuntime* runtime = GetActiveMonoScriptRuntime();
if (!runtime) {
return nullptr;
}
const ManagedComponentTypeInfo typeInfo = ResolveManagedComponentTypeInfo(componentType);
if (typeInfo.kind == ManagedComponentKind::Script) {
ScriptComponent* component = FindMatchingScriptComponent(gameObject, typeInfo);
if (!component) {
return nullptr;
}
if (!runtime->HasManagedInstance(component)) {
ScriptEngine::Get().OnScriptComponentEnabled(component);
}
return runtime->GetManagedInstanceObject(component);
}
if (!HasNativeComponent(gameObject, typeInfo.kind) || !typeInfo.monoClass) {
return nullptr;
}
return runtime->CreateManagedComponentWrapper(typeInfo.monoClass, gameObjectUUID);
}
MonoObject* InternalCall_GameObject_AddComponent(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 = gameObject->AddComponent<ScriptComponent>();
component->SetScriptClass(typeInfo.assemblyName, typeInfo.namespaceName, typeInfo.className);
return runtime->GetManagedInstanceObject(component);
}
if (!AddOrGetNativeComponent(gameObject, typeInfo.kind) || !typeInfo.monoClass) {
return nullptr;
}
return runtime->CreateManagedComponentWrapper(typeInfo.monoClass, gameObjectUUID);
}
uint64_t InternalCall_GameObject_Find(MonoString* name) {
@@ -1199,6 +1303,11 @@ bool MonoScriptRuntime::HasManagedInstance(const ScriptComponent* component) con
return instanceData != nullptr && GetManagedObject(*instanceData) != nullptr;
}
MonoObject* MonoScriptRuntime::GetManagedInstanceObject(const ScriptComponent* component) const {
const InstanceData* instanceData = FindInstance(component);
return instanceData ? GetManagedObject(*instanceData) : nullptr;
}
bool MonoScriptRuntime::TryGetFieldValue(
const ScriptComponent* component,
const std::string& fieldName,
@@ -1766,6 +1875,37 @@ bool MonoScriptRuntime::ApplyStoredFields(
return true;
}
MonoObject* MonoScriptRuntime::CreateManagedComponentWrapper(MonoClass* componentClass, uint64_t gameObjectUUID) {
if (!m_initialized || !componentClass || gameObjectUUID == 0) {
return nullptr;
}
SetCurrentDomain();
MonoMethod* constructor = mono_class_get_method_from_name(componentClass, ".ctor", 1);
if (!constructor) {
return nullptr;
}
MonoObject* managedObject = mono_object_new(m_appDomain, componentClass);
if (!managedObject) {
return nullptr;
}
void* args[1];
uint64_t uuidArgument = gameObjectUUID;
args[0] = &uuidArgument;
MonoObject* exception = nullptr;
mono_runtime_invoke(constructor, managedObject, args, &exception);
if (exception) {
RecordException(exception);
return nullptr;
}
return managedObject;
}
MonoObject* MonoScriptRuntime::CreateManagedGameObject(uint64_t gameObjectUUID) {
if (gameObjectUUID == 0 || !m_gameObjectClass || !m_gameObjectConstructor) {
return nullptr;

View File

@@ -37,7 +37,8 @@ void ScriptEngine::OnRuntimeStart(Components::Scene* scene) {
CollectScriptComponents(root);
}
for (const ScriptInstanceKey& key : m_scriptOrder) {
const std::vector<ScriptInstanceKey> startupKeys = m_scriptOrder;
for (const ScriptInstanceKey& key : startupKeys) {
auto it = m_scriptStates.find(key);
if (it == m_scriptStates.end()) {
continue;
@@ -87,7 +88,8 @@ void ScriptEngine::OnFixedUpdate(float fixedDeltaTime) {
return;
}
for (const ScriptInstanceKey& key : m_scriptOrder) {
const std::vector<ScriptInstanceKey> updateKeys = m_scriptOrder;
for (const ScriptInstanceKey& key : updateKeys) {
auto it = m_scriptStates.find(key);
if (it == m_scriptStates.end()) {
continue;
@@ -107,7 +109,8 @@ void ScriptEngine::OnUpdate(float deltaTime) {
return;
}
for (const ScriptInstanceKey& key : m_scriptOrder) {
const std::vector<ScriptInstanceKey> updateKeys = m_scriptOrder;
for (const ScriptInstanceKey& key : updateKeys) {
auto it = m_scriptStates.find(key);
if (it == m_scriptStates.end()) {
continue;
@@ -133,7 +136,8 @@ void ScriptEngine::OnLateUpdate(float deltaTime) {
return;
}
for (const ScriptInstanceKey& key : m_scriptOrder) {
const std::vector<ScriptInstanceKey> updateKeys = m_scriptOrder;
for (const ScriptInstanceKey& key : updateKeys) {
auto it = m_scriptStates.find(key);
if (it == m_scriptStates.end()) {
continue;

View File

@@ -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/ScriptComponentApiProbe.cs
${CMAKE_CURRENT_SOURCE_DIR}/GameScripts/RuntimeGameObjectProbe.cs
${CMAKE_CURRENT_SOURCE_DIR}/GameScripts/HierarchyProbe.cs
${CMAKE_CURRENT_SOURCE_DIR}/GameScripts/LifecycleProbe.cs

View File

@@ -0,0 +1,74 @@
using System;
using XCEngine;
namespace Gameplay
{
public sealed class ScriptComponentTargetProbe : MonoBehaviour
{
public int AwakeCount;
public int StartCount;
public int HostCallCount;
public string HostName = string.Empty;
public void Awake()
{
AwakeCount++;
HostName = gameObject.Name;
}
public void Start()
{
StartCount++;
}
public void IncrementFromHost()
{
HostCallCount++;
}
}
public sealed class ScriptComponentApiProbe : MonoBehaviour
{
public bool InitialHasTarget;
public bool InitialLookupReturnedNull;
public bool AddedTarget;
public bool HasTargetAfterAdd;
public bool LookupSucceededAfterAdd;
public bool TryGetSucceededAfterAdd;
public bool ReturnedSameInstance;
public bool TargetEnabledAfterAdd;
public int ObservedTargetAwakeCount;
public int ObservedTargetStartCount;
public int ObservedTargetHostCallCount;
public string ObservedTargetHostName = string.Empty;
public void Start()
{
InitialHasTarget = HasComponent<ScriptComponentTargetProbe>();
InitialLookupReturnedNull = GetComponent<ScriptComponentTargetProbe>() == null;
ScriptComponentTargetProbe added = AddComponent<ScriptComponentTargetProbe>();
AddedTarget = added != null;
HasTargetAfterAdd = HasComponent<ScriptComponentTargetProbe>();
ScriptComponentTargetProbe resolved = GetComponent<ScriptComponentTargetProbe>();
TryGetSucceededAfterAdd = TryGetComponent(out ScriptComponentTargetProbe tried);
LookupSucceededAfterAdd = resolved != null;
ReturnedSameInstance = added != null
&& ReferenceEquals(added, resolved)
&& ReferenceEquals(added, tried);
if (added == null)
{
return;
}
added.IncrementFromHost();
TargetEnabledAfterAdd = added.enabled;
ObservedTargetAwakeCount = added.AwakeCount;
ObservedTargetStartCount = added.StartCount;
ObservedTargetHostCallCount = added.HostCallCount;
ObservedTargetHostName = added.HostName ?? string.Empty;
}
}
}

View File

@@ -64,14 +64,12 @@ namespace XCEngine
public T GetComponent<T>() where T : Component
{
ulong componentOwnerUUID = InternalCalls.GameObject_GetComponent(UUID, typeof(T));
return Component.Create<T>(componentOwnerUUID);
return InternalCalls.GameObject_GetComponent(UUID, typeof(T)) as T;
}
public T AddComponent<T>() where T : Component
{
ulong componentOwnerUUID = InternalCalls.GameObject_AddComponent(UUID, typeof(T));
return Component.Create<T>(componentOwnerUUID);
return InternalCalls.GameObject_AddComponent(UUID, typeof(T)) as T;
}
public bool TryGetComponent<T>(out T component) where T : Component

View File

@@ -36,10 +36,10 @@ namespace XCEngine
internal static extern bool GameObject_HasComponent(ulong gameObjectUUID, Type componentType);
[MethodImpl(MethodImplOptions.InternalCall)]
internal static extern ulong GameObject_GetComponent(ulong gameObjectUUID, Type componentType);
internal static extern Component GameObject_GetComponent(ulong gameObjectUUID, Type componentType);
[MethodImpl(MethodImplOptions.InternalCall)]
internal static extern ulong GameObject_AddComponent(ulong gameObjectUUID, Type componentType);
internal static extern Component GameObject_AddComponent(ulong gameObjectUUID, Type componentType);
[MethodImpl(MethodImplOptions.InternalCall)]
internal static extern ulong GameObject_Find(string name);

View File

@@ -28,6 +28,24 @@ void ExpectVector3Near(const XCEngine::Math::Vector3& actual, const XCEngine::Ma
EXPECT_NEAR(actual.z, expected.z, tolerance);
}
ScriptComponent* FindScriptComponentByClass(GameObject* gameObject, const std::string& namespaceName, const std::string& className) {
if (!gameObject) {
return nullptr;
}
for (ScriptComponent* component : gameObject->GetComponents<ScriptComponent>()) {
if (!component) {
continue;
}
if (component->GetNamespaceName() == namespaceName && component->GetClassName() == className) {
return component;
}
}
return nullptr;
}
MonoScriptRuntime::Settings CreateMonoSettings() {
MonoScriptRuntime::Settings settings;
settings.assemblyDirectory = XCENGINE_TEST_MANAGED_OUTPUT_DIR;
@@ -562,6 +580,81 @@ TEST_F(MonoScriptRuntimeTest, GameObjectAddComponentApiCreatesBuiltinComponentsA
EXPECT_EQ(meshRenderer->GetRenderLayer(), 6u);
}
TEST_F(MonoScriptRuntimeTest, GameObjectComponentApiSupportsManagedScriptTypes) {
Scene* runtimeScene = CreateScene("MonoRuntimeScene");
GameObject* host = runtimeScene->CreateGameObject("Host");
ScriptComponent* component = AddScript(host, "Gameplay", "ScriptComponentApiProbe");
engine->OnRuntimeStart(runtimeScene);
engine->OnUpdate(0.016f);
bool initialHasTarget = true;
bool initialLookupReturnedNull = false;
bool addedTarget = false;
bool hasTargetAfterAdd = false;
bool lookupSucceededAfterAdd = false;
bool tryGetSucceededAfterAdd = false;
bool returnedSameInstance = false;
bool targetEnabledAfterAdd = false;
int32_t observedTargetAwakeCount = 0;
int32_t observedTargetStartCount = -1;
int32_t observedTargetHostCallCount = 0;
std::string observedTargetHostName;
EXPECT_TRUE(runtime->TryGetFieldValue(component, "InitialHasTarget", initialHasTarget));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "InitialLookupReturnedNull", initialLookupReturnedNull));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "AddedTarget", addedTarget));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "HasTargetAfterAdd", hasTargetAfterAdd));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "LookupSucceededAfterAdd", lookupSucceededAfterAdd));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "TryGetSucceededAfterAdd", tryGetSucceededAfterAdd));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ReturnedSameInstance", returnedSameInstance));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "TargetEnabledAfterAdd", targetEnabledAfterAdd));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedTargetAwakeCount", observedTargetAwakeCount));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedTargetStartCount", observedTargetStartCount));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedTargetHostCallCount", observedTargetHostCallCount));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedTargetHostName", observedTargetHostName));
EXPECT_FALSE(initialHasTarget);
EXPECT_TRUE(initialLookupReturnedNull);
EXPECT_TRUE(addedTarget);
EXPECT_TRUE(hasTargetAfterAdd);
EXPECT_TRUE(lookupSucceededAfterAdd);
EXPECT_TRUE(tryGetSucceededAfterAdd);
EXPECT_TRUE(returnedSameInstance);
EXPECT_TRUE(targetEnabledAfterAdd);
EXPECT_EQ(observedTargetAwakeCount, 1);
EXPECT_EQ(observedTargetStartCount, 0);
EXPECT_EQ(observedTargetHostCallCount, 1);
EXPECT_EQ(observedTargetHostName, "Host");
ASSERT_EQ(host->GetComponents<ScriptComponent>().size(), 2u);
ScriptComponent* targetScript = FindScriptComponentByClass(host, "Gameplay", "ScriptComponentTargetProbe");
ASSERT_NE(targetScript, nullptr);
EXPECT_TRUE(runtime->HasManagedInstance(targetScript));
EXPECT_EQ(runtime->GetManagedInstanceCount(), 2u);
int32_t awakeCount = 0;
int32_t startCount = -1;
int32_t hostCallCount = 0;
std::string hostName;
EXPECT_TRUE(runtime->TryGetFieldValue(targetScript, "AwakeCount", awakeCount));
EXPECT_TRUE(runtime->TryGetFieldValue(targetScript, "StartCount", startCount));
EXPECT_TRUE(runtime->TryGetFieldValue(targetScript, "HostCallCount", hostCallCount));
EXPECT_TRUE(runtime->TryGetFieldValue(targetScript, "HostName", hostName));
EXPECT_EQ(awakeCount, 1);
EXPECT_EQ(startCount, 0);
EXPECT_EQ(hostCallCount, 1);
EXPECT_EQ(hostName, "Host");
engine->OnUpdate(0.016f);
startCount = 0;
EXPECT_TRUE(runtime->TryGetFieldValue(targetScript, "StartCount", startCount));
EXPECT_EQ(startCount, 1);
}
TEST_F(MonoScriptRuntimeTest, GameObjectRuntimeApiCreatesFindsAndDestroysSceneObjects) {
Scene* runtimeScene = CreateScene("MonoRuntimeScene");
GameObject* host = runtimeScene->CreateGameObject("Host");