feat: expand editor scripting asset and viewport flow

This commit is contained in:
2026-04-03 13:22:30 +08:00
parent ed8c27fde2
commit a05d0b80a2
124 changed files with 10397 additions and 1737 deletions

View File

@@ -4,8 +4,13 @@
#include <XCEngine/Components/LightComponent.h>
#include <XCEngine/Components/MeshFilterComponent.h>
#include <XCEngine/Components/MeshRendererComponent.h>
#include <XCEngine/Debug/ILogSink.h>
#include <XCEngine/Debug/Logger.h>
#include <XCEngine/Core/Math/Vector2.h>
#include <XCEngine/Core/Math/Vector3.h>
#include <XCEngine/Core/Math/Vector4.h>
#include <XCEngine/Input/InputManager.h>
#include <XCEngine/Input/InputTypes.h>
#include <XCEngine/Scene/Scene.h>
#include <XCEngine/Scripting/Mono/MonoScriptRuntime.h>
#include <XCEngine/Scripting/ScriptComponent.h>
@@ -28,6 +33,30 @@ void ExpectVector3Near(const XCEngine::Math::Vector3& actual, const XCEngine::Ma
EXPECT_NEAR(actual.z, expected.z, tolerance);
}
class CapturingLogSink final : public XCEngine::Debug::ILogSink {
public:
void Log(const XCEngine::Debug::LogEntry& entry) override {
entries.push_back(entry);
}
void Flush() override {
}
std::vector<std::string> CollectMessagesWithPrefix(const char* prefix) const {
std::vector<std::string> messages;
for (const XCEngine::Debug::LogEntry& entry : entries) {
const std::string message = entry.message.CStr();
if (message.rfind(prefix, 0) == 0) {
messages.push_back(message);
}
}
return messages;
}
std::vector<XCEngine::Debug::LogEntry> entries;
};
ScriptComponent* FindScriptComponentByClass(GameObject* gameObject, const std::string& namespaceName, const std::string& className) {
if (!gameObject) {
return nullptr;
@@ -60,6 +89,8 @@ protected:
void SetUp() override {
engine = &ScriptEngine::Get();
engine->OnRuntimeStop();
engine->SetRuntimeFixedDeltaTime(ScriptEngine::DefaultFixedDeltaTime);
XCEngine::Input::InputManager::Get().Shutdown();
runtime = std::make_unique<MonoScriptRuntime>(CreateMonoSettings());
ASSERT_TRUE(runtime->Initialize()) << runtime->GetLastError();
@@ -70,6 +101,7 @@ protected:
void TearDown() override {
engine->OnRuntimeStop();
engine->SetRuntime(nullptr);
XCEngine::Input::InputManager::Get().Shutdown();
runtime.reset();
scene.reset();
}
@@ -100,6 +132,44 @@ TEST_F(MonoScriptRuntimeTest, InitializesAndDiscoversConcreteMonoBehaviourClasse
EXPECT_EQ(std::find(classNames.begin(), classNames.end(), "Gameplay.UtilityHelper"), classNames.end());
}
TEST_F(MonoScriptRuntimeTest, ScriptClassDescriptorApiReturnsConcreteManagedTypes) {
std::vector<ScriptClassDescriptor> classes;
ASSERT_TRUE(runtime->TryGetAvailableScriptClasses(classes));
ASSERT_FALSE(classes.empty());
EXPECT_TRUE(std::is_sorted(
classes.begin(),
classes.end(),
[](const ScriptClassDescriptor& lhs, const ScriptClassDescriptor& rhs) {
if (lhs.assemblyName != rhs.assemblyName) {
return lhs.assemblyName < rhs.assemblyName;
}
if (lhs.namespaceName != rhs.namespaceName) {
return lhs.namespaceName < rhs.namespaceName;
}
return lhs.className < rhs.className;
}));
EXPECT_NE(
std::find(
classes.begin(),
classes.end(),
ScriptClassDescriptor{"GameScripts", "Gameplay", "LifecycleProbe"}),
classes.end());
EXPECT_EQ(
std::find(
classes.begin(),
classes.end(),
ScriptClassDescriptor{"GameScripts", "Gameplay", "AbstractLifecycleProbe"}),
classes.end());
EXPECT_EQ(
std::find(
classes.begin(),
classes.end(),
ScriptClassDescriptor{"GameScripts", "Gameplay", "UtilityHelper"}),
classes.end());
}
TEST_F(MonoScriptRuntimeTest, ClassFieldMetadataListsSupportedPublicInstanceFields) {
std::vector<ScriptFieldMetadata> fields;
@@ -184,6 +254,8 @@ TEST_F(MonoScriptRuntimeTest, ScriptEngineAppliesStoredFieldsAndInvokesLifecycle
bool observedIsActiveAndEnabled = false;
float speed = 0.0f;
float observedFixedDeltaTime = 0.0f;
float observedConfiguredFixedDeltaTime = 0.0f;
float observedConfiguredFixedDeltaTimeInUpdate = 0.0f;
float observedUpdateDeltaTime = 0.0f;
float observedLateDeltaTime = 0.0f;
std::string label;
@@ -219,6 +291,8 @@ TEST_F(MonoScriptRuntimeTest, ScriptEngineAppliesStoredFieldsAndInvokesLifecycle
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedIsActiveAndEnabled", observedIsActiveAndEnabled));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "Speed", speed));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedFixedDeltaTime", observedFixedDeltaTime));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedConfiguredFixedDeltaTime", observedConfiguredFixedDeltaTime));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedConfiguredFixedDeltaTimeInUpdate", observedConfiguredFixedDeltaTimeInUpdate));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedUpdateDeltaTime", observedUpdateDeltaTime));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedLateDeltaTime", observedLateDeltaTime));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "Label", label));
@@ -254,6 +328,8 @@ TEST_F(MonoScriptRuntimeTest, ScriptEngineAppliesStoredFieldsAndInvokesLifecycle
EXPECT_TRUE(observedIsActiveAndEnabled);
EXPECT_FLOAT_EQ(speed, 6.0f);
EXPECT_FLOAT_EQ(observedFixedDeltaTime, 0.02f);
EXPECT_FLOAT_EQ(observedConfiguredFixedDeltaTime, 0.02f);
EXPECT_FLOAT_EQ(observedConfiguredFixedDeltaTimeInUpdate, 0.02f);
EXPECT_FLOAT_EQ(observedUpdateDeltaTime, 0.016f);
EXPECT_FLOAT_EQ(observedLateDeltaTime, 0.016f);
EXPECT_EQ(label, "Configured|Awake");
@@ -276,6 +352,233 @@ TEST_F(MonoScriptRuntimeTest, ScriptEngineAppliesStoredFieldsAndInvokesLifecycle
EXPECT_FLOAT_EQ(localRotation.w, 0.8660254f);
}
TEST_F(MonoScriptRuntimeTest, TimeFixedDeltaTimeUsesConfiguredRuntimeStepAcrossFixedAndVariableUpdates) {
Scene* runtimeScene = CreateScene("MonoRuntimeScene");
GameObject* host = runtimeScene->CreateGameObject("Host");
ScriptComponent* component = AddScript(host, "Gameplay", "LifecycleProbe");
engine->SetRuntimeFixedDeltaTime(0.05f);
engine->OnRuntimeStart(runtimeScene);
engine->OnFixedUpdate(0.05f);
engine->OnUpdate(0.016f);
float observedFixedDeltaTime = 0.0f;
float observedConfiguredFixedDeltaTime = 0.0f;
float observedConfiguredFixedDeltaTimeInUpdate = 0.0f;
float observedUpdateDeltaTime = 0.0f;
ASSERT_TRUE(runtime->TryGetFieldValue(component, "ObservedFixedDeltaTime", observedFixedDeltaTime));
ASSERT_TRUE(runtime->TryGetFieldValue(component, "ObservedConfiguredFixedDeltaTime", observedConfiguredFixedDeltaTime));
ASSERT_TRUE(runtime->TryGetFieldValue(component, "ObservedConfiguredFixedDeltaTimeInUpdate", observedConfiguredFixedDeltaTimeInUpdate));
ASSERT_TRUE(runtime->TryGetFieldValue(component, "ObservedUpdateDeltaTime", observedUpdateDeltaTime));
EXPECT_FLOAT_EQ(observedFixedDeltaTime, 0.05f);
EXPECT_FLOAT_EQ(observedConfiguredFixedDeltaTime, 0.05f);
EXPECT_FLOAT_EQ(observedConfiguredFixedDeltaTimeInUpdate, 0.05f);
EXPECT_FLOAT_EQ(observedUpdateDeltaTime, 0.016f);
}
TEST_F(MonoScriptRuntimeTest, ManagedInputApiReadsCurrentNativeInputManagerState) {
XCEngine::Input::InputManager& inputManager = XCEngine::Input::InputManager::Get();
inputManager.Initialize(nullptr);
Scene* runtimeScene = CreateScene("MonoRuntimeScene");
GameObject* host = runtimeScene->CreateGameObject("Host");
ScriptComponent* component = AddScript(host, "Gameplay", "InputProbe");
engine->OnRuntimeStart(runtimeScene);
inputManager.ProcessKeyDown(XCEngine::Input::KeyCode::A, false, false, false, false, false);
inputManager.ProcessKeyDown(XCEngine::Input::KeyCode::Space, false, false, false, false, false);
inputManager.ProcessKeyDown(XCEngine::Input::KeyCode::LeftCtrl, false, false, false, false, false);
inputManager.ProcessMouseButton(XCEngine::Input::MouseButton::Left, true, 120, 48);
inputManager.ProcessMouseMove(120, 48, 3, -2);
inputManager.ProcessMouseWheel(1.0f, 120, 48);
engine->OnUpdate(0.016f);
int32_t updateCount = 0;
bool observedKeyA = false;
bool observedKeyADown = false;
bool observedKeyAUp = false;
bool observedKeySpace = false;
bool observedJump = false;
bool observedJumpDown = false;
bool observedJumpUp = false;
bool observedFire1 = false;
bool observedFire1Down = false;
bool observedFire1Up = false;
bool observedAnyKey = false;
bool observedAnyKeyDown = false;
bool observedLeftMouse = false;
bool observedLeftMouseDown = false;
bool observedLeftMouseUp = false;
float observedHorizontal = 0.0f;
float observedHorizontalRaw = 0.0f;
XCEngine::Math::Vector2 observedMouseScrollDelta;
XCEngine::Math::Vector3 observedMousePosition;
EXPECT_TRUE(runtime->TryGetFieldValue(component, "UpdateCount", updateCount));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedKeyA", observedKeyA));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedKeyADown", observedKeyADown));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedKeyAUp", observedKeyAUp));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedKeySpace", observedKeySpace));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedJump", observedJump));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedJumpDown", observedJumpDown));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedJumpUp", observedJumpUp));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedFire1", observedFire1));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedFire1Down", observedFire1Down));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedFire1Up", observedFire1Up));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedAnyKey", observedAnyKey));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedAnyKeyDown", observedAnyKeyDown));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedLeftMouse", observedLeftMouse));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedLeftMouseDown", observedLeftMouseDown));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedLeftMouseUp", observedLeftMouseUp));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedHorizontal", observedHorizontal));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedHorizontalRaw", observedHorizontalRaw));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedMousePosition", observedMousePosition));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedMouseScrollDelta", observedMouseScrollDelta));
EXPECT_EQ(updateCount, 1);
EXPECT_TRUE(observedKeyA);
EXPECT_TRUE(observedKeyADown);
EXPECT_FALSE(observedKeyAUp);
EXPECT_TRUE(observedKeySpace);
EXPECT_TRUE(observedJump);
EXPECT_TRUE(observedJumpDown);
EXPECT_FALSE(observedJumpUp);
EXPECT_TRUE(observedFire1);
EXPECT_TRUE(observedFire1Down);
EXPECT_FALSE(observedFire1Up);
EXPECT_TRUE(observedAnyKey);
EXPECT_TRUE(observedAnyKeyDown);
EXPECT_TRUE(observedLeftMouse);
EXPECT_TRUE(observedLeftMouseDown);
EXPECT_FALSE(observedLeftMouseUp);
EXPECT_FLOAT_EQ(observedHorizontal, -1.0f);
EXPECT_FLOAT_EQ(observedHorizontalRaw, -1.0f);
EXPECT_EQ(observedMousePosition, XCEngine::Math::Vector3(120.0f, 48.0f, 0.0f));
EXPECT_EQ(observedMouseScrollDelta, XCEngine::Math::Vector2(0.0f, 1.0f));
inputManager.Update(0.016f);
engine->OnUpdate(0.016f);
EXPECT_TRUE(runtime->TryGetFieldValue(component, "UpdateCount", updateCount));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedKeyA", observedKeyA));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedKeyADown", observedKeyADown));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedKeyAUp", observedKeyAUp));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedJump", observedJump));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedJumpDown", observedJumpDown));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedJumpUp", observedJumpUp));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedFire1", observedFire1));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedFire1Down", observedFire1Down));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedFire1Up", observedFire1Up));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedAnyKey", observedAnyKey));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedAnyKeyDown", observedAnyKeyDown));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedLeftMouse", observedLeftMouse));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedLeftMouseDown", observedLeftMouseDown));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedLeftMouseUp", observedLeftMouseUp));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedHorizontalRaw", observedHorizontalRaw));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedMouseScrollDelta", observedMouseScrollDelta));
EXPECT_EQ(updateCount, 2);
EXPECT_TRUE(observedKeyA);
EXPECT_FALSE(observedKeyADown);
EXPECT_FALSE(observedKeyAUp);
EXPECT_TRUE(observedJump);
EXPECT_FALSE(observedJumpDown);
EXPECT_FALSE(observedJumpUp);
EXPECT_TRUE(observedFire1);
EXPECT_FALSE(observedFire1Down);
EXPECT_FALSE(observedFire1Up);
EXPECT_TRUE(observedAnyKey);
EXPECT_FALSE(observedAnyKeyDown);
EXPECT_TRUE(observedLeftMouse);
EXPECT_FALSE(observedLeftMouseDown);
EXPECT_FALSE(observedLeftMouseUp);
EXPECT_FLOAT_EQ(observedHorizontalRaw, -1.0f);
EXPECT_EQ(observedMouseScrollDelta, XCEngine::Math::Vector2(0.0f, 0.0f));
inputManager.ProcessKeyUp(XCEngine::Input::KeyCode::A, false, false, false, false);
inputManager.ProcessKeyUp(XCEngine::Input::KeyCode::LeftCtrl, false, false, false, false);
inputManager.ProcessMouseButton(XCEngine::Input::MouseButton::Left, false, 120, 48);
engine->OnUpdate(0.016f);
EXPECT_TRUE(runtime->TryGetFieldValue(component, "UpdateCount", updateCount));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedKeyA", observedKeyA));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedKeyAUp", observedKeyAUp));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedJump", observedJump));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedJumpUp", observedJumpUp));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedFire1", observedFire1));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedFire1Up", observedFire1Up));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedAnyKey", observedAnyKey));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedAnyKeyDown", observedAnyKeyDown));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedLeftMouse", observedLeftMouse));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedLeftMouseUp", observedLeftMouseUp));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedHorizontal", observedHorizontal));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedHorizontalRaw", observedHorizontalRaw));
EXPECT_EQ(updateCount, 3);
EXPECT_FALSE(observedKeyA);
EXPECT_TRUE(observedKeyAUp);
EXPECT_TRUE(observedJump);
EXPECT_FALSE(observedJumpUp);
EXPECT_FALSE(observedFire1);
EXPECT_TRUE(observedFire1Up);
EXPECT_TRUE(observedAnyKey);
EXPECT_FALSE(observedAnyKeyDown);
EXPECT_FALSE(observedLeftMouse);
EXPECT_TRUE(observedLeftMouseUp);
EXPECT_FLOAT_EQ(observedHorizontal, 0.0f);
EXPECT_FLOAT_EQ(observedHorizontalRaw, 0.0f);
inputManager.Update(0.016f);
inputManager.ProcessKeyUp(XCEngine::Input::KeyCode::Space, false, false, false, false);
engine->OnUpdate(0.016f);
EXPECT_TRUE(runtime->TryGetFieldValue(component, "UpdateCount", updateCount));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedKeySpace", observedKeySpace));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedJump", observedJump));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedJumpUp", observedJumpUp));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedAnyKey", observedAnyKey));
EXPECT_TRUE(runtime->TryGetFieldValue(component, "ObservedAnyKeyDown", observedAnyKeyDown));
EXPECT_EQ(updateCount, 4);
EXPECT_FALSE(observedKeySpace);
EXPECT_FALSE(observedJump);
EXPECT_TRUE(observedJumpUp);
EXPECT_FALSE(observedAnyKey);
EXPECT_FALSE(observedAnyKeyDown);
}
TEST_F(MonoScriptRuntimeTest, ManagedDebugLogBridgeWritesLifecycleTickMessagesToNativeLogger) {
auto sink = std::make_unique<CapturingLogSink>();
CapturingLogSink* sinkPtr = sink.get();
XCEngine::Debug::Logger::Get().AddSink(std::move(sink));
Scene* runtimeScene = CreateScene("MonoRuntimeScene");
GameObject* host = runtimeScene->CreateGameObject("Host");
ScriptComponent* component = AddScript(host, "Gameplay", "TickLogProbe");
ASSERT_NE(component, nullptr);
engine->OnRuntimeStart(runtimeScene);
engine->OnFixedUpdate(0.02f);
engine->OnUpdate(0.016f);
engine->OnLateUpdate(0.016f);
const std::vector<std::string> messages = sinkPtr->CollectMessagesWithPrefix("[TickLogProbe]");
const std::vector<std::string> expected = {
"[TickLogProbe] Awake",
"[TickLogProbe] FixedUpdate 1",
"[TickLogProbe] Start",
"[TickLogProbe] Update 1",
"[TickLogProbe] LateUpdate 1",
};
EXPECT_EQ(messages, expected);
XCEngine::Debug::Logger::Get().RemoveSink(sinkPtr);
}
TEST_F(MonoScriptRuntimeTest, DeserializedSceneRebindsManagedScriptsAndRestoresStoredFields) {
Scene originalScene("SerializedMonoScene");
GameObject* hostA = originalScene.CreateGameObject("HostA");