From 896a899b0fecb690217475067e12fe5349929c4f Mon Sep 17 00:00:00 2001 From: Sergio Valor Date: Wed, 20 May 2026 07:47:42 +0200 Subject: [PATCH] Fase 9a: extraer CollisionSystem de GameScene a modulo aparte GameScene::detectar_col*_* (3 funciones de deteccion de gameplay, ~170 LOC) salen a Systems::Collision en source/game/systems/collision_system.{hpp,cpp}. API: - struct Systems::Collision::Context: agrupa todo lo que las detecciones leen/modifican (ships, enemies, bullets, hit_timer, score, lives, debris, floating_score, match_config) y un callback on_player_hit para delegar la muerte del jugador. - Funciones libres: detectBulletEnemy, detectShipEnemy, detectBulletPlayer y detectAll. GameScene::update construye el Context y llama detectAll. La funcion GameScene::tocado se inyecta via lambda. El cuerpo de update queda mas legible y separa fisica de gameplay (lo decide el solver) de fisica rigida (lo decide PhysicsWorld). GameScene.cpp: 1429 -> 1274 LOC. Smoke test xvfb OK. Co-Authored-By: Claude Opus 4.7 (1M context) --- source/game/scenes/game_scene.cpp | 187 ++--------------------- source/game/scenes/game_scene.hpp | 3 - source/game/systems/collision_system.cpp | 142 +++++++++++++++++ source/game/systems/collision_system.hpp | 63 ++++++++ 4 files changed, 221 insertions(+), 174 deletions(-) create mode 100644 source/game/systems/collision_system.cpp create mode 100644 source/game/systems/collision_system.hpp diff --git a/source/game/scenes/game_scene.cpp b/source/game/scenes/game_scene.cpp index 7546cd7..b032462 100644 --- a/source/game/scenes/game_scene.cpp +++ b/source/game/scenes/game_scene.cpp @@ -21,6 +21,7 @@ #include "core/system/scene_context.hpp" #include "core/system/global_events.hpp" #include "game/stage_system/stage_loader.hpp" +#include "game/systems/collision_system.hpp" // Using declarations per simplificar el codi using SceneManager::SceneContext; @@ -504,9 +505,21 @@ void GameScene::update(float delta_time) { bullet.update(delta_time); } - detectar_col·lisions_bales_enemics(); - detectar_col·lisio_naus_enemics(); - detectar_col·lisions_bales_jugadors(); + { + Systems::Collision::Context col_ctx{ + .ships = ships_, + .enemies = enemies_, + .bullets = bullets_, + .hit_timer_per_player = hit_timer_per_player_, + .score_per_player = score_per_player_, + .lives_per_player = lives_per_player_, + .debris_manager = debris_manager_, + .floating_score_manager = floating_score_manager_, + .match_config = match_config_, + .on_player_hit = [this](uint8_t pid) { tocado(pid); }, + }; + Systems::Collision::detectAll(col_ctx); + } debris_manager_.update(delta_time); floating_score_manager_.update(delta_time); break; @@ -972,174 +985,6 @@ std::string GameScene::buildScoreboard() const { return score_p1 + " " + vides_p1 + " LEVEL " + stage_str + " " + score_p2 + " " + vides_p2; } -void GameScene::detectar_col·lisions_bales_enemics() { - // Amplificador per hitbox més generós (115%) - constexpr float AMPLIFIER = Defaults::Game::COLLISION_BULLET_ENEMY_AMPLIFIER; - - // Velocidad de explosión reduïda per efecte suau - constexpr float VELOCITAT_EXPLOSIO = 80.0F; // px/s (en lloc de 80.0f per defecte) - - // Iterar per todas las balas i enemigos - for (auto& bullet : bullets_) { - for (auto& enemy : enemies_) { - // Comprovar colisión utilitzant la interfície genèrica - if (Physics::check_collision(bullet, enemy, AMPLIFIER)) { - // *** COL·LISIÓ DETECTADA *** - - const Vec2& pos_enemic = enemy.getCenter(); - - // 1. Calculate score for enemy type - int points = 0; - switch (enemy.getType()) { - case EnemyType::PENTAGON: - points = Defaults::Enemies::Scoring::PENTAGON_SCORE; - break; - case EnemyType::QUADRAT: - points = Defaults::Enemies::Scoring::QUADRAT_SCORE; - break; - case EnemyType::MOLINILLO: - points = Defaults::Enemies::Scoring::MOLINILLO_SCORE; - break; - } - - // 2. Add score to the player who shot it - uint8_t owner_id = bullet.getOwnerId(); - score_per_player_[owner_id] += points; - - // 3. Create floating score number - floating_score_manager_.crear(points, pos_enemic); - - // 4. Destruir enemy (marca como inactiu) - enemy.destruir(); - - // 2. Crear explosión de fragments - Vec2 vel_enemic = enemy.getVelocityVector(); - debris_manager_.explode( - enemy.getShape(), // Forma vectorial del pentágono - pos_enemic, // Posición central - 0.0F, // Angle (enemy té rotación interna) - 1.0F, // Escala normal - VELOCITAT_EXPLOSIO, // 50 px/s (explosión suau) - enemy.getBrightness(), // Heredar brightness - vel_enemic, // Heredar velocity - enemy.getRotationDelta(), // Heredar velocity angular (trayectorias curvas) - 0.0F // Sin herencia visual (rotación aleatoria) - ); - - // 3. Desactivar bullet - bullet.desactivar(); - - // 4. Eixir del bucle intern (bullet solo destrueix 1 enemy) - break; - } - } - } -} - -void GameScene::detectar_col·lisio_naus_enemics() { - // Amplificador per hitbox generós (80%) - constexpr float AMPLIFIER = Defaults::Game::COLLISION_SHIP_ENEMY_AMPLIFIER; - - // Check collision for BOTH players - for (uint8_t i = 0; i < 2; i++) { - // Skip collisions if player is dead or invulnerable - if (hit_timer_per_player_[i] > 0.0F) { - continue; - } - if (!ships_[i].isAlive()) { - continue; - } - if (ships_[i].isInvulnerable()) { - continue; - } - - // Check collision with all active enemies - for (const auto& enemy : enemies_) { - // Skip collision if enemy is invulnerable - if (enemy.isInvulnerable()) { - continue; - } - - // Comprovar colisión utilitzant la interfície genèrica - if (Physics::check_collision(ships_[i], enemy, AMPLIFIER)) { - tocado(i); // Trigger death sequence for player i - break; // Only one collision per player per frame - } - } - } -} - -void GameScene::detectar_col·lisions_bales_jugadors() { - // Skip if friendly fire disabled - if (!Defaults::Game::FRIENDLY_FIRE_ENABLED) { - return; - } - - // Amplificador per hitbox exacte (100%) - constexpr float AMPLIFIER = Defaults::Game::COLLISION_BULLET_PLAYER_AMPLIFIER; - - // Check all active bullets - for (auto& bullet : bullets_) { - if (!bullet.esta_activa()) { - continue; - } - - // Skip bullets in grace period (prevents instant self-collision) - if (bullet.getGraceTimer() > 0.0F) { - continue; - } - - uint8_t bullet_owner = bullet.getOwnerId(); - - // Check collision with BOTH players - for (uint8_t player_id = 0; player_id < 2; player_id++) { - // Skip if player is dead, invulnerable, or inactive - if (hit_timer_per_player_[player_id] > 0.0F) { - continue; - } - if (!ships_[player_id].isAlive()) { - continue; - } - if (ships_[player_id].isInvulnerable()) { - continue; - } - - // Skip inactive players - bool jugador_actiu = (player_id == 0) ? match_config_.jugador1_actiu - : match_config_.jugador2_actiu; - if (!jugador_actiu) { - continue; - } - - // Comprovar colisión utilitzant la interfície genèrica - if (Physics::check_collision(bullet, ships_[player_id], AMPLIFIER)) { - // *** FRIENDLY FIRE HIT *** - - if (bullet_owner == player_id) { - // CASE 1: Self-hit (own bullet) - // Player loses 1 life, no gain - tocado(player_id); - } else { - // CASE 2: Teammate hit - // Victim loses 1 life - tocado(player_id); - - // Attacker gains 1 life (no sin) - lives_per_player_[bullet_owner]++; - } - - // Play distinct sound - Audio::get()->playSound(Defaults::Sound::FRIENDLY_FIRE_HIT, Audio::Group::GAME); - - // Deactivate bullet - bullet.desactivar(); - - break; // Bullet only hits once per frame - } - } - } -} - // [NEW] Stage system helper methods void GameScene::dibuixar_missatge_stage(const std::string& message) { diff --git a/source/game/scenes/game_scene.hpp b/source/game/scenes/game_scene.hpp index 6ce3164..b089d25 100644 --- a/source/game/scenes/game_scene.hpp +++ b/source/game/scenes/game_scene.hpp @@ -82,9 +82,6 @@ class GameScene { // Funciones privades void tocado(uint8_t player_id); - void detectar_col·lisions_bales_enemics(); // Colisiones bullet-enemy - void detectar_col·lisio_naus_enemics(); // Ship-enemy collision detection (plural) - void detectar_col·lisions_bales_jugadors(); // Bullet-player collision detection (friendly fire) void dibuixar_marges() const; // Dibuixar vores de la zona de juego void dibuixar_marcador(); // Dibuixar marcador de puntuación void disparar_bala(uint8_t player_id); // Shoot bullet from player diff --git a/source/game/systems/collision_system.cpp b/source/game/systems/collision_system.cpp new file mode 100644 index 0000000..4f82427 --- /dev/null +++ b/source/game/systems/collision_system.cpp @@ -0,0 +1,142 @@ +// collision_system.cpp - Implementación del sistema de colisiones + +#include "game/systems/collision_system.hpp" + +#include + +#include "core/audio/audio.hpp" +#include "core/physics/collision.hpp" +#include "core/types.hpp" + +namespace Systems::Collision { + +void detectBulletEnemy(Context& ctx) { + constexpr float AMPLIFIER = Defaults::Game::COLLISION_BULLET_ENEMY_AMPLIFIER; + constexpr float VELOCITAT_EXPLOSIO = 80.0F; // px/s (explosión suau) + + for (auto& bullet : ctx.bullets) { + for (auto& enemy : ctx.enemies) { + if (!Physics::check_collision(bullet, enemy, AMPLIFIER)) { + continue; + } + + // *** COLISIÓN bullet → enemy *** + const Vec2& POS_ENEMIC = enemy.getCenter(); + + // 1. Puntos según tipo + int points = 0; + switch (enemy.getType()) { + case EnemyType::PENTAGON: + points = Defaults::Enemies::Scoring::PENTAGON_SCORE; + break; + case EnemyType::QUADRAT: + points = Defaults::Enemies::Scoring::QUADRAT_SCORE; + break; + case EnemyType::MOLINILLO: + points = Defaults::Enemies::Scoring::MOLINILLO_SCORE; + break; + } + + uint8_t owner_id = bullet.getOwnerId(); + ctx.score_per_player[owner_id] += points; + ctx.floating_score_manager.crear(points, POS_ENEMIC); + + // 2. Destruir enemy + crear explosión + enemy.destruir(); + Vec2 vel_enemic = enemy.getVelocityVector(); + ctx.debris_manager.explode( + enemy.getShape(), + POS_ENEMIC, + 0.0F, // angle (la rotación es interna del enemy) + 1.0F, // escala + VELOCITAT_EXPLOSIO, + enemy.getBrightness(), + vel_enemic, + enemy.getRotationDelta(), + 0.0F // sin herencia visual + ); + + // 3. Desactivar bullet (solo destruye 1 enemy) + bullet.desactivar(); + break; + } + } +} + +void detectShipEnemy(Context& ctx) { + constexpr float AMPLIFIER = Defaults::Game::COLLISION_SHIP_ENEMY_AMPLIFIER; + + for (uint8_t i = 0; i < 2; i++) { + // Skip si ya tocado / muerto / invulnerable + if (ctx.hit_timer_per_player[i] > 0.0F || + !ctx.ships[i].isAlive() || + ctx.ships[i].isInvulnerable()) { + continue; + } + + for (const auto& enemy : ctx.enemies) { + if (enemy.isInvulnerable()) { + continue; + } + if (Physics::check_collision(ctx.ships[i], enemy, AMPLIFIER)) { + ctx.on_player_hit(i); + break; // Solo una colisión por player por frame + } + } + } +} + +void detectBulletPlayer(Context& ctx) { + if (!Defaults::Game::FRIENDLY_FIRE_ENABLED) { + return; + } + + constexpr float AMPLIFIER = Defaults::Game::COLLISION_BULLET_PLAYER_AMPLIFIER; + + for (auto& bullet : ctx.bullets) { + if (!bullet.esta_activa() || bullet.getGraceTimer() > 0.0F) { + continue; + } + const uint8_t BULLET_OWNER = bullet.getOwnerId(); + + for (uint8_t player_id = 0; player_id < 2; player_id++) { + if (ctx.hit_timer_per_player[player_id] > 0.0F || + !ctx.ships[player_id].isAlive() || + ctx.ships[player_id].isInvulnerable()) { + continue; + } + const bool JUGADOR_ACTIU = (player_id == 0) + ? ctx.match_config.jugador1_actiu + : ctx.match_config.jugador2_actiu; + if (!JUGADOR_ACTIU) { + continue; + } + + if (!Physics::check_collision(bullet, ctx.ships[player_id], AMPLIFIER)) { + continue; + } + + // *** FRIENDLY FIRE HIT *** + if (BULLET_OWNER == player_id) { + // Self-hit: víctima pierde 1 vida. + ctx.on_player_hit(player_id); + } else { + // Teammate hit: víctima pierde 1, atacante gana 1. + ctx.on_player_hit(player_id); + ctx.lives_per_player[BULLET_OWNER]++; + } + + Audio::get()->playSound(Defaults::Sound::FRIENDLY_FIRE_HIT, Audio::Group::GAME); + bullet.desactivar(); + break; // Una bullet solo impacta una vez por frame + } + } +} + +void detectAll(Context& ctx) { + detectBulletEnemy(ctx); + detectShipEnemy(ctx); + detectBulletPlayer(ctx); +} + +} // namespace Systems::Collision diff --git a/source/game/systems/collision_system.hpp b/source/game/systems/collision_system.hpp new file mode 100644 index 0000000..244dd42 --- /dev/null +++ b/source/game/systems/collision_system.hpp @@ -0,0 +1,63 @@ +// collision_system.hpp - Detección de colisiones de gameplay +// © 2025 Orni Attack +// +// Detecta colisiones bullet↔enemy, ship↔enemy y bullet↔player y aplica los +// efectos directos sobre las entidades (destruir enemy, desactivar bullet, +// crear debris/floating-score, ajustar score y lives). Las consecuencias de +// gameplay que requieren transición de estado (muerte del jugador, game over) +// se delegan a un callback `on_player_hit`. +// +// Esto es física de gameplay, NO física rígida — el PhysicsWorld ya resolvió +// los impulsos físicos antes. Aquí solo decidimos quién muere/destruye a quién. + +#pragma once + +#include +#include +#include + +#include "core/defaults.hpp" +#include "core/system/game_config.hpp" +#include "game/effects/debris_manager.hpp" +#include "game/effects/floating_score_manager.hpp" +#include "game/entities/bullet.hpp" +#include "game/entities/enemy.hpp" +#include "game/entities/ship.hpp" + +namespace Systems::Collision { + +// Todo lo que las detecciones necesitan leer/modificar. Vive en GameScene; +// se le pasa por referencia (no copia, no ownership). +struct Context { + std::array& ships; + std::array& enemies; + std::array& bullets; + std::array& hit_timer_per_player; + std::array& score_per_player; + std::array& lives_per_player; + Effects::DebrisManager& debris_manager; + Effects::FloatingScoreManager& floating_score_manager; + const GameConfig::MatchConfig& match_config; + // Trigger de muerte del jugador (GameScene::tocado). + std::function on_player_hit; +}; + +// Detecta colisiones bullet → enemy. Si hit: +// - destruye el enemy (radius=0 en physics body) +// - crea debris + floating score +// - desactiva la bullet +// - suma puntos al shooter +void detectBulletEnemy(Context& ctx); + +// Detecta colisiones ship → enemy. Si hit, llama on_player_hit(player_id). +void detectShipEnemy(Context& ctx); + +// Detecta colisiones bullet → player (friendly fire / self-hit). +// Self-hit: el shooter pierde 1 vida. Teammate-hit: la víctima pierde 1, el +// atacante gana 1. En ambos casos, llama on_player_hit y desactiva bullet. +void detectBulletPlayer(Context& ctx); + +// Las tres en orden lógico del frame. +void detectAll(Context& ctx); + +} // namespace Systems::Collision