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.
522 lines
18 KiB
C++
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 ¢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<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
|