From 44aa4e76e23157785210b654162127c9cb3de308 Mon Sep 17 00:00:00 2001 From: Sergio Valor Date: Thu, 21 May 2026 11:45:59 +0200 Subject: [PATCH] =?UTF-8?q?fix(physics):=20salta=20body-body=20collision?= =?UTF-8?q?=20quan=20algun=20cos=20t=C3=A9=20radius=3D0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- source/core/defaults/physics.hpp | 2 +- source/core/physics/physics_world.cpp | 287 +++++++++++++------------- 2 files changed, 148 insertions(+), 141 deletions(-) diff --git a/source/core/defaults/physics.hpp b/source/core/defaults/physics.hpp index 0d415eb..03603f4 100644 --- a/source/core/defaults/physics.hpp +++ b/source/core/defaults/physics.hpp @@ -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) diff --git a/source/core/physics/physics_world.cpp b/source/core/physics/physics_world.cpp index 4ccd551..7a8d733 100644 --- a/source/core/physics/physics_world.cpp +++ b/source/core/physics/physics_world.cpp @@ -10,172 +10,179 @@ namespace Physics { -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) { - std::erase(bodies_, body); -} - -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) { - for (auto* body : bodies_) { - if (body == nullptr || body->isStatic()) { - continue; + void PhysicsWorld::addBody(RigidBody* body) { + if (body == nullptr) { + return; } - - // Aplicar fuerzas acumuladas → aceleración - const Vec2 ACCELERATION = body->force_accumulator * body->inverse_mass; - body->velocity += ACCELERATION * dt; - - // Damping exponencial: equivalente a v *= exp(-damping * dt) - // Aproximación lineal cuando damping*dt es pequeño. - if (body->linear_damping > 0.0F) { - const float DAMP = std::exp(-body->linear_damping * dt); - body->velocity *= DAMP; + if (std::ranges::find(bodies_, body) == bodies_.end()) { + bodies_.push_back(body); } - if (body->angular_damping > 0.0F) { - const float DAMP = std::exp(-body->angular_damping * dt); - body->angular_velocity *= DAMP; - } - - // Actualizar posición y rotación - body->position += body->velocity * dt; - body->angle += body->angular_velocity * 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() { - const float MIN_X = bounds_.x; - const float MAX_X = bounds_.x + bounds_.w; - const float MIN_Y = bounds_.y; - const float MAX_Y = bounds_.y + bounds_.h; + void PhysicsWorld::removeBody(RigidBody* body) { + std::erase(bodies_, body); + } - for (auto* body : bodies_) { - if (body == nullptr || body->isStatic()) { - continue; + void PhysicsWorld::update(float dt) { + integrate(dt); + if (has_bounds_) { + resolveBoundsCollisions(); } - const float R = body->radius; + resolveBodyCollisions(); + } - // Pared izquierda - if (body->position.x - R < MIN_X) { - body->position.x = MIN_X + R; - if (body->velocity.x < 0.0F) { - body->velocity.x = -body->velocity.x * body->restitution; + // 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; } - } - // Pared derecha - if (body->position.x + R > MAX_X) { - body->position.x = MAX_X - R; - if (body->velocity.x > 0.0F) { - body->velocity.x = -body->velocity.x * body->restitution; + + // Aplicar fuerzas acumuladas → aceleración + const Vec2 ACCELERATION = body->force_accumulator * body->inverse_mass; + body->velocity += ACCELERATION * dt; + + // Damping exponencial: equivalente a v *= exp(-damping * dt) + // Aproximación lineal cuando damping*dt es pequeño. + if (body->linear_damping > 0.0F) { + const float DAMP = std::exp(-body->linear_damping * dt); + body->velocity *= DAMP; } - } - // Pared superior - if (body->position.y - R < MIN_Y) { - body->position.y = MIN_Y + R; - if (body->velocity.y < 0.0F) { - body->velocity.y = -body->velocity.y * body->restitution; + if (body->angular_damping > 0.0F) { + const float DAMP = std::exp(-body->angular_damping * dt); + body->angular_velocity *= DAMP; } + + // Actualizar posición y rotación + body->position += body->velocity * dt; + body->angle += body->angular_velocity * dt; + + body->clearAccumulators(); } - // Pared inferior - if (body->position.y + R > MAX_Y) { - body->position.y = MAX_Y - R; - if (body->velocity.y > 0.0F) { - body->velocity.y = -body->velocity.y * body->restitution; + } + + // 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; + const float MAX_Y = bounds_.y + bounds_.h; + + for (auto* body : bodies_) { + if (body == nullptr || body->isStatic()) { + continue; + } + const float R = body->radius; + + // Pared izquierda + if (body->position.x - R < MIN_X) { + body->position.x = MIN_X + R; + if (body->velocity.x < 0.0F) { + body->velocity.x = -body->velocity.x * body->restitution; + } + } + // Pared derecha + if (body->position.x + R > MAX_X) { + body->position.x = MAX_X - R; + if (body->velocity.x > 0.0F) { + body->velocity.x = -body->velocity.x * body->restitution; + } + } + // Pared superior + if (body->position.y - R < MIN_Y) { + body->position.y = MIN_Y + R; + if (body->velocity.y < 0.0F) { + body->velocity.y = -body->velocity.y * body->restitution; + } + } + // Pared inferior + if (body->position.y + R > MAX_Y) { + body->position.y = MAX_Y - R; + if (body->velocity.y > 0.0F) { + body->velocity.y = -body->velocity.y * body->restitution; + } } } } -} -// 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) { - auto* a = bodies_[i]; - auto* b = bodies_[j]; - if (a != nullptr && b != nullptr) { - resolveBodyPair(*a, *b); + // 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) { + auto* a = bodies_[i]; + auto* b = bodies_[j]; + if (a != nullptr && b != nullptr) { + resolveBodyPair(*a, *b); + } } } } -} -void PhysicsWorld::resolveBodyPair(RigidBody& a, RigidBody& b) { - // Dos cuerpos estáticos no necesitan resolución - if (a.isStatic() && b.isStatic()) { - return; - } + void PhysicsWorld::resolveBodyPair(RigidBody& a, RigidBody& b) { + // Dos cuerpos estáticos no necesitan resolución + if (a.isStatic() && b.isStatic()) { + return; + } - const Vec2 DELTA = b.position - a.position; - const float DIST_SQ = DELTA.lengthSquared(); - const float SUM_R = a.radius + b.radius; - if (DIST_SQ > SUM_R * SUM_R || DIST_SQ <= 0.0F) { - 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 float DIST = std::sqrt(DIST_SQ); - const Vec2 NORMAL = DELTA / DIST; // de A hacia B + const Vec2 DELTA = b.position - a.position; + const float DIST_SQ = DELTA.lengthSquared(); + const float SUM_R = a.radius + b.radius; + if (DIST_SQ > SUM_R * SUM_R || DIST_SQ <= 0.0F) { + return; + } + + const float DIST = std::sqrt(DIST_SQ); + const Vec2 NORMAL = DELTA / DIST; // de A hacia B + + // Corrección posicional (resolver penetración) + const float PENETRATION = SUM_R - DIST; + const float TOTAL_INV_MASS = a.inverse_mass + b.inverse_mass; + if (TOTAL_INV_MASS > 0.0F) { + const Vec2 CORRECTION = NORMAL * (PENETRATION / TOTAL_INV_MASS); + if (!a.isStatic()) { + a.position -= CORRECTION * a.inverse_mass; + } + if (!b.isStatic()) { + b.position += CORRECTION * b.inverse_mass; + } + } + + // Velocidad relativa proyectada sobre la normal + const Vec2 V_REL = b.velocity - a.velocity; + const float VEL_ALONG_NORMAL = V_REL.dot(NORMAL); + // Si se están separando, no aplicar impulso + if (VEL_ALONG_NORMAL > 0.0F) { + return; + } + + // Restitución promedio (Box2D usa max; promedio es más permisivo) + const float E = (a.restitution + b.restitution) * 0.5F; + const float J = -(1.0F + E) * VEL_ALONG_NORMAL / TOTAL_INV_MASS; + const Vec2 IMPULSE = NORMAL * J; - // Corrección posicional (resolver penetración) - const float PENETRATION = SUM_R - DIST; - const float TOTAL_INV_MASS = a.inverse_mass + b.inverse_mass; - if (TOTAL_INV_MASS > 0.0F) { - const Vec2 CORRECTION = NORMAL * (PENETRATION / TOTAL_INV_MASS); if (!a.isStatic()) { - a.position -= CORRECTION * a.inverse_mass; + a.velocity -= IMPULSE * a.inverse_mass; } if (!b.isStatic()) { - b.position += CORRECTION * b.inverse_mass; + b.velocity += IMPULSE * b.inverse_mass; } } - // Velocidad relativa proyectada sobre la normal - const Vec2 V_REL = b.velocity - a.velocity; - const float VEL_ALONG_NORMAL = V_REL.dot(NORMAL); - // Si se están separando, no aplicar impulso - if (VEL_ALONG_NORMAL > 0.0F) { - return; - } - - // Restitución promedio (Box2D usa max; promedio es más permisivo) - const float E = (a.restitution + b.restitution) * 0.5F; - const float J = -(1.0F + E) * VEL_ALONG_NORMAL / TOTAL_INV_MASS; - const Vec2 IMPULSE = NORMAL * J; - - if (!a.isStatic()) { - a.velocity -= IMPULSE * a.inverse_mass; - } - if (!b.isStatic()) { - b.velocity += IMPULSE * b.inverse_mass; - } -} - } // namespace Physics