feat(scripting): add runtime gameobject lifecycle api

This commit is contained in:
2026-03-27 16:30:16 +08:00
parent 26035e3940
commit a72f9f7f05
10 changed files with 395 additions and 0 deletions

View File

@@ -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<ScriptInstanceKey, ScriptInstanceState, ScriptInstanceKeyHasher> m_scriptStates;
std::vector<ScriptInstanceKey> m_scriptOrder;

View File

@@ -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<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_AddComponent", reinterpret_cast<const void*>(&InternalCall_GameObject_AddComponent));
mono_add_internal_call("XCEngine.InternalCalls::GameObject_Find", reinterpret_cast<const void*>(&InternalCall_GameObject_Find));
mono_add_internal_call("XCEngine.InternalCalls::GameObject_Create", reinterpret_cast<const void*>(&InternalCall_GameObject_Create));
mono_add_internal_call("XCEngine.InternalCalls::GameObject_Destroy", reinterpret_cast<const void*>(&InternalCall_GameObject_Destroy));
mono_add_internal_call("XCEngine.InternalCalls::Behaviour_GetEnabled", reinterpret_cast<const void*>(&InternalCall_Behaviour_GetEnabled));
mono_add_internal_call("XCEngine.InternalCalls::Behaviour_SetEnabled", reinterpret_cast<const void*>(&InternalCall_Behaviour_SetEnabled));
mono_add_internal_call("XCEngine.InternalCalls::Transform_GetLocalPosition", reinterpret_cast<const void*>(&InternalCall_Transform_GetLocalPosition));

View File

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

View File

@@ -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<ScriptComponent>()) {
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;

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

View File

@@ -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<Camera>();
MeshFilter meshFilter = foundRoot.AddComponent<MeshFilter>();
MeshRenderer meshRenderer = foundRoot.AddComponent<MeshRenderer>();
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;
}
}
}
}

View File

@@ -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<Transform>();
public Transform transform => Transform;

View File

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

View File

@@ -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<CameraComponent>();
MeshFilterComponent* meshFilter = createdRoot->GetComponent<MeshFilterComponent>();
MeshRendererComponent* meshRenderer = createdRoot->GetComponent<MeshRendererComponent>();
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<ScriptComponent>();
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");

View File

@@ -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<std::string> 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<std::string> expectedAfterUpdate = {
"Start:Spawned:Gameplay.RuntimeSpawned",
"Update:Spawned:Gameplay.RuntimeSpawned"
};
EXPECT_EQ(runtime.events, expectedAfterUpdate);
}
} // namespace