repoverse / unify /tests /test_physics.cpp
SNAPKITTYWEST's picture
October 2026 main drop: mirror from GitHub
3ce1a40 verified
Raw History Blame Contribute Delete
10.9 kB
#include "test_main.h"
#include "../engine/physics/physics.h"
#include "../engine/serialization/stream.h"
#include "../engine/core/core.h"
#include <cstring>
using namespace unify;
static const float dt = 1.0f / 60.0f;
static PhysicsBodyHandle ground(PhysicsWorld& w, float half_w = 50, float top = 0, float half_h = 0.5f) {
BodyDef d; d.type = BodyType::Static; d.shape = Shape::aabb(half_w, half_h); d.position = {0, top - half_h};
return w.create_body(d);
}
static PhysicsBodyHandle ball(PhysicsWorld& w, Vec2 p, float r, Vec2 v = {}, bool bullet = false) {
BodyDef d; d.shape = Shape::circle(r); d.position = p; d.velocity = v; d.bullet = bullet;
return w.create_body(d);
}
static PhysicsBodyHandle box(PhysicsWorld& w, Vec2 p, float hw, float hh, BodyType t = BodyType::Dynamic, float angle = 0) {
BodyDef d; d.type = t; d.shape = Shape::box(hw, hh); d.position = p; d.angle = angle;
return w.create_body(d);
}
static void run(PhysicsWorld& w, int steps) { for (int i = 0; i < steps; i++) w.step(dt); }
TEST(physics_resting_contact_and_sleep) {
PhysicsWorld w; ground(w);
auto b = ball(w, {0, 3}, 0.5f);
run(w, 240);
const Body* body = w.get(b);
CHECK_NEAR(body->position.y, 0.5f, 3 * phys::kLinearSlop + phys::kPolygonRadius);
CHECK(length(body->velocity) < 0.05f);
CHECK(!body->awake); // came to rest and slept
}
TEST(physics_box_stack_is_stable) {
PhysicsWorld w; ground(w);
std::vector<PhysicsBodyHandle> stack;
for (int i = 0; i < 6; i++) stack.push_back(box(w, {0, 0.5f + i * 1.0f}, 0.5f, 0.5f));
run(w, 600);
for (int i = 0; i < 6; i++) {
const Body* b = w.get(stack[size_t(i)]);
CHECK(std::fabs(b->position.x) < 0.05f);
CHECK_NEAR(b->position.y, 0.5f + i * 1.0f, 0.1f);
CHECK(std::fabs(b->angle) < 0.02f);
}
CHECK(w.stats.awake == 0); // whole island asleep
}
TEST(physics_bullet_does_not_tunnel_thin_wall) {
PhysicsWorld w;
w.gravity = {0, 0};
BodyDef wall; wall.type = BodyType::Static; wall.shape = Shape::aabb(0.02f, 5); wall.position = {10, 0};
w.create_body(wall);
auto b = ball(w, {0, 0}, 0.05f, {600, 0}, true); // 10 m per step vs a 4 cm wall
run(w, 10);
CHECK(w.get(b)->position.x < 10.0f);
CHECK(w.get(b)->position.x > 9.5f);
}
TEST(physics_fast_body_gets_ccd_without_bullet_flag) {
PhysicsWorld w;
auto g = ground(w, 50, 0, 0.05f);
(void)g;
auto b = box(w, {0, 20}, 0.25f, 0.25f);
w.set_velocity(b, {0, -400}); // ~6.7 m per step, floor is 0.1 m thick
run(w, 5);
CHECK(w.get(b)->position.y > 0.0f);
CHECK(w.get(b)->position.y < 1.0f);
}
TEST(physics_coincident_centers_separate_deterministically) {
PhysicsWorld w; w.gravity = {0, 0};
auto a = ball(w, {0, 0}, 0.5f), b = ball(w, {0, 0}, 0.5f);
run(w, 60);
Vec2 pa = w.get(a)->position, pb = w.get(b)->position;
CHECK(finite(pa) && finite(pb));
CHECK(length(pb - pa) > 0.9f);
CHECK(w.stats.failures == 0);
}
TEST(physics_zero_mass_falls_back_to_unit_mass) {
PhysicsWorld w; ground(w);
BodyDef d; d.shape = Shape::box(0.5f, 0.5f); d.position = {0, 2}; d.density = 0;
auto h = w.create_body(d);
CHECK_NEAR(w.get(h)->mass, 1.0f, 0);
run(w, 120);
CHECK(finite(w.get(h)->position));
CHECK_NEAR(w.get(h)->position.y, 0.5f, 0.05f);
}
TEST(physics_infinite_mass_is_immovable) {
PhysicsWorld w; w.gravity = {0, 0};
BodyDef d; d.shape = Shape::box(1, 1); d.position = {0, 0}; d.mass = INFINITY;
auto wall = w.create_body(d);
BodyDef bd; bd.shape = Shape::circle(0.25f); bd.position = {-3, 0}; bd.velocity = {10, 0}; bd.restitution = 0.5f;
auto b = w.create_body(bd);
run(w, 60);
CHECK_NEAR(w.get(wall)->position.x, 0, 1e-6);
CHECK(w.get(b)->velocity.x < 0); // bounced off
}
TEST(physics_tiny_and_huge_objects) {
PhysicsWorld w;
BodyDef g; g.type = BodyType::Static; g.shape = Shape::aabb(2000, 0.5f); g.position = {0, -0.5f};
w.create_body(g);
auto tiny = ball(w, {0, 1}, 0.01f);
auto huge = box(w, {500, 120}, 100, 100);
run(w, 600);
CHECK_NEAR(w.get(tiny)->position.y, 0.01f, 0.03f);
CHECK_NEAR(w.get(huge)->position.y, 100.0f, 0.1f);
}
TEST(physics_parallel_edges_give_stable_two_point_manifold) {
PhysicsWorld w; ground(w);
auto b = box(w, {0, 0.5f}, 0.5f, 0.5f);
run(w, 2);
Manifold m;
collide(*w.get(w.handle_at(0)), *w.get(b), m);
CHECK(m.count == 2);
uint32_t ids[2] = {m.points[0].id, m.points[1].id};
run(w, 30);
collide(*w.get(w.handle_at(0)), *w.get(b), m);
CHECK(m.count == 2 && m.points[0].id == ids[0] && m.points[1].id == ids[1]); // no reference-face flip-flop
}
TEST(physics_corner_and_edge_contacts) {
Body ground_b; ground_b.shape = Shape::box(5, 0.5f); ground_b.position = {0, -0.5f};
Body corner; corner.shape = Shape::box(0.5f, 0.5f); corner.angle = 0.785398f; corner.rot = Rot(corner.angle);
corner.position = {0, 0.70f};
Manifold m;
collide(ground_b, corner, m);
CHECK(m.count == 1); // diamond resting on its corner
Body circle; circle.shape = Shape::circle(0.5f);
circle.position = {5.2f, 0.2f}; // beside the top-right corner: vertex region
collide(ground_b, circle, m);
CHECK(m.count == 1);
WorldManifold wm = world_manifold(m, ground_b, circle);
CHECK(wm.normal.x > 0.5f && wm.normal.y > 0.1f);
circle.position = {1, 0.45f}; // above the face: edge region
collide(ground_b, circle, m);
wm = world_manifold(m, ground_b, circle);
CHECK_NEAR(wm.normal.y, 1, 1e-5);
}
TEST(physics_multiple_contacts_across_platforms) {
PhysicsWorld w;
box(w, {-1.2f, -0.5f}, 1, 0.5f, BodyType::Static);
box(w, {1.2f, -0.5f}, 1, 0.5f, BodyType::Static);
auto plank = box(w, {0, 0.25f}, 2, 0.25f);
run(w, 240);
CHECK_NEAR(w.get(plank)->position.y, 0.25f, 0.05f);
CHECK(std::fabs(w.get(plank)->angle) < 0.01f);
}
TEST(physics_trigger_events_without_response) {
PhysicsWorld w; w.gravity = {0, 0};
BodyDef s; s.type = BodyType::Static; s.shape = Shape::aabb(0.5f, 0.5f); s.position = {5, 0}; s.sensor = true; s.user = 77;
w.create_body(s);
auto b = ball(w, {0, 0}, 0.25f, {10, 0});
int begin = 0, end = 0;
for (int i = 0; i < 90; i++) {
w.step(dt);
for (auto& e : w.events()) {
if (e.type == ContactEventType::TriggerBegin) begin++;
if (e.type == ContactEventType::TriggerEnd) end++;
CHECK(e.type != ContactEventType::Begin);
}
}
CHECK(begin == 1 && end == 1);
CHECK_NEAR(w.get(b)->velocity.x, 10, 1e-4); // passed straight through
}
TEST(physics_collision_filtering) {
PhysicsWorld w;
BodyDef g; g.type = BodyType::Static; g.shape = Shape::aabb(10, 0.5f); g.position = {0, -0.5f}; g.category = 2;
w.create_body(g);
BodyDef d; d.shape = Shape::circle(0.5f); d.position = {0, 2}; d.mask = ~2u; // ignores category 2
auto b = w.create_body(d);
run(w, 60);
CHECK(w.get(b)->position.y < -1.0f); // fell through the filtered ground
}
TEST(physics_contact_begin_persist_end) {
PhysicsWorld w; ground(w);
auto b = ball(w, {0, 1}, 0.5f);
int begin = 0, persist = 0, end = 0;
for (int i = 0; i < 60; i++) { w.step(dt); for (auto& e : w.events()) { begin += e.type == ContactEventType::Begin; persist += e.type == ContactEventType::Persist; } }
w.set_velocity(b, {0, 12});
for (int i = 0; i < 10; i++) { w.step(dt); for (auto& e : w.events()) end += e.type == ContactEventType::End; }
CHECK(begin == 1);
CHECK(persist > 10);
CHECK(end == 1);
}
TEST(physics_sleeping_island_wakes_on_impulse) {
PhysicsWorld w; ground(w);
auto bottom = box(w, {0, 0.5f}, 0.5f, 0.5f);
auto top = box(w, {0, 1.5f}, 0.5f, 0.5f);
run(w, 240);
CHECK(!w.get(bottom)->awake && !w.get(top)->awake);
w.apply_impulse(bottom, {3, 0});
run(w, 1);
CHECK(w.get(bottom)->awake);
CHECK(w.get(top)->awake); // woken through the island
}
TEST(physics_nan_is_contained) {
PhysicsWorld w; ground(w);
auto good = ball(w, {2, 1}, 0.5f);
auto bad = ball(w, {0, 1}, 0.5f);
run(w, 10);
Vec2 before = w.get(bad)->position;
w.get(bad)->velocity = {NAN, 0}; // simulate corrupted input
w.step(dt);
CHECK(w.stats.failures == 1);
CHECK(finite(w.get(bad)->position));
CHECK(length(w.get(bad)->position - before) < 0.1f);
CHECK(finite(w.get(good)->position));
}
TEST(physics_deep_penetration_resolves_gently) {
PhysicsWorld w; ground(w);
auto b = box(w, {0, 0.2f}, 0.5f, 0.5f); // 0.3 m inside the ground
float max_speed = 0;
for (int i = 0; i < 120; i++) { w.step(dt); max_speed = std::max(max_speed, length(w.get(b)->velocity)); }
CHECK_NEAR(w.get(b)->position.y, 0.5f, 0.03f);
CHECK(max_speed < 3.0f); // position solver, not an explosive velocity kick
}
TEST(physics_zero_dt_and_zero_velocity) {
PhysicsWorld w; ground(w);
auto b = box(w, {0, 0.5f}, 0.5f, 0.5f);
w.step(0);
CHECK(w.get(b)->position == Vec2(0, 0.5f));
run(w, 60);
CHECK(std::fabs(w.get(b)->position.x) < 1e-4f);
}
TEST(physics_restitution_bounces) {
PhysicsWorld w; ground(w);
BodyDef d; d.shape = Shape::circle(0.25f); d.position = {0, 3}; d.restitution = 0.8f;
auto b = w.create_body(d);
float peak_after = 0;
bool bounced = false;
for (int i = 0; i < 180; i++) {
w.step(dt);
if (w.get(b)->velocity.y > 0.5f) bounced = true;
if (bounced) peak_after = std::max(peak_after, w.get(b)->position.y);
}
CHECK(bounced);
CHECK(peak_after > 1.2f);
}
TEST(physics_raycast_hits_nearest) {
PhysicsWorld w; w.gravity = {0, 0};
ground(w);
auto near_box = box(w, {0, 2}, 0.5f, 0.5f);
RayHit hit;
CHECK(w.raycast({0, 5}, {0, -5}, 0xFFFFFFFFu, hit));
CHECK(hit.body == near_box);
CHECK_NEAR(hit.point.y, 2.5f + phys::kPolygonRadius, 1e-3);
CHECK_NEAR(hit.normal.y, 1, 1e-5);
}
static uint64_t world_hash(PhysicsWorld& w) {
BinaryWriter bw;
w.serialize(bw);
return fnv1a(bw.data().data(), bw.size());
}
TEST(physics_is_deterministic_and_save_restore_is_exact) {
auto build = [](PhysicsWorld& w) {
ground(w);
for (int i = 0; i < 8; i++) box(w, {float(i % 3) * 0.3f, 1 + i * 1.1f}, 0.4f, 0.3f, BodyType::Dynamic, 0.1f * i);
for (int i = 0; i < 5; i++) ball(w, {2 + i * 0.2f, 3.0f + float(i)}, 0.3f);
};
PhysicsWorld a, b;
build(a); build(b);
run(a, 100); run(b, 100);
CHECK(world_hash(a) == world_hash(b));
BinaryWriter snap;
a.serialize(snap);
PhysicsWorld c;
BinaryReader r(snap.data());
c.deserialize(r);
CHECK(world_hash(c) == world_hash(a));
run(a, 200); run(c, 200);
CHECK(world_hash(a) == world_hash(c)); // continues bit-identically after restore
}