Fase 6e: migrar Bullet al sistema de fisica vectorial

Las balas pasan a ser cinematicas dentro del PhysicsWorld:
- body_.setMass(0.5), radius=0 (no colisionan fisicamente)
- disparar() setea body_.position + body_.velocity cartesiana (140 px/s)
- update() detecta salida del PLAYAREA via body_.position y desactiva
- postUpdate() sincroniza center_ desde body_.position
- desactivar() detiene el body para evitar deriva mientras inactiva

GameScene registra los bodies en init() y llama postUpdate(). El gameplay
sigue gestionando colisiones bullet-enemy/bullet-ship con check_collision
(el radio gameplay es BULLET_RADIUS=3, expuesto via getCollisionRadius).

Renames a camelBack (clang-tidy): get_owner_id->getOwnerId,
get_grace_timer->getGraceTimer.

MIGRATION_PLAN.md actualizado: Fase 6e cerrada, Fase 7 (SDL3 GPU) siguiente.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-19 13:50:17 +02:00
parent c50ca23135
commit 9993b2d98c
4 changed files with 128 additions and 109 deletions
+33 -34
View File
@@ -35,44 +35,37 @@ Tag de seguridad: **`beta-3.0`** (snapshot de `main` antes de empezar).
| 5 — Infraestructura física (`RigidBody`, `PhysicsWorld`) | ✅ | `0fd9360` |
| 6a+b — `body_` en Entity + `world_` en GameScene | ✅ | `0574077` |
| 6c — Migrar Ship | ✅ | `2fe22ff` |
| **6d — Migrar Enemy** | ✅ | `27242f5` |
| 6e — Migrar Bullet | 🔲 **siguiente** | — |
| 7 — Migración a SDL3 GPU (sin fallback) | 🔲 | — |
| 6d — Migrar Enemy | ✅ | `27242f5` |
| **6e — Migrar Bullet** | ✅ | (este commit) |
| 7 — Migración a SDL3 GPU (sin fallback) | 🔲 **siguiente** | — |
| 8 — Postprocesado, color, paleta por tipo | 🔲 | — |
| 9 — Refactor de GameScene (2.877 LOC → módulos) | 🔲 | — |
| 10 — Tuning final de masa/restitución/damping | 🔲 | — |
## Lo que queda inmediato (Fase 6e)
## Lo que queda inmediato (Fase 7)
Migrar `Bullet` al PhysicsWorld. Es la entidad más simple:
**Migración del renderizado a SDL3 GPU sin fallback.**
- Constructor: `body_.setMass(0.5)`, `radius = BULLET_RADIUS = 3`, `restitution = 0`
(NO rebotan, salen del PLAYAREA y se desactivan), `linear_damping = 0`.
- `init()` resetea body_.
- `disparar()`/spawn: setear `body_.position` y `body_.velocity` cartesiano a `140 px/s` en
dirección del shooter.
- `update()`: detectar si salió del PLAYAREA con margen; si sí, `desactivar()`.
- `postUpdate()`: sincronizar `center_` desde `body_.position`.
- En GameScene: registrar bodies en `init()`, llamar `postUpdate()` en `update()`.
- Cuando un bullet se desactiva: `body_.radius = 0` para no estorbar.
- **Edge case importante**: las balas NO deben rebotar contra las naves de los
jugadores. Las colisiones físicas las hace el world automáticamente. Para
evitarlo:
- Opción A: dejar que el world resuelva el impulso, pero la lógica de
gameplay (en GameScene) detecta el hit y desactiva el bullet antes de que
se vea visualmente el rebote (1 frame de margen).
- Opción B: hacer que `body_.radius = 0` para las balas, así no colisionan
físicamente, y solo las colisiones de gameplay (check_collision en
GameScene) las gestionan.
- Recomendado: **Opción B**. Las balas físicamente no necesitan colisión
del world — son cinemáticas. Solo el gameplay las usa.
Fase 6e completada: las balas son cinemáticas dentro del PhysicsWorld (radius=0
en el world → no colisionan físicamente, solo gameplay-level via check_collision).
Body con mass=0.5, restitution=0, damping=0. `disparar()` setea
`body_.position` + `body_.velocity` cartesiana; `update()` detecta salida de
PLAYAREA y desactiva; `postUpdate()` sincroniza `center_` desde body. Smoke
test xvfb pasa sin errores.
Tras 6e, hacer **Fase 10 (tuning)**: ajustar masas, restitución, damping según
feel. Probablemente:
- Ship: subir `mass` a 15-20 (más "anclado", menos empujable por enemies).
- Enemies: reducir `restitution` a 0.85 (rebote más natural, sin "pelota").
- Probar collision con friendly fire / nave-enemy en debris ahora que ambos
tienen body real.
Decisión técnica del proyecto: **no fallback a SDL_Renderer**
([memoria](./.claude/projects/-home-sergio-gitea-orni-attack/memory/project_no_sdl_fallback.md)).
La fase 7 reemplaza completamente el pipeline actual de SDL_Renderer
(`SDL_RenderLine`, `SDL_RenderGeometry`) por SDL3 GPU.
Tareas previstas Fase 7:
- Sustituir `SDL_Renderer*` por `SDL_GPUDevice*` en SDLManager.
- Setup de swapchain + shaders básicos (vertex + fragment) para líneas vectoriales.
- Migrar `Rendering::render_shape` a un pipeline GPU que reciba vertex buffers.
- Postprocesado mínimo (Fase 8): bloom/glow, paleta por tipo de entidad.
**Fase 10 (tuning)** queda pendiente para después de SDL3 GPU + postpro. El usuario
prefiere terminar la migración técnica antes de tunear feel.
## Memoria del proyecto
@@ -115,16 +108,22 @@ Resumen:
indica en qué fase estamos).
6. Tras cada commit, push automático: `git push origin rewrite/physics-gpu`.
## Configuración física actual (Fase 6d)
## Configuración física actual (Fase 6e)
| Entidad | mass | radius | restitution | linear_damping | angular_damping |
| Entidad | mass | radius (world) | restitution | linear_damping | angular_damping |
|---|---|---|---|---|---|
| Ship | 10.0 | 12 | 0.6 | 1.5 | 0.0 |
| Enemy Pentagon | 5.0 | 20 | 1.0 | 0.0 | 0.0 |
| Enemy Quadrat | 8.0 | 20 | 1.0 | 0.0 | 0.0 |
| Enemy Molinillo | 4.0 | 20 | 1.0 | 0.0 | 0.0 |
| Bullet | (pendiente Fase 6e) | 3 | 0 | 0 | 0 |
| Bullet | 0.5 | **0** (cinemática) | 0 | 0 | 0 |
| Wall (PLAYAREA bounds) | ∞ (estático) | — | — | — | — |
Nota: Bullet usa `radius=0` en el body físico para que no participe en las
colisiones del world (ni body-body ni bounds). El radio de gameplay
(`Defaults::Entities::BULLET_RADIUS = 3`) lo expone vía `getCollisionRadius()`
y lo consumen `check_collision` y la detección de salida del PLAYAREA en
`Bullet::update`.
Las paredes son implícitas: `physics_world_.setBounds(PLAYAREA)` en
`GameScene::init`. No son `RigidBody` separados, sino un AABB que rebota.
+72 -57
View File
@@ -1,4 +1,4 @@
// bullet.cpp - Implementació de projectils de la ship
// bullet.cpp - Implementación de projectils de la ship
// © 1999 Visente i Sergi (versión Pascal)
// © 2025 Port a C++20 con SDL3
@@ -17,18 +17,33 @@
#include "core/types.hpp"
#include "game/constants.hpp"
namespace {
// Velocidad escalar de las balas (px/s). Conserva el feel del Pascal original
// (7 px/frame × 20 FPS = 140 px/s).
constexpr float BULLET_SPEED = 140.0F;
} // namespace
Bullet::Bullet(SDL_Renderer* renderer)
: Entity(renderer),
velocity_(0.0F),
esta_(false),
owner_id_(0),
grace_timer_(0.0F) {
// [NUEVO] Brightness específic per balas
// Brightness específico para balas
brightness_ = Defaults::Brightness::BALA;
// [NUEVO] Carregar shape compartida desde file
shape_ = Graphics::ShapeLoader::load("bullet.shp");
// Configuración del cuerpo físico.
// Las balas son cinemáticas: no colisionan con otros bodies ni paredes.
// El gameplay (GameScene) gestiona los hits con check_collision y la
// salida del PLAYAREA. Por eso radius=0 en el world (no participa en
// resolveBodyCollisions ni resolveBoundsCollisions).
body_.setMass(0.5F); // Ligera (no afecta a nadie, pero por consistencia)
body_.radius = 0.0F; // Sin colisión física (cinemática pura)
body_.restitution = 0.0F; // Irrelevante (no rebota)
body_.linear_damping = 0.0F; // Sin fricción (movimiento rectilíneo uniforme)
body_.angular_damping = 0.0F;
// Cargar shape compartida desde archivo
shape_ = Graphics::ShapeLoader::load("bullet.shp");
if (!shape_ || !shape_->isValid()) {
std::cerr << "[Bullet] Error: no s'ha pogut load bullet.shp" << '\n';
}
@@ -39,77 +54,57 @@ void Bullet::init() {
esta_ = false;
center_ = {.x = 0.0F, .y = 0.0F};
angle_ = 0.0F;
velocity_ = 0.0F;
grace_timer_ = 0.0F;
// Reset del cuerpo físico
body_.position = Vec2{};
body_.velocity = Vec2{};
body_.angle = 0.0F;
body_.angular_velocity = 0.0F;
body_.clearAccumulators();
}
void Bullet::disparar(const Vec2& position, float angle, uint8_t owner_id) {
// Activar bullet i posicionar-la a la ship
// Basat en joc_asteroides.cpp línies 188-200
// Activar bullet
esta_ = true;
// Posición inicial = centro de la ship
center_.x = position.x;
center_.y = position.y;
// Angle = angle de la ship (dispara en la direcció que apunta)
angle_ = angle;
// Almacenar propietario (0=P1, 1=P2)
owner_id_ = owner_id;
// Velocidad alta (el juego Pascal original usava 7 px/frame)
// 7 px/frame × 20 FPS = 140 px/s
velocity_ = 140.0F;
// Activar grace period (prevents instant self-collision)
grace_timer_ = Defaults::Game::BULLET_GRACE_PERIOD;
// Posición y orientación iniciales = ship
center_ = position;
angle_ = angle;
// Sincronizar el body físico: posición + velocidad cartesiana
// angle - PI/2 porque angle=0 apunta hacia arriba (eje Y negativo SDL)
body_.position = position;
body_.angle = angle;
const float DIR_X = std::cos(angle - (Constants::PI / 2.0F));
const float DIR_Y = std::sin(angle - (Constants::PI / 2.0F));
body_.velocity = Vec2{.x = DIR_X * BULLET_SPEED, .y = DIR_Y * BULLET_SPEED};
body_.angular_velocity = 0.0F;
body_.clearAccumulators();
// Reproducir sonido de disparo láser
Audio::get()->playSound(Defaults::Sound::LASER, Audio::Group::GAME);
}
void Bullet::update(float delta_time) {
if (esta_) {
// Decrementar grace timer
if (grace_timer_ > 0.0F) {
grace_timer_ -= delta_time;
grace_timer_ = std::max(grace_timer_, 0.0F);
}
mou(delta_time);
if (!esta_) {
return;
}
}
void Bullet::draw() const {
if (esta_ && shape_) {
// [NUEVO] Usar render_shape en lloc de rota_pol
// Les balas roten segons l'angle de trajectòria
Rendering::render_shape(renderer_, shape_, center_, angle_, 1.0F, 1.0F, brightness_);
// Decrementar grace timer
if (grace_timer_ > 0.0F) {
grace_timer_ -= delta_time;
grace_timer_ = std::max(grace_timer_, 0.0F);
}
}
void Bullet::mou(float delta_time) {
// Moviment rectilini de la bullet
// Basat en el codi Pascal original: procedure mou_bales
// Copiat EXACTAMENT de joc_asteroides.cpp línies 396-419
// Calcular nueva posición (movement polar time-based)
// velocity ya está en px/s (140 px/s), solo necesario multiplicar per delta_time
float velocitat_efectiva = velocity_ * delta_time;
// Calcular desplaçament (angle-PI/2 perquè angle=0 apunta amunt)
float dy = velocitat_efectiva * std::sin(angle_ - (Constants::PI / 2.0F));
float dx = velocitat_efectiva * std::cos(angle_ - (Constants::PI / 2.0F));
// Acumulació directa con precisió subpíxel
center_.y += dy;
center_.x += dx;
// Desactivar si surt de la zona de juego (no rebota como los ORNIs)
// CORRECCIÓ: Usar límits segurs con radi de la bullet
// El movimiento real lo hace PhysicsWorld::update() (integración).
// Aquí solo lógica de estado: detectar salida del PLAYAREA y desactivar.
float min_x;
float max_x;
float min_y;
@@ -120,8 +115,28 @@ void Bullet::mou(float delta_time) {
min_y,
max_y);
if (center_.x < min_x || center_.x > max_x ||
center_.y < min_y || center_.y > max_y) {
esta_ = false;
if (body_.position.x < min_x || body_.position.x > max_x ||
body_.position.y < min_y || body_.position.y > max_y) {
desactivar();
}
}
void Bullet::postUpdate(float /*delta_time*/) {
// Sincronizar mirror desde body_ tras la integración del world.
center_ = body_.position;
// angle_ no cambia (las balas no rotan visualmente).
}
void Bullet::desactivar() {
esta_ = false;
// Detener el cuerpo físico para que no acumule deriva mientras inactiva.
body_.velocity = Vec2{};
body_.angular_velocity = 0.0F;
}
void Bullet::draw() const {
if (esta_ && shape_) {
// Les bales roten segons l'angle de trayectòria (estático tras disparo)
Rendering::render_shape(renderer_, shape_, center_, angle_, 1.0F, 1.0F, brightness_);
}
}
+12 -14
View File
@@ -20,31 +20,29 @@ class Bullet : public Entities::Entity {
void init() override;
void disparar(const Vec2& position, float angle, uint8_t owner_id);
void update(float delta_time) override;
void postUpdate(float delta_time) override;
void draw() const override;
// Override: Interfície d'Entity
[[nodiscard]] bool isActive() const override { return esta_; }
// Override: Interfaz de Entity
[[nodiscard]] auto isActive() const -> bool override { return esta_; }
// Override: Interfície de colisión
[[nodiscard]] float getCollisionRadius() const override {
// Override: Interfaz de colisión (gameplay-level: PLAYAREA bounds-check)
[[nodiscard]] auto getCollisionRadius() const -> float override {
return Defaults::Entities::BULLET_RADIUS;
}
[[nodiscard]] bool isCollidable() const override {
[[nodiscard]] auto isCollidable() const -> bool override {
return esta_ && grace_timer_ <= 0.0F;
}
// Getters (API pública sin canvis)
[[nodiscard]] bool esta_activa() const { return esta_; }
[[nodiscard]] uint8_t get_owner_id() const { return owner_id_; }
[[nodiscard]] float get_grace_timer() const { return grace_timer_; }
void desactivar() { esta_ = false; }
// Getters (API pública sin cambios)
[[nodiscard]] auto esta_activa() const -> bool { return esta_; }
[[nodiscard]] auto getOwnerId() const -> uint8_t { return owner_id_; }
[[nodiscard]] auto getGraceTimer() const -> float { return grace_timer_; }
void desactivar();
private:
// Membres específics de Bullet (heretats: renderer_, shape_, center_, angle_, brightness_)
float velocity_;
// Miembros específicos de Bullet (heredados: renderer_, shape_, center_, angle_, brightness_, body_)
bool esta_;
uint8_t owner_id_; // 0=P1, 1=P2
float grace_timer_; // Grace period timer (0.0 = vulnerable)
void mou(float delta_time);
};
+11 -4
View File
@@ -205,9 +205,13 @@ void GameScene::init() {
// DON'T call enemy.init() here - stage system handles spawning
}
// Inicialitzar balas (now 6 instead of 3)
// Inicialitzar balas (now 6 instead of 3).
// Se registran en el physics_world para integración cinemática.
// Como su body_.radius=0, no colisionan físicamente con nadie (las
// colisiones de gameplay se gestionan en detectar_col·lisions_*).
for (auto& bullet : bullets_) {
bullet.init();
physics_world_.addBody(&bullet.getBody());
}
// [ELIMINAT] Iniciar música de juego (ara es gestiona en stage_manager)
@@ -230,6 +234,9 @@ void GameScene::update(float delta_time) {
for (auto& enemy : enemies_) {
enemy.postUpdate(delta_time);
}
for (auto& bullet : bullets_) {
bullet.postUpdate(delta_time);
}
// Processar disparos (state-based, no event-based)
if (game_over_state_ == GameOverState::NONE) {
@@ -996,7 +1003,7 @@ void GameScene::detectar_col·lisions_bales_enemics() {
}
// 2. Add score to the player who shot it
uint8_t owner_id = bullet.get_owner_id();
uint8_t owner_id = bullet.getOwnerId();
score_per_player_[owner_id] += points;
// 3. Create floating score number
@@ -1078,11 +1085,11 @@ void GameScene::detectar_col·lisions_bales_jugadors() {
}
// Skip bullets in grace period (prevents instant self-collision)
if (bullet.get_grace_timer() > 0.0F) {
if (bullet.getGraceTimer() > 0.0F) {
continue;
}
uint8_t bullet_owner = bullet.get_owner_id();
uint8_t bullet_owner = bullet.getOwnerId();
// Check collision with BOTH players
for (uint8_t player_id = 0; player_id < 2; player_id++) {