From 2fe22ff91135147b8ed6672a9b88797f764e68b2 Mon Sep 17 00:00:00 2001 From: Sergio Valor Date: Tue, 19 May 2026 13:32:11 +0200 Subject: [PATCH] Fase 6c: migrar Ship al sistema de fisica vectorial MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Primera entidad migrada. La nave del jugador ya NO mantiene su propio estado cinemático ad-hoc — toda la física vive en Entity::body_ y el movimiento lo realiza Physics::PhysicsWorld. Cambios en ship.hpp: - Eliminado: float velocity_ (escalar, polar) - Eliminado: void applyPhysics() (lo hace el world) - Añadido: override postUpdate() para sincronizar center_/angle_ - getVelocityVector() ahora devuelve body_.velocity (Vec2 cartesiano) - Nuevo getter getSpeed() = body_.velocity.length() - setCenter() actualiza tanto el mirror como body_.position - markHit() detiene el body_ (velocity = 0) Cambios en ship.cpp: - Constructor configura el body_: * mass = 10.0 (referencia para impulsos en choques) * radius = SHIP_RADIUS (12.0) * restitution = 0.6 (rebote moderado en paredes) * linear_damping = 1.5 s⁻¹ (fricción exponencial) * angular_damping = 0.0 (la rotación es por input, no inercial) - init() resetea body_ a la posición/orientación nueva, velocity = 0 - processInput() ahora: * Rotación: modifica body_.angle directamente (no física) * Thrust: applyForce(direction * mass * ACCELERATION) - update() solo gestiona timer de invulnerabilidad y aplica el cap de MAX_VELOCITY (el thrust acumula fuerza sin tope; clampamos body_.velocity) - postUpdate() copia body_.position -> center_ y body_.angle -> angle_ - draw() sin cambios funcionales (usa getSpeed() en lugar de velocity_) Cambios en GameScene: - En init(): physics_world_.addBody(&ship.getBody()) por cada nave activa - En update(): physics_world_.update(dt) + ship.postUpdate(dt) al inicio del frame (las fuerzas del frame N-1 se integran en el frame N; 1 frame de latencia ~16ms, imperceptible a 60fps) Cambios de comportamiento visibles esperados: - La nave ahora rebota contra las paredes del PLAYAREA con restitution=0.6 (antes: clipping silencioso). PRIMERA muestra de la nueva física. - Inercia: tras soltar THRUST, la nave conserva velocidad y se decelera exponencialmente con linear_damping. Sensación más espacial. - Velocidad limitada en magnitud vectorial (antes: escalar). El cap preserva el feel arcade aproximado de MAX_VELOCITY = 120 px/s. Edge case pendiente para tuning: - Naves muertas siguen en el world como obstáculos físicos (radius=12). No es crítico mientras los enemies/bullets no estén migrados. Smoke test xvfb: arranca correctamente. Validación de feeling requiere test del usuario en vivo. Co-Authored-By: Claude Opus 4.7 (1M context) --- source/game/entities/ship.cpp | 205 ++++++++++++------------------ source/game/entities/ship.hpp | 47 +++---- source/game/scenes/game_scene.cpp | 11 ++ 3 files changed, 113 insertions(+), 150 deletions(-) diff --git a/source/game/entities/ship.cpp b/source/game/entities/ship.cpp index 2500084..0f1cb13 100644 --- a/source/game/entities/ship.cpp +++ b/source/game/entities/ship.cpp @@ -1,4 +1,4 @@ -// ship.cpp - Implementació de la nave del player +// ship.cpp - Implementación de la nave del player // © 1999 Visente i Sergi (versión Pascal) // © 2025 Port a C++20 con SDL3 @@ -22,103 +22,92 @@ Ship::Ship(SDL_Renderer* renderer, const char* shape_file) : Entity(renderer), - velocity_(0.0F), is_hit_(false), invulnerable_timer_(0.0F) { - // [NUEVO] Brightness específic per naves + // Brightness específico para naves brightness_ = Defaults::Brightness::NAU; - // [NUEVO] Carregar shape compartida desde file - shape_ = Graphics::ShapeLoader::load(shape_file); + // Configuración del cuerpo físico + body_.setMass(10.0F); // Masa de referencia para choques + body_.radius = Defaults::Entities::SHIP_RADIUS; // Radio de colisión + body_.restitution = 0.6F; // Rebote moderado contra paredes + body_.linear_damping = 1.5F; // Fricción exponencial (s⁻¹) + body_.angular_damping = 0.0F; // La rotación es 100% por input, no inercial + // Cargar shape compartida desde archivo + shape_ = Graphics::ShapeLoader::load(shape_file); if (!shape_ || !shape_->isValid()) { - std::cerr << "[Ship] Error: no s'ha pogut load " << shape_file << '\n'; + std::cerr << "[Ship] Error: no se ha podido cargar " << shape_file << '\n'; } } void Ship::init(const Vec2* spawn_point, bool activar_invulnerabilitat) { - // Inicialización de la ship (triangle) - // Basat en el codi Pascal original: lines 380-384 - // Copiat de joc_asteroides.cpp línies 30-44 - - // [NUEVO] Ya no necesario configure points polars - la geometria es carrega del - // file Solo inicialitzem l'state de la instància - - // Use custom spawn point if provided, otherwise use center + // Posición inicial if (spawn_point != nullptr) { - center_.x = spawn_point->x; - center_.y = spawn_point->y; + center_ = *spawn_point; } else { - // Default: center of play area float centre_x; float centre_y; Constants::obtenir_centre_zona(centre_x, centre_y); - center_.x = static_cast(centre_x); - center_.y = static_cast(centre_y); + center_ = {.x = centre_x, .y = centre_y}; } - // Estat inicial + // Reset orientación angle_ = 0.0F; - velocity_ = 0.0F; + + // Sincronizar cuerpo físico con la posición/orientación inicial + body_.position = center_; + body_.angle = angle_; + body_.velocity = Vec2{}; + body_.angular_velocity = 0.0F; + body_.clearAccumulators(); // Activar invulnerabilidad solo si es respawn - if (activar_invulnerabilitat) { - invulnerable_timer_ = Defaults::Ship::INVULNERABILITY_DURATION; - } else { - invulnerable_timer_ = 0.0F; - } - + invulnerable_timer_ = activar_invulnerabilitat ? Defaults::Ship::INVULNERABILITY_DURATION : 0.0F; is_hit_ = false; } void Ship::processInput(float delta_time, uint8_t player_id) { - // Processar input continu (como teclapuls() del Pascal original) - // Basat en joc_asteroides.cpp línies 66-85 - // Solo processa input si la ship está viva + // Solo procesa input si la nave está viva if (is_hit_) { return; } auto* input = Input::get(); - // Processar input segons el player - if (player_id == 0) { - // Jugador 1 - if (input->checkActionPlayer1(InputAction::RIGHT, Input::ALLOW_REPEAT)) { - angle_ += Defaults::Physics::ROTATION_SPEED * delta_time; - } + // Rotación: control directo del ángulo (no física, no inercial). + // Se actualiza también body_.angle para que el dibujado tras + // postUpdate refleje el cambio inmediatamente. + const bool ROTATE_RIGHT = (player_id == 0) + ? input->checkActionPlayer1(InputAction::RIGHT, Input::ALLOW_REPEAT) + : input->checkActionPlayer2(InputAction::RIGHT, Input::ALLOW_REPEAT); + const bool ROTATE_LEFT = (player_id == 0) + ? input->checkActionPlayer1(InputAction::LEFT, Input::ALLOW_REPEAT) + : input->checkActionPlayer2(InputAction::LEFT, Input::ALLOW_REPEAT); + const bool THRUST = (player_id == 0) + ? input->checkActionPlayer1(InputAction::THRUST, Input::ALLOW_REPEAT) + : input->checkActionPlayer2(InputAction::THRUST, Input::ALLOW_REPEAT); - if (input->checkActionPlayer1(InputAction::LEFT, Input::ALLOW_REPEAT)) { - angle_ -= Defaults::Physics::ROTATION_SPEED * delta_time; - } + if (ROTATE_RIGHT) { + body_.angle += Defaults::Physics::ROTATION_SPEED * delta_time; + } + if (ROTATE_LEFT) { + body_.angle -= Defaults::Physics::ROTATION_SPEED * delta_time; + } - if (input->checkActionPlayer1(InputAction::THRUST, Input::ALLOW_REPEAT)) { - if (velocity_ < Defaults::Physics::MAX_VELOCITY) { - velocity_ += Defaults::Physics::ACCELERATION * delta_time; - velocity_ = std::min(velocity_, Defaults::Physics::MAX_VELOCITY); - } - } - } else { - // Jugador 2 - if (input->checkActionPlayer2(InputAction::RIGHT, Input::ALLOW_REPEAT)) { - angle_ += Defaults::Physics::ROTATION_SPEED * delta_time; - } - - if (input->checkActionPlayer2(InputAction::LEFT, Input::ALLOW_REPEAT)) { - angle_ -= Defaults::Physics::ROTATION_SPEED * delta_time; - } - - if (input->checkActionPlayer2(InputAction::THRUST, Input::ALLOW_REPEAT)) { - if (velocity_ < Defaults::Physics::MAX_VELOCITY) { - velocity_ += Defaults::Physics::ACCELERATION * delta_time; - velocity_ = std::min(velocity_, Defaults::Physics::MAX_VELOCITY); - } - } + // Thrust: fuerza vectorial en la dirección de la nariz. + // angle - PI/2 porque angle=0 apunta hacia arriba (eje Y negativo SDL). + if (THRUST) { + const float DIR_X = std::cos(body_.angle - (Constants::PI / 2.0F)); + const float DIR_Y = std::sin(body_.angle - (Constants::PI / 2.0F)); + // Fuerza = masa * aceleración: 10 kg * 400 px/s² = 4000 (unidades arcade) + const float MAGNITUDE = body_.mass * Defaults::Physics::ACCELERATION; + body_.applyForce(Vec2{.x = DIR_X * MAGNITUDE, .y = DIR_Y * MAGNITUDE}); } } void Ship::update(float delta_time) { - // Solo update si la ship está viva + // Solo update si la nave está viva if (is_hit_) { return; } @@ -129,26 +118,35 @@ void Ship::update(float delta_time) { invulnerable_timer_ = std::max(invulnerable_timer_, 0.0F); } - // Aplicar física (movement + fricció) - applyPhysics(delta_time); + // El movimiento real lo hace PhysicsWorld::update(). + // Aquí solo lógica de estado. + + // Cap de velocidad: el thrust acumula fuerza sin límite; limitamos + // la magnitud de body_.velocity tras aplicar fuerzas para preservar + // el feel arcade del MAX_VELOCITY original. + const float CURRENT_SPEED = body_.velocity.length(); + if (CURRENT_SPEED > Defaults::Physics::MAX_VELOCITY) { + body_.velocity = body_.velocity * (Defaults::Physics::MAX_VELOCITY / CURRENT_SPEED); + } +} + +void Ship::postUpdate(float /*delta_time*/) { + // Sincronizar mirror desde body_ tras la integración del world. + center_ = body_.position; + angle_ = body_.angle; } void Ship::draw() const { - // Solo draw si la ship está viva if (is_hit_) { return; } - // Si invulnerable, parpadear (toggle on/off) + // Parpadeo si invulnerable if (isInvulnerable()) { - // Calcular ciclo de parpadeo - float blink_cycle = Defaults::Ship::BLINK_VISIBLE_TIME + - Defaults::Ship::BLINK_INVISIBLE_TIME; - float time_in_cycle = std::fmod(invulnerable_timer_, blink_cycle); - - // Si estamos en fase invisible, no dibujar - if (time_in_cycle < Defaults::Ship::BLINK_INVISIBLE_TIME) { - return; // No dibujar durante fase invisible + const float BLINK_CYCLE = Defaults::Ship::BLINK_VISIBLE_TIME + Defaults::Ship::BLINK_INVISIBLE_TIME; + const float TIME_IN_CYCLE = std::fmod(invulnerable_timer_, BLINK_CYCLE); + if (TIME_IN_CYCLE < Defaults::Ship::BLINK_INVISIBLE_TIME) { + return; } } @@ -156,58 +154,11 @@ void Ship::draw() const { return; } - // Escalar velocity per l'efecte visual (200 px/s → ~6 px de efecte) - // El codi Pascal original sumava velocity (0-6) al radi per donar - // sensació de "empenta". Ara velocity está en px/s (0-200). - // Basat en joc_asteroides.cpp línies 127-134 - // - // [NUEVO] Convertir suma de velocitat_visual a scale multiplicativa - // Radio base del ship = 12 px - // velocitat_visual = 0-6 → r = 12-18 → scale = 1.0-1.5 - float velocitat_visual = velocity_ / 33.33F; - float scale = 1.0F + (velocitat_visual / 12.0F); + // Efecto visual de empuje: escala proporcional a la velocidad. + // 0..200 px/s → escala 1.0..1.5 (manteniendo la sensación del Pascal original). + const float SPEED = getSpeed(); + const float VISUAL_PUSH = SPEED / 33.33F; + const float SCALE = 1.0F + (VISUAL_PUSH / 12.0F); - Rendering::render_shape(renderer_, shape_, center_, angle_, scale, 1.0F, brightness_); -} - -void Ship::applyPhysics(float delta_time) { - // Aplicar física de movement - // Basat en joc_asteroides.cpp línies 87-113 - - // Calcular nueva posición basada en velocity i angle - // S'usa (angle - PI/2) perquè angle=0 apunta sin amunt, no hacia la derecha - // velocity_ está en px/s, así que multipliquem per delta_time - float dy = - ((velocity_ * delta_time) * std::sin(angle_ - (Constants::PI / 2.0F))) + - center_.y; - float dx = - ((velocity_ * delta_time) * std::cos(angle_ - (Constants::PI / 2.0F))) + - center_.x; - - // Boundary checking con radi de la ship - // CORRECCIÓ: Usar límits segurs i inequalitats inclusives - float min_x; - float max_x; - float min_y; - float max_y; - Constants::obtenir_limits_zona_segurs(Defaults::Entities::SHIP_RADIUS, - min_x, - max_x, - min_y, - max_y); - - // Inequalitats inclusives (>= i <=) - if (dy >= min_y && dy <= max_y) { - center_.y = dy; - } - - if (dx >= min_x && dx <= max_x) { - center_.x = dx; - } - - // Fricció - desacceleració gradual (time-based) - if (velocity_ > 0.1F) { - velocity_ -= Defaults::Physics::FRICTION * delta_time; - velocity_ = std::max(velocity_, 0.0F); - } + Rendering::render_shape(renderer_, shape_, center_, angle_, SCALE, 1.0F, brightness_); } diff --git a/source/game/entities/ship.hpp b/source/game/entities/ship.hpp index 5a4aa06..5e1ade5 100644 --- a/source/game/entities/ship.hpp +++ b/source/game/entities/ship.hpp @@ -5,13 +5,11 @@ #pragma once #include -#include #include #include "core/defaults.hpp" #include "core/entities/entity.hpp" #include "core/types.hpp" -#include "game/constants.hpp" class Ship : public Entities::Entity { public: @@ -23,40 +21,43 @@ class Ship : public Entities::Entity { void init(const Vec2* spawn_point, bool activar_invulnerabilitat = false); void processInput(float delta_time, uint8_t player_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 !is_hit_; } + // Override: Interfaz de Entity + [[nodiscard]] auto isActive() const -> bool override { return !is_hit_; } - // Override: Interfície de colisión - [[nodiscard]] float getCollisionRadius() const override { + // Override: Interfaz de colisión + [[nodiscard]] auto getCollisionRadius() const -> float override { return Defaults::Entities::SHIP_RADIUS; } - [[nodiscard]] bool isCollidable() const override { + [[nodiscard]] auto isCollidable() const -> bool override { return !is_hit_ && invulnerable_timer_ <= 0.0F; } - // Getters (API pública sin canvis) - [[nodiscard]] bool isAlive() const { return !is_hit_; } - [[nodiscard]] bool isHit() const { return is_hit_; } - [[nodiscard]] bool isInvulnerable() const { return invulnerable_timer_ > 0.0F; } - [[nodiscard]] Vec2 getVelocityVector() const { - return { - .x = velocity_ * std::cos(angle_ - (Constants::PI / 2.0F)), - .y = velocity_ * std::sin(angle_ - (Constants::PI / 2.0F))}; - } + // Getters (API pública sin cambios) + [[nodiscard]] auto isAlive() const -> bool { return !is_hit_; } + [[nodiscard]] auto isHit() const -> bool { return is_hit_; } + [[nodiscard]] auto isInvulnerable() const -> bool { return invulnerable_timer_ > 0.0F; } + // Velocidad como vector cartesiano (ahora viene directa del body_). + [[nodiscard]] auto getVelocityVector() const -> Vec2 { return body_.velocity; } + // Velocidad escalar (utilidad para draw y debugging). + [[nodiscard]] auto getSpeed() const -> float { return body_.velocity.length(); } // Setters - void setCenter(const Vec2& nou_centre) { center_ = nou_centre; } + void setCenter(const Vec2& nou_centre) { + center_ = nou_centre; + body_.position = nou_centre; + } - // Colisiones (Fase 10) - void markHit() { is_hit_ = true; } + // Colisiones + void markHit() { + is_hit_ = true; + body_.velocity = Vec2{}; // Detener al morir + } private: - // Membres específics de Ship (heretats: renderer_, shape_, center_, angle_, brightness_) - float velocity_; // Velocidad (px/s) + // Miembros específicos de Ship (heredados: renderer_, shape_, center_, angle_, brightness_, body_) bool is_hit_; float invulnerable_timer_; // 0.0f = vulnerable, >0.0f = invulnerable - - void applyPhysics(float delta_time); }; diff --git a/source/game/scenes/game_scene.cpp b/source/game/scenes/game_scene.cpp index c646ab7..3686cec 100644 --- a/source/game/scenes/game_scene.cpp +++ b/source/game/scenes/game_scene.cpp @@ -183,6 +183,8 @@ void GameScene::init() { // Jugador active: init normalment Vec2 spawn_pos = obtenir_punt_spawn(i); ships_[i].init(&spawn_pos, false); // No invulnerability at start + // Registrar el cuerpo físico de la nave en el mundo (Fase 6c) + physics_world_.addBody(&ships_[i].getBody()); std::cout << "[GameScene] Jugador " << (i + 1) << " inicialitzat\n"; } else { // Jugador inactiu: marcar como a mort permanent @@ -214,6 +216,15 @@ void GameScene::init() { } void GameScene::update(float delta_time) { + // === FÍSICA: integrar bodies del frame anterior y resolver colisiones === + // Se ejecuta al inicio del frame: las fuerzas aplicadas en el frame N-1 + // por processInput/AI se integran ahora, y postUpdate sincroniza los + // mirrors (center_/angle_) antes de la lógica de juego que los lee. + physics_world_.update(delta_time); + for (auto& ship : ships_) { + ship.postUpdate(delta_time); + } + // Processar disparos (state-based, no event-based) if (game_over_state_ == GameOverState::NONE) { auto* input = Input::get();