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:
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user