#include #include #include #include #include #include #include #include namespace { TEST(PhysicsWorld_Test, DefaultWorldStartsUninitialized) { XCEngine::Physics::PhysicsWorld world; EXPECT_FALSE(world.IsInitialized()); EXPECT_EQ(world.GetNativeActorCount(), 0u); EXPECT_EQ(world.GetNativeShapeCount(), 0u); } TEST(PhysicsWorld_Test, InitializeStoresCreateInfoWithoutMarkingWorldReadyYet) { XCEngine::Components::Scene scene("PhysicsScene"); XCEngine::Physics::PhysicsWorld world; XCEngine::Physics::PhysicsWorldCreateInfo createInfo; createInfo.scene = &scene; createInfo.gravity = XCEngine::Math::Vector3(0.0f, -3.5f, 0.0f); const bool expectedInitialized = XCEngine::Physics::PhysicsWorld::IsPhysXAvailable(); EXPECT_EQ(world.Initialize(createInfo), expectedInitialized); EXPECT_EQ(world.IsInitialized(), expectedInitialized); EXPECT_EQ(world.GetCreateInfo().scene, &scene); EXPECT_EQ(world.GetCreateInfo().gravity, XCEngine::Math::Vector3(0.0f, -3.5f, 0.0f)); } TEST(PhysicsWorld_Test, StepWithoutInitializationIsNoOp) { XCEngine::Physics::PhysicsWorld world; world.Step(0.016f); EXPECT_FALSE(world.IsInitialized()); } TEST(PhysicsWorld_Test, InitializeBuildsNativeActorsFromSceneComponents) { using namespace XCEngine::Components; Scene scene("PhysicsScene"); GameObject* ground = scene.CreateGameObject("Ground"); ground->AddComponent(); GameObject* player = scene.CreateGameObject("Player"); player->AddComponent(); player->AddComponent(); GameObject* childTrigger = scene.CreateGameObject("ChildTrigger", player); childTrigger->AddComponent(); XCEngine::Physics::PhysicsWorld world; XCEngine::Physics::PhysicsWorldCreateInfo createInfo = {}; createInfo.scene = &scene; const bool expectedInitialized = XCEngine::Physics::PhysicsWorld::IsPhysXAvailable(); ASSERT_EQ(world.Initialize(createInfo), expectedInitialized); EXPECT_EQ(world.GetTrackedRigidbodyCount(), 1u); EXPECT_EQ(world.GetTrackedColliderCount(), 3u); if (!expectedInitialized) { EXPECT_EQ(world.GetNativeActorCount(), 0u); EXPECT_EQ(world.GetNativeShapeCount(), 0u); return; } EXPECT_EQ(world.GetNativeActorCount(), 2u); EXPECT_EQ(world.GetNativeShapeCount(), 3u); } TEST(PhysicsWorld_Test, ComponentChangesRebuildNativeActorOwnership) { using namespace XCEngine::Components; Scene scene("PhysicsScene"); GameObject* player = scene.CreateGameObject("Player"); player->AddComponent(); SphereColliderComponent* playerSphere = player->AddComponent(); BoxColliderComponent* playerBox = player->AddComponent(); GameObject* child = scene.CreateGameObject("Child", player); child->AddComponent(); XCEngine::Physics::PhysicsWorld world; XCEngine::Physics::PhysicsWorldCreateInfo createInfo = {}; createInfo.scene = &scene; const bool expectedInitialized = XCEngine::Physics::PhysicsWorld::IsPhysXAvailable(); ASSERT_EQ(world.Initialize(createInfo), expectedInitialized); EXPECT_EQ(world.GetTrackedRigidbodyCount(), 1u); EXPECT_EQ(world.GetTrackedColliderCount(), 3u); if (!expectedInitialized) { return; } EXPECT_EQ(world.GetNativeActorCount(), 1u); EXPECT_EQ(world.GetNativeShapeCount(), 3u); ASSERT_TRUE(player->RemoveComponent(playerBox)); EXPECT_EQ(world.GetTrackedRigidbodyCount(), 1u); EXPECT_EQ(world.GetTrackedColliderCount(), 2u); EXPECT_EQ(world.GetNativeActorCount(), 1u); EXPECT_EQ(world.GetNativeShapeCount(), 2u); child->AddComponent(); EXPECT_EQ(world.GetTrackedRigidbodyCount(), 2u); EXPECT_EQ(world.GetTrackedColliderCount(), 2u); EXPECT_EQ(world.GetNativeActorCount(), 2u); EXPECT_EQ(world.GetNativeShapeCount(), 2u); scene.DestroyGameObject(player); EXPECT_EQ(world.GetTrackedRigidbodyCount(), 0u); EXPECT_EQ(world.GetTrackedColliderCount(), 0u); EXPECT_EQ(world.GetNativeActorCount(), 0u); EXPECT_EQ(world.GetNativeShapeCount(), 0u); (void)playerSphere; } TEST(PhysicsWorld_Test, DynamicSimulationWritesBackTransform) { using namespace XCEngine::Components; if (!XCEngine::Physics::PhysicsWorld::IsPhysXAvailable()) { GTEST_SKIP() << "PhysX backend is not available in this build."; } Scene scene("PhysicsScene"); GameObject* ground = scene.CreateGameObject("Ground"); ground->GetTransform()->SetPosition(XCEngine::Math::Vector3(0.0f, -0.5f, 0.0f)); BoxColliderComponent* groundCollider = ground->AddComponent(); groundCollider->SetSize(XCEngine::Math::Vector3(20.0f, 1.0f, 20.0f)); GameObject* ball = scene.CreateGameObject("Ball"); ball->GetTransform()->SetPosition(XCEngine::Math::Vector3(0.0f, 4.0f, 0.0f)); RigidbodyComponent* rigidbody = ball->AddComponent(); rigidbody->SetBodyType(XCEngine::Physics::PhysicsBodyType::Dynamic); rigidbody->SetMass(1.0f); rigidbody->SetUseGravity(true); SphereColliderComponent* sphere = ball->AddComponent(); sphere->SetRadius(0.5f); XCEngine::Physics::PhysicsWorld world; XCEngine::Physics::PhysicsWorldCreateInfo createInfo = {}; createInfo.scene = &scene; ASSERT_TRUE(world.Initialize(createInfo)); const float initialY = ball->GetTransform()->GetPosition().y; for (int index = 0; index < 30; ++index) { world.Step(1.0f / 60.0f); } EXPECT_LT(ball->GetTransform()->GetPosition().y, initialY - 0.1f); } TEST(PhysicsWorld_Test, RaycastHitsClosestCollider) { using namespace XCEngine::Components; Scene scene("PhysicsScene"); GameObject* ground = scene.CreateGameObject("Ground"); ground->GetTransform()->SetPosition(XCEngine::Math::Vector3(0.0f, -0.5f, 0.0f)); BoxColliderComponent* groundCollider = ground->AddComponent(); groundCollider->SetSize(XCEngine::Math::Vector3(20.0f, 1.0f, 20.0f)); GameObject* target = scene.CreateGameObject("Target"); target->GetTransform()->SetPosition(XCEngine::Math::Vector3(0.0f, 2.0f, 0.0f)); SphereColliderComponent* sphereCollider = target->AddComponent(); sphereCollider->SetRadius(0.5f); XCEngine::Physics::PhysicsWorld world; XCEngine::Physics::PhysicsWorldCreateInfo createInfo = {}; createInfo.scene = &scene; const bool expectedInitialized = XCEngine::Physics::PhysicsWorld::IsPhysXAvailable(); ASSERT_EQ(world.Initialize(createInfo), expectedInitialized); XCEngine::Physics::RaycastHit hit; const bool result = world.Raycast( XCEngine::Math::Vector3(0.0f, 5.0f, 0.0f), XCEngine::Math::Vector3::Down(), 10.0f, hit); if (!expectedInitialized) { EXPECT_FALSE(result); EXPECT_EQ(hit.gameObject, nullptr); return; } ASSERT_TRUE(result); EXPECT_EQ(hit.gameObject, target); EXPECT_NEAR(hit.distance, 2.5f, 0.02f); EXPECT_NEAR(hit.point.x, 0.0f, 0.001f); EXPECT_NEAR(hit.point.y, 2.5f, 0.02f); EXPECT_NEAR(hit.point.z, 0.0f, 0.001f); EXPECT_NEAR(hit.normal.x, 0.0f, 0.001f); EXPECT_NEAR(hit.normal.y, 1.0f, 0.02f); EXPECT_NEAR(hit.normal.z, 0.0f, 0.001f); EXPECT_FALSE(hit.isTrigger); } TEST(PhysicsWorld_Test, RaycastMissClearsHitOutput) { using namespace XCEngine::Components; Scene scene("PhysicsScene"); GameObject* target = scene.CreateGameObject("Target"); target->AddComponent(); XCEngine::Physics::PhysicsWorld world; XCEngine::Physics::PhysicsWorldCreateInfo createInfo = {}; createInfo.scene = &scene; const bool expectedInitialized = XCEngine::Physics::PhysicsWorld::IsPhysXAvailable(); ASSERT_EQ(world.Initialize(createInfo), expectedInitialized); XCEngine::Physics::RaycastHit hit; hit.gameObject = target; hit.point = XCEngine::Math::Vector3::One(); hit.normal = XCEngine::Math::Vector3::Up(); hit.distance = 123.0f; hit.isTrigger = true; const bool result = world.Raycast( XCEngine::Math::Vector3(0.0f, 5.0f, 0.0f), XCEngine::Math::Vector3::Up(), 10.0f, hit); EXPECT_FALSE(result); EXPECT_EQ(hit.gameObject, nullptr); EXPECT_EQ(hit.point, XCEngine::Math::Vector3::Zero()); EXPECT_EQ(hit.normal, XCEngine::Math::Vector3::Zero()); EXPECT_FLOAT_EQ(hit.distance, 0.0f); EXPECT_FALSE(hit.isTrigger); } TEST(PhysicsWorld_Test, RaycastCanHitTriggerCollider) { using namespace XCEngine::Components; Scene scene("PhysicsScene"); GameObject* trigger = scene.CreateGameObject("Trigger"); SphereColliderComponent* sphereCollider = trigger->AddComponent(); sphereCollider->SetRadius(1.0f); sphereCollider->SetTrigger(true); XCEngine::Physics::PhysicsWorld world; XCEngine::Physics::PhysicsWorldCreateInfo createInfo = {}; createInfo.scene = &scene; const bool expectedInitialized = XCEngine::Physics::PhysicsWorld::IsPhysXAvailable(); ASSERT_EQ(world.Initialize(createInfo), expectedInitialized); XCEngine::Physics::RaycastHit hit; const bool result = world.Raycast( XCEngine::Math::Vector3(0.0f, 0.0f, -5.0f), XCEngine::Math::Vector3::Forward(), 10.0f, hit); if (!expectedInitialized) { EXPECT_FALSE(result); EXPECT_EQ(hit.gameObject, nullptr); return; } ASSERT_TRUE(result); EXPECT_EQ(hit.gameObject, trigger); EXPECT_NEAR(hit.distance, 4.0f, 0.02f); EXPECT_TRUE(hit.isTrigger); EXPECT_NEAR(hit.normal.x, 0.0f, 0.001f); EXPECT_NEAR(hit.normal.y, 0.0f, 0.001f); EXPECT_NEAR(hit.normal.z, -1.0f, 0.02f); } TEST(PhysicsWorld_Test, RuntimeColliderGeometryChangesUpdateRaycast) { using namespace XCEngine::Components; Scene scene("PhysicsScene"); GameObject* target = scene.CreateGameObject("Target"); BoxColliderComponent* boxCollider = target->AddComponent(); boxCollider->SetSize(XCEngine::Math::Vector3(1.0f, 1.0f, 1.0f)); XCEngine::Physics::PhysicsWorld world; XCEngine::Physics::PhysicsWorldCreateInfo createInfo = {}; createInfo.scene = &scene; const bool expectedInitialized = XCEngine::Physics::PhysicsWorld::IsPhysXAvailable(); ASSERT_EQ(world.Initialize(createInfo), expectedInitialized); XCEngine::Physics::RaycastHit hit; EXPECT_FALSE(world.Raycast( XCEngine::Math::Vector3(0.0f, 1.5f, -5.0f), XCEngine::Math::Vector3::Forward(), 10.0f, hit)); boxCollider->SetSize(XCEngine::Math::Vector3(1.0f, 4.0f, 1.0f)); const bool result = world.Raycast( XCEngine::Math::Vector3(0.0f, 1.5f, -5.0f), XCEngine::Math::Vector3::Forward(), 10.0f, hit); if (!expectedInitialized) { EXPECT_FALSE(result); EXPECT_EQ(hit.gameObject, nullptr); return; } ASSERT_TRUE(result); EXPECT_EQ(hit.gameObject, target); EXPECT_NEAR(hit.distance, 4.5f, 0.02f); } TEST(PhysicsWorld_Test, RuntimeTriggerFlagChangesUpdateRaycastHit) { using namespace XCEngine::Components; Scene scene("PhysicsScene"); GameObject* target = scene.CreateGameObject("Target"); SphereColliderComponent* sphereCollider = target->AddComponent(); sphereCollider->SetRadius(1.0f); XCEngine::Physics::PhysicsWorld world; XCEngine::Physics::PhysicsWorldCreateInfo createInfo = {}; createInfo.scene = &scene; const bool expectedInitialized = XCEngine::Physics::PhysicsWorld::IsPhysXAvailable(); ASSERT_EQ(world.Initialize(createInfo), expectedInitialized); sphereCollider->SetTrigger(true); XCEngine::Physics::RaycastHit hit; const bool result = world.Raycast( XCEngine::Math::Vector3(0.0f, 0.0f, -5.0f), XCEngine::Math::Vector3::Forward(), 10.0f, hit); if (!expectedInitialized) { EXPECT_FALSE(result); EXPECT_EQ(hit.gameObject, nullptr); return; } ASSERT_TRUE(result); EXPECT_EQ(hit.gameObject, target); EXPECT_TRUE(hit.isTrigger); } TEST(PhysicsWorld_Test, RuntimeBodyTypeChangesRebuildActorAndEnableSimulation) { using namespace XCEngine::Components; if (!XCEngine::Physics::PhysicsWorld::IsPhysXAvailable()) { GTEST_SKIP() << "PhysX backend is not available in this build."; } Scene scene("PhysicsScene"); GameObject* ground = scene.CreateGameObject("Ground"); ground->GetTransform()->SetPosition(XCEngine::Math::Vector3(0.0f, -0.5f, 0.0f)); BoxColliderComponent* groundCollider = ground->AddComponent(); groundCollider->SetSize(XCEngine::Math::Vector3(20.0f, 1.0f, 20.0f)); GameObject* body = scene.CreateGameObject("Body"); body->GetTransform()->SetPosition(XCEngine::Math::Vector3(0.0f, 4.0f, 0.0f)); RigidbodyComponent* rigidbody = body->AddComponent(); rigidbody->SetBodyType(XCEngine::Physics::PhysicsBodyType::Static); SphereColliderComponent* sphereCollider = body->AddComponent(); sphereCollider->SetRadius(0.5f); XCEngine::Physics::PhysicsWorld world; XCEngine::Physics::PhysicsWorldCreateInfo createInfo = {}; createInfo.scene = &scene; ASSERT_TRUE(world.Initialize(createInfo)); ASSERT_EQ(world.GetNativeActorCount(), 2u); const float initialY = body->GetTransform()->GetPosition().y; world.Step(1.0f / 60.0f); EXPECT_NEAR(body->GetTransform()->GetPosition().y, initialY, 0.001f); rigidbody->SetBodyType(XCEngine::Physics::PhysicsBodyType::Dynamic); world.Step(1.0f / 60.0f); EXPECT_EQ(world.GetNativeActorCount(), 2u); for (int index = 0; index < 30; ++index) { world.Step(1.0f / 60.0f); } EXPECT_LT(body->GetTransform()->GetPosition().y, initialY - 0.1f); } TEST(PhysicsWorld_Test, RuntimeLinearVelocityMovesDynamicBody) { using namespace XCEngine::Components; if (!XCEngine::Physics::PhysicsWorld::IsPhysXAvailable()) { GTEST_SKIP() << "PhysX backend is not available in this build."; } Scene scene("PhysicsScene"); GameObject* body = scene.CreateGameObject("Body"); body->GetTransform()->SetPosition(XCEngine::Math::Vector3(0.0f, 1.0f, 0.0f)); RigidbodyComponent* rigidbody = body->AddComponent(); rigidbody->SetBodyType(XCEngine::Physics::PhysicsBodyType::Dynamic); rigidbody->SetUseGravity(false); rigidbody->SetLinearVelocity(XCEngine::Math::Vector3(0.0f, 2.0f, 0.0f)); SphereColliderComponent* sphereCollider = body->AddComponent(); sphereCollider->SetRadius(0.5f); XCEngine::Physics::PhysicsWorld world; XCEngine::Physics::PhysicsWorldCreateInfo createInfo = {}; createInfo.scene = &scene; ASSERT_TRUE(world.Initialize(createInfo)); const float initialY = body->GetTransform()->GetPosition().y; for (int index = 0; index < 30; ++index) { world.Step(1.0f / 60.0f); } EXPECT_GT(body->GetTransform()->GetPosition().y, initialY + 0.9f); EXPECT_NEAR(rigidbody->GetLinearVelocity().y, 2.0f, 0.02f); } TEST(PhysicsWorld_Test, AddForceImpulseMovesDynamicBody) { using namespace XCEngine::Components; if (!XCEngine::Physics::PhysicsWorld::IsPhysXAvailable()) { GTEST_SKIP() << "PhysX backend is not available in this build."; } Scene scene("PhysicsScene"); GameObject* body = scene.CreateGameObject("Body"); body->GetTransform()->SetPosition(XCEngine::Math::Vector3(0.0f, 1.0f, 0.0f)); RigidbodyComponent* rigidbody = body->AddComponent(); rigidbody->SetBodyType(XCEngine::Physics::PhysicsBodyType::Dynamic); rigidbody->SetUseGravity(false); rigidbody->SetMass(1.0f); SphereColliderComponent* sphereCollider = body->AddComponent(); sphereCollider->SetRadius(0.5f); XCEngine::Physics::PhysicsWorld world; XCEngine::Physics::PhysicsWorldCreateInfo createInfo = {}; createInfo.scene = &scene; ASSERT_TRUE(world.Initialize(createInfo)); const float initialY = body->GetTransform()->GetPosition().y; rigidbody->AddForce( XCEngine::Math::Vector3(0.0f, 3.0f, 0.0f), XCEngine::Physics::PhysicsForceMode::Impulse); for (int index = 0; index < 10; ++index) { world.Step(1.0f / 60.0f); } EXPECT_GT(body->GetTransform()->GetPosition().y, initialY + 0.35f); EXPECT_NEAR(rigidbody->GetLinearVelocity().y, 3.0f, 0.02f); } } // namespace