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) <noreply@anthropic.com>
This commit is contained in:
2026-05-20 07:47:42 +02:00
parent e98b87243b
commit 896a899b0f
4 changed files with 221 additions and 174 deletions
+16 -171
View File
@@ -21,6 +21,7 @@
#include "core/system/scene_context.hpp" #include "core/system/scene_context.hpp"
#include "core/system/global_events.hpp" #include "core/system/global_events.hpp"
#include "game/stage_system/stage_loader.hpp" #include "game/stage_system/stage_loader.hpp"
#include "game/systems/collision_system.hpp"
// Using declarations per simplificar el codi // Using declarations per simplificar el codi
using SceneManager::SceneContext; using SceneManager::SceneContext;
@@ -504,9 +505,21 @@ void GameScene::update(float delta_time) {
bullet.update(delta_time); bullet.update(delta_time);
} }
detectar_col·lisions_bales_enemics(); {
detectar_col·lisio_naus_enemics(); Systems::Collision::Context col_ctx{
detectar_col·lisions_bales_jugadors(); .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); debris_manager_.update(delta_time);
floating_score_manager_.update(delta_time); floating_score_manager_.update(delta_time);
break; break;
@@ -972,174 +985,6 @@ std::string GameScene::buildScoreboard() const {
return score_p1 + " " + vides_p1 + " LEVEL " + stage_str + " " + score_p2 + " " + vides_p2; 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 // [NEW] Stage system helper methods
void GameScene::dibuixar_missatge_stage(const std::string& message) { void GameScene::dibuixar_missatge_stage(const std::string& message) {
-3
View File
@@ -82,9 +82,6 @@ class GameScene {
// Funciones privades // Funciones privades
void tocado(uint8_t player_id); 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_marges() const; // Dibuixar vores de la zona de juego
void dibuixar_marcador(); // Dibuixar marcador de puntuación void dibuixar_marcador(); // Dibuixar marcador de puntuación
void disparar_bala(uint8_t player_id); // Shoot bullet from player void disparar_bala(uint8_t player_id); // Shoot bullet from player
+142
View File
@@ -0,0 +1,142 @@
// collision_system.cpp - Implementación del sistema de colisiones
#include "game/systems/collision_system.hpp"
#include <cstdint>
#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
+63
View File
@@ -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 <array>
#include <cstdint>
#include <functional>
#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<Ship, 2>& ships;
std::array<Enemy, Defaults::Entities::MAX_ORNIS>& enemies;
std::array<Bullet, Defaults::Entities::MAX_BALES * 2>& bullets;
std::array<float, 2>& hit_timer_per_player;
std::array<int, 2>& score_per_player;
std::array<int, 2>& 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<void(uint8_t /*player_id*/)> 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