fix(physics): salta body-body collision quan algun cos té radius=0

resolveBodyPair afegeix early-out per a parells on a.radius<=0 o b.radius<=0.
Honra el comentari de bullet.cpp:30 ("radius=0 → sin colisión física,
cinemática pura") que abans no s'aplicava: amb bala radius=0 + enemic
radius=ENEMY_RADIUS, SUM_R era enemic radius i el body-body disparava
si la bala (a 700 px/s) penetrava el cos l'enemic entre frames.

Símptomes corregits:
- Pentagon: la bala "rebotava espectacularment" en lloc d'impactar.
- Quadrat: rebut un impulse double del cantó de la física que es
  sumava (o cancel·lava, segons l'angle) al manual, fent l'efecte
  inconsistent.

Ara la gameplay collision (Physics::checkCollision amb entity radius,
que ja és més generós) és l'única que tracta el parell bala-enemic.

A més: IMPACT_MOMENTUM_FACTOR 2.0 → 3.0 per compensar la pèrdua del
rebot físic i donar més empenta:
  - Pentagon (m=5) Δv = 210 px/s
  - Quadrat  (m=8) Δv = 131 px/s
  - Molinillo (m=4) Δv = 262 px/s

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-21 11:45:59 +02:00
parent e3af88ea8c
commit 44aa4e76e2
2 changed files with 148 additions and 141 deletions
+1 -1
View File
@@ -15,7 +15,7 @@ namespace Defaults::Physics {
// un factor de transferència [0..1]. 1.0 = transfereix tot el moment
// (col·lisió perfectament inelàstica), 0.5 = transfereix la meitat.
namespace Bullet {
constexpr float IMPACT_MOMENTUM_FACTOR = 2.0F; // Factor de transferència de moment bala→enemic
constexpr float IMPACT_MOMENTUM_FACTOR = 3.0F; // Factor de transferència de moment bala→enemic
} // namespace Bullet
// Explosions (debris physics)
+33 -26
View File
@@ -10,32 +10,32 @@
namespace Physics {
void PhysicsWorld::addBody(RigidBody* body) {
void PhysicsWorld::addBody(RigidBody* body) {
if (body == nullptr) {
return;
}
if (std::ranges::find(bodies_, body) == bodies_.end()) {
bodies_.push_back(body);
}
}
}
void PhysicsWorld::removeBody(RigidBody* body) {
void PhysicsWorld::removeBody(RigidBody* body) {
std::erase(bodies_, body);
}
}
void PhysicsWorld::update(float dt) {
void PhysicsWorld::update(float dt) {
integrate(dt);
if (has_bounds_) {
resolveBoundsCollisions();
}
resolveBodyCollisions();
}
}
// Integración semi-implícita de Euler:
// v(t+dt) = v(t) + (F/m) * dt
// x(t+dt) = x(t) + v(t+dt) * dt
// Más estable que Euler explícito para juegos. Damping exponencial.
void PhysicsWorld::integrate(float dt) {
// Integración semi-implícita de Euler:
// v(t+dt) = v(t) + (F/m) * dt
// x(t+dt) = x(t) + v(t+dt) * dt
// Más estable que Euler explícito para juegos. Damping exponencial.
void PhysicsWorld::integrate(float dt) {
for (auto* body : bodies_) {
if (body == nullptr || body->isStatic()) {
continue;
@@ -62,11 +62,11 @@ void PhysicsWorld::integrate(float dt) {
body->clearAccumulators();
}
}
}
// Rebote contra los 4 bordes del rectángulo bounds_.
// Refleja la componente normal de la velocidad por la restitución.
void PhysicsWorld::resolveBoundsCollisions() {
// Rebote contra los 4 bordes del rectángulo bounds_.
// Refleja la componente normal de la velocidad por la restitución.
void PhysicsWorld::resolveBoundsCollisions() {
const float MIN_X = bounds_.x;
const float MAX_X = bounds_.x + bounds_.w;
const float MIN_Y = bounds_.y;
@@ -107,15 +107,15 @@ void PhysicsWorld::resolveBoundsCollisions() {
}
}
}
}
}
// Colisiones cuerpo-cuerpo: O(n²) círculo-círculo + resolución por impulso.
// Para 15 enemigos + 6 balas + 2 naves = ~23 cuerpos → 253 pares. Sobra.
//
// Fórmula del impulso elástico (referencia: Chris Hecker / Box2D):
// j = -(1 + e) * (v_rel · n) / (1/m_a + 1/m_b)
// donde n es la normal del contacto (de a hacia b) y v_rel = v_a - v_b.
void PhysicsWorld::resolveBodyCollisions() {
// Colisiones cuerpo-cuerpo: O(n²) círculo-círculo + resolución por impulso.
// Para 15 enemigos + 6 balas + 2 naves = ~23 cuerpos → 253 pares. Sobra.
//
// Fórmula del impulso elástico (referencia: Chris Hecker / Box2D):
// j = -(1 + e) * (v_rel · n) / (1/m_a + 1/m_b)
// donde n es la normal del contacto (de a hacia b) y v_rel = v_a - v_b.
void PhysicsWorld::resolveBodyCollisions() {
const std::size_t COUNT = bodies_.size();
for (std::size_t i = 0; i < COUNT; ++i) {
for (std::size_t j = i + 1; j < COUNT; ++j) {
@@ -126,14 +126,21 @@ void PhysicsWorld::resolveBodyCollisions() {
}
}
}
}
}
void PhysicsWorld::resolveBodyPair(RigidBody& a, RigidBody& b) {
void PhysicsWorld::resolveBodyPair(RigidBody& a, RigidBody& b) {
// Dos cuerpos estáticos no necesitan resolución
if (a.isStatic() && b.isStatic()) {
return;
}
// Un cuerpo con radius=0 es cinemático puro (ej. la bala) y no participa
// en body-body. La detecció de gameplay (Physics::checkCollision) usa
// el radius de l'entity (no el del body) i s'encarrega d'aquesta parella.
if (a.radius <= 0.0F || b.radius <= 0.0F) {
return;
}
const Vec2 DELTA = b.position - a.position;
const float DIST_SQ = DELTA.lengthSquared();
const float SUM_R = a.radius + b.radius;
@@ -176,6 +183,6 @@ void PhysicsWorld::resolveBodyPair(RigidBody& a, RigidBody& b) {
if (!b.isStatic()) {
b.velocity += IMPULSE * b.inverse_mass;
}
}
}
} // namespace Physics