#include "physics/World.h" #include "physics/Body.h" #include "physics/Fixture.h" #include "common/Exception.h" #include "event/Event.h" #include "graphics/Graphics.h" #include "graphics/Canvas.h" #include #include #include #include namespace eve::physics { namespace { // Color lives in eve::graphics (see graphics/Canvas.h); keep the unqualified form. using eve::graphics::Color; b2BodyType parseBodyType(const std::string &type) { if (type == "static") return b2_staticBody; if (type == "kinematic") return b2_kinematicBody; if (type == "dynamic") return b2_dynamicBody; throw eve::Exception("World.newBody: unknown body type '%s' (use static|kinematic|dynamic)", type.c_str()); } Body *bodyFromFixture(b2Fixture *f) { if (!f) return nullptr; b2Body *b = f->GetBody(); if (!b) return nullptr; return static_cast(b->GetUserData()); } Fixture *fixtureFromRaw(b2Fixture *f) { return f ? static_cast(f->GetUserData()) : nullptr; } World::ContactEvent contactEventFrom(b2Contact *contact) { World::ContactEvent out; if (!contact) return out; Fixture *fa = fixtureFromRaw(contact->GetFixtureA()); Fixture *fb = fixtureFromRaw(contact->GetFixtureB()); if (fa) { out.bodyAId = fa->getBodyId(); out.fixtureATag = fa->getTag(); } if (fb) { out.bodyBId = fb->getBodyId(); out.fixtureBTag = fb->getTag(); } return out; } } // namespace class ContactRelay : public b2ContactListener { public: explicit ContactRelay(World *world) : world_(world) {} void BeginContact(b2Contact *contact) override { world_->onBeginContact(contact); } void EndContact(b2Contact *contact) override { world_->onEndContact(contact); } void PreSolve(b2Contact *contact, const b2Manifold *oldManifold) override { world_->onPreSolve(contact, oldManifold); } void PostSolve(b2Contact *contact, const b2ContactImpulse *impulse) override { world_->onPostSolve(contact, impulse); } private: World *world_; }; class DebugDraw : public b2Draw { public: DebugDraw() { SetFlags(e_shapeBit | e_jointBit | e_aabbBit); } void begin(graphics::Graphics *gfx, float meter) { gfx_ = gfx; meter_ = meter; } void end() { gfx_ = nullptr; } void DrawPolygon(const b2Vec2 *vertices, int32 vertexCount, const b2Color &color) override { drawPoly(vertices, vertexCount, color, false); } void DrawSolidPolygon(const b2Vec2 *vertices, int32 vertexCount, const b2Color &color) override { drawPoly(vertices, vertexCount, color, true); } void DrawCircle(const b2Vec2 ¢er, float32 radius, const b2Color &color) override { drawCircle(center, radius, color, false); } void DrawSolidCircle(const b2Vec2 ¢er, float32 radius, const b2Vec2 & /*axis*/, const b2Color &color) override { drawCircle(center, radius, color, true); } void DrawSegment(const b2Vec2 &p1, const b2Vec2 &p2, const b2Color &color) override { if (!gfx_) return; float x1 = p1.x * meter_, y1 = p1.y * meter_; float x2 = p2.x * meter_, y2 = p2.y * meter_; float minx = std::min(x1, x2), miny = std::min(y1, y2); float w = std::max(1.f, std::fabs(x2 - x1)); float h = std::max(1.f, std::fabs(y2 - y1)); gfx_->drawSolidRect(minx, miny, w, h, Color(color.r, color.g, color.b, color.a * 0.8f)); } void DrawTransform(const b2Transform &xf) override { DrawSegment(xf.p, xf.p + 0.5f * xf.q.GetXAxis(), b2Color(1, 0, 0)); DrawSegment(xf.p, xf.p + 0.5f * xf.q.GetYAxis(), b2Color(0, 1, 0)); } private: void drawPoly(const b2Vec2 *vertices, int32 vertexCount, const b2Color &color, bool solid) { if (!gfx_ || vertexCount < 2) return; for (int32 i = 0; i < vertexCount; ++i) { const b2Vec2 &a = vertices[i]; const b2Vec2 &b = vertices[(i + 1) % vertexCount]; DrawSegment(a, b, color); } if (solid && vertexCount >= 3) { float minx = vertices[0].x, maxx = vertices[0].x; float miny = vertices[0].y, maxy = vertices[0].y; for (int32 i = 1; i < vertexCount; ++i) { minx = std::min(minx, vertices[i].x); maxx = std::max(maxx, vertices[i].x); miny = std::min(miny, vertices[i].y); maxy = std::max(maxy, vertices[i].y); } gfx_->drawSolidRect(minx * meter_, miny * meter_, (maxx - minx) * meter_, (maxy - miny) * meter_, Color(color.r, color.g, color.b, color.a * 0.25f)); } } void drawCircle(const b2Vec2 ¢er, float32 radius, const b2Color &color, bool solid) { if (!gfx_) return; float px = (center.x - radius) * meter_; float py = (center.y - radius) * meter_; float d = radius * 2.f * meter_; float a = solid ? color.a * 0.35f : color.a * 0.7f; gfx_->drawSolidRect(px, py, d, d, Color(color.r, color.g, color.b, a)); } graphics::Graphics *gfx_ = nullptr; float meter_ = 30.f; }; World::World(float gravityX, float gravityY, bool sleep, float meter) : meter_(meter) { if (meter_ <= 0.f) meter_ = 30.f; world_ = new b2World(b2Vec2(toMeters(gravityX), toMeters(gravityY))); world_->SetAllowSleeping(sleep); relay_ = new ContactRelay(this); world_->SetContactListener(relay_); draw_ = new DebugDraw(); world_->SetDebugDraw(draw_); } World::~World() { destroy(); } void World::destroy() { if (destroyed_) return; destroyed_ = true; // Copy sets — Body/Fixture destructors erase from them. std::vector bodies(bodies_.begin(), bodies_.end()); for (Body *b : bodies) { if (b) { b->invalidate(); // Script may still hold Body*; leave object but null raw pointer. // If World is script-owned and Body is also script-owned, Body::~Body // will see null body_ and skip DestroyBody. } } bodies_.clear(); std::vector fixtures(fixtures_.begin(), fixtures_.end()); for (Fixture *f : fixtures) { if (f) f->invalidate(); } fixtures_.clear(); clearContactEvents(); if (world_) { world_->SetContactListener(nullptr); world_->SetDebugDraw(nullptr); delete world_; world_ = nullptr; } delete relay_; relay_ = nullptr; delete draw_; draw_ = nullptr; } void World::update(float dt) { updateFull(dt, 8, 3); } void World::updateFull(float dt, int velocityIterations, int positionIterations) { if (!world_ || destroyed_) return; if (dt < 0.f) dt = 0.f; // Cap to avoid spiral-of-death on hitch frames. if (dt > 0.05f) dt = 0.05f; world_->Step(dt, velocityIterations, positionIterations); } void World::setGravity(float gx, float gy) { if (!world_) return; world_->SetGravity(b2Vec2(toMeters(gx), toMeters(gy))); } float World::getGravityX() const { if (!world_) return 0.f; return toPixels(world_->GetGravity().x); } float World::getGravityY() const { if (!world_) return 0.f; return toPixels(world_->GetGravity().y); } void World::setMeter(float pixelsPerMeter) { if (pixelsPerMeter <= 0.f) throw eve::Exception("World.setMeter: pixelsPerMeter must be > 0"); meter_ = pixelsPerMeter; } float World::toMeters(float pixels) const { return pixels / meter_; } float World::toPixels(float meters) const { return meters * meter_; } int World::nextBodyId() { return nextId_++; } Body *World::newBody(const std::string &bodyType, float x, float y) { if (!world_ || destroyed_) throw eve::Exception("World.newBody: world destroyed"); b2BodyDef def; def.type = parseBodyType(bodyType); def.position = b2Vec2(toMeters(x), toMeters(y)); b2Body *raw = world_->CreateBody(&def); Body *body = new Body(this, raw, nextBodyId()); raw->SetUserData(body); bodies_.insert(body); return body; } void World::destroyBody(Body *body) { if (!body) return; body->destroy(); } void World::forgetBody(Body *body) { if (!body) return; const int id = body->getId(); bodies_.erase(body); auto touches = [id](const ContactEvent &e) { return e.bodyAId == id || e.bodyBId == id; }; beginContacts_.erase(std::remove_if(beginContacts_.begin(), beginContacts_.end(), touches), beginContacts_.end()); endContacts_.erase(std::remove_if(endContacts_.begin(), endContacts_.end(), touches), endContacts_.end()); impacts_.erase(std::remove_if(impacts_.begin(), impacts_.end(), touches), impacts_.end()); } void World::forgetFixture(Fixture *fixture) { if (!fixture) return; const int bodyId = fixture->getBodyId(); const std::string tag = fixture->getTag(); auto touches = [&](const ContactEvent &e) { return (e.bodyAId == bodyId && e.fixtureATag == tag) || (e.bodyBId == bodyId && e.fixtureBTag == tag); }; beginContacts_.erase(std::remove_if(beginContacts_.begin(), beginContacts_.end(), touches), beginContacts_.end()); endContacts_.erase(std::remove_if(endContacts_.begin(), endContacts_.end(), touches), endContacts_.end()); impacts_.erase(std::remove_if(impacts_.begin(), impacts_.end(), touches), impacts_.end()); b2Fixture *raw = fixture->raw(); for (auto it = preSolve_.begin(); it != preSolve_.end();) { b2Contact *contact = it->first; if (contact && (contact->GetFixtureA() == raw || contact->GetFixtureB() == raw)) it = preSolve_.erase(it); else ++it; } fixtures_.erase(fixture); } void World::onBeginContact(b2Contact *contact) { if (!contact || !fixtureFromRaw(contact->GetFixtureA()) || !fixtureFromRaw(contact->GetFixtureB())) return; Body *a = bodyFromFixture(contact->GetFixtureA()); Body *b = bodyFromFixture(contact->GetFixtureB()); if (!a || !b) return; beginContacts_.push_back(contactEventFrom(contact)); auto *ev = eve::ModuleManager::getInstance("Event"); if (!ev) return; std::vector args = {eve::event::Variant::makeInt(a->getId()), eve::event::Variant::makeInt(b->getId())}; ev->push(new eve::event::Message("begincontact", args)); } void World::onEndContact(b2Contact *contact) { preSolve_.erase(contact); if (!contact || !fixtureFromRaw(contact->GetFixtureA()) || !fixtureFromRaw(contact->GetFixtureB())) return; Body *a = bodyFromFixture(contact->GetFixtureA()); Body *b = bodyFromFixture(contact->GetFixtureB()); if (!a || !b) return; endContacts_.push_back(contactEventFrom(contact)); auto *ev = eve::ModuleManager::getInstance("Event"); if (!ev) return; std::vector args = {eve::event::Variant::makeInt(a->getId()), eve::event::Variant::makeInt(b->getId())}; ev->push(new eve::event::Message("endcontact", args)); } void World::onPreSolve(b2Contact *contact, const b2Manifold * /*oldManifold*/) { if (!contact || !world_) return; b2WorldManifold manifold; contact->GetWorldManifold(&manifold); const b2Manifold *local = contact->GetManifold(); if (!local || local->pointCount <= 0) return; b2Body *a = contact->GetFixtureA()->GetBody(); b2Body *b = contact->GetFixtureB()->GetBody(); if (!a || !b) return; const b2Vec2 point = manifold.points[0]; const b2Vec2 va = a->GetLinearVelocityFromWorldPoint(point); const b2Vec2 vb = b->GetLinearVelocityFromWorldPoint(point); PreSolveData data; data.pointX = toPixels(point.x); data.pointY = toPixels(point.y); data.normalX = manifold.normal.x; data.normalY = manifold.normal.y; data.relativeNormalSpeed = toPixels(std::max(0.f, b2Dot(va - vb, manifold.normal))); preSolve_[contact] = data; } void World::onPostSolve(b2Contact *contact, const b2ContactImpulse *impulse) { if (!contact || !impulse) return; auto found = preSolve_.find(contact); if (found == preSolve_.end()) return; ImpactEvent out; static_cast(out) = contactEventFrom(contact); out.pointX = found->second.pointX; out.pointY = found->second.pointY; out.normalX = found->second.normalX; out.normalY = found->second.normalY; out.relativeNormalSpeed = found->second.relativeNormalSpeed; const int count = contact->GetManifold() ? contact->GetManifold()->pointCount : 0; for (int i = 0; i < count; ++i) { out.normalImpulse += toPixels(impulse->normalImpulses[i]); out.tangentImpulse += toPixels(std::fabs(impulse->tangentImpulses[i])); } if (out.normalImpulse > 0.f) impacts_.push_back(std::move(out)); } namespace { template const Event *eventAt(const std::vector &events, int index) { return index >= 0 && index < int(events.size()) ? &events[size_t(index)] : nullptr; } } // namespace int World::getBeginContactBodyAId(int index) const { auto *e = eventAt(beginContacts_, index); return e ? e->bodyAId : 0; } int World::getBeginContactBodyBId(int index) const { auto *e = eventAt(beginContacts_, index); return e ? e->bodyBId : 0; } std::string World::getBeginContactFixtureATag(int index) const { auto *e = eventAt(beginContacts_, index); return e ? e->fixtureATag : std::string(); } std::string World::getBeginContactFixtureBTag(int index) const { auto *e = eventAt(beginContacts_, index); return e ? e->fixtureBTag : std::string(); } int World::getEndContactBodyAId(int index) const { auto *e = eventAt(endContacts_, index); return e ? e->bodyAId : 0; } int World::getEndContactBodyBId(int index) const { auto *e = eventAt(endContacts_, index); return e ? e->bodyBId : 0; } std::string World::getEndContactFixtureATag(int index) const { auto *e = eventAt(endContacts_, index); return e ? e->fixtureATag : std::string(); } std::string World::getEndContactFixtureBTag(int index) const { auto *e = eventAt(endContacts_, index); return e ? e->fixtureBTag : std::string(); } int World::getImpactBodyAId(int index) const { auto *e = eventAt(impacts_, index); return e ? e->bodyAId : 0; } int World::getImpactBodyBId(int index) const { auto *e = eventAt(impacts_, index); return e ? e->bodyBId : 0; } std::string World::getImpactFixtureATag(int index) const { auto *e = eventAt(impacts_, index); return e ? e->fixtureATag : std::string(); } std::string World::getImpactFixtureBTag(int index) const { auto *e = eventAt(impacts_, index); return e ? e->fixtureBTag : std::string(); } float World::getImpactPointX(int index) const { auto *e = eventAt(impacts_, index); return e ? e->pointX : 0.f; } float World::getImpactPointY(int index) const { auto *e = eventAt(impacts_, index); return e ? e->pointY : 0.f; } float World::getImpactNormalX(int index) const { auto *e = eventAt(impacts_, index); return e ? e->normalX : 0.f; } float World::getImpactNormalY(int index) const { auto *e = eventAt(impacts_, index); return e ? e->normalY : 0.f; } float World::getImpactRelativeNormalSpeed(int index) const { auto *e = eventAt(impacts_, index); return e ? e->relativeNormalSpeed : 0.f; } float World::getImpactNormalImpulse(int index) const { auto *e = eventAt(impacts_, index); return e ? e->normalImpulse : 0.f; } float World::getImpactTangentImpulse(int index) const { auto *e = eventAt(impacts_, index); return e ? e->tangentImpulse : 0.f; } void World::clearContactEvents() { beginContacts_.clear(); endContacts_.clear(); impacts_.clear(); preSolve_.clear(); } void World::drawDebug(graphics::Graphics *gfx) { if (!world_ || !draw_ || !gfx) return; draw_->begin(gfx, meter_); world_->DrawDebugData(); draw_->end(); } int World::rayCast(float x1, float y1, float x2, float y2) { rayHitBodyId_ = -1; rayHitX_ = 0.f; rayHitY_ = 0.f; rayHitNormalX_ = 0.f; rayHitNormalY_ = 0.f; rayHitFraction_ = 0.f; if (!world_ || destroyed_) return -1; struct Closest : public b2RayCastCallback { World *world = nullptr; float best = 1.f; Body *hit = nullptr; b2Vec2 point{}; b2Vec2 normal{}; float32 ReportFixture(b2Fixture *fixture, const b2Vec2 &pointIn, const b2Vec2 &normalIn, float32 fraction) override { Body *b = bodyFromFixture(fixture); if (!b) return -1.f; if (fraction < best) { best = fraction; hit = b; point = pointIn; normal = normalIn; } return fraction; } } cb; cb.world = this; b2Vec2 p1(toMeters(x1), toMeters(y1)); b2Vec2 p2(toMeters(x2), toMeters(y2)); world_->RayCast(&cb, p1, p2); if (!cb.hit) return -1; rayHitBodyId_ = cb.hit->getId(); rayHitX_ = toPixels(cb.point.x); rayHitY_ = toPixels(cb.point.y); rayHitNormalX_ = cb.normal.x; rayHitNormalY_ = cb.normal.y; rayHitFraction_ = cb.best; return rayHitBodyId_; } int World::queryAABB(float x, float y, float w, float h) { queryBodyIds_.clear(); if (!world_ || destroyed_) return 0; struct Collector : public b2QueryCallback { World *world = nullptr; std::vector *ids = nullptr; std::unordered_set seen; bool ReportFixture(b2Fixture *fixture) override { Body *b = bodyFromFixture(fixture); if (!b) return true; int id = b->getId(); if (seen.insert(id).second) ids->push_back(id); return true; } } cb; cb.world = this; cb.ids = &queryBodyIds_; b2AABB aabb; float x0 = toMeters(x); float y0 = toMeters(y); float x1 = toMeters(x + w); float y1 = toMeters(y + h); aabb.lowerBound = b2Vec2(std::min(x0, x1), std::min(y0, y1)); aabb.upperBound = b2Vec2(std::max(x0, x1), std::max(y0, y1)); world_->QueryAABB(&cb, aabb); return static_cast(queryBodyIds_.size()); } int World::getQueryBodyId(int index) const { if (index < 0 || index >= static_cast(queryBodyIds_.size())) throw eve::Exception("World.getQueryBodyId: index out of range"); return queryBodyIds_[static_cast(index)]; } } // namespace eve::physics