#include "MetaCorePhysics/MetaCorePhysics.h" #include "MetaCoreScene/MetaCoreScene.h" #include "MetaCoreScene/MetaCoreSceneSerializer.h" #include "MetaCoreFoundation/MetaCoreGeneratedReflection.h" #include #include #include #include #include #include namespace { int Failures = 0; void Expect(bool condition, const char* message) { if (!condition) { ++Failures; std::cerr << "FAIL: " << message << '\n'; } } MetaCore::MetaCoreGameObject AddBox(MetaCore::MetaCoreScene& scene, const char* name, glm::vec3 position, MetaCore::MetaCoreRigidBodyType type, bool trigger = false, std::uint32_t layer = 0) { auto object = scene.CreateGameObject(name); object.GetComponent().Position = position; auto& collider = object.AddComponent(); collider.IsTrigger = trigger; collider.CollisionLayer = layer; auto& body = object.AddComponent(); body.BodyType = type; return object; } } int main() { using namespace MetaCore; { auto settings = MetaCoreBuildDefaultPhysicsSettings(); Expect(settings.LayerNames.size() == 32U && settings.CollisionMasks.size() == 32U, "default settings expose 32 layers"); Expect(settings.Version == 2U && settings.Gravity == glm::vec3(0.0F, 0.0F, -9.81F), "default settings use MetaCore Z-up gravity"); settings.FixedTimeStep = -1.0; std::vector issues; Expect(!MetaCoreValidatePhysicsSettings(settings, &issues) && !issues.empty(), "invalid settings are repaired and diagnosed"); } { MetaCoreScene scene; auto ground = AddBox(scene, "Ground", {0.0F, 0.0F, -1.0F}, MetaCoreRigidBodyType::Static); ground.GetComponent().Size = {20.0F, 20.0F, 1.0F}; const auto falling = AddBox(scene, "Falling", {0.0F, 0.0F, 4.0F}, MetaCoreRigidBodyType::Dynamic); MetaCorePhysicsWorld world(scene, MetaCoreBuildDefaultPhysicsSettings()); auto hit = world.Raycast({0.0F, 0.0F, 10.0F}, {0.0F, 0.0F, -1.0F}, 30.0F); Expect(hit.has_value() && hit->ColliderObjectId == falling.GetId(), "raycast returns closest collider"); const auto invalid = world.Raycast(glm::vec3(0.0F), glm::vec3(0.0F), 10.0F); Expect(!invalid.has_value(), "zero direction is rejected"); for (int i = 0; i < 180; ++i) { scene.IncrementRevision(); world.Step(1.0 / 60.0); } const float z = scene.FindGameObject(falling.GetId()).GetComponent().Position.z; Expect(z > -0.1F && z < 1.0F, "dynamic body falls along -Z and rests on static body"); Expect(world.GetStatistics().BodyCount == 2U, "world reports body statistics"); } { MetaCoreScene scene; auto trigger = AddBox(scene, "Trigger", glm::vec3(0.0F), MetaCoreRigidBodyType::Static, true, 2U); auto settings = MetaCoreBuildDefaultPhysicsSettings(); settings.CollisionMasks[2] = 1U << 2U; MetaCorePhysicsWorld world(scene, settings); MetaCorePhysicsQueryFilter filter; filter.Triggers = MetaCorePhysicsTriggerQuery::Ignore; Expect(world.Overlap({}, glm::vec3(0.0F), filter).empty(), "query can ignore triggers"); filter.Triggers = MetaCorePhysicsTriggerQuery::Collide; filter.LayerMask = 1U << 2U; const auto overlaps = world.Overlap({}, glm::vec3(0.0F), filter); Expect(overlaps.size() == 1U && overlaps[0].ColliderObjectId == trigger.GetId(), "overlap honors trigger and layer filter"); } { MetaCoreScene scene; auto a=AddBox(scene,"A",glm::vec3(0.0F),MetaCoreRigidBodyType::Dynamic); auto b=AddBox(scene,"B",{0.0F,0.0F,2.0F},MetaCoreRigidBodyType::Static); auto& constraint=a.AddComponent(); constraint.Type=MetaCorePhysicsConstraintType::Fixed; constraint.TargetObjectId=b.GetId(); MetaCorePhysicsWorld world(scene,MetaCoreBuildDefaultPhysicsSettings()); Expect(world.GetDiagnostics().empty(),"fixed constraint resolves both bodies"); Expect(world.SetConstraintEnabled(a.GetId(), false) && world.SetConstraintEnabled(a.GetId(), true), "constraint can be enabled and disabled at runtime"); world.Step(1.0/60.0); } { MetaCoreScene scene; auto a=AddBox(scene,"MotorA",glm::vec3(0.0F),MetaCoreRigidBodyType::Dynamic); auto b=AddBox(scene,"MotorB",{0.0F,0.0F,2.0F},MetaCoreRigidBodyType::Static); auto& constraint=a.AddComponent(); constraint.Type=MetaCorePhysicsConstraintType::Hinge; constraint.TargetObjectId=b.GetId(); MetaCorePhysicsWorld world(scene,MetaCoreBuildDefaultPhysicsSettings()); Expect(world.SetConstraintMotor(a.GetId(), true, 1.0F, 5.0F), "hinge motor can be configured at runtime"); } { MetaCoreScene scene; auto ground = AddBox(scene, "CharacterGround", {0.0F, 0.0F, -0.5F}, MetaCoreRigidBodyType::Static); ground.GetComponent().Size = {20.0F, 20.0F, 1.0F}; auto character=scene.CreateGameObject("Character"); character.GetComponent().Position={0.0F,0.0F,2.0F}; character.AddComponent(); MetaCorePhysicsWorld world(scene,MetaCoreBuildDefaultPhysicsSettings()); Expect(world.GetStatistics().BodyCount==2U,"character controller creates an implicit capsule body"); Expect(world.CharacterMove(character.GetId(),{0.1F,0.0F,0.0F}),"character accepts movement commands"); for (int index = 0; index < 120; ++index) world.Step(1.0 / 60.0); Expect(world.IsCharacterGrounded(character.GetId()), "implicit capsule character lands and reports grounded"); Expect(world.CharacterJump(character.GetId(), 5.0F), "grounded character accepts jump"); } { MetaCoreScene scene; auto settings = MetaCoreBuildDefaultPhysicsSettings(); settings.Gravity = glm::vec3(0.0F); auto trigger = AddBox(scene, "LifecycleTrigger", glm::vec3(0.0F), MetaCoreRigidBodyType::Static, true); auto actor = AddBox(scene, "LifecycleActor", glm::vec3(0.0F), MetaCoreRigidBodyType::Dynamic); int enters = 0, stays = 0, exits = 0; MetaCorePhysicsWorld world(scene, settings); world.SetEventCallback([&](const MetaCorePhysicsContactEvent& event) { if (event.Type == MetaCorePhysicsEventType::TriggerEnter) ++enters; if (event.Type == MetaCorePhysicsEventType::TriggerStay) ++stays; if (event.Type == MetaCorePhysicsEventType::TriggerExit) ++exits; }); world.Step(1.0 / 60.0); world.Step(1.0 / 60.0); Expect(enters == 1 && stays == 1, "trigger lifecycle emits one Enter then one Stay"); (void)scene.DeleteGameObjects({actor.GetId()}); world.Step(1.0 / 60.0); Expect(exits == 1, "deleting a touching object emits one Exit"); (void)trigger; } { MetaCoreScene scene; auto object=AddBox(scene,"Serializable",glm::vec3(1.0F),MetaCoreRigidBodyType::Dynamic); object.AddComponent(); MetaCoreSceneDocument document; document.Name="Physics"; document.GameObjects=scene.CaptureSnapshot().GameObjects; MetaCoreTypeRegistry registry; MetaCoreRegisterFoundationGeneratedTypes(registry); MetaCoreRegisterSceneGeneratedTypes(registry); const auto path=std::filesystem::temp_directory_path()/"metacore_physics_scene_test.mcscene.json"; Expect(MetaCoreSceneSerializer::SaveSceneToJson(path,document,registry),"physics components serialize to scene JSON"); const auto loaded=MetaCoreSceneSerializer::LoadSceneFromJson(path,registry); Expect(loaded&&loaded->GameObjects.size()==1U&&loaded->GameObjects[0].Collider&&loaded->GameObjects[0].RigidBody&&loaded->GameObjects[0].CharacterController,"physics components survive scene JSON roundtrip"); std::error_code error;std::filesystem::remove(path,error); } std::cout << "MetaCorePhysicsTests: " << (Failures == 0 ? "PASS" : "FAIL") << '\n'; return Failures == 0 ? EXIT_SUCCESS : EXIT_FAILURE; }