Fase 6c: migrar Ship al sistema de fisica vectorial
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) <noreply@anthropic.com>
This commit is contained in:
+78
-127
@@ -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<int>(centre_x);
|
||||
center_.y = static_cast<int>(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_);
|
||||
}
|
||||
|
||||
@@ -5,13 +5,11 @@
|
||||
#pragma once
|
||||
#include <SDL3/SDL.h>
|
||||
|
||||
#include <cmath>
|
||||
#include <cstdint>
|
||||
|
||||
#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);
|
||||
};
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user