Fase 9d: descomponer GameScene::update en sub-pasos privados

update() pasa de 339 LOC monolitico (cognitive complexity ~137) a
18 LOC orquestadores. Cada seccion logica vive en su propio metodo
privado con responsabilidad unica:

- stepPhysics(dt): physics_world.update + postUpdate.
- stepShootingInput(): SHOOT de P1/P2.
- stepMidGameJoin(): START de jugador inactivo o muerto sin vidas.
- stepContinueScreen(dt): wrapping del Systems::ContinueScreen +
  update de fondo. Devuelve true si frame debe terminar.
- stepGameOver(dt): timer final + transicion a TITLE. Devuelve true
  si frame debe terminar.
- stepDeathSequence(dt): death timer/respawn/transicion a CONTINUE.
- stepStageStateMachine(dt): despacha a runStage{InitHud,LevelStart,
  Playing,LevelCompleted} segun el estado actual.
- runCollisionDetections(): construye el Systems::Collision::Context
  y llama detectAll.

GameScene.cpp acumulado tras Fase 9 (a+b+c+d): 1429 -> 1015 LOC.
update() solo: 339 -> 18 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:57:36 +02:00
parent a4942fcbae
commit 808abb28ea
2 changed files with 287 additions and 295 deletions
+156 -184
View File
@@ -226,10 +226,28 @@ void GameScene::init() {
} }
void GameScene::update(float delta_time) { void GameScene::update(float delta_time) {
// === FÍSICA: integrar bodies del frame anterior y resolver colisiones === // Orquestador delgado: cada paso vive en su propia función para
// Se ejecuta al inicio del frame: las fuerzas aplicadas en el frame N-1 // mantener update() legible y reducir complejidad cognitiva.
// por processInput/AI se integran ahora, y postUpdate sincroniza los stepPhysics(delta_time);
// mirrors (center_/angle_) antes de la lógica de juego que los lee.
if (game_over_state_ == GameOverState::NONE) {
stepShootingInput();
stepMidGameJoin();
}
if (stepContinueScreen(delta_time)) {
return;
}
if (stepGameOver(delta_time)) {
return;
}
stepDeathSequence(delta_time);
stepStageStateMachine(delta_time);
}
void GameScene::stepPhysics(float delta_time) {
// Las fuerzas aplicadas en el frame N-1 por processInput/AI se integran
// ahora; postUpdate sincroniza los mirrors (center_/angle_) antes de la
// lógica de juego que los lee.
physics_world_.update(delta_time); physics_world_.update(delta_time);
for (auto& ship : ships_) { for (auto& ship : ships_) {
ship.postUpdate(delta_time); ship.postUpdate(delta_time);
@@ -240,64 +258,57 @@ void GameScene::update(float delta_time) {
for (auto& bullet : bullets_) { for (auto& bullet : bullets_) {
bullet.postUpdate(delta_time); bullet.postUpdate(delta_time);
} }
}
// Processar disparos (state-based, no event-based) void GameScene::stepShootingInput() {
if (game_over_state_ == GameOverState::NONE) {
auto* input = Input::get(); auto* input = Input::get();
if (match_config_.jugador1_actiu &&
// Jugador 1 dispara (solo si está active) input->checkActionPlayer1(InputAction::SHOOT, Input::DO_NOT_ALLOW_REPEAT)) {
if (match_config_.jugador1_actiu) {
if (input->checkActionPlayer1(InputAction::SHOOT, Input::DO_NOT_ALLOW_REPEAT)) {
disparar_bala(0); disparar_bala(0);
} }
} if (match_config_.jugador2_actiu &&
input->checkActionPlayer2(InputAction::SHOOT, Input::DO_NOT_ALLOW_REPEAT)) {
// Jugador 2 dispara (solo si está active)
if (match_config_.jugador2_actiu) {
if (input->checkActionPlayer2(InputAction::SHOOT, Input::DO_NOT_ALLOW_REPEAT)) {
disparar_bala(1); disparar_bala(1);
} }
} }
// [FIXED] Allow mid-game join: inactive or dead player presses START void GameScene::stepMidGameJoin() {
// Only during PLAYING state (not INIT_HUD, CONTINUE, GAME_OVER) // Permitir join solo durante PLAYING.
if (stage_manager_->get_estat() == StageSystem::EstatStage::PLAYING) { if (stage_manager_->get_estat() != StageSystem::EstatStage::PLAYING) {
// Check if at least one player is alive and playing (game in progress) return;
bool algun_jugador_viu = false;
if (match_config_.jugador1_actiu && hit_timer_per_player_[0] != 999.0F) {
algun_jugador_viu = true;
}
if (match_config_.jugador2_actiu && hit_timer_per_player_[1] != 999.0F) {
algun_jugador_viu = true;
} }
// Only allow join if there's an active game // Solo se permite join si hay al menos un jugador vivo (no se puede
if (algun_jugador_viu) { // hacer join en pantalla vacía).
// P2 can join if not currently playing (never joined OR dead without lives) const bool ALGU_VIU =
bool p2_no_juga = !match_config_.jugador2_actiu || // Never joined (match_config_.jugador1_actiu && hit_timer_per_player_[0] != 999.0F) ||
hit_timer_per_player_[1] == 999.0F; // Dead without lives (match_config_.jugador2_actiu && hit_timer_per_player_[1] != 999.0F);
if (!ALGU_VIU) {
if (p2_no_juga) { return;
if (input->checkActionPlayer2(InputAction::START, Input::DO_NOT_ALLOW_REPEAT)) {
unir_jugador(1);
}
} }
// P1 can join if not currently playing (never joined OR dead without lives) auto* input = Input::get();
bool p1_no_juga = !match_config_.jugador1_actiu || // Never joined for (uint8_t pid = 0; pid < 2; pid++) {
hit_timer_per_player_[0] == 999.0F; // Dead without lives const bool ACTIU = (pid == 0) ? match_config_.jugador1_actiu
: match_config_.jugador2_actiu;
if (p1_no_juga) { const bool MUERTO_SIN_VIDAS = hit_timer_per_player_[pid] == 999.0F;
if (input->checkActionPlayer1(InputAction::START, Input::DO_NOT_ALLOW_REPEAT)) { if (ACTIU && !MUERTO_SIN_VIDAS) {
unir_jugador(0); continue; // jugador ya está jugando
}
} }
const bool START_PRESSED = (pid == 0)
? input->checkActionPlayer1(InputAction::START, Input::DO_NOT_ALLOW_REPEAT)
: input->checkActionPlayer2(InputAction::START, Input::DO_NOT_ALLOW_REPEAT);
if (START_PRESSED) {
unir_jugador(pid);
} }
} }
} }
// Handle CONTINUE screen auto GameScene::stepContinueScreen(float delta_time) -> bool {
if (game_over_state_ == GameOverState::CONTINUE) { if (game_over_state_ != GameOverState::CONTINUE) {
return false;
}
Systems::ContinueScreen::Context cont_ctx{ Systems::ContinueScreen::Context cont_ctx{
.state = game_over_state_, .state = game_over_state_,
.counter = continue_counter_, .counter = continue_counter_,
@@ -314,7 +325,7 @@ void GameScene::update(float delta_time) {
Systems::ContinueScreen::update(cont_ctx, delta_time); Systems::ContinueScreen::update(cont_ctx, delta_time);
Systems::ContinueScreen::processInput(cont_ctx); Systems::ContinueScreen::processInput(cont_ctx);
// Still update enemies, bullets, and effects during continue screen // Enemies, bullets y efectos siguen moviéndose en background.
for (auto& enemy : enemies_) { for (auto& enemy : enemies_) {
enemy.update(delta_time); enemy.update(delta_time);
} }
@@ -323,204 +334,194 @@ void GameScene::update(float delta_time) {
} }
debris_manager_.update(delta_time); debris_manager_.update(delta_time);
floating_score_manager_.update(delta_time); floating_score_manager_.update(delta_time);
return; return true;
} }
// Handle final GAME OVER state auto GameScene::stepGameOver(float delta_time) -> bool {
if (game_over_state_ == GameOverState::GAME_OVER) { if (game_over_state_ != GameOverState::GAME_OVER) {
// Game over: only update timer, enemies, bullets, and debris return false;
game_over_timer_ -= delta_time; }
game_over_timer_ -= delta_time;
if (game_over_timer_ <= 0.0F) { if (game_over_timer_ <= 0.0F) {
// Aturar música de juego antes de tornar al título
Audio::get()->stopMusic(); Audio::get()->stopMusic();
// Transición a pantalla de título
context_.setNextScene(SceneType::TITLE); context_.setNextScene(SceneType::TITLE);
SceneManager::actual = SceneType::TITLE; SceneManager::actual = SceneType::TITLE;
return; return true;
} }
// Enemies and bullets continue moving during game over // Enemies, bullets y efectos siguen moviéndose como fondo.
for (auto& enemy : enemies_) { for (auto& enemy : enemies_) {
enemy.update(delta_time); enemy.update(delta_time);
} }
for (auto& bullet : bullets_) { for (auto& bullet : bullets_) {
bullet.update(delta_time); bullet.update(delta_time);
} }
debris_manager_.update(delta_time); debris_manager_.update(delta_time);
floating_score_manager_.update(delta_time); floating_score_manager_.update(delta_time);
return; return true;
} }
// Check death sequence state for BOTH players auto GameScene::stepDeathSequence(float delta_time) -> bool {
bool algun_jugador_mort = false; bool algun_mort = false;
for (uint8_t i = 0; i < 2; i++) { for (uint8_t i = 0; i < 2; i++) {
if (hit_timer_per_player_[i] > 0.0F && hit_timer_per_player_[i] < 999.0F) { if (hit_timer_per_player_[i] <= 0.0F || hit_timer_per_player_[i] >= 999.0F) {
algun_jugador_mort = true; continue;
// Death sequence active: update timer }
algun_mort = true;
hit_timer_per_player_[i] += delta_time; hit_timer_per_player_[i] += delta_time;
// Check if death duration completed (only trigger ONCE using sentinel value) if (hit_timer_per_player_[i] < Defaults::Game::DEATH_DURATION) {
if (hit_timer_per_player_[i] >= Defaults::Game::DEATH_DURATION) { continue;
}
// *** PHASE 3: RESPAWN OR GAME OVER *** // *** PHASE 3: RESPAWN OR GAME OVER ***
// Decrement lives for this player (only once)
lives_per_player_[i]--; lives_per_player_[i]--;
if (lives_per_player_[i] > 0) { if (lives_per_player_[i] > 0) {
// Respawn ship en spawn position con invulnerabilidad
Vec2 spawn_pos = obtenir_punt_spawn(i); Vec2 spawn_pos = obtenir_punt_spawn(i);
ships_[i].init(&spawn_pos, true); ships_[i].init(&spawn_pos, /*activar_invulnerabilitat=*/true);
hit_timer_per_player_[i] = 0.0F; hit_timer_per_player_[i] = 0.0F;
} else { continue;
// Player is permanently dead (out of lives) }
// Set sentinel value to prevent re-entering this block
// Sin vidas: marcar definitivamente muerto y comprobar transición a CONTINUE.
hit_timer_per_player_[i] = 999.0F; hit_timer_per_player_[i] = 999.0F;
const bool P1_DEAD = !match_config_.jugador1_actiu || lives_per_player_[0] <= 0;
// Check if ALL ACTIVE players are dead (trigger continue screen) const bool P2_DEAD = !match_config_.jugador2_actiu || lives_per_player_[1] <= 0;
bool p1_dead = !match_config_.jugador1_actiu || lives_per_player_[0] <= 0; if (P1_DEAD && P2_DEAD) {
bool p2_dead = !match_config_.jugador2_actiu || lives_per_player_[1] <= 0;
if (p1_dead && p2_dead) {
game_over_state_ = GameOverState::CONTINUE; game_over_state_ = GameOverState::CONTINUE;
continue_counter_ = Defaults::Game::CONTINUE_COUNT_START; continue_counter_ = Defaults::Game::CONTINUE_COUNT_START;
continue_tick_timer_ = Defaults::Game::CONTINUE_TICK_DURATION; continue_tick_timer_ = Defaults::Game::CONTINUE_TICK_DURATION;
} }
} }
}
}
}
// If any player is dead, still update enemies/bullets/effects // Si hay algún muerto, los enemigos/balas/efectos siguen actualizándose
if (algun_jugador_mort) { // aunque otros jugadores aún jueguen.
// Enemies and bullets continue moving during death sequence if (algun_mort) {
for (auto& enemy : enemies_) { for (auto& enemy : enemies_) {
enemy.update(delta_time); enemy.update(delta_time);
} }
for (auto& bullet : bullets_) { for (auto& bullet : bullets_) {
bullet.update(delta_time); bullet.update(delta_time);
} }
debris_manager_.update(delta_time); debris_manager_.update(delta_time);
floating_score_manager_.update(delta_time); floating_score_manager_.update(delta_time);
}
// Don't return - allow alive players to continue playing return algun_mort;
} }
// *** STAGE SYSTEM STATE MACHINE *** void GameScene::stepStageStateMachine(float delta_time) {
const StageSystem::EstatStage STATE = stage_manager_->get_estat();
StageSystem::EstatStage state = stage_manager_->get_estat(); switch (STATE) {
case StageSystem::EstatStage::INIT_HUD:
switch (state) { runStageInitHud(delta_time);
case StageSystem::EstatStage::INIT_HUD: { break;
// Update stage manager timer (pot canviar l'state!) case StageSystem::EstatStage::LEVEL_START:
stage_manager_->update(delta_time); runStageLevelStart(delta_time);
break;
// [FIX] Si l'state ha canviat durante update(), salir immediatament case StageSystem::EstatStage::PLAYING:
// per evitar recalcular la posición de la ship con el nuevo timer runStagePlaying(delta_time);
if (stage_manager_->get_estat() != StageSystem::EstatStage::INIT_HUD) { break;
case StageSystem::EstatStage::LEVEL_COMPLETED:
runStageLevelCompleted(delta_time);
break; break;
} }
}
void GameScene::runStageInitHud(float delta_time) {
// Update stage manager timer (puede cambiar el state).
stage_manager_->update(delta_time);
// Si el state cambió, salir para no usar el timer del nuevo state.
if (stage_manager_->get_estat() != StageSystem::EstatStage::INIT_HUD) {
return;
}
// Calcular global progress (0.0 al inicio → 1.0 al final)
float global_progress = 1.0F - (stage_manager_->get_timer_transicio() / Defaults::Game::INIT_HUD_DURATION); float global_progress = 1.0F - (stage_manager_->get_timer_transicio() / Defaults::Game::INIT_HUD_DURATION);
global_progress = std::min(1.0F, global_progress); global_progress = std::min(1.0F, global_progress);
// [NEW] Calcular progress independiente para cada nave const float SHIP1_P = Systems::InitHud::computeRangeProgress(
float ship1_progress = Systems::InitHud::computeRangeProgress(
global_progress, global_progress,
Defaults::Game::INIT_HUD_SHIP1_RATIO_INIT, Defaults::Game::INIT_HUD_SHIP1_RATIO_INIT,
Defaults::Game::INIT_HUD_SHIP1_RATIO_END); Defaults::Game::INIT_HUD_SHIP1_RATIO_END);
const float SHIP2_P = Systems::InitHud::computeRangeProgress(
float ship2_progress = Systems::InitHud::computeRangeProgress(
global_progress, global_progress,
Defaults::Game::INIT_HUD_SHIP2_RATIO_INIT, Defaults::Game::INIT_HUD_SHIP2_RATIO_INIT,
Defaults::Game::INIT_HUD_SHIP2_RATIO_END); Defaults::Game::INIT_HUD_SHIP2_RATIO_END);
// [MODIFICAT] Animar AMBAS naves con sus progress respectivos if (match_config_.jugador1_actiu && SHIP1_P < 1.0F) {
if (match_config_.jugador1_actiu && ship1_progress < 1.0F) { ships_[0].setCenter(Systems::InitHud::computeShipPosition(SHIP1_P, obtenir_punt_spawn(0)));
Vec2 pos_p1 = Systems::InitHud::computeShipPosition(ship1_progress, obtenir_punt_spawn(0)); }
ships_[0].setCenter(pos_p1); if (match_config_.jugador2_actiu && SHIP2_P < 1.0F) {
ships_[1].setCenter(Systems::InitHud::computeShipPosition(SHIP2_P, obtenir_punt_spawn(1)));
}
} }
if (match_config_.jugador2_actiu && ship2_progress < 1.0F) { void GameScene::runStageLevelStart(float delta_time) {
Vec2 pos_p2 = Systems::InitHud::computeShipPosition(ship2_progress, obtenir_punt_spawn(1));
ships_[1].setCenter(pos_p2);
}
// Una vez l'animación acaba, permetre control normal
// pero mantenir la posición inicial especial hasta LEVEL_START
break;
}
case StageSystem::EstatStage::LEVEL_START: {
// [DEBUG] Log entrada a LEVEL_START
static bool first_entry = true;
if (first_entry) {
std::cout << "[LEVEL_START] ENTERED with P1 pos.y=" << ships_[0].getCenter().y << '\n';
first_entry = false;
}
// Update countdown timer
stage_manager_->update(delta_time); stage_manager_->update(delta_time);
// [NEW] Allow both ships movement and shooting during intro // Ambas naves pueden moverse y disparar durante el intro.
for (uint8_t i = 0; i < 2; i++) { for (uint8_t i = 0; i < 2; i++) {
bool jugador_actiu = (i == 0) ? match_config_.jugador1_actiu : match_config_.jugador2_actiu; const bool ACTIU = (i == 0) ? match_config_.jugador1_actiu : match_config_.jugador2_actiu;
if (jugador_actiu && hit_timer_per_player_[i] == 0.0F) { // Only active, alive players if (ACTIU && hit_timer_per_player_[i] == 0.0F) {
ships_[i].processInput(delta_time, i); ships_[i].processInput(delta_time, i);
ships_[i].update(delta_time); ships_[i].update(delta_time);
} }
} }
// [NEW] Update bullets
for (auto& bullet : bullets_) { for (auto& bullet : bullets_) {
bullet.update(delta_time); bullet.update(delta_time);
} }
// [NEW] Update debris
debris_manager_.update(delta_time); debris_manager_.update(delta_time);
break;
} }
case StageSystem::EstatStage::PLAYING: { void GameScene::runStagePlaying(float delta_time) {
// [NEW] Update stage manager (spawns enemies, pause if BOTH dead) const bool PAUSE_SPAWN = (hit_timer_per_player_[0] > 0.0F && hit_timer_per_player_[1] > 0.0F);
bool pause_spawn = (hit_timer_per_player_[0] > 0.0F && hit_timer_per_player_[1] > 0.0F); stage_manager_->getSpawnController().update(delta_time, enemies_, PAUSE_SPAWN);
stage_manager_->getSpawnController().update(delta_time, enemies_, pause_spawn);
// [NEW] Check stage completion (only when at least one player alive) // Stage completado: cuando al menos un jugador está vivo y todos los enemies muertos.
bool algun_jugador_viu = (hit_timer_per_player_[0] == 0.0F || hit_timer_per_player_[1] == 0.0F); const bool ALGU_VIU = (hit_timer_per_player_[0] == 0.0F || hit_timer_per_player_[1] == 0.0F);
if (algun_jugador_viu) { if (ALGU_VIU && stage_manager_->getSpawnController().tots_enemics_destruits(enemies_)) {
auto& spawn_ctrl = stage_manager_->getSpawnController();
if (spawn_ctrl.tots_enemics_destruits(enemies_)) {
stage_manager_->stage_completat(); stage_manager_->stage_completat();
Audio::get()->playSound(Defaults::Sound::GOOD_JOB_COMMANDER, Audio::Group::GAME); Audio::get()->playSound(Defaults::Sound::GOOD_JOB_COMMANDER, Audio::Group::GAME);
break; return;
}
} }
// [EXISTING] Normal gameplay - update active players // Gameplay normal: ships activos + entidades + colisiones + efectos.
for (uint8_t i = 0; i < 2; i++) { for (uint8_t i = 0; i < 2; i++) {
bool jugador_actiu = (i == 0) ? match_config_.jugador1_actiu : match_config_.jugador2_actiu; const bool ACTIU = (i == 0) ? match_config_.jugador1_actiu : match_config_.jugador2_actiu;
if (jugador_actiu && hit_timer_per_player_[i] == 0.0F) { // Only active, alive players if (ACTIU && hit_timer_per_player_[i] == 0.0F) {
ships_[i].processInput(delta_time, i); ships_[i].processInput(delta_time, i);
ships_[i].update(delta_time); ships_[i].update(delta_time);
} }
} }
for (auto& enemy : enemies_) { for (auto& enemy : enemies_) {
enemy.update(delta_time); enemy.update(delta_time);
} }
for (auto& bullet : bullets_) { for (auto& bullet : bullets_) {
bullet.update(delta_time); bullet.update(delta_time);
} }
{ runCollisionDetections();
debris_manager_.update(delta_time);
floating_score_manager_.update(delta_time);
}
void GameScene::runStageLevelCompleted(float delta_time) {
stage_manager_->update(delta_time);
for (uint8_t i = 0; i < 2; i++) {
const bool ACTIU = (i == 0) ? match_config_.jugador1_actiu : match_config_.jugador2_actiu;
if (ACTIU && hit_timer_per_player_[i] == 0.0F) {
ships_[i].processInput(delta_time, i);
ships_[i].update(delta_time);
}
}
for (auto& bullet : bullets_) {
bullet.update(delta_time);
}
debris_manager_.update(delta_time);
floating_score_manager_.update(delta_time);
}
void GameScene::runCollisionDetections() {
Systems::Collision::Context col_ctx{ Systems::Collision::Context col_ctx{
.ships = ships_, .ships = ships_,
.enemies = enemies_, .enemies = enemies_,
@@ -535,35 +536,6 @@ void GameScene::update(float delta_time) {
}; };
Systems::Collision::detectAll(col_ctx); Systems::Collision::detectAll(col_ctx);
} }
debris_manager_.update(delta_time);
floating_score_manager_.update(delta_time);
break;
}
case StageSystem::EstatStage::LEVEL_COMPLETED:
// Update countdown timer
stage_manager_->update(delta_time);
// [NEW] Allow both ships movement and shooting during outro
for (uint8_t i = 0; i < 2; i++) {
bool jugador_actiu = (i == 0) ? match_config_.jugador1_actiu : match_config_.jugador2_actiu;
if (jugador_actiu && hit_timer_per_player_[i] == 0.0F) { // Only active, alive players
ships_[i].processInput(delta_time, i);
ships_[i].update(delta_time);
}
}
// [NEW] Update bullets (allow last shots to continue)
for (auto& bullet : bullets_) {
bullet.update(delta_time);
}
// [NEW] Update debris (from last destroyed enemies)
debris_manager_.update(delta_time);
floating_score_manager_.update(delta_time);
break;
}
}
void GameScene::draw() { void GameScene::draw() {
// Handle CONTINUE screen // Handle CONTINUE screen
+20
View File
@@ -96,4 +96,24 @@ class GameScene {
// [NEW] Función helper del marcador // [NEW] Función helper del marcador
[[nodiscard]] std::string buildScoreboard() const; [[nodiscard]] std::string buildScoreboard() const;
// Sub-pasos de update() (descompuestos en Fase 9d para reducir
// complejidad cognitiva; cada uno es responsable de una sección).
void stepPhysics(float delta_time);
void stepShootingInput();
void stepMidGameJoin();
// Devuelven true si el frame debe salir tras esta sección.
[[nodiscard]] auto stepContinueScreen(float delta_time) -> bool;
[[nodiscard]] auto stepGameOver(float delta_time) -> bool;
// Avanza el death timer / respawn / transition a CONTINUE. Devuelve
// true si algun jugador está en secuencia de muerte (para que el
// caller actualice efectos sin gameplay).
[[nodiscard]] auto stepDeathSequence(float delta_time) -> bool;
void stepStageStateMachine(float delta_time);
void runStageInitHud(float delta_time);
void runStageLevelStart(float delta_time);
void runStagePlaying(float delta_time);
void runStageLevelCompleted(float delta_time);
// Helper: ejecuta colisiones de gameplay con el Context preparado.
void runCollisionDetections();
}; };