feat(audio): cicle de música per estat (INTRO/PLAYING/OUTRO) amb callback de fi i spawn escalonat
This commit is contained in:
1 parent
91f664c4d5
commit
404db74889
13 files changed
+199
-128
No files matched your search
@@ -378,11 +378,10 @@ GameScene::GameScene(Config config)
|
||||
players_.emplace_back(std::make_unique<Player>(ANIM_P1, ID_P1, p1_x, SPAWN.y, CONTROLS_P1, Map::makeInfo(), P1_PAINT));
|
||||
players_.emplace_back(std::make_unique<Player>(ANIM_P2, ID_P2, p2_x, SPAWN.y, CONTROLS_P2, Map::makeInfo(), P2_PAINT));
|
||||
|
||||
// Tots els slots arrenquen DISABLED. Els actius els reactivarà
|
||||
// `updateRoomTransition` quan s'esgote el delay corresponent dins
|
||||
// de ROOM_INTRO (`player_spawn_delay_ms` per al primer, i
|
||||
// `player2_spawn_delay_ms` per al segon en mode BOTH). Els inactius
|
||||
// es queden DISABLED tot el cicle de sala.
|
||||
// Tots els slots arrenquen DISABLED. Els actius els reactivarà el
|
||||
// callback de fi de música d'INTRO via `completeIntro()` (spawn aeri
|
||||
// simultani) en arribar a la fi de la pista `room_intro.ogg`. Els
|
||||
// inactius es queden DISABLED tot el cicle de sala.
|
||||
for (auto& p : players_) { p->disable(); }
|
||||
|
||||
// PaintLayer: font única del pintat del nivell. Es crea després de
|
||||
@@ -465,21 +464,12 @@ GameScene::GameScene(Config config)
|
||||
// vegada; canvis requereixen reiniciar el joc.
|
||||
transition_config_ = RoomFsm::loadConfig(Defaults::GameScene::CONFIG_YAML);
|
||||
|
||||
// Preload del so del rètol d'INTRO (no hi ha manifest de sons; el
|
||||
// pack agafa tot `data/` automàticament i ací registrem la clau
|
||||
// al Resource::Cache perquè Audio::playSound la trobe). Si el camp
|
||||
// del YAML està buit, no carreguem res — el dispar consultarà el
|
||||
// cache i caurà al silenci sense crash.
|
||||
if (!transition_config_.intro_text_sound.empty()) {
|
||||
Resource::Cache::get().loadSound(
|
||||
transition_config_.intro_text_sound,
|
||||
"data/sounds/" + transition_config_.intro_text_sound);
|
||||
}
|
||||
|
||||
// Log d'estat inicial: la FSM arrenca a ROOM_INTRO. En aquesta fita
|
||||
// les transicions són instantànies (sense fades ni text) i el primer
|
||||
// tick d'`updateRoomTransition` ja saltarà a PLAYING.
|
||||
std::cout << "[ROOM] state=ROOM_INTRO (initial)" << '\n';
|
||||
// Cinemàtica d'entrada de la primera sala: arrenca la pista
|
||||
// `room_intro.ogg` i arma el callback de fi que farà spawn + salt
|
||||
// a PLAYING. Cal cridar `enterRoomIntro` ací (i no només des
|
||||
// d'`advanceToNextRoom`) perquè la primera entrada a la GameScene
|
||||
// no passa pel flux d'avanç de sala.
|
||||
enterRoomIntro();
|
||||
}
|
||||
|
||||
GameScene::~GameScene() = default;
|
||||
@@ -563,19 +553,41 @@ void GameScene::enterRoomIntro() {
|
||||
}
|
||||
state_ = RoomFsm::State::ROOM_INTRO;
|
||||
transition_ = {};
|
||||
room_intro_elapsed_ms_ = 0;
|
||||
intro_completed_ = false;
|
||||
intro_spawn_elapsed_ms_ = 0;
|
||||
players_spawned_in_intro_.fill(false);
|
||||
intro_text_sound_played_ = false;
|
||||
// La cinemàtica d'entrada és silenciosa per disseny: és INTRO qui
|
||||
// posseeix l'aturada. L'OUTRO no toca la música — així el cicle
|
||||
// OUTRO → INTRO transmet la sensació de "tall" en entrar a la sala
|
||||
// nova, no de "fade-out" anterior. Si ja no sona res, stopMusic és
|
||||
// no-op.
|
||||
if (Audio::get() != nullptr) { Audio::get()->stopMusic(); }
|
||||
outro_music_ended_ = false;
|
||||
|
||||
// INTRO arrenca amb la pista `room_intro.ogg` (loop=0, una sola
|
||||
// passada). El callback de fi de música és l'únic responsable de
|
||||
// disparar el spawn dels jugadors i el salt a PLAYING — la durada
|
||||
// real ve marcada per la pista, no per cap timer numèric.
|
||||
auto* audio = Audio::get();
|
||||
if (audio == nullptr) {
|
||||
// Sense Audio inicialitzat, no podem esperar cap callback.
|
||||
// Completem síncronament perquè la sala no quede bloquejada.
|
||||
std::cout << "[ROOM] INTRO: Audio absent — completant síncronament\n";
|
||||
completeIntro();
|
||||
return;
|
||||
}
|
||||
audio->stopMusic();
|
||||
audio->setOnMusicEnded([this]() { completeIntro(); });
|
||||
audio->playMusic("room_intro.ogg", 0);
|
||||
if (Audio::getMusicState() != Audio::MusicState::PLAYING) {
|
||||
// Fallback: la pista no s'ha pogut arrencar (resource absent,
|
||||
// música deshabilitada, etc.). Netegem el callback per no
|
||||
// deixar-lo penjat i fem el spawn + transició com si el track
|
||||
// hagués acabat ja. Així cap fallada d'asset deixa la sala
|
||||
// congelada al fons negre de l'INTRO.
|
||||
std::cout << "[ROOM] INTRO: música no arrencada — completant síncronament\n";
|
||||
audio->setOnMusicEnded({});
|
||||
completeIntro();
|
||||
}
|
||||
}
|
||||
|
||||
void GameScene::triggerPlayerSpawn(std::size_t slot_index) {
|
||||
if (slot_index >= players_spawned_in_intro_.size()) { return; }
|
||||
if (slot_index >= players_.size()) { return; }
|
||||
if (!players_starts_active_[slot_index]) { return; }
|
||||
if (players_spawned_in_intro_[slot_index]) { return; }
|
||||
players_spawned_in_intro_[slot_index] = true;
|
||||
|
||||
@@ -600,23 +612,76 @@ void GameScene::triggerPlayerSpawn(std::size_t slot_index) {
|
||||
std::cout << "[ROOM] spawn cinematic P" << (slot_index + 1) << ": airborne entry\n";
|
||||
}
|
||||
|
||||
void GameScene::completeIntro() {
|
||||
if (intro_completed_) { return; }
|
||||
intro_completed_ = true;
|
||||
intro_spawn_elapsed_ms_ = 0;
|
||||
// Spawn dels slots amb delay <= 0 (el cas habitual de P1). Els
|
||||
// altres cauen frame a frame dins `updateRoomTransition` a mesura
|
||||
// que `intro_spawn_elapsed_ms_` supera el seu delay. Així P1 cau
|
||||
// alhora que la música acaba i la transició a PLAYING; P2 (en
|
||||
// mode BOTH) entra uns frames més tard amb el seu offset propi.
|
||||
spawnDuePlayers();
|
||||
setState(RoomFsm::State::PLAYING);
|
||||
}
|
||||
|
||||
void GameScene::spawnDuePlayers() {
|
||||
if (!intro_completed_) { return; }
|
||||
const bool BOTH_ACTIVE = players_starts_active_[0] && players_starts_active_[1];
|
||||
for (std::size_t i = 0; i < players_starts_active_.size(); ++i) {
|
||||
if (!players_starts_active_[i]) { continue; }
|
||||
if (players_spawned_in_intro_[i]) { continue; }
|
||||
// Single-player: el slot actiu (P1 o P2) sempre fa servir
|
||||
// `player_spawn_delay_ms`. Mode BOTH: P1 fa player_spawn_delay_ms,
|
||||
// P2 fa player2_spawn_delay_ms (offset respecte P1).
|
||||
const int DELAY = (i == 1 && BOTH_ACTIVE)
|
||||
? transition_config_.player2_spawn_delay_ms
|
||||
: transition_config_.player_spawn_delay_ms;
|
||||
if (intro_spawn_elapsed_ms_ >= DELAY) { triggerPlayerSpawn(i); }
|
||||
}
|
||||
}
|
||||
|
||||
void GameScene::setState(RoomFsm::State new_state) {
|
||||
if (new_state == state_) { return; }
|
||||
std::cout << "[ROOM] " << RoomFsm::stateName(state_) << " -> " << RoomFsm::stateName(new_state) << '\n';
|
||||
state_ = new_state;
|
||||
transition_ = {};
|
||||
// En entrar a un nou cicle d'INTRO, resetegem el timer i el flag de
|
||||
// l'spawn aeri perquè es torne a disparar al delay configurat.
|
||||
// Reset defensiu dels flags de cicle quan tornem a INTRO via aquest
|
||||
// camí (no canònic — `enterRoomIntro` és el punt d'entrada normal).
|
||||
if (new_state == RoomFsm::State::ROOM_INTRO) {
|
||||
room_intro_elapsed_ms_ = 0;
|
||||
intro_completed_ = false;
|
||||
intro_spawn_elapsed_ms_ = 0;
|
||||
players_spawned_in_intro_.fill(false);
|
||||
intro_text_sound_played_ = false;
|
||||
outro_music_ended_ = false;
|
||||
}
|
||||
// En sortir d'INTRO cap a PLAYING, la música arrenca: és el moment
|
||||
// en què la cinemàtica d'entrada acaba i el jugador agafa control.
|
||||
// L'OUTRO no atura la música; ho fa el següent `enterRoomIntro`.
|
||||
if (new_state == RoomFsm::State::PLAYING && Audio::get() != nullptr) {
|
||||
Audio::get()->playMusic(config_.music_track, -1);
|
||||
auto* audio = Audio::get();
|
||||
// PLAYING: arrenca la pista de joc en loop infinit. Netegem el
|
||||
// callback de fi (residual d'INTRO, ja consumit) perquè un futur
|
||||
// stopMusic no dispari lògica obsoleta.
|
||||
if (new_state == RoomFsm::State::PLAYING && audio != nullptr) {
|
||||
audio->setOnMusicEnded({});
|
||||
audio->playMusic(config_.music_track, -1);
|
||||
}
|
||||
// ROOM_OUTRO: corte sec de `playing.ogg`, arrenca `room_outro.ogg`
|
||||
// (loop=0). El callback de fi marca `outro_music_ended_ = true`,
|
||||
// que junt amb el final del text_exit gata el fade visual a negre
|
||||
// dins `updateRoomTransition`. Si la música no arrenca, marquem el
|
||||
// flag immediatament perquè el fade quedi gata només contra el text.
|
||||
if (new_state == RoomFsm::State::ROOM_OUTRO) {
|
||||
outro_music_ended_ = false;
|
||||
if (audio == nullptr) {
|
||||
std::cout << "[ROOM] OUTRO: Audio absent — fade gata només contra text\n";
|
||||
outro_music_ended_ = true;
|
||||
} else {
|
||||
audio->stopMusic();
|
||||
audio->setOnMusicEnded([this]() { outro_music_ended_ = true; });
|
||||
audio->playMusic("room_outro.ogg", 0);
|
||||
if (Audio::getMusicState() != Audio::MusicState::PLAYING) {
|
||||
std::cout << "[ROOM] OUTRO: música no arrencada — fade gata només contra text\n";
|
||||
audio->setOnMusicEnded({});
|
||||
outro_music_ended_ = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
// L'avanç de sala (OUTRO → INTRO) NO viu ací: el dispara
|
||||
// `updateRoomTransition` quan la FSM detecta el final de l'OUTRO,
|
||||
@@ -627,43 +692,38 @@ void GameScene::setState(RoomFsm::State new_state) {
|
||||
}
|
||||
|
||||
void GameScene::updateRoomTransition(int delta_ms) {
|
||||
// Disparador d'spawn aeri: timer propi (independent de les fases de
|
||||
// la FSM) que arrenca a l'entrada de ROOM_INTRO. La separació deixa
|
||||
// el delay desacoblat del fade-in i, demà, reutilitzable per al
|
||||
// respawn post-mort sense que la FSM de sala hi haja d'intervenir.
|
||||
if (state_ == RoomFsm::State::ROOM_INTRO) {
|
||||
room_intro_elapsed_ms_ += delta_ms;
|
||||
// Cada slot actiu pot caure en un moment distint quan els dos
|
||||
// estan actius (mode BOTH): P1 amb player_spawn_delay_ms i P2
|
||||
// amb player2_spawn_delay_ms. En single-player (P1 o P2 sols)
|
||||
// l'slot actiu fa servir player_spawn_delay_ms — el segon valor
|
||||
// queda inactiu, com ha demanat el disseny.
|
||||
const bool BOTH_ACTIVE = players_starts_active_[0] && players_starts_active_[1];
|
||||
for (std::size_t i = 0; i < players_starts_active_.size(); ++i) {
|
||||
if (!players_starts_active_[i]) { continue; }
|
||||
if (players_spawned_in_intro_[i]) { continue; }
|
||||
const int DELAY = (i == 1 && BOTH_ACTIVE)
|
||||
? transition_config_.player2_spawn_delay_ms
|
||||
: transition_config_.player_spawn_delay_ms;
|
||||
if (room_intro_elapsed_ms_ >= DELAY) {
|
||||
triggerPlayerSpawn(i);
|
||||
}
|
||||
}
|
||||
// Tick del timer d'spawn aeri escalonat. Comença al callback de fi
|
||||
// de música d'INTRO (`completeIntro` posa intro_completed_=true) i
|
||||
// segueix corrent cap a PLAYING fins que tots els slots actius hagin
|
||||
// caigut. Cap cost extra un cop tots han caigut (`spawnDuePlayers`
|
||||
// és no-op llavors).
|
||||
if (intro_completed_) {
|
||||
intro_spawn_elapsed_ms_ += delta_ms;
|
||||
spawnDuePlayers();
|
||||
}
|
||||
|
||||
const auto NEXT = RoomFsm::advance(state_, transition_, transition_config_, delta_ms);
|
||||
|
||||
// Disparador del so del rètol: la FSM acaba d'avançar; si estem a
|
||||
// INTRO i ja hem deixat enrere phase 0 (fade_in), reproduïm el so
|
||||
// una sola vegada. El so es dispara al final del fade-in, anticipant
|
||||
// el text que apareix a phase 2 (text_enter): així sembla que el so
|
||||
// "anuncia" l'arribada del rètol enlloc de coincidir amb la seva
|
||||
// entrada visual.
|
||||
if (state_ == RoomFsm::State::ROOM_INTRO && transition_.phase >= 1 && !intro_text_sound_played_ && !transition_config_.intro_text_sound.empty() && Audio::get() != nullptr) {
|
||||
Audio::get()->playSound(transition_config_.intro_text_sound, Audio::Group::GAME);
|
||||
intro_text_sound_played_ = true;
|
||||
// OUTRO: gate del fade visual sobre la doble condició (música
|
||||
// acabada AND text fora). Si la FSM ha avançat a phase 3 (fade_out)
|
||||
// pel timer del text però el callback de fi de música encara no
|
||||
// ha disparat, congelem el comptador a 0 perquè el fade no comence
|
||||
// a pintar el negre. Quan el callback alliberi `outro_music_ended_`,
|
||||
// les pròximes crides a `advance` tornaran a acumular delta_ms i
|
||||
// el fade arrencarà des de zero, polit.
|
||||
if (state_ == RoomFsm::State::ROOM_OUTRO &&
|
||||
transition_.phase == 3 &&
|
||||
!outro_music_ended_) {
|
||||
transition_.elapsed_ms = 0;
|
||||
}
|
||||
|
||||
if (NEXT != state_) {
|
||||
// INTRO → PLAYING per timer: ignorat. El callback de fi de
|
||||
// música d'INTRO és l'únic responsable d'aquesta transició.
|
||||
if (state_ == RoomFsm::State::ROOM_INTRO &&
|
||||
NEXT == RoomFsm::State::PLAYING) {
|
||||
return;
|
||||
}
|
||||
// El final de l'OUTRO no es resol amb un setState normal: cal
|
||||
// demanar al LevelManager el següent nivell i carregar-lo abans
|
||||
// d'entrar a INTRO. `advanceToNextRoom` fa tot el bloc i acaba
|
||||
|
||||
Reference in new issue
Block a user