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) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,178 @@
|
||||
// physics_world.cpp - Implementación del mundo físico
|
||||
// © 2025 Orni Attack
|
||||
|
||||
#include "core/physics/physics_world.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
|
||||
#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
|
||||
@@ -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 <SDL3/SDL.h>
|
||||
|
||||
#include <vector>
|
||||
|
||||
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<RigidBody*>& { return bodies_; }
|
||||
|
||||
private:
|
||||
std::vector<RigidBody*> 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
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user