time-based: Game::update(dt_s) sense gate, propaga dt a totes les entitats i sub-comptadors (counter_/death/stage/time-stopped/enemy-deploy/shake/game-completed)

This commit is contained in:
2026-05-19 17:38:39 +02:00
parent 91c5b9d2b2
commit 36d50ade82
3 changed files with 448 additions and 103 deletions
+383 -64
View File
@@ -20,13 +20,14 @@
#include "core/rendering/texture.h" // for Texture
#include "core/resources/asset.h" // for Asset
#include "core/resources/resource.h"
#include "game/defaults.hpp" // for PLAY_AREA_CENTER_X, BLOCK, PLAY_AREA_CEN...
#include "game/entities/balloon.h" // for Balloon, Balloon::VELX_NEGATIVE, BALLOON_...
#include "game/entities/bullet.h" // for Bullet, Bullet::Kind::LEFT, Bullet::Kind::RIGHT, BULLE...
#include "game/entities/item.h" // for Item
#include "game/entities/player.h" // for Player
#include "game/options.hpp" // for Options
#include "game/ui/menu.h" // for Menu
#include "core/system/delta_time.hpp" // for DeltaTime
#include "game/defaults.hpp" // for PLAY_AREA_CENTER_X, BLOCK, PLAY_AREA_CEN...
#include "game/entities/balloon.h" // for Balloon, Balloon::VELX_NEGATIVE, BALLOON_...
#include "game/entities/bullet.h" // for Bullet, Bullet::Kind::LEFT, Bullet::Kind::RIGHT, BULLE...
#include "game/entities/item.h" // for Item
#include "game/entities/player.h" // for Player
#include "game/options.hpp" // for Options
#include "game/ui/menu.h" // for Menu
namespace Ja {
struct Sound;
} // namespace Ja
@@ -102,6 +103,9 @@ Game::Game(int num_players, int current_stage, SDL_Renderer *renderer, bool demo
// Inicializa las variables necesarias para la sección 'Game'
init();
// Reset del rellotge perquè el primer dt_s no inclogui el temps de càrrega.
DeltaTime::reset();
}
Game::~Game() {
@@ -200,18 +204,27 @@ void Game::init() {
game_completed_ = false;
game_completed_counter_ = 0;
game_completed_counter_s_ = 0.0F;
section_->name = SECTION_PROG_GAME;
section_->subsection = SUBSECTION_GAME_PLAY_1P;
menace_current_ = 0;
menace_threshold_ = 0;
hi_score_achieved_ = false;
stage_bitmap_counter_ = STAGE_COUNTER;
stage_bitmap_counter_s_ = STAGE_COUNTER / 60.0F;
death_counter_ = Player::DEATH_COUNTER;
death_counter_s_ = Player::DEATH_COUNTER / 60.0F;
time_stopped_ = false;
time_stopped_counter_ = 0;
time_stopped_counter_s_ = 0.0F;
counter_ = 0;
elapsed_s_ = 0.0F;
last_enemy_deploy_ = 0;
enemy_deploy_counter_ = 0;
enemy_deploy_counter_s_ = 0.0F;
enemy_deploy_phase_s_ = 0.0F;
shake_phase_s_ = 0.0F;
helper_counter_s_ = 0.0F;
enemy_speed_ = default_enemy_speed_;
effect_.flash = false;
effect_.shake = false;
@@ -284,15 +297,15 @@ void Game::init() {
// Con los globos creados, calcula el nivel de amenaza
evaluateAndSetMenace();
// Inicializa el bitmap de 1000 puntos
// Inicializa el bitmap de 1000 puntos (px/s i px/s²; -0.5 px/frame → -30 px/s)
n1000_sprite_->setPosX(0);
n1000_sprite_->setPosY(0);
n1000_sprite_->setWidth(26);
n1000_sprite_->setHeight(9);
n1000_sprite_->setVelX(0.0F);
n1000_sprite_->setVelY(-0.5F);
n1000_sprite_->setVelY(-30.0F);
n1000_sprite_->setAccelX(0.0F);
n1000_sprite_->setAccelY(-0.1F);
n1000_sprite_->setAccelY(-360.0F);
n1000_sprite_->setSpriteClip(0, 0, 26, 9);
n1000_sprite_->setEnabled(false);
n1000_sprite_->setEnabledCounter(0);
@@ -305,9 +318,9 @@ void Game::init() {
n2500_sprite_->setWidth(28);
n2500_sprite_->setHeight(9);
n2500_sprite_->setVelX(0.0F);
n2500_sprite_->setVelY(-0.5F);
n2500_sprite_->setVelY(-30.0F);
n2500_sprite_->setAccelX(0.0F);
n2500_sprite_->setAccelY(-0.1F);
n2500_sprite_->setAccelY(-360.0F);
n2500_sprite_->setSpriteClip(26, 0, 28, 9);
n2500_sprite_->setEnabled(false);
n2500_sprite_->setEnabledCounter(0);
@@ -320,9 +333,9 @@ void Game::init() {
n5000_sprite_->setWidth(28);
n5000_sprite_->setHeight(9);
n5000_sprite_->setVelX(0.0F);
n5000_sprite_->setVelY(-0.5F);
n5000_sprite_->setVelY(-30.0F);
n5000_sprite_->setAccelX(0.0F);
n5000_sprite_->setAccelY(-0.1F);
n5000_sprite_->setAccelY(-360.0F);
n5000_sprite_->setSpriteClip(54, 0, 28, 9);
n5000_sprite_->setEnabled(false);
n5000_sprite_->setEnabledCounter(0);
@@ -1450,6 +1463,26 @@ void Game::updatePlayers() {
}
}
// Actualiza las variables del jugador (time-based)
void Game::updatePlayers(float dt_s) {
for (auto *player : players_) {
player->update(dt_s);
if (checkPlayerBalloonCollision(player)) {
if (player->isAlive()) {
if (demo_.enabled) {
section_->name = SECTION_PROG_TITLE;
section_->subsection = SUBSECTION_TITLE_INSTRUCTIONS;
} else {
killPlayer(player);
}
}
}
checkPlayerItemCollision(player);
}
}
// Dibuja a los jugadores
void Game::renderPlayers() {
for (auto *player : players_) {
@@ -1497,6 +1530,45 @@ void Game::updateStage() {
}
}
// Actualiza las variables de la fase (time-based)
void Game::updateStage(float dt_s) {
if (stage_[current_stage_].current_power >= stage_[current_stage_].power_to_complete) {
current_stage_++;
last_stage_reached_ = current_stage_;
if (current_stage_ == 10) {
game_completed_ = true;
current_stage_ = 9;
stage_[current_stage_].current_power = 0;
destroyAllBalloons();
stage_[current_stage_].current_power = 0;
menace_current_ = 255;
for (auto *player : players_) {
if (player->isAlive()) {
player->addScore(1000000);
}
}
updateHiScore();
Audio::get()->stopMusic();
}
Audio::get()->playSound(stage_change_sound_);
stage_bitmap_counter_ = 0;
stage_bitmap_counter_s_ = 0.0F;
enemy_speed_ = default_enemy_speed_;
setBalloonSpeed(enemy_speed_);
effect_.flash = true;
effect_.shake = true;
}
if (stage_bitmap_counter_s_ < (STAGE_COUNTER / 60.0F)) {
stage_bitmap_counter_s_ += dt_s;
}
stage_bitmap_counter_ = std::min<int>(STAGE_COUNTER, static_cast<int>(stage_bitmap_counter_s_ * 60.0F));
if (game_completed_) {
stage_bitmap_counter_ = std::min<int>(stage_bitmap_counter_, 100);
}
}
// Actualiza el estado de muerte
void Game::updateDeath() {
// Comprueba si todos los jugadores estan muertos
@@ -1523,6 +1595,39 @@ void Game::updateDeath() {
}
}
// Actualiza el estado de muerte (time-based). Detecta el creuament dels llindars
// 250/200/180/120/60 (en frames) per a reproduir els bubbles als mateixos moments.
void Game::updateDeath(float dt_s) {
bool all_dead = true;
for (const auto *player : players_) {
all_dead &= (!player->isAlive());
}
if (!all_dead) { return; }
if (death_counter_s_ <= 0.0F) {
section_->subsection = SUBSECTION_GAME_GAMEOVER;
return;
}
const float PREV_S = death_counter_s_;
death_counter_s_ = std::max(0.0F, death_counter_s_ - dt_s);
death_counter_ = static_cast<Uint16>(death_counter_s_ * 60.0F);
auto crossed = [&](float threshold_frames) {
const float TS = threshold_frames / 60.0F;
return (PREV_S > TS) && (death_counter_s_ <= TS);
};
if (crossed(250.0F) || crossed(200.0F) || crossed(180.0F) || crossed(120.0F) || crossed(60.0F)) {
if (!demo_.enabled) {
const Uint8 INDEX = rand() % 4;
Ja::Sound *sound[4] = {bubble1_sound_, bubble2_sound_, bubble3_sound_, bubble4_sound_};
Audio::get()->playSound(sound[INDEX]);
}
}
}
// Renderiza el fade final cuando se acaba la partida
void Game::renderDeathFade(int counter) { // Counter debe ir de 0 a 150
SDL_SetRenderDrawColor(renderer_, 0x27, 0x27, 0x36, 255);
@@ -1555,6 +1660,13 @@ void Game::updateBalloons() {
}
}
// Actualiza los globos (time-based)
void Game::updateBalloons(float dt_s) {
for (auto *balloon : balloons_) {
balloon->update(dt_s);
}
}
// Pinta en pantalla todos los globos activos
void Game::renderBalloons() {
for (auto *balloon : balloons_) {
@@ -1914,6 +2026,17 @@ void Game::moveBullets() {
}
}
// Mueve las balas activas (time-based)
void Game::moveBullets(float dt_s) {
for (auto *bullet : bullets_) {
if (bullet->isEnabled()) {
if (bullet->move(dt_s) == Bullet::MoveResult::OUT) {
players_[bullet->getOwner()]->decScoreMultiplier();
}
}
}
}
// Pinta las balas activas
void Game::renderBullets() {
for (auto *bullet : bullets_) {
@@ -1954,6 +2077,19 @@ void Game::updateItems() {
}
}
// Actualiza los items (time-based)
void Game::updateItems(float dt_s) {
for (auto *item : items_) {
if (item->isEnabled()) {
item->update(dt_s);
if (item->isOnFloor()) {
Audio::get()->playSound(coffee_machine_sound_);
effect_.shake = true;
}
}
}
}
// Pinta los items activos
void Game::renderItems() {
for (auto *item : items_) {
@@ -2079,6 +2215,30 @@ void Game::updateShakeEffect() {
}
}
// Actualiza el efecto de agitar la pantalla (time-based). Decrementa
// `shake_counter` a cadència fixa de 60Hz amb un acumulador de fase
// (independent del framerate) — el render de `updateBackground` segueix
// llegint la paritat del counter per fer vibrar els edificis.
void Game::updateShakeEffect(float dt_s) {
if (!effect_.shake) {
shake_phase_s_ = 0.0F;
return;
}
constexpr float STEP_S = 1.0F / 60.0F;
shake_phase_s_ += dt_s;
while (shake_phase_s_ >= STEP_S) {
shake_phase_s_ -= STEP_S;
if (effect_.shake_counter > 0) {
effect_.shake_counter--;
} else {
effect_.shake = false;
effect_.shake_counter = SHAKE_COUNTER;
shake_phase_s_ = 0.0F;
break;
}
}
}
// Crea un SmartSprite para arrojar el item café al recibir un impacto
void Game::throwCoffee(int x, int y) {
auto *ss = new SmartSprite(item_textures_[4], renderer_);
@@ -2088,10 +2248,12 @@ void Game::throwCoffee(int x, int y) {
ss->setPosY(y - 8);
ss->setWidth(16);
ss->setHeight(16);
ss->setVelX(-1.0F + ((rand() % 5) * 0.5F));
ss->setVelY(-4.0F);
// Conversió a px/s i px/s² (era -1..1 px/frame i 0.2 px/frame²)
const float VX_PX_PER_S = (-1.0F + (static_cast<float>(rand() % 5) * 0.5F)) * 60.0F;
ss->setVelX(VX_PX_PER_S);
ss->setVelY(-240.0F);
ss->setAccelX(0.0F);
ss->setAccelY(0.2F);
ss->setAccelY(720.0F);
ss->setDestX(x + (ss->getVelX() * 50));
ss->setDestY(GAMECANVAS_HEIGHT + 1);
ss->setEnabled(true);
@@ -2109,6 +2271,13 @@ void Game::updateSmartSprites() {
}
}
// Actualiza los SmartSprites (time-based)
void Game::updateSmartSprites(float dt_s) {
for (auto *ss : smart_sprites_) {
ss->update(dt_s);
}
}
// Pinta los SmartSprites activos
void Game::renderSmartSprites() {
for (auto *ss : smart_sprites_) {
@@ -2196,11 +2365,13 @@ auto Game::isTimeStopped() const -> bool {
// Establece el valor de la variable
void Game::setTimeStoppedCounter(Uint16 value) {
time_stopped_counter_ = value;
time_stopped_counter_s_ = static_cast<float>(value) / 60.0F;
}
// Incrementa el valor de la variable
void Game::incTimeStoppedCounter(Uint16 value) {
time_stopped_counter_ += value;
time_stopped_counter_s_ += static_cast<float>(value) / 60.0F;
}
// Actualiza y comprueba el valor de la variable
@@ -2215,6 +2386,18 @@ void Game::updateTimeStoppedCounter() {
}
}
// Actualiza y comprueba el valor de la variable (time-based)
void Game::updateTimeStoppedCounter(float dt_s) {
if (!isTimeStopped()) { return; }
if (time_stopped_counter_s_ > 0.0F) {
time_stopped_counter_s_ = std::max(0.0F, time_stopped_counter_s_ - dt_s);
time_stopped_counter_ = static_cast<Uint16>(time_stopped_counter_s_ * 60.0F);
stopAllBalloons(TIME_STOPPED_COUNTER);
} else {
disableTimeStopItem();
}
}
// Actualiza la variable enemyDeployCounter
void Game::updateEnemyDeployCounter() {
if (enemy_deploy_counter_ > 0) {
@@ -2222,6 +2405,19 @@ void Game::updateEnemyDeployCounter() {
}
}
// Actualiza enemy_deploy_counter_ (time-based). El comptador es decrementa
// a cadència fixa de 60Hz amb un acumulador de fase: és un comptador
// discret consultat per `canPowerBallBeCreated()` i altres.
void Game::updateEnemyDeployCounter(float dt_s) {
if (enemy_deploy_counter_ <= 0) { return; }
constexpr float STEP_S = 1.0F / 60.0F;
enemy_deploy_phase_s_ += dt_s;
while (enemy_deploy_phase_s_ >= STEP_S && enemy_deploy_counter_ > 0) {
enemy_deploy_phase_s_ -= STEP_S;
enemy_deploy_counter_--;
}
}
// Actualiza el juego
void Game::update() {
// Actualiza el audio
@@ -2299,6 +2495,48 @@ void Game::update() {
}
}
// Actualiza el juego (time-based). Sense el gate del SDL_GetTicks: la cadència
// la dicta dt_s, propagat des de iterate() via DeltaTime::tick(). El comptador
// global `counter_` queda derivat de `elapsed_s_*60` perquè els lectors
// existents (render de l'herba, paths del get_ready, etc.) segueixin valuant.
void Game::update(float dt_s) {
Audio::update();
updateDeathShake();
updateDeathSequence();
if (death_sequence_.phase == DeathPhase::SHAKING || death_sequence_.phase == DeathPhase::WAITING) {
return;
}
// Acumulador i derivació del comptador legacy
elapsed_s_ += dt_s;
counter_ = static_cast<Uint32>(elapsed_s_ * 60.0F);
checkGameInput();
updatePlayers(dt_s);
updateBackground(dt_s);
updateBalloons(dt_s);
moveBullets(dt_s);
updateItems(dt_s);
updateStage(dt_s);
updateDeath(dt_s);
updateSmartSprites(dt_s);
updateTimeStoppedCounter(dt_s);
updateEnemyDeployCounter(dt_s);
updateShakeEffect(dt_s);
updateHelper(dt_s);
checkBulletBalloonCollision();
updateMenace();
updateBalloonSpeed();
updateGameCompleted(dt_s);
freeBullets();
freeBalloons();
freeItems();
freeSmartSprites();
}
// Actualiza el fondo
void Game::updateBackground() {
if (!game_completed_) { // Si el juego no esta completo, la velocidad de las nubes es igual a los globos explotados
@@ -2314,13 +2552,13 @@ void Game::updateBackground() {
// Calcula la velocidad en función de los globos explotados y el total de globos a explotar para acabar el juego
const float SPEED = (-0.2F) + (-3.00F * ((float)clouds_speed_ / (float)total_power_to_complete_game_));
// Aplica la velocidad calculada a las nubes
// Aplica la velocidad calculada a las nubes (px/frame)
clouds1_a_->setVelX(SPEED);
clouds1_b_->setVelX(SPEED);
clouds2_a_->setVelX(SPEED / 2);
clouds2_b_->setVelX(SPEED / 2);
// Mueve las nubes
// Mueve las nubes (frame-based)
clouds1_a_->move();
clouds1_b_->move();
clouds2_a_->move();
@@ -2357,6 +2595,53 @@ void Game::updateBackground() {
}
}
// Actualiza el fondo (time-based). Velocitats dels núvols expressades com a
// px/frame (la conversió a px/s la fa cloud->move(dt_s) multiplicant per 60
// internament — perquè MovingSprite comparteix vx_ entre frame i time-based,
// aquí passem la mateixa "velocitat per frame" * 60).
void Game::updateBackground(float dt_s) {
if (!game_completed_) {
clouds_speed_ = balloons_popped_;
} else {
if (clouds_speed_ > 400) {
clouds_speed_ -= 25;
} else {
clouds_speed_ = 200;
}
}
// Velocitat per frame (mateixa fórmula); en time-based la passem com a px/s.
const float SPEED_PX_PER_FRAME = (-0.2F) + (-3.00F * ((float)clouds_speed_ / (float)total_power_to_complete_game_));
const float SPEED_PX_PER_S = SPEED_PX_PER_FRAME * 60.0F;
clouds1_a_->setVelX(SPEED_PX_PER_S);
clouds1_b_->setVelX(SPEED_PX_PER_S);
clouds2_a_->setVelX(SPEED_PX_PER_S / 2.0F);
clouds2_b_->setVelX(SPEED_PX_PER_S / 2.0F);
clouds1_a_->move(dt_s);
clouds1_b_->move(dt_s);
clouds2_a_->move(dt_s);
clouds2_b_->move(dt_s);
if (clouds1_a_->getPosX() < -clouds1_a_->getWidth()) { clouds1_a_->setPosX(clouds1_a_->getWidth()); }
if (clouds1_b_->getPosX() < -clouds1_b_->getWidth()) { clouds1_b_->setPosX(clouds1_b_->getWidth()); }
if (clouds2_a_->getPosX() < -clouds2_a_->getWidth()) { clouds2_a_->setPosX(clouds2_a_->getWidth()); }
if (clouds2_b_->getPosX() < -clouds2_b_->getWidth()) { clouds2_b_->setPosX(clouds2_b_->getWidth()); }
// Herba: `counter_` derivat de `elapsed_s_*60` ja oscil·la a 60Hz.
grass_sprite_->setSpriteClip(0, (6 * (counter_ / 20 % 2)), 256, 6);
if (death_shake_.active) {
const int V[] = {-1, 1, -1, 1, -1, 1, -1, 0};
buildings_sprite_->setPosX(V[death_shake_.step]);
} else if (effect_.shake) {
buildings_sprite_->setPosX(((effect_.shake_counter % 2) * 2) - 1);
} else {
buildings_sprite_->setPosX(0);
}
}
// Dibuja el fondo
void Game::renderBackground() {
const float GRADIENT_NUMBER = std::min(((float)balloons_popped_ / 1250.0F), 3.0F);
@@ -2679,55 +2964,62 @@ auto Game::isDeathShaking() const -> bool {
// Ejecuta un frame del juego
void Game::iterate() {
// En modo demo, no hay pausa ni game over
if (demo_.enabled) {
if (section_->subsection == SUBSECTION_GAME_PAUSE || section_->subsection == SUBSECTION_GAME_GAMEOVER) {
section_->name = SECTION_PROG_TITLE;
section_->subsection = SUBSECTION_TITLE_INSTRUCTIONS;
return;
}
// Consum del temps real des de l'última iteració. Sempre s'ha de cridar
// perquè el rellotge no acumuli a través de transicions/sub-estats.
const float DELTA_TIME_S = DeltaTime::tick();
// En modo demo, ni pause ni game over: torna immediatament al títol
if (demo_.enabled && (section_->subsection == SUBSECTION_GAME_PAUSE || section_->subsection == SUBSECTION_GAME_GAMEOVER)) {
section_->name = SECTION_PROG_TITLE;
section_->subsection = SUBSECTION_TITLE_INSTRUCTIONS;
return;
}
// Sección juego en pausa
if (section_->subsection == SUBSECTION_GAME_PAUSE) {
if (!pause_initialized_) {
enterPausedGame();
}
updatePausedGame();
renderPausedGame();
switch (section_->subsection) {
case SUBSECTION_GAME_PAUSE:
iteratePaused();
break;
case SUBSECTION_GAME_GAMEOVER:
iterateGameOver();
break;
case SUBSECTION_GAME_PLAY_1P:
case SUBSECTION_GAME_PLAY_2P:
iteratePlaying(DELTA_TIME_S);
break;
default:
break;
}
}
// Rama de iterate(): pause
void Game::iteratePaused() {
if (!pause_initialized_) { enterPausedGame(); }
updatePausedGame();
renderPausedGame();
}
// Rama de iterate(): game over
void Game::iterateGameOver() {
if (!game_over_initialized_) { enterGameOverScreen(); }
updateGameOverScreen();
renderGameOverScreen();
}
// Rama de iterate(): joc actiu
void Game::iteratePlaying(float dt_s) {
// Si veníem de Pause/GameOver, el dt acumulat seria enorme; descarta'l.
if (pause_initialized_ || game_over_initialized_) {
DeltaTime::reset();
}
pause_initialized_ = false;
game_over_initialized_ = false;
if (Audio::getRealMusicState() == Audio::MusicState::STOPPED && !game_completed_ && !demo_.enabled && players_[0]->isAlive()) {
Audio::get()->playMusic(game_music_);
}
// Sección Game Over
else if (section_->subsection == SUBSECTION_GAME_GAMEOVER) {
if (!game_over_initialized_) {
enterGameOverScreen();
}
updateGameOverScreen();
renderGameOverScreen();
}
// Sección juego jugando
else if ((section_->subsection == SUBSECTION_GAME_PLAY_1P) || (section_->subsection == SUBSECTION_GAME_PLAY_2P)) {
// Resetea los flags de inicialización de sub-estados
pause_initialized_ = false;
game_over_initialized_ = false;
// Si la música no está sonando
if ((Audio::getRealMusicState() == Audio::MusicState::STOPPED) || (Audio::getRealMusicState() == Audio::MusicState::STOPPED)) {
// Reproduce la música (nunca en modo demo: deja sonar la del título)
if (!game_completed_ && !demo_.enabled) {
if (players_[0]->isAlive()) {
Audio::get()->playMusic(game_music_);
}
}
}
// Actualiza la lógica del juego
update();
// Dibuja los objetos
render();
}
update(dt_s);
render();
}
// Indica si el juego ha terminado
@@ -3109,6 +3401,19 @@ void Game::updateGameCompleted() {
}
}
// Actualiza el tramo final de juego (time-based)
void Game::updateGameCompleted(float dt_s) {
if (!game_completed_) { return; }
const int PREV = game_completed_counter_;
game_completed_counter_s_ += dt_s;
game_completed_counter_ = static_cast<int>(game_completed_counter_s_ * 60.0F);
if (PREV < GAME_COMPLETED_END && game_completed_counter_ >= GAME_COMPLETED_END) {
section_->subsection = SUBSECTION_GAME_GAMEOVER;
}
}
// Actualiza las variables de ayuda
void Game::updateHelper() {
// Solo ofrece ayuda cuando la amenaza es elevada
@@ -3124,6 +3429,20 @@ void Game::updateHelper() {
}
}
// Actualiza las variables de ayuda (time-based). De moment cap timer real
// dins helper_; clonem la lògica per consistència d'API.
void Game::updateHelper([[maybe_unused]] float dt_s) {
if (menace_current_ > 15) {
for (const auto *player : players_) {
helper_.need_coffee = player->getCoffees() == 0;
helper_.need_coffee_machine = !player->isPowerUp();
}
} else {
helper_.need_coffee = false;
helper_.need_coffee_machine = false;
}
}
// Comprueba si todos los jugadores han muerto
auto Game::allPlayersAreDead() -> bool {
bool success = true;