MetaCore/tests/MetaCorePhysicsTests.cpp
2026-07-22 14:14:54 +08:00

189 lines
16 KiB
C++

#include "MetaCorePhysics/MetaCorePhysics.h"
#include "MetaCoreScene/MetaCoreScene.h"
#include "MetaCoreScene/MetaCoreSceneSerializer.h"
#include "MetaCoreFoundation/MetaCoreGeneratedReflection.h"
#include <algorithm>
#include <cmath>
#include <cstdlib>
#include <iostream>
#include <filesystem>
#include <vector>
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<MetaCore::MetaCoreTransformComponent>().Position = position;
auto& collider = object.AddComponent<MetaCore::MetaCoreColliderComponent>(); collider.IsTrigger = trigger; collider.CollisionLayer = layer;
auto& body = object.AddComponent<MetaCore::MetaCoreRigidBodyComponent>(); 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<std::string> 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<MetaCoreColliderComponent>().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");
Expect(!world.BuildDebugLines().empty()&&world.GetStatistics().QueryCount>0U,"debug geometry includes bodies and recent queries");
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<MetaCoreTransformComponent>().Position.z;
Expect(z > -0.1F && z < 1.0F, "dynamic body falls along -Z and rests on static body");
Expect(world.GetStatistics().ContactCount>0U&&world.BuildDebugLines().size()>12U,"debug geometry includes active contacts and normals");
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 owner=scene.CreateGameObject("CompoundOwner"); owner.AddComponent<MetaCoreColliderComponent>();owner.AddComponent<MetaCoreRigidBodyComponent>().BodyType=MetaCoreRigidBodyType::Dynamic;
auto child=scene.CreateGameObject("ChildCollider"); child.SetParentId(owner.GetId()); child.GetComponent<MetaCoreTransformComponent>().Position={2.0F,0.0F,0.0F}; child.AddComponent<MetaCoreColliderComponent>();
auto nested=scene.CreateGameObject("NestedBody"); nested.SetParentId(owner.GetId()); nested.AddComponent<MetaCoreRigidBodyComponent>().BodyType=MetaCoreRigidBodyType::Static;
auto nestedCollider=scene.CreateGameObject("NestedCollider"); nestedCollider.SetParentId(nested.GetId()); nestedCollider.AddComponent<MetaCoreColliderComponent>();
auto settings=MetaCoreBuildDefaultPhysicsSettings();settings.Gravity=glm::vec3(0.0F);MetaCorePhysicsWorld world(scene,settings);
Expect(world.GetStatistics().BodyCount==2U,"child colliders aggregate until a nested rigid body boundary");
const auto hit=world.Raycast({2.0F,0.0F,5.0F},{0.0F,0.0F,-1.0F},10.0F);
Expect(hit&&hit->ColliderObjectId==child.GetId()&&hit->RigidBodyObjectId==owner.GetId(),"compound query reports child collider and rigid body owner");
}
{
MetaCoreScene scene;auto owner=scene.CreateGameObject("MixedCompound");owner.AddComponent<MetaCoreColliderComponent>();owner.AddComponent<MetaCoreRigidBodyComponent>();
auto child=scene.CreateGameObject("MixedTrigger");child.SetParentId(owner.GetId());child.AddComponent<MetaCoreColliderComponent>().IsTrigger=true;
MetaCorePhysicsWorld world(scene,MetaCoreBuildDefaultPhysicsSettings());
Expect(!world.GetDiagnostics().empty()&&world.GetStatistics().BodyCount==0U,"mixed Trigger/solid compound semantics are rejected explicitly");
}
{
MetaCorePhysicsMeshData source;source.Positions={{0,0,0},{1,0,0},{0,1,0}};source.Indices={0,1,2};
const auto bytes=MetaCoreEncodePhysicsCollisionDerivedData(source);const auto decoded=MetaCoreDecodePhysicsCollisionDerivedData(bytes);
Expect(decoded&&decoded->Positions==source.Positions&&decoded->Indices==source.Indices,"versioned collision derived data roundtrips");
MetaCoreAssetGuid guid{};guid.Bytes[0]=1U;
const auto keyA=MetaCoreBuildPhysicsCollisionCacheKey(guid,10U,{1,1,1},4U);
const auto keyB=MetaCoreBuildPhysicsCollisionCacheKey(guid,11U,{1,1,1},4U);
const auto keyC=MetaCoreBuildPhysicsCollisionCacheKey(guid,10U,{2,1,1},4U);
Expect(keyA!=keyB&&keyA!=keyC,"collision cache invalidates on payload hash and effective scale");
auto corrupt=bytes;corrupt[4]=std::byte{99};Expect(!MetaCoreDecodePhysicsCollisionDerivedData(corrupt),"incompatible collision derived data is rejected");
}
{
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<MetaCorePhysicsConstraintComponent>(); 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<MetaCorePhysicsConstraintComponent>(); 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");
}
for(const auto constraintType:{MetaCorePhysicsConstraintType::Slider,MetaCorePhysicsConstraintType::SixDof}) {
MetaCoreScene scene;auto a=AddBox(scene,"MotorOwner",glm::vec3(0.0F),MetaCoreRigidBodyType::Dynamic);auto b=AddBox(scene,"MotorTarget",{0,0,2},MetaCoreRigidBodyType::Static);
auto& constraint=a.AddComponent<MetaCorePhysicsConstraintComponent>();constraint.Type=constraintType;constraint.TargetObjectId=b.GetId();constraint.LinearLowerLimit={-1,0,0};constraint.LinearUpperLimit={1,0,0};
MetaCorePhysicsWorld world(scene,MetaCoreBuildDefaultPhysicsSettings());Expect(world.SetConstraintMotor(a.GetId(),true,1.0F,4.0F),"slider and 6DoF motors can be configured");
}
{
MetaCoreScene scene;auto a=AddBox(scene,"BrokenOwner",glm::vec3(0.0F),MetaCoreRigidBodyType::Dynamic);auto& constraint=a.AddComponent<MetaCorePhysicsConstraintComponent>();constraint.BreakForce=0.01F;
MetaCorePhysicsWorld world(scene,MetaCoreBuildDefaultPhysicsSettings());int broken=0;world.SetEventCallback([&](const MetaCorePhysicsContactEvent& event){if(event.Type==MetaCorePhysicsEventType::ConstraintBroken)++broken;});
(void)world.AddImpulse(a.GetId(),{100,0,0});world.Step(1.0/60.0);Expect(broken==1,"constraint breaking emits one event");
}
{
MetaCoreScene scene;auto a=AddBox(scene,"InvalidConstraint",glm::vec3(0.0F),MetaCoreRigidBodyType::Dynamic);a.AddComponent<MetaCorePhysicsConstraintComponent>().TargetObjectId=999999U;
MetaCorePhysicsWorld world(scene,MetaCoreBuildDefaultPhysicsSettings());Expect(!world.GetDiagnostics().empty(),"invalid constraint target is diagnosed");
}
{
MetaCoreScene scene;
auto ground = AddBox(scene, "CharacterGround", {0.0F, 0.0F, -0.5F}, MetaCoreRigidBodyType::Static);
ground.GetComponent<MetaCoreColliderComponent>().Size = {20.0F, 20.0F, 1.0F};
auto character=scene.CreateGameObject("Character"); character.GetComponent<MetaCoreTransformComponent>().Position={0.0F,0.0F,2.0F}; character.AddComponent<MetaCoreCharacterControllerComponent>();
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 ground=AddBox(scene,"StepGround",{0,0,-0.5F},MetaCoreRigidBodyType::Static);ground.GetComponent<MetaCoreColliderComponent>().Size={10,10,1};
auto step=AddBox(scene,"Step",{0.9F,0,0.1F},MetaCoreRigidBodyType::Static);step.GetComponent<MetaCoreColliderComponent>().Size={0.5F,2.0F,0.2F};
auto character=scene.CreateGameObject("Stepper");character.GetComponent<MetaCoreTransformComponent>().Position={0,0,0.9F};auto& controller=character.AddComponent<MetaCoreCharacterControllerComponent>();controller.StepHeight=0.35F;
auto settings=MetaCoreBuildDefaultPhysicsSettings();settings.Gravity=glm::vec3(0.0F);MetaCorePhysicsWorld world(scene,settings);
Expect(world.CharacterMove(character.GetId(),{1.2F,0,0}),"character step move is accepted");world.Step(1.0/60.0);
Expect(scene.FindGameObject(character.GetId()).GetComponent<MetaCoreTransformComponent>().Position.x>0.5F,"character climbs an obstacle below StepHeight");
}
{
MetaCoreScene scene;
auto character=scene.CreateGameObject("Pusher");character.GetComponent<MetaCoreTransformComponent>().Position={0,0,0.9F};auto& controller=character.AddComponent<MetaCoreCharacterControllerComponent>();controller.PushForce=3.0F;
auto box=AddBox(scene,"PushBody",{1.1F,0,0.5F},MetaCoreRigidBodyType::Dynamic);
auto settings=MetaCoreBuildDefaultPhysicsSettings();settings.Gravity=glm::vec3(0.0F);MetaCorePhysicsWorld world(scene,settings);
Expect(world.CharacterMove(character.GetId(),{1.0F,0,0}),"character push move is accepted");
const auto pushedVelocity=world.GetLinearVelocity(box.GetId());Expect(pushedVelocity&&pushedVelocity->x>0.0F,"character applies configured push impulse to dynamic rigid bodies");
}
{
MetaCoreScene scene;auto slope=AddBox(scene,"Slope",{0,0,0},MetaCoreRigidBodyType::Static);slope.GetComponent<MetaCoreColliderComponent>().Size={10,10,0.2F};slope.GetComponent<MetaCoreTransformComponent>().RotationEulerDegrees={0,60,0};
auto steep=scene.CreateGameObject("SteepLimit");steep.GetComponent<MetaCoreTransformComponent>().Position={0,-2,1};steep.AddComponent<MetaCoreCharacterControllerComponent>().MaxSlopeDegrees=45.0F;
auto permissive=scene.CreateGameObject("PermissiveLimit");permissive.GetComponent<MetaCoreTransformComponent>().Position={0,2,1};permissive.AddComponent<MetaCoreCharacterControllerComponent>().MaxSlopeDegrees=70.0F;
auto settings=MetaCoreBuildDefaultPhysicsSettings();settings.Gravity=glm::vec3(0.0F);MetaCorePhysicsWorld world(scene,settings);
Expect(!world.IsCharacterGrounded(steep.GetId())&&world.IsCharacterGrounded(permissive.GetId()),"character grounding honors configured maximum slope");
}
{
MetaCoreScene scene;auto ground=AddBox(scene,"CeilingGround",{0,0,-0.5F},MetaCoreRigidBodyType::Static);ground.GetComponent<MetaCoreColliderComponent>().Size={10,10,1};
auto ceiling=AddBox(scene,"Ceiling",{0,0,2.0F},MetaCoreRigidBodyType::Static);ceiling.GetComponent<MetaCoreColliderComponent>().Size={4,4,0.2F};
auto character=scene.CreateGameObject("BlockedJumper");character.GetComponent<MetaCoreTransformComponent>().Position={0,0,0.9F};character.AddComponent<MetaCoreCharacterControllerComponent>();MetaCorePhysicsWorld world(scene,MetaCoreBuildDefaultPhysicsSettings());
Expect(world.CharacterJump(character.GetId(),8.0F),"grounded character starts a jump below a ceiling");float maximumZ=0.9F;for(int index=0;index<45;++index){world.Step(1.0/60.0);maximumZ=std::max(maximumZ,scene.FindGameObject(character.GetId()).GetComponent<MetaCoreTransformComponent>().Position.z);}Expect(maximumZ<1.05F,"ceiling blocks the character capsule during 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<MetaCoreCharacterControllerComponent>();
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;
}