feat(scripting): add mono csharp runtime foundation

This commit is contained in:
2026-03-27 13:07:39 +08:00
parent 134a80b334
commit b06932724c
33 changed files with 4227 additions and 18 deletions

View File

@@ -0,0 +1,253 @@
#include <gtest/gtest.h>
#include <XCEngine/Scene/SceneRuntime.h>
#include <XCEngine/Scripting/IScriptRuntime.h>
#include <XCEngine/Scripting/ScriptComponent.h>
#include <XCEngine/Scripting/ScriptEngine.h>
#include <memory>
#include <string>
#include <vector>
using namespace XCEngine::Components;
using namespace XCEngine::Scripting;
namespace {
std::string LifecycleMethodToString(ScriptLifecycleMethod method) {
switch (method) {
case ScriptLifecycleMethod::Awake: return "Awake";
case ScriptLifecycleMethod::OnEnable: return "OnEnable";
case ScriptLifecycleMethod::Start: return "Start";
case ScriptLifecycleMethod::FixedUpdate: return "FixedUpdate";
case ScriptLifecycleMethod::Update: return "Update";
case ScriptLifecycleMethod::LateUpdate: return "LateUpdate";
case ScriptLifecycleMethod::OnDisable: return "OnDisable";
case ScriptLifecycleMethod::OnDestroy: return "OnDestroy";
}
return "Unknown";
}
class OrderedObserverComponent : public Component {
public:
explicit OrderedObserverComponent(std::vector<std::string>* events)
: m_events(events) {
}
std::string GetName() const override { return "OrderedObserver"; }
void Start() override {
if (m_events) {
m_events->push_back("NativeStart:" + GetGameObject()->GetName());
}
}
void FixedUpdate() override {
if (m_events) {
m_events->push_back("NativeFixedUpdate:" + GetGameObject()->GetName());
}
}
void Update(float deltaTime) override {
(void)deltaTime;
if (m_events) {
m_events->push_back("NativeUpdate:" + GetGameObject()->GetName());
}
}
void LateUpdate(float deltaTime) override {
(void)deltaTime;
if (m_events) {
m_events->push_back("NativeLateUpdate:" + GetGameObject()->GetName());
}
}
private:
std::vector<std::string>* m_events = nullptr;
};
class RecordingScriptRuntime : public IScriptRuntime {
public:
explicit RecordingScriptRuntime(std::vector<std::string>* events)
: m_events(events) {
}
void OnRuntimeStart(Scene* scene) override {
if (m_events) {
m_events->push_back("RuntimeStart:" + (scene ? scene->GetName() : std::string("null")));
}
}
void OnRuntimeStop(Scene* scene) override {
if (m_events) {
m_events->push_back("RuntimeStop:" + (scene ? scene->GetName() : std::string("null")));
}
}
bool CreateScriptInstance(const ScriptRuntimeContext& context) override {
if (m_events) {
m_events->push_back("Create:" + Describe(context));
}
return true;
}
void DestroyScriptInstance(const ScriptRuntimeContext& context) override {
if (m_events) {
m_events->push_back("Destroy:" + Describe(context));
}
}
void InvokeMethod(
const ScriptRuntimeContext& context,
ScriptLifecycleMethod method,
float deltaTime) override {
(void)deltaTime;
if (m_events) {
m_events->push_back(LifecycleMethodToString(method) + ":" + Describe(context));
}
}
private:
static std::string Describe(const ScriptRuntimeContext& context) {
const std::string gameObjectName = context.gameObject ? context.gameObject->GetName() : "null";
const std::string className = context.component ? context.component->GetFullClassName() : "null";
return gameObjectName + ":" + className;
}
std::vector<std::string>* m_events = nullptr;
};
class SceneRuntimeTest : public ::testing::Test {
protected:
void SetUp() override {
scriptEngine = &ScriptEngine::Get();
scriptEngine->OnRuntimeStop();
runtimeImpl = std::make_unique<RecordingScriptRuntime>(&events);
scriptEngine->SetRuntime(runtimeImpl.get());
}
void TearDown() override {
runtime.Stop();
scriptEngine->OnRuntimeStop();
scriptEngine->SetRuntime(nullptr);
runtimeImpl.reset();
scene.reset();
}
Scene* CreateScene(const std::string& sceneName) {
scene = std::make_unique<Scene>(sceneName);
return scene.get();
}
ScriptComponent* AddScript(GameObject* gameObject, const std::string& namespaceName, const std::string& className) {
ScriptComponent* component = gameObject->AddComponent<ScriptComponent>();
component->SetScriptClass("GameScripts", namespaceName, className);
return component;
}
std::vector<std::string> events;
std::unique_ptr<Scene> scene;
std::unique_ptr<RecordingScriptRuntime> runtimeImpl;
ScriptEngine* scriptEngine = nullptr;
SceneRuntime runtime;
};
TEST_F(SceneRuntimeTest, StartAndStopForwardToScriptEngine) {
Scene* runtimeScene = CreateScene("RuntimeScene");
GameObject* host = runtimeScene->CreateGameObject("Host");
AddScript(host, "Gameplay", "Bootstrap");
runtime.Start(runtimeScene);
runtime.Stop();
const std::vector<std::string> expected = {
"RuntimeStart:RuntimeScene",
"Create:Host:Gameplay.Bootstrap",
"Awake:Host:Gameplay.Bootstrap",
"OnEnable:Host:Gameplay.Bootstrap",
"OnDisable:Host:Gameplay.Bootstrap",
"OnDestroy:Host:Gameplay.Bootstrap",
"Destroy:Host:Gameplay.Bootstrap",
"RuntimeStop:RuntimeScene"
};
EXPECT_EQ(events, expected);
EXPECT_FALSE(runtime.IsRunning());
EXPECT_EQ(runtime.GetScene(), nullptr);
}
TEST_F(SceneRuntimeTest, FrameOrderRunsScriptLifecycleBeforeNativeComponents) {
Scene* runtimeScene = CreateScene("RuntimeScene");
GameObject* host = runtimeScene->CreateGameObject("Host");
host->AddComponent<OrderedObserverComponent>(&events);
AddScript(host, "Gameplay", "Mover");
runtime.Start(runtimeScene);
events.clear();
runtime.FixedUpdate(0.02f);
runtime.Update(0.016f);
runtime.LateUpdate(0.016f);
const std::vector<std::string> expected = {
"FixedUpdate:Host:Gameplay.Mover",
"NativeFixedUpdate:Host",
"Start:Host:Gameplay.Mover",
"Update:Host:Gameplay.Mover",
"NativeStart:Host",
"NativeUpdate:Host",
"LateUpdate:Host:Gameplay.Mover",
"NativeLateUpdate:Host"
};
EXPECT_EQ(events, expected);
}
TEST_F(SceneRuntimeTest, InactiveSceneSkipsFrameExecution) {
Scene* runtimeScene = CreateScene("RuntimeScene");
GameObject* host = runtimeScene->CreateGameObject("Host");
host->AddComponent<OrderedObserverComponent>(&events);
AddScript(host, "Gameplay", "PausedScript");
runtimeScene->SetActive(false);
runtime.Start(runtimeScene);
events.clear();
runtime.FixedUpdate(0.02f);
runtime.Update(0.016f);
runtime.LateUpdate(0.016f);
EXPECT_TRUE(events.empty());
}
TEST_F(SceneRuntimeTest, StartingNewSceneStopsPreviousRuntimeFirst) {
Scene* firstScene = CreateScene("FirstScene");
GameObject* firstHost = firstScene->CreateGameObject("FirstHost");
AddScript(firstHost, "Gameplay", "FirstScript");
runtime.Start(firstScene);
std::unique_ptr<Scene> secondScene = std::make_unique<Scene>("SecondScene");
GameObject* secondHost = secondScene->CreateGameObject("SecondHost");
ScriptComponent* secondScript = AddScript(secondHost, "Gameplay", "SecondScript");
events.clear();
runtime.Start(secondScene.get());
const std::vector<std::string> expected = {
"OnDisable:FirstHost:Gameplay.FirstScript",
"OnDestroy:FirstHost:Gameplay.FirstScript",
"Destroy:FirstHost:Gameplay.FirstScript",
"RuntimeStop:FirstScene",
"RuntimeStart:SecondScene",
"Create:SecondHost:Gameplay.SecondScript",
"Awake:SecondHost:Gameplay.SecondScript",
"OnEnable:SecondHost:Gameplay.SecondScript"
};
EXPECT_EQ(events, expected);
EXPECT_EQ(runtime.GetScene(), secondScene.get());
EXPECT_TRUE(scriptEngine->HasRuntimeInstance(secondScript));
runtime.Stop();
}
} // namespace