From 0fd93600293d77c77fe6d1de760048e7314ecdbb Mon Sep 17 00:00:00 2001 From: Sergio Valor Date: Tue, 19 May 2026 13:12:06 +0200 Subject: [PATCH] Fase 5: infraestructura del sistema de fisica vectorial Crea los componentes base del nuevo motor de fisica sin alterar todavia el comportamiento del juego. La migracion de Ship/Enemy/ Bullet al nuevo sistema queda para Fase 6. Nuevos archivos: - core/physics/rigid_body.hpp - struct POD con: * Vec2 position, velocity (cartesianas, NO polares) * float angle, angular_velocity * mass, inverse_mass (cacheado; 0 = estatico) * restitution (elasticidad 0..1) * linear_damping, angular_damping (s-1, exponencial) * radius (circulo de colision) * applyForce / applyImpulse / clearAccumulators * setStatic() para paredes/obstaculos - core/physics/physics_world.hpp/.cpp - mundo fisico: * Almacena RigidBody* (no-owning, ownership en entidades) * setBounds(SDL_FRect) para paredes implicitas (PLAYAREA) * update(dt) = integrate + resolveBoundsCollisions + resolveBodyCollisions * Integrador semi-implicito de Euler + damping exponencial * Resolucion circulo-circulo con correccion posicional e impulsos elasticos * Formula del impulso: j = -(1+e)*(v_rel . n) / (1/m_a + 1/m_b) * Broadphase trivial O(n^2): suficiente para ~25 cuerpos del juego Decisiones de diseno: - Velocidad en cartesianas (Vec2) en lugar de la representacion polar actual (escalar velocidad + cos/sin del angulo cada frame). Adios al acoplamiento entre orientacion y direccion de movimiento. - Composicion sobre herencia: RigidBody es un struct independiente que las entidades incrustaran como member en Fase 6, no una clase base. - El integrador semi-implicito es la version estandar para juegos arcade (mas estable que Euler explicito sin coste extra). - Damping exponencial (exp(-damping*dt)) en lugar de lineal: mantiene el feeling consistente independientemente del framerate. - Sin gravedad: el juego es top-down, no necesita campo de fuerzas global. Las entidades aplican sus propias fuerzas (thrust). Pendiente Fase 6: - Anadir RigidBody body_ a Entity (member, no pointer) - Migrar Ship: thrust como applyForce, en lugar de velocity_ escalar - Migrar Enemy: cambios de direccion via applyImpulse, rebotes los hace PhysicsWorld - Migrar Bullet: lineal sin damping, restitution=0 (no rebotan) - Anadir PhysicsWorld a GameScene, registrar bodies, llamar update() Compila y enlaza. Smoke test xvfb OK: el juego arranca igual que antes (la nueva infraestructura aun no se invoca). Co-Authored-By: Claude Opus 4.7 (1M context) --- source/core/physics/physics_world.cpp | 178 ++++++++++++++++++++++++++ source/core/physics/physics_world.hpp | 64 +++++++++ source/core/physics/rigid_body.hpp | 75 +++++++++++ 3 files changed, 317 insertions(+) create mode 100644 source/core/physics/physics_world.cpp create mode 100644 source/core/physics/physics_world.hpp create mode 100644 source/core/physics/rigid_body.hpp diff --git a/source/core/physics/physics_world.cpp b/source/core/physics/physics_world.cpp new file mode 100644 index 0000000..3d1e7a5 --- /dev/null +++ b/source/core/physics/physics_world.cpp @@ -0,0 +1,178 @@ +// physics_world.cpp - Implementación del mundo físico +// © 2025 Orni Attack + +#include "core/physics/physics_world.hpp" + +#include +#include + +#include "core/physics/rigid_body.hpp" + +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; + } + + // 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 (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; + + 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) { + continue; + } + // Dos cuerpos estáticos no necesitan resolución + if (a->isStatic() && b->isStatic()) { + continue; + } + + 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) { + continue; + } + + 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) { + continue; + } + + // 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 diff --git a/source/core/physics/physics_world.hpp b/source/core/physics/physics_world.hpp new file mode 100644 index 0000000..28314f8 --- /dev/null +++ b/source/core/physics/physics_world.hpp @@ -0,0 +1,64 @@ +// physics_world.hpp - Mundo físico 2D +// © 2025 Orni Attack +// +// Gestiona un conjunto de RigidBody, integra sus movimientos y detecta +// colisiones por frame. Diseño minimalista para arcade: broadphase trivial +// O(n²) suficiente para <50 cuerpos (15 enemigos + balas + paredes). +// +// Los RigidBody viven en las entidades (las entidades poseen sus bodies); +// PhysicsWorld solo guarda punteros no-owning. La entidad es responsable +// de añadir/quitar su body del mundo en init/destroy. + +#pragma once + +#include + +#include + +namespace Physics { + +struct RigidBody; + +class PhysicsWorld { + public: + PhysicsWorld() = default; + + // Añade un cuerpo al mundo (no toma ownership). + void addBody(RigidBody* body); + + // Elimina un cuerpo. No-op si no está registrado. + void removeBody(RigidBody* body); + + // Vacía la lista (no destruye los cuerpos). + void clear() { bodies_.clear(); } + + // Define los límites del mundo (paredes implícitas). Pasa un rect + // PLAYAREA para que los cuerpos reboten contra los bordes según su + // restitution. Vacío = sin paredes. + void setBounds(const SDL_FRect& bounds) { + bounds_ = bounds; + has_bounds_ = true; + } + void clearBounds() { has_bounds_ = false; } + + // Avanza la simulación dt segundos: + // 1. Integra cada cuerpo (semi-implicit Euler + damping) + // 2. Resuelve colisiones contra los bounds (si configurados) + // 3. Resuelve colisiones cuerpo-cuerpo (impulsos elásticos) + void update(float dt); + + // Consultas + [[nodiscard]] auto getBodyCount() const -> std::size_t { return bodies_.size(); } + [[nodiscard]] auto getBodies() const -> const std::vector& { return bodies_; } + + private: + std::vector bodies_; + SDL_FRect bounds_{0.0F, 0.0F, 0.0F, 0.0F}; + bool has_bounds_{false}; + + void integrate(float dt); + void resolveBoundsCollisions(); + void resolveBodyCollisions(); +}; + +} // namespace Physics diff --git a/source/core/physics/rigid_body.hpp b/source/core/physics/rigid_body.hpp new file mode 100644 index 0000000..c90b840 --- /dev/null +++ b/source/core/physics/rigid_body.hpp @@ -0,0 +1,75 @@ +// rigid_body.hpp - Cuerpo rígido 2D para el sistema de física +// © 2025 Orni Attack +// +// Estructura POD-like que encapsula el estado físico de una entidad: +// posición, velocidad lineal/angular, masa, restitución y damping. +// El integrador es semi-implícito de Euler (estable para juegos arcade). +// +// Convenciones: +// - position: coordenadas lógicas (px), donde la entidad está en el mundo +// - angle: radianes; 0 apunta hacia arriba (eje Y negativo en SDL) +// - velocity: px/s en cartesianas (NO polares — adiós a cos/sin por entidad) +// - mass = 0 (inverse_mass = 0) representa un cuerpo estático (masa infinita) +// - restitution 0 = inelástico, 1 = elástico perfecto +// - linear_damping en s⁻¹ (fricción exponencial: v *= exp(-damping * dt)) + +#pragma once + +#include "core/types.hpp" + +namespace Physics { + +struct RigidBody { + // --- Estado cinemático --- + Vec2 position{}; // Posición del centro (px) + Vec2 velocity{}; // Velocidad lineal (px/s) + float angle{0.0F}; // Orientación (rad) + float angular_velocity{0.0F}; // Velocidad angular (rad/s) + + // --- Propiedades físicas --- + float mass{1.0F}; // Masa (kg, escala libre) + float inverse_mass{1.0F}; // 1/mass cacheado (0 = estático) + float restitution{0.5F}; // Elasticidad (0..1) + float linear_damping{0.0F}; // Fricción lineal (s⁻¹) + float angular_damping{0.0F}; // Fricción angular (s⁻¹) + float radius{0.0F}; // Radio de colisión (círculo) + + // --- Fuerzas acumuladas (reseteadas tras cada integrate) --- + Vec2 force_accumulator{}; + float torque_accumulator{0.0F}; + + // Configura la masa y precalcula inverse_mass. + // mass <= 0 marca el cuerpo como estático (inmovible por impulsos). + void setMass(float new_mass) { + mass = new_mass; + inverse_mass = (new_mass > 0.0F) ? 1.0F / new_mass : 0.0F; + } + + // Marca el cuerpo como estático (paredes, obstáculos fijos). + void setStatic() { + mass = 0.0F; + inverse_mass = 0.0F; + velocity = Vec2{}; + angular_velocity = 0.0F; + } + + [[nodiscard]] auto isStatic() const -> bool { return inverse_mass == 0.0F; } + + // Aplica una fuerza instantánea (acumulada para el siguiente integrate). + void applyForce(const Vec2& force) { force_accumulator += force; } + + // Aplica un impulso (cambio inmediato de velocidad: Δv = J / m). + void applyImpulse(const Vec2& impulse) { + if (!isStatic()) { + velocity += impulse * inverse_mass; + } + } + + // Resetea los acumuladores tras la integración. + void clearAccumulators() { + force_accumulator = Vec2{}; + torque_accumulator = 0.0F; + } +}; + +} // namespace Physics