feat(physics): dispatch PhysX simulation events to scene scripts

This commit is contained in:
2026-04-15 13:36:39 +08:00
parent 077786e4c7
commit 51aea47c83
20 changed files with 955 additions and 7 deletions

View File

@@ -43,6 +43,19 @@ std::string LifecycleMethodToString(ScriptLifecycleMethod method) {
return "Unknown";
}
std::string PhysicsMessageToString(ScriptPhysicsMessage message) {
switch (message) {
case ScriptPhysicsMessage::CollisionEnter: return "CollisionEnter";
case ScriptPhysicsMessage::CollisionStay: return "CollisionStay";
case ScriptPhysicsMessage::CollisionExit: return "CollisionExit";
case ScriptPhysicsMessage::TriggerEnter: return "TriggerEnter";
case ScriptPhysicsMessage::TriggerStay: return "TriggerStay";
case ScriptPhysicsMessage::TriggerExit: return "TriggerExit";
}
return "Unknown";
}
class OrderedObserverComponent : public Component {
public:
explicit OrderedObserverComponent(std::vector<std::string>* events)
@@ -81,6 +94,38 @@ private:
std::vector<std::string>* m_events = nullptr;
};
class PhysicsObserverComponent : public Component {
public:
explicit PhysicsObserverComponent(std::vector<std::string>* events)
: m_events(events) {
}
std::string GetName() const override { return "PhysicsObserver"; }
void OnCollisionEnter(GameObject* other) override { Record("NativeCollisionEnter", other); }
void OnCollisionStay(GameObject* other) override { Record("NativeCollisionStay", other); }
void OnCollisionExit(GameObject* other) override { Record("NativeCollisionExit", other); }
void OnTriggerEnter(GameObject* other) override { Record("NativeTriggerEnter", other); }
void OnTriggerStay(GameObject* other) override { Record("NativeTriggerStay", other); }
void OnTriggerExit(GameObject* other) override { Record("NativeTriggerExit", other); }
private:
void Record(const char* prefix, GameObject* other) {
if (!m_events || !GetGameObject()) {
return;
}
m_events->push_back(
std::string(prefix)
+ ":"
+ GetGameObject()->GetName()
+ ":"
+ (other ? other->GetName() : std::string("null")));
}
std::vector<std::string>* m_events = nullptr;
};
class TempFileScope {
public:
TempFileScope(std::string stem, std::string extension, std::string contents) {
@@ -256,6 +301,20 @@ public:
}
}
void InvokePhysicsMessage(
const ScriptRuntimeContext& context,
ScriptPhysicsMessage message,
GameObject* other) override {
if (m_events) {
m_events->push_back(
PhysicsMessageToString(message)
+ ":"
+ Describe(context)
+ ":"
+ (other ? other->GetName() : std::string("null")));
}
}
private:
static std::string Describe(const ScriptRuntimeContext& context) {
const std::string gameObjectName = context.gameObject ? context.gameObject->GetName() : "null";
@@ -427,6 +486,101 @@ TEST_F(SceneRuntimeTest, FrameOrderRunsScriptLifecycleBeforeNativeComponents) {
EXPECT_EQ(events, expected);
}
TEST_F(SceneRuntimeTest, FixedUpdateDispatchesTriggerPhysicsMessagesToNativeAndScripts) {
if (!XCEngine::Physics::PhysicsWorld::IsPhysXAvailable()) {
GTEST_SKIP() << "PhysX is not available in this configuration.";
}
Scene* runtimeScene = CreateScene("RuntimeScene");
GameObject* mover = runtimeScene->CreateGameObject("Mover");
mover->AddComponent<PhysicsObserverComponent>(&events);
mover->AddComponent<SphereColliderComponent>();
RigidbodyComponent* rigidbody = mover->AddComponent<RigidbodyComponent>();
rigidbody->SetUseGravity(false);
AddScript(mover, "Gameplay", "TriggerProbe");
mover->GetTransform()->SetPosition(XCEngine::Math::Vector3(-3.0f, 0.0f, 0.0f));
GameObject* triggerZone = runtimeScene->CreateGameObject("TriggerZone");
BoxColliderComponent* triggerCollider = triggerZone->AddComponent<BoxColliderComponent>();
triggerCollider->SetSize(XCEngine::Math::Vector3(2.0f, 2.0f, 2.0f));
triggerCollider->SetTrigger(true);
runtime.Start(runtimeScene);
events.clear();
rigidbody->SetLinearVelocity(XCEngine::Math::Vector3(150.0f, 0.0f, 0.0f));
runtime.FixedUpdate(0.02f);
rigidbody->SetLinearVelocity(XCEngine::Math::Vector3::Zero());
runtime.FixedUpdate(0.02f);
rigidbody->SetLinearVelocity(XCEngine::Math::Vector3(150.0f, 0.0f, 0.0f));
runtime.FixedUpdate(0.02f);
rigidbody->SetLinearVelocity(XCEngine::Math::Vector3::Zero());
runtime.FixedUpdate(0.02f);
const std::vector<std::string> expected = {
"FixedUpdate:Mover:Gameplay.TriggerProbe",
"FixedUpdate:Mover:Gameplay.TriggerProbe",
"NativeTriggerEnter:Mover:TriggerZone",
"TriggerEnter:Mover:Gameplay.TriggerProbe:TriggerZone",
"NativeTriggerStay:Mover:TriggerZone",
"TriggerStay:Mover:Gameplay.TriggerProbe:TriggerZone",
"FixedUpdate:Mover:Gameplay.TriggerProbe",
"NativeTriggerStay:Mover:TriggerZone",
"TriggerStay:Mover:Gameplay.TriggerProbe:TriggerZone",
"FixedUpdate:Mover:Gameplay.TriggerProbe",
"NativeTriggerExit:Mover:TriggerZone",
"TriggerExit:Mover:Gameplay.TriggerProbe:TriggerZone"
};
EXPECT_EQ(events, expected);
}
TEST_F(SceneRuntimeTest, FixedUpdateDispatchesCollisionPhysicsMessagesToNativeAndScripts) {
if (!XCEngine::Physics::PhysicsWorld::IsPhysXAvailable()) {
GTEST_SKIP() << "PhysX is not available in this configuration.";
}
Scene* runtimeScene = CreateScene("RuntimeScene");
GameObject* mover = runtimeScene->CreateGameObject("Mover");
mover->AddComponent<PhysicsObserverComponent>(&events);
mover->AddComponent<SphereColliderComponent>();
RigidbodyComponent* rigidbody = mover->AddComponent<RigidbodyComponent>();
rigidbody->SetUseGravity(false);
AddScript(mover, "Gameplay", "CollisionProbe");
mover->GetTransform()->SetPosition(XCEngine::Math::Vector3(-3.0f, 0.0f, 0.0f));
GameObject* wall = runtimeScene->CreateGameObject("Wall");
BoxColliderComponent* wallCollider = wall->AddComponent<BoxColliderComponent>();
wallCollider->SetSize(XCEngine::Math::Vector3(2.0f, 2.0f, 2.0f));
runtime.Start(runtimeScene);
events.clear();
rigidbody->SetLinearVelocity(XCEngine::Math::Vector3(150.0f, 0.0f, 0.0f));
runtime.FixedUpdate(0.02f);
rigidbody->SetLinearVelocity(XCEngine::Math::Vector3::Zero());
runtime.FixedUpdate(0.02f);
rigidbody->SetLinearVelocity(XCEngine::Math::Vector3(-150.0f, 0.0f, 0.0f));
runtime.FixedUpdate(0.02f);
rigidbody->SetLinearVelocity(XCEngine::Math::Vector3::Zero());
runtime.FixedUpdate(0.02f);
const std::vector<std::string> expected = {
"FixedUpdate:Mover:Gameplay.CollisionProbe",
"FixedUpdate:Mover:Gameplay.CollisionProbe",
"NativeCollisionEnter:Mover:Wall",
"CollisionEnter:Mover:Gameplay.CollisionProbe:Wall",
"NativeCollisionStay:Mover:Wall",
"CollisionStay:Mover:Gameplay.CollisionProbe:Wall",
"FixedUpdate:Mover:Gameplay.CollisionProbe",
"NativeCollisionStay:Mover:Wall",
"CollisionStay:Mover:Gameplay.CollisionProbe:Wall",
"FixedUpdate:Mover:Gameplay.CollisionProbe",
"NativeCollisionExit:Mover:Wall",
"CollisionExit:Mover:Gameplay.CollisionProbe:Wall"
};
EXPECT_EQ(events, expected);
}
TEST_F(SceneRuntimeTest, InactiveSceneSkipsFrameExecution) {
Scene* runtimeScene = CreateScene("RuntimeScene");
GameObject* host = runtimeScene->CreateGameObject("Host");