Files
EVEngine/src/modules/physics/World.cpp
T
西风逍遥游 a2292a8427 fix(modules): instance state for PlacementSystem; declare procgen deps for editor/voxel
PlacementSystem's eight function-local static registries/counters move into a single instance's member state. Editor's heightmap API is guarded by EVENGINE_HAS_PROCGEN (web builds exclude procgen) and voxel now declares its hard procgen dependency; both module layers are corrected to match their real includes so the declared-layer check passes.
2026-08-19 22:44:52 +08:00

522 lines
18 KiB
C++

#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 <Box2D/Box2D.h>
#include <algorithm>
#include <cmath>
#include <cstring>
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<Body *>(b->GetUserData());
}
Fixture *fixtureFromRaw(b2Fixture *f) {
return f ? static_cast<Fixture *>(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 &center, float32 radius, const b2Color &color) override {
drawCircle(center, radius, color, false);
}
void DrawSolidCircle(const b2Vec2 &center, 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 &center, 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<Body *> 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<Fixture *> 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<eve::event::Event>("Event");
if (!ev) return;
std::vector<eve::event::Variant> 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<eve::event::Event>("Event");
if (!ev) return;
std::vector<eve::event::Variant> 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<ContactEvent &>(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 <typename Event>
const Event *eventAt(const std::vector<Event> &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<int> *ids = nullptr;
std::unordered_set<int> 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<int>(queryBodyIds_.size());
}
int World::getQueryBodyId(int index) const {
if (index < 0 || index >= static_cast<int>(queryBodyIds_.size()))
throw eve::Exception("World.getQueryBodyId: index out of range");
return queryBodyIds_[static_cast<size_t>(index)];
}
} // namespace eve::physics