diff --git a/engine/include/XCEngine/Components/Component.h b/engine/include/XCEngine/Components/Component.h index a50f6165..4a3674a6 100644 --- a/engine/include/XCEngine/Components/Component.h +++ b/engine/include/XCEngine/Components/Component.h @@ -23,6 +23,12 @@ public: virtual void OnEnable() {} virtual void OnDisable() {} virtual void OnDestroy() {} + virtual void OnCollisionEnter(GameObject* other) {} + virtual void OnCollisionStay(GameObject* other) {} + virtual void OnCollisionExit(GameObject* other) {} + virtual void OnTriggerEnter(GameObject* other) {} + virtual void OnTriggerStay(GameObject* other) {} + virtual void OnTriggerExit(GameObject* other) {} virtual std::string GetName() const = 0; diff --git a/engine/include/XCEngine/Physics/PhysicsEvents.h b/engine/include/XCEngine/Physics/PhysicsEvents.h new file mode 100644 index 00000000..677738f7 --- /dev/null +++ b/engine/include/XCEngine/Physics/PhysicsEvents.h @@ -0,0 +1,28 @@ +#pragma once + +namespace XCEngine { +namespace Components { + +class GameObject; + +} // namespace Components + +namespace Physics { + +enum class PhysicsEventType { + CollisionEnter = 0, + CollisionStay, + CollisionExit, + TriggerEnter, + TriggerStay, + TriggerExit +}; + +struct PhysicsEvent { + PhysicsEventType type = PhysicsEventType::CollisionEnter; + Components::GameObject* self = nullptr; + Components::GameObject* other = nullptr; +}; + +} // namespace Physics +} // namespace XCEngine diff --git a/engine/include/XCEngine/Physics/PhysicsWorld.h b/engine/include/XCEngine/Physics/PhysicsWorld.h index 72dcdfb6..fad5501e 100644 --- a/engine/include/XCEngine/Physics/PhysicsWorld.h +++ b/engine/include/XCEngine/Physics/PhysicsWorld.h @@ -1,11 +1,13 @@ #pragma once +#include #include #include #include #include #include +#include namespace XCEngine { namespace Components { @@ -30,6 +32,7 @@ public: bool Initialize(const PhysicsWorldCreateInfo& createInfo); void Shutdown(); void Step(float fixedDeltaTime); + void ConsumeSimulationEvents(std::vector& outEvents); bool Raycast( const Math::Vector3& origin, const Math::Vector3& direction, diff --git a/engine/include/XCEngine/Scripting/IScriptRuntime.h b/engine/include/XCEngine/Scripting/IScriptRuntime.h index 30deb16d..ba077585 100644 --- a/engine/include/XCEngine/Scripting/IScriptRuntime.h +++ b/engine/include/XCEngine/Scripting/IScriptRuntime.h @@ -28,6 +28,15 @@ enum class ScriptLifecycleMethod { OnDestroy }; +enum class ScriptPhysicsMessage { + CollisionEnter = 0, + CollisionStay, + CollisionExit, + TriggerEnter, + TriggerStay, + TriggerExit +}; + struct ScriptClassDescriptor { std::string assemblyName; std::string namespaceName; @@ -96,6 +105,10 @@ public: const ScriptRuntimeContext& context, ScriptLifecycleMethod method, float deltaTime) = 0; + virtual void InvokePhysicsMessage( + const ScriptRuntimeContext& context, + ScriptPhysicsMessage message, + Components::GameObject* other) = 0; }; } // namespace Scripting diff --git a/engine/include/XCEngine/Scripting/Mono/MonoScriptRuntime.h b/engine/include/XCEngine/Scripting/Mono/MonoScriptRuntime.h index 8b8bb248..16a9e0b9 100644 --- a/engine/include/XCEngine/Scripting/Mono/MonoScriptRuntime.h +++ b/engine/include/XCEngine/Scripting/Mono/MonoScriptRuntime.h @@ -104,9 +104,19 @@ public: bool CreateScriptInstance(const ScriptRuntimeContext& context) override; void DestroyScriptInstance(const ScriptRuntimeContext& context) override; void InvokeMethod(const ScriptRuntimeContext& context, ScriptLifecycleMethod method, float deltaTime) override; + void InvokePhysicsMessage( + const ScriptRuntimeContext& context, + ScriptPhysicsMessage message, + Components::GameObject* other) override; private: static constexpr size_t LifecycleMethodCount = 8; + static constexpr size_t PhysicsMessageCount = 6; + + struct PhysicsMessageMethods { + MonoMethod* withGameObject = nullptr; + MonoMethod* withoutArgs = nullptr; + }; struct FieldMetadata { ScriptFieldType type = ScriptFieldType::None; @@ -123,6 +133,7 @@ private: std::string fullName; MonoClass* monoClass = nullptr; std::array lifecycleMethods{}; + std::array physicsMessageMethods{}; std::unordered_map fields; }; @@ -161,6 +172,7 @@ private: FieldMetadata BuildFieldMetadata(MonoClassField* field) const; static const char* ToLifecycleMethodName(ScriptLifecycleMethod method); + static const char* ToPhysicsMessageMethodName(ScriptPhysicsMessage message); const ClassMetadata* FindClassMetadata( const std::string& assemblyName, @@ -184,7 +196,7 @@ private: void ClearManagedInstances(); void ClearClassCache(); - bool InvokeManagedMethod(MonoObject* instance, MonoMethod* method); + bool InvokeManagedMethod(MonoObject* instance, MonoMethod* method, void** args = nullptr); void RecordException(MonoObject* exception); void SetError(const std::string& error); diff --git a/engine/include/XCEngine/Scripting/NullScriptRuntime.h b/engine/include/XCEngine/Scripting/NullScriptRuntime.h index 75fc8fb0..dee2ed93 100644 --- a/engine/include/XCEngine/Scripting/NullScriptRuntime.h +++ b/engine/include/XCEngine/Scripting/NullScriptRuntime.h @@ -38,6 +38,10 @@ public: const ScriptRuntimeContext& context, ScriptLifecycleMethod method, float deltaTime) override; + void InvokePhysicsMessage( + const ScriptRuntimeContext& context, + ScriptPhysicsMessage message, + Components::GameObject* other) override; }; } // namespace Scripting diff --git a/engine/include/XCEngine/Scripting/ScriptEngine.h b/engine/include/XCEngine/Scripting/ScriptEngine.h index 488bcb19..700c9875 100644 --- a/engine/include/XCEngine/Scripting/ScriptEngine.h +++ b/engine/include/XCEngine/Scripting/ScriptEngine.h @@ -33,6 +33,10 @@ public: void OnFixedUpdate(float fixedDeltaTime); void OnUpdate(float deltaTime); void OnLateUpdate(float deltaTime); + void DispatchPhysicsMessage( + Components::GameObject* gameObject, + ScriptPhysicsMessage message, + Components::GameObject* other); void OnScriptComponentEnabled(ScriptComponent* component); void OnScriptComponentDisabled(ScriptComponent* component); @@ -140,6 +144,10 @@ private: bool ShouldScriptRun(const ScriptInstanceState& state) const; bool EnsureScriptReady(ScriptInstanceState& state, bool invokeEnableIfNeeded); void InvokeLifecycleMethod(ScriptInstanceState& state, ScriptLifecycleMethod method, float deltaTime = 0.0f); + void InvokePhysicsMessage( + ScriptInstanceState& state, + ScriptPhysicsMessage message, + Components::GameObject* other); void StopTrackingScript(ScriptInstanceState& state, bool runtimeStopping); NullScriptRuntime m_nullRuntime; diff --git a/engine/src/Physics/PhysX/PhysXWorldBackend.cpp b/engine/src/Physics/PhysX/PhysXWorldBackend.cpp index fc7cf27b..c84e2f97 100644 --- a/engine/src/Physics/PhysX/PhysXWorldBackend.cpp +++ b/engine/src/Physics/PhysX/PhysXWorldBackend.cpp @@ -13,6 +13,7 @@ #include #include +#include #include #include #include @@ -124,6 +125,34 @@ void VisitGameObjectHierarchy( } // namespace +template +struct PointerPairKey { + T* first = nullptr; + T* second = nullptr; + + bool operator==(const PointerPairKey& other) const { + return first == other.first && second == other.second; + } +}; + +template +PointerPairKey MakePointerPairKey(T* first, T* second) { + if (std::less{}(second, first)) { + std::swap(first, second); + } + + return PointerPairKey{first, second}; +} + +template +struct PointerPairKeyHasher { + size_t operator()(const PointerPairKey& key) const { + const size_t h1 = std::hash{}(reinterpret_cast(key.first)); + const size_t h2 = std::hash{}(reinterpret_cast(key.second)); + return h1 ^ (h2 + 0x9e3779b97f4a7c15ULL + (h1 << 6) + (h1 >> 2)); + } +}; + struct ShapeBinding { Components::ColliderComponent* collider = nullptr; Components::GameObject* sourceGameObject = nullptr; @@ -139,6 +168,240 @@ struct ActorBinding { std::vector shapes; }; +class PhysXSimulationEventCollector final : public physx::PxSimulationEventCallback { +public: + using ShapePairKey = PointerPairKey; + using GameObjectPairKey = PointerPairKey; + + struct DirectedGameObjectPair { + Components::GameObject* first = nullptr; + Components::GameObject* second = nullptr; + }; + + struct ActivePairAggregate { + DirectedGameObjectPair pair; + size_t count = 0; + }; + + using ShapePairMap = std::unordered_map>; + using ActivePairMap = std::unordered_map>; + + static physx::PxFilterFlags FilterShader( + physx::PxFilterObjectAttributes attributes0, + physx::PxFilterData filterData0, + physx::PxFilterObjectAttributes attributes1, + physx::PxFilterData filterData1, + physx::PxPairFlags& pairFlags, + const void* constantBlock, + physx::PxU32 constantBlockSize); + + void Clear() { + m_pendingEvents.clear(); + m_activeContactShapePairs.clear(); + m_activeTriggerShapePairs.clear(); + m_activeContactPairs.clear(); + m_activeTriggerPairs.clear(); + } + + void Consume(std::vector& outEvents) { + outEvents.clear(); + outEvents.swap(m_pendingEvents); + } + + void FlushStayEvents() { + EmitStayEvents(m_activeContactPairs, PhysicsEventType::CollisionStay); + EmitStayEvents(m_activeTriggerPairs, PhysicsEventType::TriggerStay); + } + + void onConstraintBreak(physx::PxConstraintInfo*, physx::PxU32) override { + } + + void onWake(physx::PxActor**, physx::PxU32) override { + } + + void onSleep(physx::PxActor**, physx::PxU32) override { + } + + void onAdvance(const physx::PxRigidBody* const*, const physx::PxTransform*, const physx::PxU32) override { + } + + void onContact( + const physx::PxContactPairHeader& pairHeader, + const physx::PxContactPair* pairs, + physx::PxU32 count) override { + (void)pairHeader; + + for (physx::PxU32 pairIndex = 0; pairIndex < count; ++pairIndex) { + const physx::PxContactPair& pair = pairs[pairIndex]; + physx::PxShape* shape0 = pair.shapes[0]; + physx::PxShape* shape1 = pair.shapes[1]; + if (!shape0 || !shape1) { + continue; + } + + const physx::PxPairFlags events = pair.events; + if (events.isSet(physx::PxPairFlag::eNOTIFY_TOUCH_FOUND)) { + HandleShapePairFound( + m_activeContactShapePairs, + m_activeContactPairs, + shape0, + shape1, + PhysicsEventType::CollisionEnter); + } + + if (events.isSet(physx::PxPairFlag::eNOTIFY_TOUCH_LOST)) { + HandleShapePairLost( + m_activeContactShapePairs, + m_activeContactPairs, + shape0, + shape1, + PhysicsEventType::CollisionExit); + } + } + } + + void onTrigger(physx::PxTriggerPair* pairs, physx::PxU32 count) override { + for (physx::PxU32 pairIndex = 0; pairIndex < count; ++pairIndex) { + const physx::PxTriggerPair& pair = pairs[pairIndex]; + physx::PxShape* triggerShape = pair.triggerShape; + physx::PxShape* otherShape = pair.otherShape; + if (!triggerShape || !otherShape) { + continue; + } + + if (pair.status == physx::PxPairFlag::eNOTIFY_TOUCH_FOUND) { + HandleShapePairFound( + m_activeTriggerShapePairs, + m_activeTriggerPairs, + triggerShape, + otherShape, + PhysicsEventType::TriggerEnter); + } else if (pair.status == physx::PxPairFlag::eNOTIFY_TOUCH_LOST) { + HandleShapePairLost( + m_activeTriggerShapePairs, + m_activeTriggerPairs, + triggerShape, + otherShape, + PhysicsEventType::TriggerExit); + } + } + } + +private: + static Components::GameObject* ResolveShapeGameObject(const physx::PxShape* shape) { + if (!shape) { + return nullptr; + } + + auto* collider = static_cast(shape->userData); + return collider ? collider->GetGameObject() : nullptr; + } + + static DirectedGameObjectPair ResolveDirectedGameObjectPair( + const physx::PxShape* firstShape, + const physx::PxShape* secondShape) { + return DirectedGameObjectPair{ + ResolveShapeGameObject(firstShape), + ResolveShapeGameObject(secondShape) + }; + } + + static bool IsValidDirectedPair(const DirectedGameObjectPair& pair) { + return pair.first != nullptr && pair.second != nullptr && pair.first != pair.second; + } + + static bool IncrementPair(ActivePairMap& pairs, const GameObjectPairKey& key, const DirectedGameObjectPair& pair) { + auto [it, inserted] = pairs.try_emplace(key, ActivePairAggregate{pair, 0u}); + if (inserted) { + it->second.pair = pair; + } + + ++it->second.count; + return it->second.count == 1u; + } + + static bool DecrementPair(ActivePairMap& pairs, const GameObjectPairKey& key, DirectedGameObjectPair& outPair) { + const auto it = pairs.find(key); + if (it == pairs.end()) { + return false; + } + + outPair = it->second.pair; + if (it->second.count > 1u) { + --it->second.count; + return false; + } + + pairs.erase(it); + return true; + } + + void EmitDirectionalPairEvents(PhysicsEventType type, const DirectedGameObjectPair& pair) { + if (!IsValidDirectedPair(pair)) { + return; + } + + m_pendingEvents.push_back(PhysicsEvent{type, pair.first, pair.second}); + m_pendingEvents.push_back(PhysicsEvent{type, pair.second, pair.first}); + } + + void EmitStayEvents(const ActivePairMap& pairs, PhysicsEventType type) { + for (const auto& [_, aggregate] : pairs) { + EmitDirectionalPairEvents(type, aggregate.pair); + } + } + + void HandleShapePairFound( + ShapePairMap& activeShapePairs, + ActivePairMap& activePairs, + physx::PxShape* firstShape, + physx::PxShape* secondShape, + PhysicsEventType enterType) { + const ShapePairKey shapeKey = MakePointerPairKey(firstShape, secondShape); + if (activeShapePairs.find(shapeKey) != activeShapePairs.end()) { + return; + } + + const DirectedGameObjectPair pair = ResolveDirectedGameObjectPair(firstShape, secondShape); + if (!IsValidDirectedPair(pair)) { + return; + } + + const GameObjectPairKey gameObjectKey = MakePointerPairKey(pair.first, pair.second); + activeShapePairs.emplace(shapeKey, gameObjectKey); + if (IncrementPair(activePairs, gameObjectKey, pair)) { + EmitDirectionalPairEvents(enterType, pair); + } + } + + void HandleShapePairLost( + ShapePairMap& activeShapePairs, + ActivePairMap& activePairs, + physx::PxShape* firstShape, + physx::PxShape* secondShape, + PhysicsEventType exitType) { + const ShapePairKey shapeKey = MakePointerPairKey(firstShape, secondShape); + const auto shapeIt = activeShapePairs.find(shapeKey); + if (shapeIt == activeShapePairs.end()) { + return; + } + + const GameObjectPairKey gameObjectKey = shapeIt->second; + activeShapePairs.erase(shapeIt); + + DirectedGameObjectPair pair = {}; + if (DecrementPair(activePairs, gameObjectKey, pair)) { + EmitDirectionalPairEvents(exitType, pair); + } + } + + std::vector m_pendingEvents; + ShapePairMap m_activeContactShapePairs; + ShapePairMap m_activeTriggerShapePairs; + ActivePairMap m_activeContactPairs; + ActivePairMap m_activeTriggerPairs; +}; + struct PhysXWorldBackend::NativeState { physx::PxDefaultAllocator allocator; physx::PxDefaultErrorCallback errorCallback; @@ -147,9 +410,36 @@ struct PhysXWorldBackend::NativeState { physx::PxDefaultCpuDispatcher* dispatcher = nullptr; physx::PxScene* scene = nullptr; bool extensionsInitialized = false; + PhysXSimulationEventCollector simulationEventCollector; std::unordered_map actorsByOwner; }; +physx::PxFilterFlags PhysXSimulationEventCollector::FilterShader( + physx::PxFilterObjectAttributes attributes0, + physx::PxFilterData filterData0, + physx::PxFilterObjectAttributes attributes1, + physx::PxFilterData filterData1, + physx::PxPairFlags& pairFlags, + const void* constantBlock, + physx::PxU32 constantBlockSize) { + (void)filterData0; + (void)filterData1; + (void)constantBlock; + (void)constantBlockSize; + + if (physx::PxFilterObjectIsTrigger(attributes0) || physx::PxFilterObjectIsTrigger(attributes1)) { + pairFlags = physx::PxPairFlag::eTRIGGER_DEFAULT + | physx::PxPairFlag::eNOTIFY_TOUCH_FOUND + | physx::PxPairFlag::eNOTIFY_TOUCH_LOST; + return physx::PxFilterFlag::eDEFAULT; + } + + pairFlags = physx::PxPairFlag::eCONTACT_DEFAULT + | physx::PxPairFlag::eNOTIFY_TOUCH_FOUND + | physx::PxPairFlag::eNOTIFY_TOUCH_LOST; + return physx::PxFilterFlag::eDEFAULT; +} + namespace { void ReleaseActorBinding(PhysXWorldBackend::NativeState& native, ActorBinding& binding) { @@ -183,6 +473,8 @@ void ReleaseActorBinding(PhysXWorldBackend::NativeState& native, ActorBinding& b } void ClearActorBindings(PhysXWorldBackend::NativeState& native) { + native.simulationEventCollector.Clear(); + for (auto& [_, binding] : native.actorsByOwner) { ReleaseActorBinding(native, binding); } @@ -399,8 +691,6 @@ void SyncActorBindingState(ActorBinding& binding) { binding.dynamicActor->setAngularVelocity(ToPxVec3(binding.rigidbody->GetAngularVelocity())); ApplyPendingForces(*binding.dynamicActor, *binding.rigidbody); } else { - binding.dynamicActor->setLinearVelocity(physx::PxVec3(0.0f), false); - binding.dynamicActor->setAngularVelocity(physx::PxVec3(0.0f), false); ClearPendingForces(*binding.rigidbody); } @@ -488,7 +778,10 @@ bool PhysXWorldBackend::Initialize(const PhysicsWorldCreateInfo& createInfo) { physx::PxSceneDesc sceneDesc(native.physics->getTolerancesScale()); sceneDesc.gravity = ToPxVec3(createInfo.gravity); sceneDesc.cpuDispatcher = native.dispatcher; - sceneDesc.filterShader = physx::PxDefaultSimulationFilterShader; + sceneDesc.filterShader = PhysXSimulationEventCollector::FilterShader; + sceneDesc.simulationEventCallback = &native.simulationEventCollector; + sceneDesc.staticKineFilteringMode = physx::PxPairFilteringMode::eKEEP; + sceneDesc.kineKineFilteringMode = physx::PxPairFilteringMode::eKEEP; if (!sceneDesc.isValid()) { Shutdown(); return false; @@ -511,6 +804,7 @@ bool PhysXWorldBackend::Initialize(const PhysicsWorldCreateInfo& createInfo) { void PhysXWorldBackend::Shutdown() { #if XCENGINE_ENABLE_PHYSX if (m_native) { + m_native->simulationEventCollector.Clear(); ClearActorBindings(*m_native); if (m_native->scene) { @@ -558,6 +852,7 @@ void PhysXWorldBackend::Step(float fixedDeltaTime) { m_native->scene->simulate(fixedDeltaTime); m_native->scene->fetchResults(true); + m_native->simulationEventCollector.FlushStayEvents(); for (auto& [_, binding] : m_native->actorsByOwner) { if (!binding.dynamicActor || !binding.owner || !binding.rigidbody) { @@ -579,6 +874,19 @@ void PhysXWorldBackend::Step(float fixedDeltaTime) { #endif } +void PhysXWorldBackend::ConsumeSimulationEvents(std::vector& outEvents) { +#if XCENGINE_ENABLE_PHYSX + if (!m_native) { + outEvents.clear(); + return; + } + + m_native->simulationEventCollector.Consume(outEvents); +#else + outEvents.clear(); +#endif +} + void PhysXWorldBackend::SyncSceneState() { RebuildSceneState(); } diff --git a/engine/src/Physics/PhysX/PhysXWorldBackend.h b/engine/src/Physics/PhysX/PhysXWorldBackend.h index d43b56b9..14ccf228 100644 --- a/engine/src/Physics/PhysX/PhysXWorldBackend.h +++ b/engine/src/Physics/PhysX/PhysXWorldBackend.h @@ -1,10 +1,12 @@ #pragma once +#include #include #include #include #include +#include namespace XCEngine { namespace Components { @@ -28,6 +30,7 @@ public: bool Initialize(const PhysicsWorldCreateInfo& createInfo); void Shutdown(); void Step(float fixedDeltaTime); + void ConsumeSimulationEvents(std::vector& outEvents); void SyncSceneState(); void AddComponent(Components::Component* component); void RemoveComponent(Components::Component* component); diff --git a/engine/src/Physics/PhysicsWorld.cpp b/engine/src/Physics/PhysicsWorld.cpp index c9b131a9..17985be5 100644 --- a/engine/src/Physics/PhysicsWorld.cpp +++ b/engine/src/Physics/PhysicsWorld.cpp @@ -89,6 +89,15 @@ void PhysicsWorld::Step(float fixedDeltaTime) { m_backend->Step(fixedDeltaTime); } +void PhysicsWorld::ConsumeSimulationEvents(std::vector& outEvents) { + outEvents.clear(); + if (!m_backend || !m_initialized) { + return; + } + + m_backend->ConsumeSimulationEvents(outEvents); +} + bool PhysicsWorld::Raycast( const Math::Vector3& origin, const Math::Vector3& direction, diff --git a/engine/src/Scene/SceneRuntime.cpp b/engine/src/Scene/SceneRuntime.cpp index e3fef3b7..f9331080 100644 --- a/engine/src/Scene/SceneRuntime.cpp +++ b/engine/src/Scene/SceneRuntime.cpp @@ -7,6 +7,51 @@ namespace XCEngine { namespace Components { +namespace { + +Scripting::ScriptPhysicsMessage ToScriptPhysicsMessage(Physics::PhysicsEventType type) { + switch (type) { + case Physics::PhysicsEventType::CollisionEnter: + return Scripting::ScriptPhysicsMessage::CollisionEnter; + case Physics::PhysicsEventType::CollisionStay: + return Scripting::ScriptPhysicsMessage::CollisionStay; + case Physics::PhysicsEventType::CollisionExit: + return Scripting::ScriptPhysicsMessage::CollisionExit; + case Physics::PhysicsEventType::TriggerEnter: + return Scripting::ScriptPhysicsMessage::TriggerEnter; + case Physics::PhysicsEventType::TriggerStay: + return Scripting::ScriptPhysicsMessage::TriggerStay; + case Physics::PhysicsEventType::TriggerExit: + default: + return Scripting::ScriptPhysicsMessage::TriggerExit; + } +} + +void DispatchPhysicsEventToComponent(Component& component, const Physics::PhysicsEvent& event) { + switch (event.type) { + case Physics::PhysicsEventType::CollisionEnter: + component.OnCollisionEnter(event.other); + return; + case Physics::PhysicsEventType::CollisionStay: + component.OnCollisionStay(event.other); + return; + case Physics::PhysicsEventType::CollisionExit: + component.OnCollisionExit(event.other); + return; + case Physics::PhysicsEventType::TriggerEnter: + component.OnTriggerEnter(event.other); + return; + case Physics::PhysicsEventType::TriggerStay: + component.OnTriggerStay(event.other); + return; + case Physics::PhysicsEventType::TriggerExit: + component.OnTriggerExit(event.other); + return; + } +} + +} // namespace + SceneRuntime::SceneRuntime() : m_uiRuntime(std::make_unique()) { } @@ -74,6 +119,27 @@ void SceneRuntime::FixedUpdate(float fixedDeltaTime) { m_scene->FixedUpdate(fixedDeltaTime); if (m_physicsWorld) { m_physicsWorld->Step(fixedDeltaTime); + + std::vector physicsEvents; + m_physicsWorld->ConsumeSimulationEvents(physicsEvents); + for (const Physics::PhysicsEvent& event : physicsEvents) { + if (!event.self || !event.other || !event.self->IsActiveInHierarchy()) { + continue; + } + + for (Component* component : event.self->GetComponents()) { + if (!component || !component->IsEnabled()) { + continue; + } + + DispatchPhysicsEventToComponent(*component, event); + } + + Scripting::ScriptEngine::Get().DispatchPhysicsMessage( + event.self, + ToScriptPhysicsMessage(event.type), + event.other); + } } } diff --git a/engine/src/Scripting/Mono/MonoScriptRuntime.cpp b/engine/src/Scripting/Mono/MonoScriptRuntime.cpp index c586e475..f1b28c2c 100644 --- a/engine/src/Scripting/Mono/MonoScriptRuntime.cpp +++ b/engine/src/Scripting/Mono/MonoScriptRuntime.cpp @@ -2097,10 +2097,46 @@ void MonoScriptRuntime::InvokeMethod( const float previousDeltaTime = GetInternalCallDeltaTime(); GetInternalCallDeltaTime() = deltaTime; - InvokeManagedMethod(instance, managedMethod); + InvokeManagedMethod(instance, managedMethod, nullptr); GetInternalCallDeltaTime() = previousDeltaTime; } +void MonoScriptRuntime::InvokePhysicsMessage( + const ScriptRuntimeContext& context, + ScriptPhysicsMessage message, + Components::GameObject* other) { + const InstanceData* instanceData = FindInstance(context); + if (!instanceData || !instanceData->classMetadata) { + return; + } + + const PhysicsMessageMethods& methods = + instanceData->classMetadata->physicsMessageMethods[static_cast(message)]; + MonoMethod* managedMethod = methods.withGameObject ? methods.withGameObject : methods.withoutArgs; + if (!managedMethod) { + return; + } + + MonoObject* instance = GetManagedObject(*instanceData); + if (!instance) { + return; + } + + if (methods.withGameObject) { + MonoObject* managedOther = other ? CreateManagedGameObject(other->GetUUID()) : nullptr; + if (other && !managedOther) { + return; + } + + void* args[1]; + args[0] = managedOther; + InvokeManagedMethod(instance, managedMethod, args); + return; + } + + InvokeManagedMethod(instance, managedMethod, nullptr); +} + size_t MonoScriptRuntime::InstanceKeyHasher::operator()(const InstanceKey& key) const { const size_t h1 = std::hash{}(key.gameObjectUUID); const size_t h2 = std::hash{}(key.scriptComponentUUID); @@ -2346,6 +2382,18 @@ void MonoScriptRuntime::DiscoverScriptClassesInImage(const std::string& assembly 0); } + for (size_t methodIndex = 0; methodIndex < PhysicsMessageCount; ++methodIndex) { + PhysicsMessageMethods& methods = metadata.physicsMessageMethods[methodIndex]; + methods.withGameObject = mono_class_get_method_from_name( + monoClass, + ToPhysicsMessageMethodName(static_cast(methodIndex)), + 1); + methods.withoutArgs = mono_class_get_method_from_name( + monoClass, + ToPhysicsMessageMethodName(static_cast(methodIndex)), + 0); + } + void* fieldIterator = nullptr; while (MonoClassField* field = mono_class_get_fields(monoClass, &fieldIterator)) { const uint32_t fieldFlags = mono_field_get_flags(field); @@ -2564,6 +2612,19 @@ const char* MonoScriptRuntime::ToLifecycleMethodName(ScriptLifecycleMethod metho return ""; } +const char* MonoScriptRuntime::ToPhysicsMessageMethodName(ScriptPhysicsMessage message) { + switch (message) { + case ScriptPhysicsMessage::CollisionEnter: return "OnCollisionEnter"; + case ScriptPhysicsMessage::CollisionStay: return "OnCollisionStay"; + case ScriptPhysicsMessage::CollisionExit: return "OnCollisionExit"; + case ScriptPhysicsMessage::TriggerEnter: return "OnTriggerEnter"; + case ScriptPhysicsMessage::TriggerStay: return "OnTriggerStay"; + case ScriptPhysicsMessage::TriggerExit: return "OnTriggerExit"; + } + + return ""; +} + const MonoScriptRuntime::ClassMetadata* MonoScriptRuntime::FindClassMetadata( const std::string& assemblyName, const std::string& namespaceName, @@ -3207,7 +3268,7 @@ void MonoScriptRuntime::ClearClassCache() { m_classes.clear(); } -bool MonoScriptRuntime::InvokeManagedMethod(MonoObject* instance, MonoMethod* method) { +bool MonoScriptRuntime::InvokeManagedMethod(MonoObject* instance, MonoMethod* method, void** args) { if (!instance || !method) { return false; } @@ -3215,7 +3276,7 @@ bool MonoScriptRuntime::InvokeManagedMethod(MonoObject* instance, MonoMethod* me SetCurrentDomain(); MonoObject* exception = nullptr; - mono_runtime_invoke(method, instance, nullptr, &exception); + mono_runtime_invoke(method, instance, args, &exception); if (exception) { RecordException(exception); return false; diff --git a/engine/src/Scripting/NullScriptRuntime.cpp b/engine/src/Scripting/NullScriptRuntime.cpp index d12c98d8..c07c9ab5 100644 --- a/engine/src/Scripting/NullScriptRuntime.cpp +++ b/engine/src/Scripting/NullScriptRuntime.cpp @@ -82,5 +82,14 @@ void NullScriptRuntime::InvokeMethod( (void)deltaTime; } +void NullScriptRuntime::InvokePhysicsMessage( + const ScriptRuntimeContext& context, + ScriptPhysicsMessage message, + Components::GameObject* other) { + (void)context; + (void)message; + (void)other; +} + } // namespace Scripting } // namespace XCEngine diff --git a/engine/src/Scripting/ScriptEngine.cpp b/engine/src/Scripting/ScriptEngine.cpp index e157d1e7..99595a69 100644 --- a/engine/src/Scripting/ScriptEngine.cpp +++ b/engine/src/Scripting/ScriptEngine.cpp @@ -258,6 +258,28 @@ void ScriptEngine::OnLateUpdate(float deltaTime) { } } +void ScriptEngine::DispatchPhysicsMessage( + Components::GameObject* gameObject, + ScriptPhysicsMessage message, + Components::GameObject* other) { + if (!m_runtimeRunning || !gameObject) { + return; + } + + for (ScriptComponent* component : gameObject->GetComponents()) { + if (!component) { + continue; + } + + ScriptInstanceState* state = FindState(component); + if (!state || !ShouldScriptRun(*state) || !EnsureScriptReady(*state, true) || !state->enabled) { + continue; + } + + InvokePhysicsMessage(*state, message, other); + } +} + void ScriptEngine::OnScriptComponentEnabled(ScriptComponent* component) { if (!m_runtimeRunning || !component) { return; @@ -887,6 +909,14 @@ void ScriptEngine::InvokeLifecycleMethod(ScriptInstanceState& state, ScriptLifec m_runtime->SyncManagedFieldsToStorage(state.context); } +void ScriptEngine::InvokePhysicsMessage( + ScriptInstanceState& state, + ScriptPhysicsMessage message, + Components::GameObject* other) { + m_runtime->InvokePhysicsMessage(state.context, message, other); + m_runtime->SyncManagedFieldsToStorage(state.context); +} + void ScriptEngine::StopTrackingScript(ScriptInstanceState& state, bool runtimeStopping) { if (state.enabled) { InvokeLifecycleMethod(state, ScriptLifecycleMethod::OnDisable); diff --git a/managed/CMakeLists.txt b/managed/CMakeLists.txt index da9ee4b5..aca9b30c 100644 --- a/managed/CMakeLists.txt +++ b/managed/CMakeLists.txt @@ -139,6 +139,7 @@ set(XCENGINE_GAME_SCRIPT_SOURCES ${CMAKE_CURRENT_SOURCE_DIR}/GameScripts/MeshComponentProbe.cs ${CMAKE_CURRENT_SOURCE_DIR}/GameScripts/MeshRendererEdgeCaseProbe.cs ${CMAKE_CURRENT_SOURCE_DIR}/GameScripts/ObjectApiProbe.cs + ${CMAKE_CURRENT_SOURCE_DIR}/GameScripts/PhysicsEventProbe.cs ${CMAKE_CURRENT_SOURCE_DIR}/GameScripts/RenderPipelineApiProbe.cs ${CMAKE_CURRENT_SOURCE_DIR}/GameScripts/SerializeFieldProbe.cs ${CMAKE_CURRENT_SOURCE_DIR}/GameScripts/TagLayerProbe.cs diff --git a/managed/GameScripts/PhysicsEventProbe.cs b/managed/GameScripts/PhysicsEventProbe.cs new file mode 100644 index 00000000..99897561 --- /dev/null +++ b/managed/GameScripts/PhysicsEventProbe.cs @@ -0,0 +1,54 @@ +using XCEngine; + +namespace Gameplay +{ + public class PhysicsEventProbe : MonoBehaviour + { + public int CollisionEnterCount; + public int CollisionStayCount; + public int CollisionExitCount; + public int TriggerEnterCount; + public int TriggerStayCount; + public int TriggerExitCount; + public bool CollisionStayFallbackInvoked; + public bool TriggerStayFallbackInvoked; + public string LastCollisionOtherName = string.Empty; + public string LastTriggerOtherName = string.Empty; + + private void OnCollisionEnter(GameObject other) + { + CollisionEnterCount++; + LastCollisionOtherName = other != null ? other.name : string.Empty; + } + + private void OnCollisionStay() + { + CollisionStayCount++; + CollisionStayFallbackInvoked = true; + } + + private void OnCollisionExit(GameObject other) + { + CollisionExitCount++; + LastCollisionOtherName = other != null ? other.name : string.Empty; + } + + private void OnTriggerEnter(GameObject other) + { + TriggerEnterCount++; + LastTriggerOtherName = other != null ? other.name : string.Empty; + } + + private void OnTriggerStay() + { + TriggerStayCount++; + TriggerStayFallbackInvoked = true; + } + + private void OnTriggerExit(GameObject other) + { + TriggerExitCount++; + LastTriggerOtherName = other != null ? other.name : string.Empty; + } + } +} diff --git a/tests/Scene/test_scene_runtime.cpp b/tests/Scene/test_scene_runtime.cpp index 369c63c7..9fa7b851 100644 --- a/tests/Scene/test_scene_runtime.cpp +++ b/tests/Scene/test_scene_runtime.cpp @@ -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* events) @@ -81,6 +94,38 @@ private: std::vector* m_events = nullptr; }; +class PhysicsObserverComponent : public Component { +public: + explicit PhysicsObserverComponent(std::vector* 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* 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(&events); + mover->AddComponent(); + RigidbodyComponent* rigidbody = mover->AddComponent(); + 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(); + 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 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(&events); + mover->AddComponent(); + RigidbodyComponent* rigidbody = mover->AddComponent(); + 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(); + 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 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"); diff --git a/tests/scripting/CMakeLists.txt b/tests/scripting/CMakeLists.txt index 907374bd..de4548bb 100644 --- a/tests/scripting/CMakeLists.txt +++ b/tests/scripting/CMakeLists.txt @@ -50,6 +50,10 @@ target_include_directories(scripting_tests PRIVATE ${CMAKE_SOURCE_DIR}/engine/include ) +if(WIN32 AND XCENGINE_ENABLE_PHYSX) + xcengine_copy_physx_runtime_dlls(scripting_tests) +endif() + if(TARGET xcengine_managed_assemblies) add_dependencies(scripting_tests xcengine_managed_assemblies) xcengine_attach_recursive_build_target(scripting_tests xcengine_managed_assemblies) diff --git a/tests/scripting/test_mono_script_runtime.cpp b/tests/scripting/test_mono_script_runtime.cpp index f6b8f249..35b2aae5 100644 --- a/tests/scripting/test_mono_script_runtime.cpp +++ b/tests/scripting/test_mono_script_runtime.cpp @@ -1,18 +1,23 @@ #include +#include #include #include #include #include +#include +#include #include #include #include #include #include #include +#include #include #include #include +#include #include #include #include @@ -486,6 +491,107 @@ TEST_F(MonoScriptRuntimeTest, TimeFixedDeltaTimeUsesConfiguredRuntimeStepAcrossF EXPECT_FLOAT_EQ(observedUpdateDeltaTime, 0.016f); } +TEST_F(MonoScriptRuntimeTest, SceneRuntimeDispatchesManagedTriggerMessagesWithGameObjectArgumentsAndFallbacks) { + if (!XCEngine::Physics::PhysicsWorld::IsPhysXAvailable()) { + GTEST_SKIP() << "PhysX is not available in this configuration."; + } + + Scene* runtimeScene = CreateScene("MonoRuntimeScene"); + GameObject* mover = runtimeScene->CreateGameObject("Mover"); + mover->AddComponent(); + RigidbodyComponent* rigidbody = mover->AddComponent(); + rigidbody->SetUseGravity(false); + ScriptComponent* component = AddScript(mover, "Gameplay", "PhysicsEventProbe"); + mover->GetTransform()->SetPosition(XCEngine::Math::Vector3(-3.0f, 0.0f, 0.0f)); + + GameObject* triggerZone = runtimeScene->CreateGameObject("TriggerZone"); + BoxColliderComponent* triggerCollider = triggerZone->AddComponent(); + triggerCollider->SetSize(XCEngine::Math::Vector3(2.0f, 2.0f, 2.0f)); + triggerCollider->SetTrigger(true); + + SceneRuntime sceneRuntime; + sceneRuntime.Start(runtimeScene); + + rigidbody->SetLinearVelocity(XCEngine::Math::Vector3(150.0f, 0.0f, 0.0f)); + sceneRuntime.FixedUpdate(0.02f); + rigidbody->SetLinearVelocity(XCEngine::Math::Vector3::Zero()); + sceneRuntime.FixedUpdate(0.02f); + rigidbody->SetLinearVelocity(XCEngine::Math::Vector3(150.0f, 0.0f, 0.0f)); + sceneRuntime.FixedUpdate(0.02f); + rigidbody->SetLinearVelocity(XCEngine::Math::Vector3::Zero()); + sceneRuntime.FixedUpdate(0.02f); + + int32_t triggerEnterCount = 0; + int32_t triggerStayCount = 0; + int32_t triggerExitCount = 0; + bool triggerStayFallbackInvoked = false; + std::string lastTriggerOtherName; + + EXPECT_TRUE(runtime->TryGetFieldValue(component, "TriggerEnterCount", triggerEnterCount)); + EXPECT_TRUE(runtime->TryGetFieldValue(component, "TriggerStayCount", triggerStayCount)); + EXPECT_TRUE(runtime->TryGetFieldValue(component, "TriggerExitCount", triggerExitCount)); + EXPECT_TRUE(runtime->TryGetFieldValue(component, "TriggerStayFallbackInvoked", triggerStayFallbackInvoked)); + EXPECT_TRUE(runtime->TryGetFieldValue(component, "LastTriggerOtherName", lastTriggerOtherName)); + + EXPECT_EQ(triggerEnterCount, 1); + EXPECT_EQ(triggerStayCount, 2); + EXPECT_EQ(triggerExitCount, 1); + EXPECT_TRUE(triggerStayFallbackInvoked); + EXPECT_EQ(lastTriggerOtherName, "TriggerZone"); + + sceneRuntime.Stop(); +} + +TEST_F(MonoScriptRuntimeTest, SceneRuntimeDispatchesManagedCollisionMessagesWithGameObjectArgumentsAndFallbacks) { + if (!XCEngine::Physics::PhysicsWorld::IsPhysXAvailable()) { + GTEST_SKIP() << "PhysX is not available in this configuration."; + } + + Scene* runtimeScene = CreateScene("MonoRuntimeScene"); + GameObject* mover = runtimeScene->CreateGameObject("Mover"); + mover->AddComponent(); + RigidbodyComponent* rigidbody = mover->AddComponent(); + rigidbody->SetUseGravity(false); + ScriptComponent* component = AddScript(mover, "Gameplay", "PhysicsEventProbe"); + mover->GetTransform()->SetPosition(XCEngine::Math::Vector3(-3.0f, 0.0f, 0.0f)); + + GameObject* wall = runtimeScene->CreateGameObject("Wall"); + BoxColliderComponent* wallCollider = wall->AddComponent(); + wallCollider->SetSize(XCEngine::Math::Vector3(2.0f, 2.0f, 2.0f)); + + SceneRuntime sceneRuntime; + sceneRuntime.Start(runtimeScene); + + rigidbody->SetLinearVelocity(XCEngine::Math::Vector3(150.0f, 0.0f, 0.0f)); + sceneRuntime.FixedUpdate(0.02f); + rigidbody->SetLinearVelocity(XCEngine::Math::Vector3::Zero()); + sceneRuntime.FixedUpdate(0.02f); + rigidbody->SetLinearVelocity(XCEngine::Math::Vector3(-150.0f, 0.0f, 0.0f)); + sceneRuntime.FixedUpdate(0.02f); + rigidbody->SetLinearVelocity(XCEngine::Math::Vector3::Zero()); + sceneRuntime.FixedUpdate(0.02f); + + int32_t collisionEnterCount = 0; + int32_t collisionStayCount = 0; + int32_t collisionExitCount = 0; + bool collisionStayFallbackInvoked = false; + std::string lastCollisionOtherName; + + EXPECT_TRUE(runtime->TryGetFieldValue(component, "CollisionEnterCount", collisionEnterCount)); + EXPECT_TRUE(runtime->TryGetFieldValue(component, "CollisionStayCount", collisionStayCount)); + EXPECT_TRUE(runtime->TryGetFieldValue(component, "CollisionExitCount", collisionExitCount)); + EXPECT_TRUE(runtime->TryGetFieldValue(component, "CollisionStayFallbackInvoked", collisionStayFallbackInvoked)); + EXPECT_TRUE(runtime->TryGetFieldValue(component, "LastCollisionOtherName", lastCollisionOtherName)); + + EXPECT_EQ(collisionEnterCount, 1); + EXPECT_EQ(collisionStayCount, 2); + EXPECT_EQ(collisionExitCount, 1); + EXPECT_TRUE(collisionStayFallbackInvoked); + EXPECT_EQ(lastCollisionOtherName, "Wall"); + + sceneRuntime.Stop(); +} + TEST_F(MonoScriptRuntimeTest, ManagedInputApiReadsCurrentNativeInputManagerState) { XCEngine::Input::InputManager& inputManager = XCEngine::Input::InputManager::Get(); inputManager.Initialize(nullptr); diff --git a/tests/scripting/test_script_engine.cpp b/tests/scripting/test_script_engine.cpp index d0582013..3e183681 100644 --- a/tests/scripting/test_script_engine.cpp +++ b/tests/scripting/test_script_engine.cpp @@ -32,6 +32,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 FakeScriptRuntime : public IScriptRuntime { public: void OnRuntimeStart(Scene* scene) override { @@ -118,6 +131,18 @@ public: events.push_back(LifecycleMethodToString(method) + ":" + Describe(context)); } + void InvokePhysicsMessage( + const ScriptRuntimeContext& context, + ScriptPhysicsMessage message, + GameObject* other) override { + events.push_back( + PhysicsMessageToString(message) + + ":" + + Describe(context) + + ":" + + (other ? other->GetName() : std::string("null"))); + } + void Clear() { events.clear(); } @@ -388,6 +413,50 @@ TEST_F(ScriptEngineTest, ChangingScriptClassWhileRuntimeRunningRecreatesTrackedI EXPECT_TRUE(engine->HasRuntimeInstance(component)); } +TEST_F(ScriptEngineTest, DispatchPhysicsMessageInvokesTrackedActiveScripts) { + Scene* runtimeScene = CreateScene("RuntimeScene"); + GameObject* host = runtimeScene->CreateGameObject("Host"); + GameObject* other = runtimeScene->CreateGameObject("Other"); + AddScriptComponent(host, "Gameplay", "TriggerListener"); + + engine->OnRuntimeStart(runtimeScene); + runtime.Clear(); + + engine->DispatchPhysicsMessage(host, ScriptPhysicsMessage::TriggerEnter, other); + + const std::vector expected = { + "TriggerEnter:Host:Gameplay.TriggerListener:Other" + }; + EXPECT_EQ(runtime.events, expected); +} + +TEST_F(ScriptEngineTest, DispatchPhysicsMessageSkipsInactiveAndDisabledScripts) { + Scene* runtimeScene = CreateScene("RuntimeScene"); + GameObject* activeHost = runtimeScene->CreateGameObject("ActiveHost"); + GameObject* inactiveHost = runtimeScene->CreateGameObject("InactiveHost"); + GameObject* disabledHost = runtimeScene->CreateGameObject("DisabledHost"); + GameObject* other = runtimeScene->CreateGameObject("Other"); + + AddScriptComponent(activeHost, "Gameplay", "ActiveListener"); + AddScriptComponent(inactiveHost, "Gameplay", "InactiveListener"); + ScriptComponent* disabledComponent = AddScriptComponent(disabledHost, "Gameplay", "DisabledListener"); + + inactiveHost->SetActive(false); + disabledComponent->SetEnabled(false); + + engine->OnRuntimeStart(runtimeScene); + runtime.Clear(); + + engine->DispatchPhysicsMessage(activeHost, ScriptPhysicsMessage::CollisionEnter, other); + engine->DispatchPhysicsMessage(inactiveHost, ScriptPhysicsMessage::CollisionEnter, other); + engine->DispatchPhysicsMessage(disabledHost, ScriptPhysicsMessage::CollisionEnter, other); + + const std::vector expected = { + "CollisionEnter:ActiveHost:Gameplay.ActiveListener:Other" + }; + EXPECT_EQ(runtime.events, expected); +} + TEST_F(ScriptEngineTest, ClearingScriptClassWhileRuntimeRunningDestroysTrackedInstance) { Scene* runtimeScene = CreateScene("RuntimeScene"); GameObject* host = runtimeScene->CreateGameObject("Host");