diff --git a/source/core/rendering/animatedsprite.cpp b/source/core/rendering/animatedsprite.cpp index 43e0496..1425e68 100644 --- a/source/core/rendering/animatedsprite.cpp +++ b/source/core/rendering/animatedsprite.cpp @@ -8,88 +8,95 @@ namespace { -// Normalitza CRLF: fitxers .ani amb terminadors de Windows fan que -// line == "[animation]" no faci match i el parser entri en bucle -// infinit / no carregui cap animació. -void stripCr(std::string &s) { - if (!s.empty() && s.back() == '\r') { - s.pop_back(); - } -} - -void parseFramesList(const std::string &value, Animation &buffer, int frame_width, int frame_height, int frames_per_row, int max_tiles) { - std::stringstream ss(value); - std::string tmp; - SDL_Rect rect = {0, 0, frame_width, frame_height}; - while (getline(ss, tmp, ',')) { - const int NUM_TILE = std::stoi(tmp) > max_tiles ? 0 : std::stoi(tmp); - rect.x = (NUM_TILE % frames_per_row) * frame_width; - rect.y = (NUM_TILE / frames_per_row) * frame_height; - buffer.frames.push_back(rect); - } -} - -void parseAnimationField(const std::string &line, int pos, Animation &buffer, int frame_width, int frame_height, int frames_per_row, int max_tiles, const std::string &filename) { - const std::string KEY = line.substr(0, pos); - const std::string VALUE = line.substr(pos + 1, line.length()); - if (KEY == "name") { - buffer.name = VALUE; - } else if (KEY == "speed") { - buffer.speed = std::stoi(VALUE); - } else if (KEY == "loop") { - buffer.loop = std::stoi(VALUE); - } else if (KEY == "frames") { - parseFramesList(VALUE, buffer, frame_width, frame_height, frames_per_row, max_tiles); - } else { - std::cout << "Warning: file " << filename.c_str() << "\n, unknown parameter \"" << KEY.c_str() << "\"" << '\n'; - } -} - -auto parseAnimationBlock(std::istream &file, int frame_width, int frame_height, int frames_per_row, int max_tiles, const std::string &filename) -> Animation { - Animation buffer; - buffer.speed = 0; - buffer.loop = -1; - buffer.counter = 0; - buffer.current_frame = 0; - buffer.completed = false; - - std::string line; - do { - if (!std::getline(file, line)) { - break; + // Normalitza CRLF: fitxers .ani amb terminadors de Windows fan que + // line == "[animation]" no faci match i el parser entri en bucle + // infinit / no carregui cap animació. + void stripCr(std::string &s) { + if (!s.empty() && s.back() == '\r') { + s.pop_back(); } - stripCr(line); - int pos = line.find('='); - if (pos != (int)std::string::npos) { - parseAnimationField(line, pos, buffer, frame_width, frame_height, frames_per_row, max_tiles, filename); + } + + void parseFramesList(const std::string &value, Animation &buffer, int frame_width, int frame_height, int frames_per_row, int max_tiles) { + std::stringstream ss(value); + std::string tmp; + SDL_Rect rect = {0, 0, frame_width, frame_height}; + while (getline(ss, tmp, ',')) { + const int NUM_TILE = std::stoi(tmp) > max_tiles ? 0 : std::stoi(tmp); + rect.x = (NUM_TILE % frames_per_row) * frame_width; + rect.y = (NUM_TILE / frames_per_row) * frame_height; + buffer.frames.push_back(rect); } - } while (line != "[/animation]"); - - return buffer; -} - -void parseGlobalField(const std::string &line, int pos, int &frames_per_row, int &frame_width, int &frame_height, int &max_tiles, const Texture *texture, const std::string &filename) { - const std::string KEY = line.substr(0, pos); - const std::string VALUE = line.substr(pos + 1, line.length()); - if (KEY == "framesPerRow") { - frames_per_row = std::stoi(VALUE); - } else if (KEY == "frameWidth") { - frame_width = std::stoi(VALUE); - } else if (KEY == "frameHeight") { - frame_height = std::stoi(VALUE); - } else { - std::cout << "Warning: file " << filename.c_str() << "\n, unknown parameter \"" << KEY.c_str() << "\"" << '\n'; } - if (frames_per_row == 0 && frame_width > 0) { - frames_per_row = texture->getWidth() / frame_width; + void parseAnimationField(const std::string &line, int pos, Animation &buffer, int frame_width, int frame_height, int frames_per_row, int max_tiles, const std::string &filename) { + const std::string KEY = line.substr(0, pos); + const std::string VALUE = line.substr(pos + 1, line.length()); + if (KEY == "name") { + buffer.name = VALUE; + } else if (KEY == "speed") { + buffer.speed = std::stoi(VALUE); + // Time-based: el valor del .ani s'expressa en "ticks per frame + // d'animació" (assumint 60 Hz). El camp `speed` (int) es manté per al + // fallback frame-based; el nou `step_duration_s` (float) és el que + // gasta animate(dt). + buffer.step_duration_s = static_cast(buffer.speed) / 60.0F; + } else if (KEY == "loop") { + buffer.loop = std::stoi(VALUE); + } else if (KEY == "frames") { + parseFramesList(VALUE, buffer, frame_width, frame_height, frames_per_row, max_tiles); + } else { + std::cout << "Warning: file " << filename.c_str() << "\n, unknown parameter \"" << KEY.c_str() << "\"" << '\n'; + } } - if (max_tiles == 0 && frame_width > 0 && frame_height > 0) { - const int W = texture->getWidth() / frame_width; - const int H = texture->getHeight() / frame_height; - max_tiles = W * H; + + auto parseAnimationBlock(std::istream &file, int frame_width, int frame_height, int frames_per_row, int max_tiles, const std::string &filename) -> Animation { + Animation buffer; + buffer.speed = 0; + buffer.step_duration_s = 0.0F; + buffer.loop = -1; + buffer.counter = 0; + buffer.current_frame = 0; + buffer.completed = false; + buffer.time_accumulator_s = 0.0F; + + std::string line; + do { + if (!std::getline(file, line)) { + break; + } + stripCr(line); + int pos = line.find('='); + if (pos != (int)std::string::npos) { + parseAnimationField(line, pos, buffer, frame_width, frame_height, frames_per_row, max_tiles, filename); + } + } while (line != "[/animation]"); + + return buffer; + } + + void parseGlobalField(const std::string &line, int pos, int &frames_per_row, int &frame_width, int &frame_height, int &max_tiles, const Texture *texture, const std::string &filename) { + const std::string KEY = line.substr(0, pos); + const std::string VALUE = line.substr(pos + 1, line.length()); + if (KEY == "framesPerRow") { + frames_per_row = std::stoi(VALUE); + } else if (KEY == "frameWidth") { + frame_width = std::stoi(VALUE); + } else if (KEY == "frameHeight") { + frame_height = std::stoi(VALUE); + } else { + std::cout << "Warning: file " << filename.c_str() << "\n, unknown parameter \"" << KEY.c_str() << "\"" << '\n'; + } + + if (frames_per_row == 0 && frame_width > 0) { + frames_per_row = texture->getWidth() / frame_width; + } + if (max_tiles == 0 && frame_width > 0 && frame_height > 0) { + const int W = texture->getWidth() / frame_width; + const int H = texture->getHeight() / frame_height; + max_tiles = W * H; + } } -} } // namespace @@ -204,33 +211,26 @@ auto AnimatedSprite::getIndex(const std::string &name) -> int { return -1; } -// Calcula el frame correspondiente a la animación -void AnimatedSprite::animate() { - if (!enabled_ || animation_[current_animation_].speed == 0) { +// Avança l'acumulador i calcula el frame actual a partir de `step_duration_s`. +void AnimatedSprite::animate(float dt_s) { + Animation &anim = animation_[current_animation_]; + if (!enabled_ || anim.step_duration_s <= 0.0F) { return; } - // Calcula el frame actual a partir del contador - animation_[current_animation_].current_frame = animation_[current_animation_].counter / animation_[current_animation_].speed; + anim.time_accumulator_s += dt_s; + anim.current_frame = static_cast(anim.time_accumulator_s / anim.step_duration_s); - // Si alcanza el final de la animación, reinicia el contador de la animación - // en función de la variable loop y coloca el nuevo frame - if (animation_[current_animation_].current_frame >= (int)animation_[current_animation_].frames.size()) { - if (animation_[current_animation_].loop == -1) { // Si no hay loop, deja el último frame - animation_[current_animation_].current_frame = animation_[current_animation_].frames.size(); - animation_[current_animation_].completed = true; - } else { // Si hay loop, vuelve al frame indicado - animation_[current_animation_].counter = 0; - animation_[current_animation_].current_frame = animation_[current_animation_].loop; + if (anim.current_frame >= (int)anim.frames.size()) { + if (anim.loop == -1) { + anim.current_frame = anim.frames.size(); + anim.completed = true; + } else { + anim.time_accumulator_s = 0.0F; + anim.current_frame = anim.loop; } - } - // En caso contrario - else { - // Escoge el frame correspondiente de la animación - setSpriteClip(animation_[current_animation_].frames[animation_[current_animation_].current_frame]); - - // Incrementa el contador de la animacion - animation_[current_animation_].counter++; + } else { + setSpriteClip(anim.frames[anim.current_frame]); } } @@ -344,10 +344,10 @@ void AnimatedSprite::setCurrentAnimation(int index) { } } -// Actualiza las variables del objeto -void AnimatedSprite::update() { - animate(); - MovingSprite::update(); +// animate(dt) + MovingSprite::update(dt) (move + rotació) +void AnimatedSprite::update(float dt_s) { + animate(dt_s); + MovingSprite::update(dt_s); } // Establece el rectangulo para un frame de una animación diff --git a/source/core/rendering/animatedsprite.h b/source/core/rendering/animatedsprite.h index 1f445b6..eaaf1fa 100644 --- a/source/core/rendering/animatedsprite.h +++ b/source/core/rendering/animatedsprite.h @@ -12,11 +12,13 @@ class Texture; struct Animation { std::string name; // Nombre de la animacion std::vector frames; // Cada uno de los frames que componen la animación - int speed; // Velocidad de la animación + int speed; // Velocidad de la animación (frame-based: ticks per frame) + float step_duration_s; // Time-based: segons per frame d'animació (derivat de speed al parse: speed/60) int loop; // Indica a que frame vuelve la animación al terminar. -1 para que no vuelva bool completed; // Indica si ha finalizado la animación int current_frame; // Frame actual - int counter; // Contador para las animaciones + int counter; // Contador per a les animacions (frame-based) + float time_accumulator_s; // Acumulador de temps (time-based) }; struct AnimatedSpriteData { @@ -37,7 +39,7 @@ class AnimatedSprite : public MovingSprite { ~AnimatedSprite() override; // Destructor - void animate(); // Calcula el frame correspondiente a la animación actual + void animate(float dt_s); // Calcula el frame correspondiente a la animación actual auto getNumFrames() -> int; // Obtiene el numero de frames de la animación actual void setCurrentFrame(int num); // Establece el frame actual de la animación void setAnimationCounter(const std::string &name, int num); // Establece el valor del contador @@ -56,13 +58,13 @@ class AnimatedSprite : public MovingSprite { auto getAnimationClip(const std::string &name = "default", Uint8 index = 0) -> SDL_Rect; // Devuelve el rectangulo de una animación y frame concreto auto getAnimationClip(int index_a = 0, Uint8 index_f = 0) -> SDL_Rect; - auto getIndex(const std::string &name) -> int; // Obtiene el indice de la animación a partir del nombre + auto getIndex(const std::string &name) -> int; // Obtiene el indice de la animación a partir del nombre auto loadFromVector(const std::vector *source) -> bool; // Carga la animación desde un vector void setCurrentAnimation(const std::string &name = "default"); // Establece la animacion actual void setCurrentAnimation(int index = 0); - void update() override; // Actualiza las variables del objeto + void update(float dt_s) override; // Actualiza las variables del objeto void setAnimationFrames(Uint8 index_animation, Uint8 index_frame, int x, int y, int w, int h); // OLD - Establece el rectangulo para un frame de una animación void setAnimationCounter(int value); // OLD - Establece el contador para todas las animaciones diff --git a/source/core/rendering/fade.cpp b/source/core/rendering/fade.cpp index 0495775..f0f183f 100644 --- a/source/core/rendering/fade.cpp +++ b/source/core/rendering/fade.cpp @@ -31,6 +31,7 @@ void Fade::init(Uint8 r, Uint8 g, Uint8 b) { enabled_ = false; finished_ = false; counter_ = 0; + elapsed_s_ = 0.0F; r_ = r; g_ = g; b_ = b; @@ -155,11 +156,15 @@ void Fade::renderFadeRandomSquare() { } } -// Actualiza las variables internas -void Fade::update() { - if (enabled_) { - counter_++; - } +// Actualiza les variables internes. `counter_` (Uint16, frames a la cadència +// de referència 60Hz) es deriva de `elapsed_s_` perquè els helpers de +// `render()` (renderFadeFullscreen / Center / RandomSquare) segueixin +// llegint-lo igual que abans. +void Fade::update(float dt_s) { + if (!enabled_) { return; } + elapsed_s_ += dt_s; + constexpr float FADE_STEPS_PER_S = 60.0F; + counter_ = static_cast(elapsed_s_ * FADE_STEPS_PER_S); } // Activa el fade @@ -167,6 +172,7 @@ void Fade::activateFade() { enabled_ = true; finished_ = false; counter_ = 0; + elapsed_s_ = 0.0F; squares_drawn_ = 0; last_square_ticks_ = 0; fullscreen_done_ = false; diff --git a/source/core/rendering/fade.h b/source/core/rendering/fade.h index d159d5a..bb4b9d2 100644 --- a/source/core/rendering/fade.h +++ b/source/core/rendering/fade.h @@ -18,7 +18,7 @@ class Fade { void init(Uint8 r, Uint8 g, Uint8 b); // Inicializa las variables void render(); // Pinta una transición en pantalla - void update(); // Actualiza las variables internas + void update(float dt_s); // Actualiza las variables internas void activateFade(); // Activa el fade [[nodiscard]] auto hasEnded() const -> bool; // Comprueba si ha terminado la transicion @@ -34,7 +34,8 @@ class Fade { SDL_Renderer *renderer_ = nullptr; // El renderizador de la ventana SDL_Texture *backbuffer_ = nullptr; // Textura para usar como backbuffer Type fade_type_{Type::FULLSCREEN}; // Tipo de fade a realizar - Uint16 counter_ = 0; // Contador interno + Uint16 counter_ = 0; // Contador intern (frame-based) + float elapsed_s_ = 0.0F; // Acumulador de temps (time-based) bool enabled_ = false; // Indica si el fade está activo bool finished_ = false; // Indica si ha terminado la transición Uint8 r_ = 0, g_ = 0, b_ = 0; // Colores para el fade diff --git a/source/core/rendering/movingsprite.cpp b/source/core/rendering/movingsprite.cpp index 8a6ea30..a0cfe0a 100644 --- a/source/core/rendering/movingsprite.cpp +++ b/source/core/rendering/movingsprite.cpp @@ -34,22 +34,22 @@ void MovingSprite::clear() { center_ = nullptr; rotate_speed_ = 0; rotate_amount_ = 0.0; - counter_ = 0; current_flip_ = SDL_FLIP_NONE; } -// Mueve el sprite -void MovingSprite::move() { +// Mueve el sprite. vx_/vy_ en px/s, ax_/ay_ en px/s². Integració d'Euler +// senzilla — suficient per a moviments sense col·lisions sensibles. +void MovingSprite::move(float dt_s) { if (enabled_) { x_prev_ = x_; y_prev_ = y_; - x_ += vx_; - y_ += vy_; + x_ += vx_ * dt_s; + y_ += vy_ * dt_s; - vx_ += ax_; - vy_ += ay_; + vx_ += ax_ * dt_s; + vy_ += ay_ * dt_s; } } @@ -180,17 +180,6 @@ auto MovingSprite::getRotateSpeed() const -> Uint16 { return rotate_speed_; } -// Establece la rotacion -void MovingSprite::rotate() { - if (enabled_) { - if (rotate_enabled_) { - if (counter_ % rotate_speed_ == 0) { - incAngle(rotate_amount_); - } - } - } -} - // Establece el valor de la variable void MovingSprite::setRotate(bool value) { rotate_enabled_ = value; @@ -216,13 +205,15 @@ void MovingSprite::disableRotate() { angle_ = (double)0; } -// Actualiza las variables internas del objeto -void MovingSprite::update() { - move(); - rotate(); - - if (enabled_) { - ++counter_ %= 60000; +// Actualiza les variables internes (move + rotació integrada). La rotació +// frame-based original era `incAngle(rotate_amount_)` cada `rotate_speed_` +// frames a 60Hz, equivalent a velocitat angular constant +// = rotate_amount_ * 60 / rotate_speed_ graus/s. +void MovingSprite::update(float dt_s) { + move(dt_s); + if (enabled_ && rotate_enabled_) { + const double ANGULAR_VELOCITY_DEG_PER_S = rotate_amount_ * 60.0 / static_cast(rotate_speed_); + incAngle(ANGULAR_VELOCITY_DEG_PER_S * dt_s); } } diff --git a/source/core/rendering/movingsprite.h b/source/core/rendering/movingsprite.h index 5e3fdc8..b97d2b4 100644 --- a/source/core/rendering/movingsprite.h +++ b/source/core/rendering/movingsprite.h @@ -10,11 +10,10 @@ class MovingSprite : public Sprite { public: explicit MovingSprite(float x = 0, float y = 0, int w = 0, int h = 0, float velx = 0, float vely = 0, float accelx = 0, float accely = 0, Texture *texture = nullptr, SDL_Renderer *renderer = nullptr); // Constructor - void move(); // Mueve el sprite - void rotate(); // Rota el sprite - virtual void update(); // Actualiza las variables internas del objeto - void clear(); // Reinicia todas las variables - void render() override; // Muestra el sprite por pantalla + void move(float dt_s); // Mueve el sprite (vx/vy/ax/ay en px/s i px/s^2) + virtual void update(float dt_s); // Actualiza les variables internes (move + rotació integrada) + void clear(); // Reinicia todas las variables + void render() override; // Muestra el sprite por pantalla // cppcheck-suppress duplInheritedMember ; shadow intencional: Sprite::getPosX retorna int (sprites estàtics), MovingSprite::getPosX retorna float (sub-pixel). No s'accedeix via Sprite*: la jerarquia de joc treballa amb el tipus concret [[nodiscard]] auto getPosX() const -> float; // Obten el valor de la variable @@ -83,9 +82,8 @@ class MovingSprite : public Sprite { double angle_{0.0}; // Angulo para dibujarlo bool rotate_enabled_{false}; // Indica si ha de rotar - int rotate_speed_{0}; // Velocidad de giro - double rotate_amount_{0.0}; // Cantidad de grados a girar en cada iteración - int counter_{0}; // Contador interno + int rotate_speed_{0}; // Velocidad de giro (frames per pas de rotació al ritme de referència 60Hz) + double rotate_amount_{0.0}; // Cantidad de grados a girar en cada pas SDL_Point *center_{nullptr}; // Centro de rotación SDL_FlipMode current_flip_{SDL_FLIP_NONE}; // Indica como se voltea el sprite }; diff --git a/source/core/rendering/smartsprite.cpp b/source/core/rendering/smartsprite.cpp index e19e245..84ae5f2 100644 --- a/source/core/rendering/smartsprite.cpp +++ b/source/core/rendering/smartsprite.cpp @@ -20,21 +20,18 @@ void SmartSprite::init() { on_destination_ = false; dest_x_ = 0; dest_y_ = 0; - counter_ = 0; finished_ = false; } -// Actualiza la posición y comprueba si ha llegado a su destino -void SmartSprite::update() { +// La velocitat i acceleració són en px/s i px/s²; el temps de permanència +// després d'arribar al destí ve donat per setRemainingTime(). +void SmartSprite::update(float dt_s) { if (enabled_) { // NOLINTNEXTLINE(bugprone-parent-virtual-call): salt deliberat a l'avi — SmartSprite hereta d'AnimatedSprite només per reutilitzar API, però no usa animació de frames, així que es salta AnimatedSprite::update() (que cridaria animate()) - MovingSprite::update(); + MovingSprite::update(dt_s); - // Comprueba el movimiento checkMove(); - - // Comprueba si ha terminado - checkFinished(); + checkFinished(dt_s); } } @@ -56,6 +53,15 @@ void SmartSprite::setEnabledCounter(int value) { enabled_counter_ = value; } +// Time-based: temps de visibilitat post-arribada +void SmartSprite::setRemainingTime(float seconds) { + remaining_time_s_ = seconds; +} + +auto SmartSprite::getRemainingTime() const -> float { + return remaining_time_s_; +} + // Establece el valor de la variable void SmartSprite::setDestX(int x) { dest_x_ = x; @@ -129,16 +135,15 @@ void SmartSprite::checkMove() { } } -// Comprueba si ha terminado -void SmartSprite::checkFinished() { - // Comprueba si ha llegado a su destino +// Decrementa el temps restant cada crida si està al destí +void SmartSprite::checkFinished(float dt_s) { on_destination_ = getPosX() == dest_x_ && getPosY() == dest_y_; - if (on_destination_) { // Si esta en el destino comprueba su contador - if (enabled_counter_ == 0) { // Si ha llegado a cero, deshabilita el objeto y lo marca como finalizado + if (on_destination_) { + if (remaining_time_s_ <= 0.0F) { finished_ = true; - } else { // Si no ha llegado a cero, decrementa el contador - enabled_counter_--; + } else { + remaining_time_s_ -= dt_s; } } } diff --git a/source/core/rendering/smartsprite.h b/source/core/rendering/smartsprite.h index ceec070..8179bb3 100644 --- a/source/core/rendering/smartsprite.h +++ b/source/core/rendering/smartsprite.h @@ -10,27 +10,30 @@ class SmartSprite : public AnimatedSprite { public: SmartSprite(Texture *texture, SDL_Renderer *renderer); // Constructor - void init(); // Inicializa el objeto - void update() override; // Actualiza la posición y comprueba si ha llegado a su destino - void render() override; // Pinta el objeto en pantalla + void init(); // Inicializa el objeto + void update(float dt_s) override; // Actualiza la posicion + void render() override; // Pinta el objeto en pantalla - [[nodiscard]] auto getEnabledCounter() const -> int; // Obtiene el valor de la variable - void setEnabledCounter(int value); // Establece el valor de la variable - void setDestX(int x); // Establece el valor de la variable - void setDestY(int y); // Establece el valor de la variable - [[nodiscard]] auto getDestX() const -> int; // Obtiene el valor de la variable - [[nodiscard]] auto getDestY() const -> int; // Obtiene el valor de la variable - [[nodiscard]] auto isOnDestination() const -> bool; // Obtiene el valor de la variable - [[nodiscard]] auto hasFinished() const -> bool; // Obtiene el valor de la variable + [[nodiscard]] auto getEnabledCounter() const -> int; // Obtiene el valor de la variable + void setEnabledCounter(int value); // Establece el valor de la variable + void setRemainingTime(float seconds); // Time-based: temps que es queda visible despres d'arribar al desti + [[nodiscard]] auto getRemainingTime() const -> float; // Time-based: temps restant + void setDestX(int x); // Establece el valor de la variable + void setDestY(int y); // Establece el valor de la variable + [[nodiscard]] auto getDestX() const -> int; // Obtiene el valor de la variable + [[nodiscard]] auto getDestY() const -> int; // Obtiene el valor de la variable + [[nodiscard]] auto isOnDestination() const -> bool; // Obtiene el valor de la variable + [[nodiscard]] auto hasFinished() const -> bool; // Obtiene el valor de la variable private: // Variables - bool on_destination_; // Indica si está en el destino - int dest_x_; // Posicion de destino en el eje X - int dest_y_; // Posicion de destino en el eje Y - int enabled_counter_; // Contador para deshabilitarlo - bool finished_; // Indica si ya ha terminado + bool on_destination_; // Indica si está en el destino + int dest_x_; // Posicion de destino en el eje X + int dest_y_; // Posicion de destino en el eje Y + int enabled_counter_; // Contador (frames, derivat de remaining_time_s_ * 60) + float remaining_time_s_{0.0F}; // Temps restant per a deshabilitar-lo + bool finished_; // Indica si ya ha terminado - void checkMove(); // Comprueba el movimiento - void checkFinished(); // Comprueba si ha terminado + void checkMove(); // Comprueba el movimiento + void checkFinished(float dt_s); // Comprueba si ha terminado }; diff --git a/source/core/rendering/writer.cpp b/source/core/rendering/writer.cpp index 305fa3a..a16a8a6 100644 --- a/source/core/rendering/writer.cpp +++ b/source/core/rendering/writer.cpp @@ -7,30 +7,27 @@ Writer::Writer(Text *text) : text_(text) { } -// Actualiza el objeto -void Writer::update() { - if (enabled_) { - if (!completed_) { // No completado - if (writing_counter_ > 0) { - writing_counter_--; - } +// Avança un caracter cada `seconds_per_char_` i un cop completat es queda +// visible `remaining_time_s_` segons abans de finalitzar. +void Writer::update(float dt_s) { + if (!enabled_) { return; } - else if (writing_counter_ == 0) { - index_++; - writing_counter_ = speed_; - } - - if (index_ == length_) { - completed_ = true; - } + if (!completed_) { + char_timer_s_ += dt_s; + while (char_timer_s_ >= seconds_per_char_ && index_ < length_) { + char_timer_s_ -= seconds_per_char_; + ++index_; } + if (index_ >= length_) { + completed_ = true; + } + } - if (completed_) { // Completado - if (enabled_counter_ > 0) { - enabled_counter_--; - } else if (enabled_counter_ == 0) { - finished_ = true; - } + if (completed_) { + if (remaining_time_s_ <= 0.0F) { + finished_ = true; + } else { + remaining_time_s_ -= dt_s; } } } @@ -63,10 +60,10 @@ void Writer::setCaption(const std::string &text) { length_ = text.length(); } -// Establece el valor de la variable -void Writer::setSpeed(int value) { - speed_ = value; - writing_counter_ = value; +// Segons per caracter. Quan s'usa, l'update(dt) avança index. +void Writer::setSecondsPerChar(float seconds) { + seconds_per_char_ = seconds; + char_timer_s_ = 0.0F; } // Establece el valor de la variable @@ -79,14 +76,13 @@ auto Writer::isEnabled() const -> bool { return enabled_; } -// Establece el valor de la variable -void Writer::setEnabledCounter(int time) { - enabled_counter_ = time; +// Temps que es mante visible despres de completar el text. +void Writer::setRemainingTime(float seconds) { + remaining_time_s_ = seconds; } -// Obtiene el valor de la variable -auto Writer::getEnabledCounter() const -> int { - return enabled_counter_; +auto Writer::getRemainingTime() const -> float { + return remaining_time_s_; } // Centra la cadena de texto a un punto X diff --git a/source/core/rendering/writer.h b/source/core/rendering/writer.h index 2a01ff4..dda5bc8 100644 --- a/source/core/rendering/writer.h +++ b/source/core/rendering/writer.h @@ -8,19 +8,19 @@ class Writer { public: explicit Writer(Text *text); // Constructor - void update(); // Actualiza el objeto - void render(); // Dibuja el objeto en pantalla + void update(float dt_s); // Actualiza el objeto + void render(); // Dibuja el objeto en pantalla void setPosX(int value); // Establece el valor de la variable void setPosY(int value); // Establece el valor de la variable void setKerning(int value); // Establece el valor de la variable void setCaption(const std::string &text); // Establece el valor de la variable - void setSpeed(int value); // Establece el valor de la variable + void setSecondsPerChar(float seconds); // Segons per caracter void setEnabled(bool value); // Establece el valor de la variable [[nodiscard]] auto isEnabled() const -> bool; // Obtiene el valor de la variable - void setEnabledCounter(int time); // Establece el valor de la variable - [[nodiscard]] auto getEnabledCounter() const -> int; // Obtiene el valor de la variable + void setRemainingTime(float seconds); // Temps despres de completar + [[nodiscard]] auto getRemainingTime() const -> float; // Temps restant void center(int x); // Centra la cadena de texto a un punto X [[nodiscard]] auto hasFinished() const -> bool; // Obtiene el valor de la variable @@ -30,16 +30,16 @@ class Writer { Text *text_; // Objeto encargado de escribir el texto // Variables - int pos_x_{0}; // Posicion en el eje X donde empezar a escribir el texto - int pos_y_{0}; // Posicion en el eje Y donde empezar a escribir el texto - int kerning_{0}; // Kerning del texto, es decir, espaciado entre caracteres - std::string caption_; // El texto para escribir - int speed_{0}; // Velocidad de escritura - int writing_counter_{0}; // Temporizador de escritura para cada caracter - int index_{0}; // Posición del texto que se está escribiendo - int length_{0}; // Longitud de la cadena a escribir - bool completed_{false}; // Indica si se ha escrito todo el texto - bool enabled_{false}; // Indica si el objeto está habilitado - int enabled_counter_{0}; // Temporizador para deshabilitar el objeto - bool finished_{false}; // Indica si ya ha terminado + int pos_x_{0}; // Posicion en el eje X donde empezar a escribir el texto + int pos_y_{0}; // Posicion en el eje Y donde empezar a escribir el texto + int kerning_{0}; // Kerning del texto, es decir, espaciado entre caracteres + std::string caption_; // El texto para escribir + float seconds_per_char_{0.0F}; // Segons per caracter + float char_timer_s_{0.0F}; // Acumulador d'avanç de caracter + int index_{0}; // Posición del texto que se está escribiendo + int length_{0}; // Longitud de la cadena a escribir + bool completed_{false}; // Indica si se ha escrito todo el texto + bool enabled_{false}; // Indica si el objeto está habilitado + float remaining_time_s_{0.0F}; // Temps restant per a deshabilitar + bool finished_{false}; // Indica si ya ha terminado }; diff --git a/source/core/system/delta_time.cpp b/source/core/system/delta_time.cpp new file mode 100644 index 0000000..115a6a8 --- /dev/null +++ b/source/core/system/delta_time.cpp @@ -0,0 +1,22 @@ +#include "core/system/delta_time.hpp" + +#include + +namespace DeltaTime { + + namespace { + Uint64 last_time_ms = 0; + } + + void reset() { + last_time_ms = SDL_GetTicks(); + } + + auto tick() -> float { + const Uint64 NOW_MS = SDL_GetTicks(); + const float DELTA_S = static_cast(NOW_MS - last_time_ms) / 1000.0F; + last_time_ms = NOW_MS; + return DELTA_S; + } + +} // namespace DeltaTime diff --git a/source/core/system/delta_time.hpp b/source/core/system/delta_time.hpp new file mode 100644 index 0000000..351916c --- /dev/null +++ b/source/core/system/delta_time.hpp @@ -0,0 +1,20 @@ +#pragma once + +// Font única de delta_time per al joc. El loop principal NO té vsync ni +// gates: cada escena crida `tick()` al començament del seu iterate() i rep +// els segons reals transcorreguts des de l'última crida. Així el moviment és +// independent del framerate (visualment suau a 2000 FPS o a 60 FPS). +// +// `reset()` reinicia el rellotge intern: cal cridar-lo en cada canvi +// d'escena (després de càrregues llargues que podrien generar un primer +// delta enorme) i quan es reprèn d'una pausa. + +namespace DeltaTime { + + // Reinicia el rellotge a "ara". Cap delta acumulat del passat. + void reset(); + + // Retorna els segons des de l'última crida a `tick()` o `reset()`. + auto tick() -> float; + +} // namespace DeltaTime diff --git a/source/game/entities/balloon.cpp b/source/game/entities/balloon.cpp index 364fd95..4d03bec 100644 --- a/source/game/entities/balloon.cpp +++ b/source/game/entities/balloon.cpp @@ -1,6 +1,7 @@ #include "game/entities/balloon.h" -#include // for std::fabs +#include // for std::max +#include // for std::fabs #include "core/rendering/animatedsprite.h" // for AnimatedSprite #include "core/rendering/movingsprite.h" // for MovingSprite @@ -17,194 +18,144 @@ Balloon::Balloon(float x, float y, Uint8 kind, float velx, float speed, Uint16 c switch (kind) { case BALLOON_1: - // Alto y ancho del objeto width_ = WIDTH_1; height_ = WIDTH_1; size_ = SIZE_1; power_ = 1; - // Inicializa los valores de velocidad y gravedad - this->vel_x_ = velx; - vel_y_ = 0; - max_vel_y_ = 3.0F; - gravity_ = 0.09F; - default_vel_y_ = 2.6F; + vel_x_s_ = velx; + vel_y_s_ = 0.0F; + max_vel_y_s_ = 180.0F; + gravity_s_ = 324.0F; + default_vel_y_s_ = 156.0F; - // Puntos que da el globo al ser destruido score_ = SCORE_1; - - // Amenaza que genera el globo menace_ = 1; - break; case BALLOON_2: - // Alto y ancho del objeto width_ = WIDTH_2; height_ = WIDTH_2; size_ = SIZE_2; power_ = 3; - // Inicializa los valores de velocidad y gravedad - this->vel_x_ = velx; - vel_y_ = 0; - max_vel_y_ = 3.0F; - gravity_ = 0.10F; - default_vel_y_ = 3.5F; + vel_x_s_ = velx; + vel_y_s_ = 0.0F; + max_vel_y_s_ = 180.0F; + gravity_s_ = 360.0F; + default_vel_y_s_ = 210.0F; - // Puntos que da el globo al ser destruido score_ = SCORE_2; - - // Amenaza que genera el globo menace_ = 2; - break; case BALLOON_3: - // Alto y ancho del objeto width_ = WIDTH_3; height_ = WIDTH_3; size_ = SIZE_3; power_ = 7; - // Inicializa los valores de velocidad y gravedad - this->vel_x_ = velx; - vel_y_ = 0; - max_vel_y_ = 3.0F; - gravity_ = 0.10F; - default_vel_y_ = 4.50F; + vel_x_s_ = velx; + vel_y_s_ = 0.0F; + max_vel_y_s_ = 180.0F; + gravity_s_ = 360.0F; + default_vel_y_s_ = 270.0F; - // Puntos que da el globo al ser destruido score_ = SCORE_3; - - // Amenaza que genera el globo menace_ = 4; - break; case BALLOON_4: - // Alto y ancho del objeto width_ = WIDTH_4; height_ = WIDTH_4; size_ = SIZE_4; power_ = 15; - // Inicializa los valores de velocidad y gravedad - this->vel_x_ = velx; - vel_y_ = 0; - max_vel_y_ = 3.0F; - gravity_ = 0.10F; - default_vel_y_ = 4.95F; + vel_x_s_ = velx; + vel_y_s_ = 0.0F; + max_vel_y_s_ = 180.0F; + gravity_s_ = 360.0F; + default_vel_y_s_ = 297.0F; - // Puntos que da el globo al ser destruido score_ = SCORE_4; - - // Amenaza que genera el globo menace_ = 8; - break; case HEXAGON_1: - // Alto y ancho del objeto width_ = WIDTH_1; height_ = WIDTH_1; size_ = SIZE_1; power_ = 1; - // Inicializa los valores de velocidad y gravedad - this->vel_x_ = velx; - vel_y_ = std::fabs(velx) * 2; - max_vel_y_ = std::fabs(velx) * 2; - gravity_ = 0.00F; - default_vel_y_ = std::fabs(velx) * 2; + vel_x_s_ = velx; + vel_y_s_ = std::fabs(velx) * 2.0F; + max_vel_y_s_ = std::fabs(velx) * 2.0F; + gravity_s_ = 0.0F; + default_vel_y_s_ = std::fabs(velx) * 2.0F; - // Puntos que da el globo al ser destruido score_ = SCORE_1; - - // Amenaza que genera el globo menace_ = 1; - break; case HEXAGON_2: - // Alto y ancho del objeto width_ = WIDTH_2; height_ = WIDTH_2; size_ = SIZE_2; power_ = 3; - // Inicializa los valores de velocidad y gravedad - this->vel_x_ = velx; - vel_y_ = std::fabs(velx) * 2; - max_vel_y_ = std::fabs(velx) * 2; - gravity_ = 0.00F; - default_vel_y_ = std::fabs(velx) * 2; + vel_x_s_ = velx; + vel_y_s_ = std::fabs(velx) * 2.0F; + max_vel_y_s_ = std::fabs(velx) * 2.0F; + gravity_s_ = 0.0F; + default_vel_y_s_ = std::fabs(velx) * 2.0F; - // Puntos que da el globo al ser destruido score_ = SCORE_2; - - // Amenaza que genera el globo menace_ = 2; - break; case HEXAGON_3: - // Alto y ancho del objeto width_ = WIDTH_3; height_ = WIDTH_3; size_ = SIZE_3; power_ = 7; - // Inicializa los valores de velocidad y gravedad - this->vel_x_ = velx; - vel_y_ = std::fabs(velx) * 2; - max_vel_y_ = std::fabs(velx) * 2; - gravity_ = 0.00F; - default_vel_y_ = std::fabs(velx) * 2; + vel_x_s_ = velx; + vel_y_s_ = std::fabs(velx) * 2.0F; + max_vel_y_s_ = std::fabs(velx) * 2.0F; + gravity_s_ = 0.0F; + default_vel_y_s_ = std::fabs(velx) * 2.0F; - // Puntos que da el globo al ser destruido score_ = SCORE_3; - - // Amenaza que genera el globo menace_ = 4; - break; case HEXAGON_4: - // Alto y ancho del objeto width_ = WIDTH_4; height_ = WIDTH_4; size_ = SIZE_4; power_ = 15; - // Inicializa los valores de velocidad y gravedad - this->vel_x_ = velx; - vel_y_ = std::fabs(velx) * 2; - max_vel_y_ = std::fabs(velx) * 2; - gravity_ = 0.00F; - default_vel_y_ = std::fabs(velx) * 2; + vel_x_s_ = velx; + vel_y_s_ = std::fabs(velx) * 2.0F; + max_vel_y_s_ = std::fabs(velx) * 2.0F; + gravity_s_ = 0.0F; + default_vel_y_s_ = std::fabs(velx) * 2.0F; - // Puntos que da el globo al ser destruido score_ = SCORE_4; - - // Amenaza que genera el globo menace_ = 8; - break; case POWER_BALL: - // Alto y ancho del objeto width_ = WIDTH_4; height_ = WIDTH_4; size_ = 4; power_ = 0; - // Inicializa los valores de velocidad y gravedad - this->vel_x_ = velx; - vel_y_ = 0; - max_vel_y_ = 3.0F; - gravity_ = 0.10F; - default_vel_y_ = 4.95F; + vel_x_s_ = velx; + vel_y_s_ = 0.0F; + max_vel_y_s_ = 180.0F; + gravity_s_ = 360.0F; + default_vel_y_s_ = 297.0F; // Puntos que da el globo al ser destruido score_ = 0; @@ -217,7 +168,7 @@ Balloon::Balloon(float x, float y, Uint8 kind, float velx, float speed, Uint16 c // sempre que canvia. setRotateSpeed(1) evita la UB del `counter_ // % 0` dins MovingSprite::rotate(). sprite_->setRotateSpeed(1); - sprite_->setRotateAmount(vel_x_ > 0.0F ? 2.0 : -2.0); + sprite_->setRotateAmount(vel_x_s_ > 0.0F ? 2.0 : -2.0); break; @@ -257,18 +208,21 @@ Balloon::Balloon(float x, float y, Uint8 kind, float velx, float speed, Uint16 c // Inicializa variables stopped_ = true; stopped_counter_ = 0; + stopped_counter_s_ = 0.0F; blinking_ = false; visible_ = true; creation_counter_ = creationtimer; creation_counter_ini_ = creationtimer; + creation_counter_s_ = static_cast(creationtimer) / 60.0F; + creation_counter_ini_s_ = creation_counter_s_; + creation_phase_s_ = 0.0F; + bounce_phase_s_ = 0.0F; popping_ = false; // Valores iniciales dependentes del timer being_created_ = creation_counter_ != 0; invulnerable_ = being_created_; - counter_ = 0; - travel_y_ = 1.0F; this->speed_ = speed; // Tipo @@ -334,87 +288,38 @@ void Balloon::render() { } } -// Actualiza la posición y estados del globo -void Balloon::move() { - // Comprueba si se puede mover +// Actualiza la posición y estados del globo. Integració contínua: gravetat i +// posició s'apliquen escalades per `speed_` (tempo del joc) cada tick. +void Balloon::move(float dt_s) { if (!isStopped()) { - // Lo mueve a izquierda o derecha - pos_x_ += (vel_x_ * speed_); + // Eix X + pos_x_ += vel_x_s_ * speed_ * dt_s; - // Si queda fuera de pantalla, corregimos su posición y cambiamos su sentido if ((pos_x_ < PLAY_AREA_LEFT) || (pos_x_ + width_ > PLAY_AREA_RIGHT)) { - // Corrige posición - pos_x_ -= (vel_x_ * speed_); - - // Invierte sentido - vel_x_ = -vel_x_; - - // Invierte la rotación + pos_x_ -= vel_x_s_ * speed_ * dt_s; + vel_x_s_ = -vel_x_s_; sprite_->switchRotate(); - - // Activa el efecto de rebote - if (kind_ != POWER_BALL) { - bounceStart(); - } + if (kind_ != POWER_BALL) { bounceStart(); } } - // Mueve el globo hacia arriba o hacia abajo - pos_y_ += (vel_y_ * speed_); + // Eix Y + pos_y_ += vel_y_s_ * speed_ * dt_s; - // Si se sale por arriba if (pos_y_ < PLAY_AREA_TOP) { - // Corrige pos_y_ = PLAY_AREA_TOP; - - // Invierte sentido - vel_y_ = -vel_y_; - - // Activa el efecto de rebote - if (kind_ != POWER_BALL) { - bounceStart(); - } + vel_y_s_ = -vel_y_s_; + if (kind_ != POWER_BALL) { bounceStart(); } } - // Si el globo se sale por la parte inferior if (pos_y_ + height_ > PLAY_AREA_BOTTOM) { - // Corrige pos_y_ = PLAY_AREA_BOTTOM - height_; - - // Invierte colocando una velocidad por defecto - vel_y_ = -default_vel_y_; - - // Activa el efecto de rebote - if (kind_ != POWER_BALL) { - bounceStart(); - } + vel_y_s_ = -default_vel_y_s_; + if (kind_ != POWER_BALL) { bounceStart(); } } - /* + // Gravetat contínua (el tempo `speed_` escala també la gravetat). + vel_y_s_ += gravity_s_ * speed_ * dt_s; - Para aplicar la gravedad, el diseño original la aplicaba en cada iteración del bucle - Al añadir el modificador de velocidad se reduce la distancia que recorre el objeto y por - tanto recibe mas gravedad. Para solucionarlo se va a aplicar la gravedad cuando se haya - recorrido una distancia igual a la velocidad en Y, que era el cálculo inicial - - */ - - // Incrementa la variable que calcula la distancia acumulada en Y - travel_y_ += speed_; - - // Si la distancia acumulada en Y es igual a la velocidad, se aplica la gravedad - if (travel_y_ >= 1.0F) { - // Quita el excedente - travel_y_ -= 1.0F; - - // Aplica la gravedad al objeto sin pasarse de una velocidad máxima - vel_y_ += gravity_; - - // Al parecer esta asignación se quedó sin hacer y ahora el juego no funciona - // correctamente si se aplica, así que se deja sin efecto - // velY = std::min(velY, maxVelY); - } - - // Actualiza la posición del sprite sprite_->setPosX(getPosX()); sprite_->setPosY(getPosY()); } @@ -427,16 +332,19 @@ void Balloon::disable() { collider_.r = 0; collider_.x = 0; collider_.y = 0; - counter_ = 0; creation_counter_ = 0; creation_counter_ini_ = 0; - default_vel_y_ = 0.0F; + creation_counter_s_ = 0.0F; + creation_counter_ini_s_ = 0.0F; + creation_phase_s_ = 0.0F; + bounce_phase_s_ = 0.0F; + default_vel_y_s_ = 0.0F; enabled_ = false; - gravity_ = 0.0F; + gravity_s_ = 0.0F; height_ = 0; invulnerable_ = false; kind_ = 0; - max_vel_y_ = 0.0F; + max_vel_y_s_ = 0.0F; menace_ = 0; popping_ = false; pos_x_ = 0.0F; @@ -447,9 +355,9 @@ void Balloon::disable() { speed_ = 0; stopped_ = false; stopped_counter_ = 0; - travel_y_ = 0; - vel_x_ = 0.0F; - vel_y_ = 0.0F; + stopped_counter_s_ = 0.0F; + vel_x_s_ = 0.0F; + vel_y_s_ = 0.0F; visible_ = false; width_ = 0; sprite_->clear(); @@ -466,30 +374,29 @@ void Balloon::pop() { } // Actualiza al globo a su posicion, animación y controla los contadores -void Balloon::update() { +void Balloon::update(float dt_s) { if (enabled_) { - sprite_->MovingSprite::update(); - move(); - updateAnimation(); + // MovingSprite::update(dt_s) avança la rotació (entre altres). La posició + // del sprite la posa move(dt_s) directament des de pos_x_/pos_y_. + sprite_->MovingSprite::update(dt_s); + move(dt_s); + updateAnimation(dt_s); updateColliders(); - updateState(); - updateBounce(); - counter_++; + updateState(dt_s); + updateBounce(dt_s); } } // Actualiza los estados del globo -void Balloon::updateState() { +void Balloon::updateState(float dt_s) { if (isPopping()) { updateStatePopping(); } if (isBeingCreated()) { - updateStateBeingCreated(); - } - // Solo comprueba el estado detenido cuando no se está creando - else if (isStopped()) { - updateStateStopped(); + updateStateBeingCreated(dt_s); + } else if (isStopped()) { + updateStateStopped(dt_s); } } @@ -502,58 +409,54 @@ void Balloon::updateStatePopping() { } } -// Rama de updateState: globo creándose -void Balloon::updateStateBeingCreated() { +// Rama de updateState: globo creándose. Manté el chunk pattern original: +// cada CREATION_STEP_S s'aplica un step de drift (equivalent a "cada 10 +// frames" del codi original). El drift en X usa vel_x_s_/60 per a obtenir +// el mateix delta px-per-step. +void Balloon::updateStateBeingCreated(float dt_s) { setStop(true); setInvulnerable(true); - // Todavia tiene tiempo en el contador - if (creation_counter_ > 0) { - // Desplaza lentamente el globo hacia abajo y hacia un lado - if (creation_counter_ % 10 == 0) { - pos_y_++; - pos_x_ += vel_x_; + if (creation_counter_s_ > 0.0F) { + creation_phase_s_ += dt_s; + while (creation_phase_s_ >= CREATION_STEP_S) { + creation_phase_s_ -= CREATION_STEP_S; + pos_y_ += 1.0F; + const float DRIFT_X = vel_x_s_ / 60.0F; + pos_x_ += DRIFT_X; - // Comprueba no se salga por los laterales if ((pos_x_ < PLAY_AREA_LEFT) || (pos_x_ > (PLAY_AREA_RIGHT - width_))) { - // Corrige y cambia el sentido de la velocidad - pos_x_ -= vel_x_; - vel_x_ = -vel_x_; + pos_x_ -= DRIFT_X; + vel_x_s_ = -vel_x_s_; } - // Actualiza la posición del sprite sprite_->setPosX(getPosX()); sprite_->setPosY(getPosY()); - - // Actualiza la posición del circulo de colisión updateColliders(); } - creation_counter_--; - } - // El contador ha llegado a cero - else { + creation_counter_s_ = std::max(0.0F, creation_counter_s_ - dt_s); + creation_counter_ = static_cast(creation_counter_s_ * 60.0F); + } else { setBeingCreated(false); - setStop(false); // reactiva la rotació de la PowerBall si escau + setStop(false); setVisible(true); setInvulnerable(false); } } // Rama de updateState: globo detenido (no creándose) -void Balloon::updateStateStopped() { - // Reduce el contador - if (stopped_counter_ > 0) { - stopped_counter_--; - } - // Quitarles el estado "detenido" si no estan explosionando - else if (!isPopping()) { - setStop(false); // reactiva la rotació de la PowerBall si escau +void Balloon::updateStateStopped(float dt_s) { + if (stopped_counter_s_ > 0.0F) { + stopped_counter_s_ = std::max(0.0F, stopped_counter_s_ - dt_s); + stopped_counter_ = static_cast(stopped_counter_s_ * 60.0F); + } else if (!isPopping()) { + setStop(false); } } // Establece la animación correspondiente al estado -void Balloon::updateAnimation() { +void Balloon::updateAnimation(float dt_s) { std::string creating_animation = "blue"; std::string normal_animation = "orange"; @@ -565,7 +468,6 @@ void Balloon::updateAnimation() { normal_animation = "green"; } - // Establece el frame de animación if (isPopping()) { sprite_->setCurrentAnimation("pop"); } else if (isBeingCreated()) { @@ -574,7 +476,7 @@ void Balloon::updateAnimation() { sprite_->setCurrentAnimation(normal_animation); } - sprite_->animate(); + sprite_->animate(dt_s); } // Comprueba si el globo está habilitado @@ -592,11 +494,6 @@ auto Balloon::getPosY() const -> float { return pos_y_; } -// Obtiene del valor de la variable -auto Balloon::getVelY() const -> float { - return vel_y_; -} - // Obtiene del valor de la variable auto Balloon::getWidth() const -> int { return width_; @@ -607,9 +504,9 @@ auto Balloon::getHeight() const -> int { return height_; } -// Establece el valor de la variable +// Establece el valor de la variable (px/s) void Balloon::setVelY(float vel_y) { - this->vel_y_ = vel_y; + this->vel_y_s_ = vel_y; } // Establece el valor de la variable @@ -709,6 +606,7 @@ auto Balloon::isPopping() const -> bool { // Establece el valor de la variable void Balloon::setStoppedTimer(Uint16 time) { stopped_counter_ = time; + stopped_counter_s_ = static_cast(time) / 60.0F; } // Obtiene del valor de la variable @@ -766,17 +664,27 @@ void Balloon::bounceStop() { bouncing_.desp_y = 0.0F; } -void Balloon::updateBounce() { - if (bouncing_.enabled) { - bouncing_.zoom_width = bouncing_.w[bouncing_.counter / bouncing_.speed]; - bouncing_.zoom_height = bouncing_.h[bouncing_.counter / bouncing_.speed]; - sprite_->setZoomW(bouncing_.zoom_width); - sprite_->setZoomH(bouncing_.zoom_height); - bouncing_.desp_x = (sprite_->getSpriteClip().w - (sprite_->getSpriteClip().w * bouncing_.zoom_width)); - bouncing_.desp_y = (sprite_->getSpriteClip().h - (sprite_->getSpriteClip().h * bouncing_.zoom_height)); +void Balloon::updateBounce(float dt_s) { + if (!bouncing_.enabled) { return; } + + bounce_phase_s_ += dt_s; + const float STEP_S = static_cast(bouncing_.speed) * BOUNCE_STEP_S; + while (bounce_phase_s_ >= STEP_S) { + bounce_phase_s_ -= STEP_S; bouncing_.counter++; - if ((bouncing_.counter / bouncing_.speed) > (MAX_BOUNCE - 1)) { - bounceStop(); - } } -} \ No newline at end of file + + const int IDX = bouncing_.counter / bouncing_.speed; + if (IDX > (MAX_BOUNCE - 1)) { + bounceStop(); + bounce_phase_s_ = 0.0F; + return; + } + + bouncing_.zoom_width = bouncing_.w[IDX]; + bouncing_.zoom_height = bouncing_.h[IDX]; + sprite_->setZoomW(bouncing_.zoom_width); + sprite_->setZoomH(bouncing_.zoom_height); + bouncing_.desp_x = (sprite_->getSpriteClip().w - (sprite_->getSpriteClip().w * bouncing_.zoom_width)); + bouncing_.desp_y = (sprite_->getSpriteClip().h - (sprite_->getSpriteClip().h * bouncing_.zoom_height)); +} diff --git a/source/game/entities/balloon.h b/source/game/entities/balloon.h index 6a1e658..457550b 100644 --- a/source/game/entities/balloon.h +++ b/source/game/entities/balloon.h @@ -39,9 +39,9 @@ class Balloon { static constexpr int BALLOON_CLASS = 0; static constexpr int HEXAGON_CLASS = 1; - // Velocidad del globo - static constexpr float VELX_POSITIVE = 0.7F; - static constexpr float VELX_NEGATIVE = -0.7F; + // Velocitat horitzontal en px/s (era 0.7 px/frame * 60). + static constexpr float VELX_POSITIVE = 42.0F; + static constexpr float VELX_NEGATIVE = -42.0F; // Velocidades a las que se mueven los globos static constexpr float SPEED_1 = 0.60F; @@ -66,12 +66,12 @@ class Balloon { Balloon(const Balloon &) = delete; auto operator=(const Balloon &) -> Balloon & = delete; - void allignTo(int x); // Centra el globo en la posición X - void render(); // Pinta el globo en la pantalla - void move(); // Actualiza la posición y estados del globo - void disable(); // Deshabilita el globo y pone a cero todos los valores - void pop(); // Explosiona el globo - void update(); // Actualiza al globo a su posicion, animación y controla los contadores + void allignTo(int x); // Centra el globo en la posición X + void render(); // Pinta el globo en la pantalla + void move(float dt_s); // Actualiza la posición y estados del globo + void disable(); // Deshabilita el globo y pone a cero todos los valores + void pop(); // Explosiona el globo + void update(float dt_s); // Actualiza al globo [[nodiscard]] auto isEnabled() const -> bool; // Comprueba si el globo está habilitado [[nodiscard]] auto isStopped() const -> bool; // Obtiene del valor de la variable @@ -83,7 +83,6 @@ class Balloon { [[nodiscard]] auto getPosX() const -> float; // Obtiene del valor de la variable [[nodiscard]] auto getPosY() const -> float; // Obtiene del valor de la variable - [[nodiscard]] auto getVelY() const -> float; // Obtiene del valor de la variable [[nodiscard]] auto getWidth() const -> int; // Obtiene del valor de la variable [[nodiscard]] auto getHeight() const -> int; // Obtiene del valor de la variable [[nodiscard]] auto getKind() const -> int; // Obtiene del valor de la variable @@ -109,6 +108,11 @@ class Balloon { // Cantidad de elementos del vector con los valores de la deformación del globo al rebotar static constexpr int MAX_BOUNCE = 10; + // Time-based: la creació "desplaça" el globus cada 10 frames a 60Hz ⇒ 1/6 s. + static constexpr float CREATION_STEP_S = 10.0F / 60.0F; + // Time-based: el bounce avança w/h cada `speed_` frames a 60Hz; mantenim el mateix temps per pas. + static constexpr float BOUNCE_STEP_S = 1.0F / 60.0F; // 1 frame + // Estructura para las variables para el efecto de los rebotes struct Bouncing { bool enabled; // Si el efecto está activo @@ -126,47 +130,50 @@ class Balloon { AnimatedSprite *sprite_; // Sprite del objeto globo // Variables - float pos_x_; // Posición en el eje X - float pos_y_; // Posición en el eje Y - Uint8 width_; // Ancho - Uint8 height_; // Alto - float vel_x_; // Velocidad en el eje X. Cantidad de pixeles a desplazarse - float vel_y_; // Velocidad en el eje Y. Cantidad de pixeles a desplazarse - float gravity_; // Aceleración en el eje Y. Modifica la velocidad - float default_vel_y_; // Velocidad inicial que tienen al rebotar contra el suelo - float max_vel_y_; // Máxima velocidad que puede alcanzar el objeto en el eje Y - bool being_created_; // Indica si el globo se está creando - bool blinking_; // Indica si el globo está intermitente - bool enabled_; // Indica si el globo esta activo - bool invulnerable_; // Indica si el globo es invulnerable - bool popping_; // Indica si el globo está explotando - bool stopped_; // Indica si el globo está parado - bool visible_; // Indica si el globo es visible - Circle collider_; // Circulo de colisión del objeto - Uint16 creation_counter_; // Temporizador para controlar el estado "creandose" - Uint16 creation_counter_ini_; // Valor inicial para el temporizador para controlar el estado "creandose" - Uint16 score_; // Puntos que da el globo al ser destruido - Uint16 stopped_counter_; // Contador para controlar el estado "parado" - Uint8 kind_; // Tipo de globo - Uint8 menace_; // Cantidad de amenaza que genera el globo - Uint32 counter_; // Contador interno - float travel_y_; // Distancia que ha de recorrer el globo en el eje Y antes de que se le aplique la gravedad - float speed_; // Velocidad a la que se mueven los globos - Uint8 size_; // Tamaño del globo - Uint8 power_; // Cantidad de poder que alberga el globo - Bouncing bouncing_; // Contiene las variables para el efecto de rebote + float pos_x_; // Posición en el eje X + float pos_y_; // Posición en el eje Y + Uint8 width_; // Ancho + Uint8 height_; // Alto + float vel_x_s_{0.0F}; // Velocidad en X (px/s) + float vel_y_s_{0.0F}; // Velocidad en Y (px/s) + float gravity_s_{0.0F}; // Aceleración Y (px/s²) + float default_vel_y_s_{0.0F}; // Velocitat inicial al rebotar (px/s) + float max_vel_y_s_{0.0F}; // Velocitat màxima en Y (px/s, no aplicada en time-based actual) + bool being_created_; // Indica si el globo se está creando + bool blinking_; // Indica si el globo está intermitente + bool enabled_; // Indica si el globo esta activo + bool invulnerable_; // Indica si el globo es invulnerable + bool popping_; // Indica si el globo está explotando + bool stopped_; // Indica si el globo está parado + bool visible_; // Indica si el globo es visible + Circle collider_; // Circulo de colisión del objeto + Uint16 creation_counter_; // Temporizador (frames, derivat de creation_counter_s_ per a render alpha) + Uint16 creation_counter_ini_; // Valor inicial del temporizador (frames) + float creation_counter_s_{0.0F}; // Temporizador (font de veritat, segons) + float creation_counter_ini_s_{0.0F}; // Valor inicial del temporizador (segons) + float creation_phase_s_{0.0F}; // Acumulador de fase per als steps de creació + Uint16 score_; // Puntos que da el globo al ser destruido + Uint16 stopped_counter_; // Contador (frames, derivat de stopped_counter_s_) + float stopped_counter_s_{0.0F}; // Contador (font de veritat, segons) + Uint8 kind_; // Tipo de globo + Uint8 menace_; // Cantidad de amenaza que genera el globo + float speed_; // Tempo del joc (multiplicador adimensional) + Uint8 size_; // Tamaño del globo + Uint8 power_; // Cantidad de poder que alberga el globo + Bouncing bouncing_; // Contiene las variables para el efecto de rebote + float bounce_phase_s_{0.0F}; // Fase del bounce void updateColliders(); // Alinea el circulo de colisión con la posición del objeto globo void bounceStart(); // Activa el efecto void bounceStop(); // Detiene el efecto - void updateBounce(); // Aplica el efecto - void updateAnimation(); // Establece la animación correspondiente + void updateBounce(float dt_s); // Aplica el efecto + void updateAnimation(float dt_s); // Establece la animación correspondiente void setBeingCreated(bool value); // Establece el valor de la variable - void updateState(); // Actualiza los estados del globo + void updateState(float dt_s); // Actualiza los estados del globo // Helpers de updateState, uno por cada rama de estado void updateStatePopping(); - void updateStateBeingCreated(); - void updateStateStopped(); + void updateStateBeingCreated(float dt_s); + void updateStateStopped(float dt_s); }; diff --git a/source/game/entities/bullet.cpp b/source/game/entities/bullet.cpp index cf031fa..8386823 100644 --- a/source/game/entities/bullet.cpp +++ b/source/game/entities/bullet.cpp @@ -11,13 +11,15 @@ Bullet::Bullet(int x, int y, Bullet::Kind kind, bool powered_up, int owner, Text // Posición inicial del objeto pos_x_ = x; pos_y_ = y; + pos_x_f_ = static_cast(x); + pos_y_f_ = static_cast(y); // Alto y ancho del objeto width_ = 10; height_ = 10; // Velocidad inicial en el eje Y - vel_y_ = -3; + vel_y_s_ = VEL_Y_PX_PER_S; // Tipo de bala this->kind_ = kind; @@ -29,7 +31,7 @@ Bullet::Bullet(int x, int y, Bullet::Kind kind, bool powered_up, int owner, Text switch (kind) { case Bullet::Kind::UP: // Establece la velocidad inicial - vel_x_ = 0; + vel_x_s_ = 0.0F; // Rectangulo con los gráficos del objeto if (!powered_up) { @@ -41,7 +43,7 @@ Bullet::Bullet(int x, int y, Bullet::Kind kind, bool powered_up, int owner, Text case Bullet::Kind::LEFT: // Establece la velocidad inicial - vel_x_ = -2; + vel_x_s_ = VEL_X_LEFT_PX_PER_S; // Rectangulo con los gráficos del objeto if (!powered_up) { @@ -53,7 +55,7 @@ Bullet::Bullet(int x, int y, Bullet::Kind kind, bool powered_up, int owner, Text case Bullet::Kind::RIGHT: // Establece la velocidad inicial - vel_x_ = 2; + vel_x_s_ = VEL_X_RIGHT_PX_PER_S; // Rectangulo con los gráficos del objeto if (!powered_up) { @@ -84,40 +86,29 @@ void Bullet::render() { sprite_->render(); } -// Actualiza la posición y estado del objeto en horizontal -auto Bullet::move() -> MoveResult { - // Variable con el valor de retorno +// Actualiza la posición y estado del objeto +auto Bullet::move(float dt_s) -> MoveResult { MoveResult msg = MoveResult::OK; - // Mueve el objeto a su nueva posición - pos_x_ += vel_x_; + pos_x_f_ += vel_x_s_ * dt_s; + pos_x_ = static_cast(pos_x_f_); - // Si el objeto se sale del area de juego por los laterales if ((pos_x_ < PLAY_AREA_LEFT - width_) || (pos_x_ > PLAY_AREA_RIGHT)) { - // Se deshabilita kind_ = Bullet::Kind::NONE; - - // Mensaje de salida msg = MoveResult::OUT; } - // Mueve el objeto a su nueva posición en vertical - pos_y_ += vel_y_; + pos_y_f_ += vel_y_s_ * dt_s; + pos_y_ = static_cast(pos_y_f_); - // Si el objeto se sale del area de juego por la parte superior if (pos_y_ < PLAY_AREA_TOP - height_) { - // Se deshabilita kind_ = Bullet::Kind::NONE; - - // Mensaje de salida msg = MoveResult::OUT; } - // Actualiza la posición del sprite sprite_->setPosX(pos_x_); sprite_->setPosY(pos_y_); - // Alinea el circulo de colisión con el objeto shiftColliders(); return msg; @@ -146,16 +137,13 @@ auto Bullet::getPosY() const -> int { // Establece el valor de la variable void Bullet::setPosX(int x) { pos_x_ = x; + pos_x_f_ = static_cast(x); } // Establece el valor de la variable void Bullet::setPosY(int y) { pos_y_ = y; -} - -// Obtiene el valor de la variable -auto Bullet::getVelY() const -> int { - return vel_y_; + pos_y_f_ = static_cast(y); } // Obtiene el valor de la variable diff --git a/source/game/entities/bullet.h b/source/game/entities/bullet.h index d3cfa84..e31aaf5 100644 --- a/source/game/entities/bullet.h +++ b/source/game/entities/bullet.h @@ -32,14 +32,13 @@ class Bullet { Bullet(const Bullet &) = delete; auto operator=(const Bullet &) -> Bullet & = delete; - void render(); // Pinta el objeto en pantalla - auto move() -> MoveResult; // Actualiza la posición y estado del objeto - void disable(); // Deshabilita el objeto + void render(); // Pinta el objeto en pantalla + auto move(float dt_s) -> MoveResult; // Actualiza la posición y estado del objeto + void disable(); // Deshabilita el objeto [[nodiscard]] auto isEnabled() const -> bool; // Comprueba si el objeto está habilitado [[nodiscard]] auto getPosX() const -> int; // Obtiene el valor de la variable [[nodiscard]] auto getPosY() const -> int; // Obtiene el valor de la variable - [[nodiscard]] auto getVelY() const -> int; // Obtiene el valor de la variable [[nodiscard]] auto getKind() const -> Kind; // Obtiene el valor de la variable [[nodiscard]] auto getOwner() const -> int; // Obtiene el valor de la variable @@ -49,19 +48,26 @@ class Bullet { auto getCollider() -> Circle &; // Obtiene el circulo de colisión private: + // Velocitats en px/s (derivades de les antigues px/frame * 60). + static constexpr float VEL_Y_PX_PER_S = -180.0F; // Era -3 px/frame + static constexpr float VEL_X_LEFT_PX_PER_S = -120.0F; // Era -2 px/frame + static constexpr float VEL_X_RIGHT_PX_PER_S = 120.0F; // Era +2 px/frame + // Objetos y punteros Sprite *sprite_; // Sprite con los graficos y métodos de pintado // Variables - int pos_x_; // Posición en el eje X - int pos_y_; // Posición en el eje Y - Uint8 width_; // Ancho del objeto - Uint8 height_; // Alto del objeto - int vel_x_; // Velocidad en el eje X - int vel_y_; // Velocidad en el eje Y - Kind kind_; // Tipo de objeto - int owner_; // Identificador del dueño del objeto - Circle collider_; // Circulo de colisión del objeto + int pos_x_; // Posición en el eje X (px enters per al sprite/collider) + int pos_y_; // Posición en el eje Y + float pos_x_f_{0}; // Acumulador subpíxel + float pos_y_f_{0}; // Acumulador subpíxel + Uint8 width_; // Ancho del objeto + Uint8 height_; // Alto del objeto + float vel_x_s_{0}; // Velocidad en el eje X (px/s) + float vel_y_s_{0}; // Velocidad en el eje Y (px/s) + Kind kind_; // Tipo de objeto + int owner_; // Identificador del dueño del objeto + Circle collider_; // Circulo de colisión del objeto void shiftColliders(); // Alinea el circulo de colisión con el objeto }; diff --git a/source/game/entities/item.cpp b/source/game/entities/item.cpp index 234bd22..c5a2c13 100644 --- a/source/game/entities/item.cpp +++ b/source/game/entities/item.cpp @@ -1,6 +1,7 @@ #include "game/entities/item.h" -#include // for rand +#include // for max +#include // for rand #include "core/rendering/animatedsprite.h" // for AnimatedSprite #include "game/defaults.hpp" // for PLAY_AREA_LEFT, PLAY_AREA_RIGHT, PLAY_AR... @@ -13,7 +14,8 @@ Item::Item(Id id, float x, float y, Texture *texture, const std::vectorid_ = id; enabled_ = true; time_to_live_ = 600; - accel_x_ = 0.0F; + time_to_live_s_ = TIME_TO_LIVE_S; + accel_x_s_ = 0.0F; floor_collision_ = false; if (id == Item::Id::COFFEE_MACHINE) { @@ -21,18 +23,20 @@ Item::Item(Id id, float x, float y, Texture *texture, const std::vector(RAND_STEP)) * ITEM_VEL_X_STEP_PX_PER_S; + vel_y_s_ = ITEM_VEL_Y_PX_PER_S; + accel_y_s_ = ITEM_ACCEL_Y_PX_PER_S2; collider_.r = width_ / 2; } @@ -74,49 +78,41 @@ void Item::render() { } // Actualiza la posición y estados del objeto -void Item::move() { +void Item::move(float dt_s) { floor_collision_ = false; - // Calcula la nueva posición - pos_x_ += vel_x_; - pos_y_ += vel_y_; + // Posició + pos_x_ += vel_x_s_ * dt_s; + pos_y_ += vel_y_s_ * dt_s; - // Aplica las aceleraciones a la velocidad - vel_x_ += accel_x_; - vel_y_ += accel_y_; + // Acceleració + vel_x_s_ += accel_x_s_ * dt_s; + vel_y_s_ += accel_y_s_ * dt_s; - // Si queda fuera de pantalla, corregimos su posición y cambiamos su sentido + // Si surt per laterals, corregeix i inverteix if ((pos_x_ < PLAY_AREA_LEFT) || (pos_x_ + width_ > PLAY_AREA_RIGHT)) { - // Corregir posición - pos_x_ -= vel_x_; - - // Invertir sentido - vel_x_ = -vel_x_; + pos_x_ -= vel_x_s_ * dt_s; + vel_x_s_ = -vel_x_s_; } - // Si se sale por arriba rebota (excepto la maquina de café) + // Rebot per dalt (excepte la màquina de cafè) if ((pos_y_ < PLAY_AREA_TOP) && !(id_ == Item::Id::COFFEE_MACHINE)) { - // Corrige - pos_y_ -= vel_y_; - - // Invierte el sentido - vel_y_ = -vel_y_; + pos_y_ -= vel_y_s_ * dt_s; + vel_y_s_ = -vel_y_s_; } - // Si el objeto se sale por la parte inferior + // Topa amb el terra if (pos_y_ + height_ > PLAY_AREA_BOTTOM) { - // Detiene el objeto - vel_y_ = 0; - vel_x_ = 0; - accel_x_ = 0; - accel_y_ = 0; + vel_y_s_ = 0; + vel_x_s_ = 0; + accel_x_s_ = 0; + accel_y_s_ = 0; pos_y_ = PLAY_AREA_BOTTOM - height_; if (id_ == Item::Id::COFFEE_MACHINE) { floor_collision_ = true; } } - // Actualiza la posición del sprite sprite_->setPosX(int(pos_x_)); sprite_->setPosY(int(pos_y_)); shiftColliders(); @@ -128,18 +124,22 @@ void Item::disable() { } // Actualiza el objeto a su posicion, animación y controla los contadores -void Item::update() { - move(); - sprite_->animate(); - updateTimeToLive(); +void Item::update(float dt_s) { + move(dt_s); + sprite_->animate(dt_s); + updateTimeToLive(dt_s); checkTimeToLive(); } -// Actualiza el contador -void Item::updateTimeToLive() { - if (time_to_live_ > 0) { - time_to_live_--; +// Actualiza el contador. Manté time_to_live_ (frames) sincronitzat amb el +// segons per a que render() segueixi funcionant amb la mateixa condició de +// parpelleig. +void Item::updateTimeToLive(float dt_s) { + if (time_to_live_s_ > 0.0F) { + time_to_live_s_ = std::max(0.0F, time_to_live_s_ - dt_s); } + constexpr float FRAMES_PER_S = 60.0F; + time_to_live_ = static_cast(time_to_live_s_ * FRAMES_PER_S); } // Comprueba si el objeto sigue vivo diff --git a/source/game/entities/item.h b/source/game/entities/item.h index 62db6eb..9d427d8 100644 --- a/source/game/entities/item.h +++ b/source/game/entities/item.h @@ -29,12 +29,12 @@ class Item { Item(const Item &) = delete; auto operator=(const Item &) -> Item & = delete; - void allignTo(int x); // Centra el objeto en la posición X - void render(); // Pinta el objeto en la pantalla - void disable(); // Pone a cero todos los valores del objeto - void update(); // Actualiza al objeto a su posicion, animación y controla los contadores - void updateTimeToLive(); // Actualiza el contador - void checkTimeToLive(); // Comprueba si el objeto sigue vivo + void allignTo(int x); // Centra el objeto en la posición X + void render(); // Pinta el objeto en la pantalla + void disable(); // Pone a cero todos los valores del objeto + void update(float dt_s); // Actualiza al objeto + void updateTimeToLive(float dt_s); // Actualiza el contador + void checkTimeToLive(); // Comprueba si el objeto sigue vivo [[nodiscard]] auto getPosX() const -> float; // Obtiene del valor de la variable [[nodiscard]] auto getPosY() const -> float; // Obtiene del valor de la variable @@ -47,24 +47,36 @@ class Item { auto getCollider() -> Circle &; // Obtiene el circulo de colisión private: + // Time-based: equivalents en unitats físiques precalculades a 60Hz. + static constexpr float ITEM_VEL_Y_PX_PER_S = -240.0F; // Era -4.0 px/frame + static constexpr float ITEM_ACCEL_Y_PX_PER_S2 = 720.0F; // Era +0.2 px/frame² + static constexpr float ITEM_VEL_X_STEP_PX_PER_S = 30.0F; // Era 0.5 px/frame, 5 valors en [-1.0, 1.0] + static constexpr float COFFEE_VEL_Y_PX_PER_S = -6.0F; // Era -0.1 px/frame + static constexpr float COFFEE_ACCEL_Y_PX_PER_S2 = 360.0F; // Era +0.1 px/frame² + static constexpr float TIME_TO_LIVE_S = 10.0F; // Era 600 frames + static constexpr float BLINK_START_S = TIME_TO_LIVE_S - (200.0F / 60.0F); // Era time_to_live_ > 200 + static constexpr float BLINK_PERIOD_S = 20.0F / 60.0F; // Era % 20 + static constexpr float BLINK_OFF_S = 10.0F / 60.0F; // Era % 20 <= 10 + // Objetos y punteros AnimatedSprite *sprite_; // Sprite con los graficos del objeto // Variables - float pos_x_; // Posición X del objeto - float pos_y_; // Posición Y del objeto - Uint8 width_; // Ancho del objeto - Uint8 height_; // Alto del objeto - float vel_x_; // Velocidad en el eje X - float vel_y_; // Velocidad en el eje Y - float accel_x_; // Aceleración en el eje X - float accel_y_; // Aceleración en el eje Y - bool floor_collision_; // Indica si el objeto colisiona con el suelo - Id id_; // Especifica el tipo de objeto que es - bool enabled_; // Especifica si el objeto está habilitado - Uint16 time_to_live_; // Temporizador con el tiempo que el objeto está presente - Circle collider_; // Circulo de colisión del objeto + float pos_x_; // Posición X del objeto + float pos_y_; // Posición Y del objeto + Uint8 width_; // Ancho del objeto + Uint8 height_; // Alto del objeto + float vel_x_s_{0.0F}; // Velocidad en el eje X (px/s) + float vel_y_s_{0.0F}; // Velocidad en el eje Y (px/s) + float accel_x_s_{0.0F}; // Aceleración en el eje X (px/s²) + float accel_y_s_{0.0F}; // Aceleración en el eje Y (px/s²) + bool floor_collision_; // Indica si el objeto colisiona con el suelo + Id id_; // Especifica el tipo de objeto que es + bool enabled_; // Especifica si el objeto está habilitado + Uint16 time_to_live_; // Temporizador (frames, derivat de time_to_live_s_ per a render()) + float time_to_live_s_{0.0F}; // Temporizador (font de veritat, segons) + Circle collider_; // Circulo de colisión del objeto void shiftColliders(); // Alinea el circulo de colisión con la posición del objeto - void move(); // Actualiza la posición y estados del objeto + void move(float dt_s); // Actualiza la posición y estados del objeto }; diff --git a/source/game/entities/player.cpp b/source/game/entities/player.cpp index 531c571..56e2c08 100644 --- a/source/game/entities/player.cpp +++ b/source/game/entities/player.cpp @@ -1,6 +1,7 @@ #include "game/entities/player.h" #include +#include // for fmod #include // for rand #include "core/input/input.h" // for InputAction @@ -42,12 +43,15 @@ void Player::init() { // Inicializa variables de estado alive_ = true; death_counter_ = DEATH_COUNTER; + death_counter_s_ = DEATH_DURATION_S; status_walking_ = STATUS_WALKING_STOP; status_firing_ = STATUS_FIRING_NO; invulnerable_ = false; invulnerable_counter_ = INVULNERABLE_COUNTER; + invulnerable_counter_s_ = INVULNERABLE_DURATION_S; power_up_ = false; power_up_counter_ = POWERUP_COUNTER; + power_up_counter_s_ = POWERUP_DURATION_S; extra_hit_ = false; coffees_ = 0; input_ = true; @@ -63,11 +67,10 @@ void Player::init() { shiftColliders(); // Establece la velocidad inicial - vel_x_ = 0; - vel_y_ = 0; + vel_x_s_ = 0.0F; // Establece la velocidad base - base_speed_ = 1.5; + base_speed_s_ = BASE_SPEED_PX_PER_S; // Establece la puntuación inicial score_ = 0; @@ -77,6 +80,7 @@ void Player::init() { // Inicia el contador para la cadencia de disparo cooldown_ = 10; + cooldown_s_ = COOLDOWN_S; // Establece la posición del sprite legs_sprite_->setPosX(pos_x_); @@ -98,12 +102,12 @@ void Player::init() { void Player::setInput(Input::Action input) { switch (input) { case Input::Action::LEFT: - vel_x_ = -base_speed_; + vel_x_s_ = -base_speed_s_; setWalkingStatus(STATUS_WALKING_LEFT); break; case Input::Action::RIGHT: - vel_x_ = base_speed_; + vel_x_s_ = base_speed_s_; setWalkingStatus(STATUS_WALKING_RIGHT); break; @@ -120,24 +124,21 @@ void Player::setInput(Input::Action input) { break; default: - vel_x_ = 0; + vel_x_s_ = 0.0F; setWalkingStatus(STATUS_WALKING_STOP); break; } } // Mueve el jugador a la posición y animación que le corresponde -void Player::move() { +void Player::move(float dt_s) { if (isAlive()) { - // Mueve el jugador a derecha o izquierda - pos_x_ += vel_x_; + pos_x_ += vel_x_s_ * dt_s; - // Si el jugador abandona el area de juego por los laterales - if ((pos_x_ < PLAY_AREA_LEFT - 5) || (pos_x_ + width_ > PLAY_AREA_RIGHT + 5)) { // Restaura su posición - pos_x_ -= vel_x_; + if ((pos_x_ < PLAY_AREA_LEFT - 5) || (pos_x_ + width_ > PLAY_AREA_RIGHT + 5)) { + pos_x_ -= vel_x_s_ * dt_s; } - // Actualiza la posición del sprite legs_sprite_->setPosX(getPosX()); legs_sprite_->setPosY(pos_y_); @@ -150,14 +151,11 @@ void Player::move() { fire_sprite_->setPosX(getPosX() - 2); fire_sprite_->setPosY(pos_y_ - 8); } else { - death_sprite_->update(); + death_sprite_->update(dt_s); - // Si el cadaver abandona el area de juego por los laterales - if ((death_sprite_->getPosX() < PLAY_AREA_LEFT) || (death_sprite_->getPosX() + width_ > PLAY_AREA_RIGHT)) { // Restaura su posición + if ((death_sprite_->getPosX() < PLAY_AREA_LEFT) || (death_sprite_->getPosX() + width_ > PLAY_AREA_RIGHT)) { const float VX = death_sprite_->getVelX(); - death_sprite_->setPosX(death_sprite_->getPosX() - VX); - - // Rebota + death_sprite_->setPosX(death_sprite_->getPosX() - (VX * dt_s)); death_sprite_->setVelX(-VX); } } @@ -199,8 +197,7 @@ void Player::setFiringStatus(Uint8 status) { } // Establece la animación correspondiente al estado -void Player::setAnimation() { - // Crea cadenas de texto para componer el nombre de la animación +void Player::setAnimation(float dt_s) { std::string body_coffees; std::string head_coffees; if (coffees_ > 0) { @@ -215,27 +212,25 @@ void Player::setAnimation() { const SDL_FlipMode FLIP_WALK = status_walking_ == STATUS_WALKING_RIGHT ? SDL_FLIP_HORIZONTAL : SDL_FLIP_NONE; const SDL_FlipMode FLIP_FIRE = status_firing_ == STATUS_FIRING_RIGHT ? SDL_FLIP_HORIZONTAL : SDL_FLIP_NONE; - // Establece la animación a partir de las cadenas legs_sprite_->setCurrentAnimation(WALKING); legs_sprite_->setFlip(FLIP_WALK); - if (status_firing_ == STATUS_FIRING_NO) { // No esta disparando + if (status_firing_ == STATUS_FIRING_NO) { body_sprite_->setCurrentAnimation(WALKING + body_coffees + POWER_UP); body_sprite_->setFlip(FLIP_WALK); head_sprite_->setCurrentAnimation(WALKING + head_coffees + POWER_UP); head_sprite_->setFlip(FLIP_WALK); - } else { // Está disparando + } else { body_sprite_->setCurrentAnimation(FIRING + body_coffees + POWER_UP); body_sprite_->setFlip(FLIP_FIRE); head_sprite_->setCurrentAnimation(FIRING + head_coffees + POWER_UP); head_sprite_->setFlip(FLIP_FIRE); } - // Actualiza las animaciones de los sprites - legs_sprite_->animate(); - body_sprite_->animate(); - head_sprite_->animate(); + legs_sprite_->animate(dt_s); + body_sprite_->animate(dt_s); + head_sprite_->animate(dt_s); - fire_sprite_->animate(); + fire_sprite_->animate(dt_s); fire_sprite_->setFlip(FLIP_WALK); } @@ -268,30 +263,37 @@ auto Player::canFire() const -> bool { // Establece el valor de la variable void Player::setFireCooldown(int time) { cooldown_ = time; + cooldown_s_ = static_cast(time) / 60.0F; } -// Actualiza el valor de la variable -void Player::updateCooldown() { - if (cooldown_ > 0) { - cooldown_--; - if (power_up_) { - cooldown_--; - } +// Establece el valor del cooldown en segons (time-based) +void Player::setFireCooldownS(float seconds) { + cooldown_s_ = seconds; + cooldown_ = static_cast(seconds * 60.0F); +} + +// Actualiza el cooldown. Quan està en mode PowerUp, el cooldown +// es consumeix el doble de ràpid (equivalent a decrementar 2 frames per tick). +void Player::updateCooldown(float dt_s) { + if (cooldown_s_ > 0.0F) { + const float RATE = power_up_ ? 2.0F : 1.0F; + cooldown_s_ = std::max(0.0F, cooldown_s_ - (dt_s * RATE)); + cooldown_ = static_cast(cooldown_s_ * 60.0F); } else { setFiringStatus(STATUS_FIRING_NO); } } // Actualiza al jugador a su posicion, animación y controla los contadores -void Player::update() { - move(); - setAnimation(); +void Player::update(float dt_s) { + move(dt_s); + setAnimation(dt_s); shiftColliders(); - updateCooldown(); - updatePowerUpCounter(); - updateInvulnerableCounter(); - updateDeathCounter(); - updatePowerUpHeadOffset(); + updateCooldown(dt_s); + updatePowerUpCounter(dt_s); + updateInvulnerableCounter(dt_s); + updateDeathCounter(dt_s); + updatePowerUpHeadOffset(dt_s); } // Obtiene la puntuación del jugador @@ -321,11 +323,13 @@ void Player::setAlive(bool value) { if (!value) { death_sprite_->setPosX(head_sprite_->getRect().x); death_sprite_->setPosY(head_sprite_->getRect().y); - death_sprite_->setAccelY(0.2F); - death_sprite_->setVelY(-6.6F); - death_sprite_->setVelX(3.3F); + // Física del cadàver en px/s i px/s² — Game crida Player::update(dt_s) + // que delega a death_sprite_->update(dt_s) (time-based). + death_sprite_->setAccelY(DEATH_ACCEL_Y_PX_PER_S2); + death_sprite_->setVelY(DEATH_VEL_Y_PX_PER_S); + death_sprite_->setVelX(DEATH_VEL_X_PX_PER_S); if (rand() % 2 == 0) { - death_sprite_->setVelX(-3.3F); + death_sprite_->setVelX(-DEATH_VEL_X_PX_PER_S); } } } @@ -376,25 +380,30 @@ auto Player::getInvulnerableCounter() const -> Uint16 { // Establece el valor de la variable void Player::setInvulnerableCounter(Uint16 value) { invulnerable_counter_ = value; + invulnerable_counter_s_ = static_cast(value) / 60.0F; } -// Actualiza el valor de la variable -void Player::updateInvulnerableCounter() { +// Actualiza el contador d'invulnerabilitat. Manté el counter enter +// sincronitzat perquè render() segueixi parpellejant igual. +void Player::updateInvulnerableCounter(float dt_s) { if (invulnerable_) { - if (invulnerable_counter_ > 0) { - invulnerable_counter_--; + if (invulnerable_counter_s_ > 0.0F) { + invulnerable_counter_s_ = std::max(0.0F, invulnerable_counter_s_ - dt_s); + invulnerable_counter_ = static_cast(invulnerable_counter_s_ * 60.0F); } else { invulnerable_ = false; invulnerable_counter_ = INVULNERABLE_COUNTER; + invulnerable_counter_s_ = INVULNERABLE_DURATION_S; } } } -// Actualiza el valor de la variable -void Player::updateDeathCounter() { +// Actualiza el comptador de mort +void Player::updateDeathCounter(float dt_s) { if (!alive_) { - if (death_counter_ > 0) { - death_counter_--; + if (death_counter_s_ > 0.0F) { + death_counter_s_ = std::max(0.0F, death_counter_s_ - dt_s); + death_counter_ = static_cast(death_counter_s_ * 60.0F); } } } @@ -417,15 +426,18 @@ auto Player::getPowerUpCounter() const -> Uint16 { // Establece el valor de la variable void Player::setPowerUpCounter(Uint16 value) { power_up_counter_ = value; + power_up_counter_s_ = static_cast(value) / 60.0F; } -// Actualiza el valor de la variable -void Player::updatePowerUpCounter() { - if ((power_up_counter_ > 0) && (power_up_)) { - power_up_counter_--; +// Actualiza el comptador de PowerUp +void Player::updatePowerUpCounter(float dt_s) { + if ((power_up_counter_s_ > 0.0F) && (power_up_)) { + power_up_counter_s_ = std::max(0.0F, power_up_counter_s_ - dt_s); + power_up_counter_ = static_cast(power_up_counter_s_ * 60.0F); } else { power_up_ = false; power_up_counter_ = POWERUP_COUNTER; + power_up_counter_s_ = POWERUP_DURATION_S; } } @@ -451,6 +463,7 @@ void Player::removeExtraHit() { } invulnerable_ = true; invulnerable_counter_ = INVULNERABLE_COUNTER; + invulnerable_counter_s_ = INVULNERABLE_DURATION_S; } // Habilita la entrada de ordenes @@ -490,21 +503,13 @@ auto Player::getDeathCounter() const -> Uint16 { return death_counter_; } -// Actualiza el valor de la variable -void Player::updatePowerUpHeadOffset() { - if (!power_up_) { - // powerUpHeadOffset = 0; - } else { - // powerUpHeadOffset = 96; - if (power_up_counter_ < 300) { - if (power_up_counter_ % 10 > 4) { - // powerUpHeadOffset = 96; - fire_sprite_->setEnabled(false); - } else { - // powerUpHeadOffset = 0; - fire_sprite_->setEnabled(true); - } - } +// Actualiza l'offset. dt_s no s'usa directament: el blink final depèn de +// power_up_counter_s_ que ja s'està actualitzant a updatePowerUpCounter. +void Player::updatePowerUpHeadOffset([[maybe_unused]] float dt_s) { + if (!power_up_) { return; } + if (power_up_counter_s_ < POWERUP_BLINK_THRESHOLD_S) { + const float PHASE = std::fmod(power_up_counter_s_, BLINK_PERIOD_S); + fire_sprite_->setEnabled(PHASE <= BLINK_OFF_S); } } diff --git a/source/game/entities/player.h b/source/game/entities/player.h index 373c8c0..3881f4c 100644 --- a/source/game/entities/player.h +++ b/source/game/entities/player.h @@ -5,8 +5,8 @@ #include // for string #include // for vector -#include "utils/utils.h" // for Circle #include "core/input/input.h" // for Input::Action +#include "utils/utils.h" // for Circle class AnimatedSprite; class Texture; @@ -21,14 +21,14 @@ class Player { Player(const Player &) = delete; auto operator=(const Player &) -> Player & = delete; - void init(); // Iniciador - void update(); // Actualiza al jugador a su posicion, animación y controla los contadores - void render(); // Pinta el jugador en pantalla - void move(); // Mueve el jugador a la posición y animación que le corresponde + void init(); // Iniciador + void update(float dt_s); // Actualiza al jugador + void render(); // Pinta el jugador en pantalla + void move(float dt_s); // Mueve el jugador void setPlayerTextures(const std::vector &texture); // Pone las texturas del jugador void setInput(Input::Action input); // Actua en consecuencia de la entrada recibida - void setAnimation(); // Establece la animación correspondiente al estado + void setAnimation(float dt_s); // Establece la animación correspondiente al estado [[nodiscard]] auto getPosX() const -> int; // Obtiene el valor de la variable [[nodiscard]] auto getPosY() const -> int; // Obtiene el valor de la variable @@ -36,8 +36,9 @@ class Player { [[nodiscard]] auto getHeight() const -> int; // Obtiene el valor de la variable [[nodiscard]] auto canFire() const -> bool; // Indica si el jugador puede disparar - void setFireCooldown(int time); // Establece el valor de la variable - void updateCooldown(); // Actualiza el valor de la variable + void setFireCooldown(int time); // Establece el valor de la variable (frames) + void setFireCooldownS(float seconds); // Establece el valor de la variable (segons) + void updateCooldown(float dt_s); // Actualiza el valor de la variable [[nodiscard]] auto getScore() const -> Uint32; // Obtiene la puntuación del jugador void setScore(Uint32 score); // Asigna un valor a la puntuación del jugador @@ -60,7 +61,7 @@ class Player { void setPowerUp(bool value); // Establece el valor de la variable [[nodiscard]] auto getPowerUpCounter() const -> Uint16; // Obtiene el valor de la variable void setPowerUpCounter(Uint16 value); // Establece el valor de la variable - void updatePowerUpCounter(); // Actualiza el valor de la variable + void updatePowerUpCounter(float dt_s); // Actualiza el valor de la variable [[nodiscard]] auto hasExtraHit() const -> bool; // Obtiene el valor de la variable void giveExtraHit(); // Concede un toque extra al jugador @@ -89,6 +90,20 @@ class Player { static constexpr int INVULNERABLE_COUNTER = 200; static constexpr int POWERUP_COUNTER = 1500; + // Time-based: equivalents en segons/px·s a 60Hz (font de veritat). + static constexpr float BASE_SPEED_PX_PER_S = 90.0F; // Era 1.5 px/frame + static constexpr float COOLDOWN_S = 10.0F / 60.0F; // Era 10 frames + static constexpr float INVULNERABLE_DURATION_S = 200.0F / 60.0F; + static constexpr float POWERUP_DURATION_S = 1500.0F / 60.0F; + static constexpr float DEATH_DURATION_S = 350.0F / 60.0F; + static constexpr float POWERUP_BLINK_THRESHOLD_S = 300.0F / 60.0F; // Era power_up_counter_ < 300 + static constexpr float BLINK_PERIOD_S = 10.0F / 60.0F; // Era % 10 + static constexpr float BLINK_OFF_S = 4.0F / 60.0F; // Era % 10 > 4 + // Death sprite (cadàver) — physics convertides de px/frame a px/s. + static constexpr float DEATH_ACCEL_Y_PX_PER_S2 = 0.2F * 60.0F * 60.0F; // = 720 + static constexpr float DEATH_VEL_Y_PX_PER_S = -6.6F * 60.0F; // = -396 + static constexpr float DEATH_VEL_X_PX_PER_S = 3.3F * 60.0F; // = 198 + // Objetos y punteros SDL_Renderer *renderer_; // El renderizador de la ventana AnimatedSprite *head_sprite_; // Sprite para dibujar la cabeza @@ -104,11 +119,11 @@ class Player { Uint8 width_; // Anchura Uint8 height_; // Altura - float vel_x_; // Cantidad de pixeles a desplazarse en el eje X - int vel_y_; // Cantidad de pixeles a desplazarse en el eje Y + float vel_x_s_{0.0F}; // Velocidad en el eje X (px/s) - float base_speed_; // Velocidad base del jugador - int cooldown_; // Contador durante el cual no puede disparar + float base_speed_s_{0.0F}; // Velocidad base del jugador (px/s) + int cooldown_; // Contador durante el cual no puede disparar (frames, derivat de cooldown_s_) + float cooldown_s_{0.0F}; // Contador durante el cual no puede disparar (font de veritat, segons) Uint32 score_; // Puntos del jugador float score_multiplier_; // Multiplicador de puntos @@ -116,22 +131,25 @@ class Player { Uint8 status_walking_; // Estado del jugador Uint8 status_firing_; // Estado del jugador - bool alive_; // Indica si el jugador está vivo - Uint16 death_counter_; // Contador para la animación de morirse - bool invulnerable_; // Indica si el jugador es invulnerable - Uint16 invulnerable_counter_; // Contador para la invulnerabilidad - bool extra_hit_; // Indica si el jugador tiene un toque extra - Uint8 coffees_; // Indica cuantos cafes lleva acumulados - bool power_up_; // Indica si el jugador tiene activo el modo PowerUp - Uint16 power_up_counter_; // Temporizador para el modo PowerUp - bool input_; // Indica si puede recibir ordenes de entrada - Circle collider_; // Circulo de colisión del jugador + bool alive_; // Indica si el jugador está vivo + Uint16 death_counter_; // Contador (frame-based) + float death_counter_s_{0.0F}; // Contador (time-based) + bool invulnerable_; // Indica si el jugador es invulnerable + Uint16 invulnerable_counter_; // Contador (frame-based) + float invulnerable_counter_s_{0.0F}; // Contador (time-based) + bool extra_hit_; // Indica si el jugador tiene un toque extra + Uint8 coffees_; // Indica cuantos cafes lleva acumulados + bool power_up_; // Indica si el jugador tiene activo el modo PowerUp + Uint16 power_up_counter_; // Temporizador (frame-based) + float power_up_counter_s_{0.0F}; // Temporizador (time-based) + bool input_; // Indica si puede recibir ordenes de entrada + Circle collider_; // Circulo de colisión del jugador void setWalkingStatus(Uint8 status); // Establece el estado del jugador void setFiringStatus(Uint8 status); // Establece el estado del jugador - void shiftColliders(); // Actualiza el circulo de colisión a la posición del jugador - void updateInvulnerableCounter(); // Actualiza el valor de la variable - void updateDeathCounter(); // Actualiza el valor de la variable - void updatePowerUpHeadOffset(); // Actualiza el valor de la variable + void shiftColliders(); // Actualiza el circulo de colisión a la posición del jugador + void updateInvulnerableCounter(float dt_s); // Actualiza el valor de la variable + void updateDeathCounter(float dt_s); // Actualiza el valor de la variable + void updatePowerUpHeadOffset(float dt_s); // Actualiza el valor de la variable }; diff --git a/source/game/game.cpp b/source/game/game.cpp index 10d2904..95e9878 100644 --- a/source/game/game.cpp +++ b/source/game/game.cpp @@ -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() { @@ -146,9 +150,6 @@ Game::~Game() { // Inicializa las variables necesarias para la sección 'Game' void Game::init() { - ticks_ = 0; - ticks_speed_ = 15; - // Elimina qualquier jugador que hubiese antes de crear los nuevos for (auto *player : players_) { delete player; @@ -200,18 +201,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 +294,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 +315,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 +330,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); @@ -1429,11 +1439,10 @@ void Game::renderScoreBoard() { } // Actualiza las variables del jugador -void Game::updatePlayers() { +void Game::updatePlayers(float dt_s) { for (auto *player : players_) { - player->update(); + player->update(dt_s); - // Comprueba la colisión entre el jugador y los globos if (checkPlayerBalloonCollision(player)) { if (player->isAlive()) { if (demo_.enabled) { @@ -1445,7 +1454,6 @@ void Game::updatePlayers() { } } - // Comprueba las colisiones entre el jugador y los items checkPlayerItemCollision(player); } } @@ -1458,19 +1466,18 @@ void Game::renderPlayers() { } // Actualiza las variables de la fase -void Game::updateStage() { +void Game::updateStage(float dt_s) { if (stage_[current_stage_].current_power >= stage_[current_stage_].power_to_complete) { - // Cambio de fase current_stage_++; last_stage_reached_ = current_stage_; - if (current_stage_ == 10) { // Ha llegado al final el juego - game_completed_ = true; // Marca el juego como completado - current_stage_ = 9; // Deja el valor dentro de los limites - stage_[current_stage_].current_power = 0; // Deja el poder a cero para que no vuelva a entrar en esta condición - destroyAllBalloons(); // Destruye a todos los enemigos - stage_[current_stage_].current_power = 0; // Vuelve a dejar el poder a cero, por lo que hubiera podido subir al destruir todos lo globos - menace_current_ = 255; // Sube el nivel de amenaza para que no cree mas globos - for (auto *player : players_) { // Añade un millon de puntos a los jugadores que queden vivos + 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); } @@ -1480,45 +1487,52 @@ void Game::updateStage() { } 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; } - // Incrementa el contador del bitmap que aparece mostrando el cambio de fase - if (stage_bitmap_counter_ < STAGE_COUNTER) { - stage_bitmap_counter_++; + if (stage_bitmap_counter_s_ < (STAGE_COUNTER / 60.0F)) { + stage_bitmap_counter_s_ += dt_s; } + stage_bitmap_counter_ = std::min(STAGE_COUNTER, static_cast(stage_bitmap_counter_s_ * 60.0F)); - // Si el juego se ha completado, el bitmap se detiene en el centro de la pantalla if (game_completed_) { stage_bitmap_counter_ = std::min(stage_bitmap_counter_, 100); } } -// Actualiza el estado de muerte -void Game::updateDeath() { - // Comprueba si todos los jugadores estan muertos +// Actualiza el estado de muerte. 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) { - if (death_counter_ > 0) { - death_counter_--; + if (!all_dead) { return; } - if ((death_counter_ == 250) || (death_counter_ == 200) || (death_counter_ == 180) || (death_counter_ == 120) || (death_counter_ == 60)) { - // Hace sonar aleatoriamente uno de los 4 sonidos de burbujas - 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]); - } - } - } else { - section_->subsection = SUBSECTION_GAME_GAMEOVER; + 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(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]); } } } @@ -1549,9 +1563,9 @@ void Game::renderDeathFade(int counter) { // Counter debe ir de 0 a 150 } // Actualiza los globos -void Game::updateBalloons() { +void Game::updateBalloons(float dt_s) { for (auto *balloon : balloons_) { - balloon->update(); + balloon->update(dt_s); } } @@ -1662,7 +1676,7 @@ void Game::popBalloon(Balloon *balloon) { const int INDEX = createBalloon(0, balloon->getPosY(), balloon->getKind() - 1, Balloon::VELX_NEGATIVE, enemy_speed_, 0); balloons_[INDEX]->allignTo(balloon->getPosX() + (balloon->getWidth() / 2)); if (balloons_[INDEX]->getClass() == Balloon::BALLOON_CLASS) { - balloons_[INDEX]->setVelY(-2.50F); + balloons_[INDEX]->setVelY(-150.0F); // -2.5 px/frame ⇒ -150 px/s } else { balloons_[INDEX]->setVelY(Balloon::VELX_NEGATIVE); } @@ -1670,7 +1684,7 @@ void Game::popBalloon(Balloon *balloon) { const int INDEX2 = createBalloon(0, balloon->getPosY(), balloon->getKind() - 1, Balloon::VELX_POSITIVE, enemy_speed_, 0); balloons_[INDEX2]->allignTo(balloon->getPosX() + (balloon->getWidth() / 2)); if (balloons_[INDEX2]->getClass() == Balloon::BALLOON_CLASS) { - balloons_[INDEX2]->setVelY(-2.50F); + balloons_[INDEX2]->setVelY(-150.0F); } else { balloons_[INDEX2]->setVelY(Balloon::VELX_NEGATIVE); } @@ -1904,10 +1918,10 @@ void Game::resolveBulletBalloonHit(Bullet *bullet, Balloon *balloon) { } // Mueve las balas activas -void Game::moveBullets() { +void Game::moveBullets(float dt_s) { for (auto *bullet : bullets_) { if (bullet->isEnabled()) { - if (bullet->move() == Bullet::MoveResult::OUT) { + if (bullet->move(dt_s) == Bullet::MoveResult::OUT) { players_[bullet->getOwner()]->decScoreMultiplier(); } } @@ -1942,10 +1956,10 @@ void Game::freeBullets() { } // Actualiza los items -void Game::updateItems() { +void Game::updateItems(float dt_s) { for (auto *item : items_) { if (item->isEnabled()) { - item->update(); + item->update(dt_s); if (item->isOnFloor()) { Audio::get()->playSound(coffee_machine_sound_); effect_.shake = true; @@ -2067,14 +2081,26 @@ void Game::renderFlashEffect() { } } -// Actualiza el efecto de agitar la pantalla -void Game::updateShakeEffect() { - if (effect_.shake) { +// Actualiza el efecto de agitar la pantalla. 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; } } } @@ -2088,10 +2114,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(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); @@ -2103,9 +2131,9 @@ void Game::throwCoffee(int x, int y) { } // Actualiza los SmartSprites -void Game::updateSmartSprites() { +void Game::updateSmartSprites(float dt_s) { for (auto *ss : smart_sprites_) { - ss->update(); + ss->update(dt_s); } } @@ -2196,114 +2224,87 @@ 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(value) / 60.0F; } // Incrementa el valor de la variable void Game::incTimeStoppedCounter(Uint16 value) { time_stopped_counter_ += value; + time_stopped_counter_s_ += static_cast(value) / 60.0F; } // Actualiza y comprueba el valor de la variable -void Game::updateTimeStoppedCounter() { - if (isTimeStopped()) { - if (time_stopped_counter_ > 0) { - time_stopped_counter_--; - stopAllBalloons(TIME_STOPPED_COUNTER); - } else { - disableTimeStopItem(); - } +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(time_stopped_counter_s_ * 60.0F); + stopAllBalloons(TIME_STOPPED_COUNTER); + } else { + disableTimeStopItem(); } } -// Actualiza la variable enemyDeployCounter -void Game::updateEnemyDeployCounter() { - if (enemy_deploy_counter_ > 0) { +// Actualiza la variable enemyDeployCounter. Decrementa a 60Hz fixe amb +// 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 +// Actualiza el juego. 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(); - // Actualiza los efectos basados en tiempo real (no en el throttle del juego) updateDeathShake(); updateDeathSequence(); - // Durante la secuencia de muerte, congela el resto del juego if (death_sequence_.phase == DeathPhase::SHAKING || death_sequence_.phase == DeathPhase::WAITING) { return; } - // Comprueba que la diferencia de ticks sea mayor a la velocidad del juego - if (SDL_GetTicks() - ticks_ > ticks_speed_) { - // Actualiza el contador de ticks - ticks_ = SDL_GetTicks(); + // Acumulador i derivació del comptador legacy + elapsed_s_ += dt_s; + counter_ = static_cast(elapsed_s_ * 60.0F); - // Actualiza el contador de juego - counter_++; + 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); - // Comprueba el teclado/mando - checkGameInput(); - - // Actualiza las variables del jugador - updatePlayers(); - - // Actualiza el fondo - updateBackground(); - - // Mueve los globos - updateBalloons(); - - // Mueve las balas - moveBullets(); - - // Actualiza los items - updateItems(); - - // Actualiza el valor de currentStage - updateStage(); - - // Actualiza el estado de muerte - updateDeath(); - - // Actualiza los SmartSprites - updateSmartSprites(); - - // Actualiza los contadores de estado y efectos - updateTimeStoppedCounter(); - updateEnemyDeployCounter(); - updateShakeEffect(); - - // Actualiza el ayudante - updateHelper(); - - // Comprueba las colisiones entre globos y balas - checkBulletBalloonCollision(); - - // Comprueba el nivel de amenaza para ver si se han de crear nuevos enemigos - updateMenace(); - - // Actualiza la velocidad de los enemigos - updateBalloonSpeed(); - - // Actualiza el tramo final de juego, una vez completado - updateGameCompleted(); - - // Vacia los vectores - freeBullets(); - freeBalloons(); - freeItems(); - freeSmartSprites(); - } + 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 +// Actualiza el fondo. Velocitats dels núvols expressades com a px/s. +void Game::updateBackground(float dt_s) { + if (!game_completed_) { clouds_speed_ = balloons_popped_; - } else { // Si el juego está completado, se reduce la velocidad de las nubes + } else { if (clouds_speed_ > 400) { clouds_speed_ -= 25; } else { @@ -2311,42 +2312,28 @@ 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_)); + // 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; - // Aplica la velocidad calculada a las nubes - clouds1_a_->setVelX(SPEED); - clouds1_b_->setVelX(SPEED); - clouds2_a_->setVelX(SPEED / 2); - clouds2_b_->setVelX(SPEED / 2); + 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); - // Mueve las nubes - clouds1_a_->move(); - clouds1_b_->move(); - clouds2_a_->move(); - clouds2_b_->move(); + clouds1_a_->move(dt_s); + clouds1_b_->move(dt_s); + clouds2_a_->move(dt_s); + clouds2_b_->move(dt_s); - // Calcula el offset de las nubes - if (clouds1_a_->getPosX() < -clouds1_a_->getWidth()) { - clouds1_a_->setPosX(clouds1_a_->getWidth()); - } + 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()); } - 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()); - } - - // Calcula el frame de la hierba + // Herba: `counter_` derivat de `elapsed_s_*60` ja oscil·la a 60Hz. grass_sprite_->setSpriteClip(0, (6 * (counter_ / 20 % 2)), 256, 6); - // Mueve los edificios en funcion de si está activo el efecto de agitarlos if (death_shake_.active) { const int V[] = {-1, 1, -1, 1, -1, 1, -1, 0}; buildings_sprite_->setPosX(V[death_shake_.step]); @@ -2679,55 +2666,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(DELTA_TIME_S); + break; + case SUBSECTION_GAME_GAMEOVER: + iterateGameOver(DELTA_TIME_S); + 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(float dt_s) { + if (!pause_initialized_) { enterPausedGame(); } + updatePausedGame(dt_s); + renderPausedGame(); +} + +// Rama de iterate(): game over +void Game::iterateGameOver(float dt_s) { + if (!game_over_initialized_) { enterGameOverScreen(); } + updateGameOverScreen(dt_s); + 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 @@ -2765,54 +2759,50 @@ void Game::run() { } } -// Actualiza las variables del menu de pausa del juego -void Game::updatePausedGame() { - if (SDL_GetTicks() - ticks_ <= ticks_speed_) { - return; - } - ticks_ = SDL_GetTicks(); - - // Atalls globals (zoom finestra, fullscreen, shaders, presets, ...) +// Actualiza el menu de pausa +void Game::updatePausedGame(float dt_s) { GlobalInputs::handle(); if (leaving_pause_menu_) { - updateLeavingPauseMenu(); + updateLeavingPauseMenu(dt_s); } else { - updatePauseMenuUI(); + updatePauseMenuUI(dt_s); } } -// Rama de updatePausedGame: cuenta atrás de salida y vuelta al juego -void Game::updateLeavingPauseMenu() { - if (pause_counter_ > 0) { // El contador está descendiendo - const bool A = pause_counter_ == 90; - const bool B = pause_counter_ == 60; - const bool C = pause_counter_ == 30; - if (A || B || C) { - Audio::get()->playSound(clock_sound_); +// Rama de updatePausedGame: cuenta atrás de salida y vuelta al juego. +// Decrementa pause_counter_ a 60Hz exactes amb un acumulador de fase per a +// mantenir els sons de rellotge als llindars originals (90, 60, 30 frames = +// 1.5s, 1.0s, 0.5s restants). +void Game::updateLeavingPauseMenu(float dt_s) { + if (pause_counter_ <= 0) { + section_->name = SECTION_PROG_GAME; + section_->subsection = num_players_ == 1 ? SUBSECTION_GAME_PLAY_1P : SUBSECTION_GAME_PLAY_2P; + if (Audio::getRealMusicState() == Audio::MusicState::PAUSED) { + Audio::get()->resumeMusic(); } - pause_counter_--; return; } - // Ha finalizado el contador - section_->name = SECTION_PROG_GAME; - section_->subsection = num_players_ == 1 ? SUBSECTION_GAME_PLAY_1P : SUBSECTION_GAME_PLAY_2P; - - if (Audio::getRealMusicState() == Audio::MusicState::PAUSED) { - Audio::get()->resumeMusic(); + constexpr float STEP_S = 1.0F / 60.0F; + pause_counter_phase_s_ += dt_s; + while (pause_counter_phase_s_ >= STEP_S && pause_counter_ > 0) { + pause_counter_phase_s_ -= STEP_S; + if (pause_counter_ == 90 || pause_counter_ == 60 || pause_counter_ == 30) { + Audio::get()->playSound(clock_sound_); + } + pause_counter_--; } } -// Rama de updatePausedGame: lógica del menú de pausa -void Game::updatePauseMenuUI() { +// Lógica del menú de pausa (time-based) +void Game::updatePauseMenuUI(float dt_s) { pause_menu_->update(); pause_menu_->checkInput(); - // F12 (Action::PAUSE) també tanca el menú de pausa — mateix comportament - // que seleccionar "Continue" / cancel·lar amb BACKSPACE. if (Input::get()->checkInput(Input::Action::PAUSE, Input::Repeat::OFF)) { leaving_pause_menu_ = true; + pause_counter_phase_s_ = 0.0F; if (!Options::gameplay.pause_countdown) { pause_counter_ = 0; } @@ -2822,21 +2812,20 @@ void Game::updatePauseMenuUI() { switch (pause_menu_->getItemSelected()) { case 1: leaving_pause_menu_ = true; + pause_counter_phase_s_ = 0.0F; if (!Options::gameplay.pause_countdown) { - pause_counter_ = 0; // salta el compte enrere de 3 segons + pause_counter_ = 0; } break; - case 2: fade_->setFadeType(Fade::Type::CENTER); fade_->activateFade(); break; - default: break; } - fade_->update(); + fade_->update(dt_s); if (fade_->hasEnded()) { section_->name = SECTION_PROG_TITLE; section_->subsection = SUBSECTION_TITLE_1; @@ -2904,60 +2893,42 @@ void Game::enterPausedGame() { } // Actualiza los elementos de la pantalla de game over -void Game::updateGameOverScreen() { - // Calcula la lógica de los objetos - if (SDL_GetTicks() - ticks_ > ticks_speed_) { - // Actualiza el contador de ticks - ticks_ = SDL_GetTicks(); +void Game::updateGameOverScreen(float dt_s) { + GlobalInputs::handle(); - // Atalls globals (zoom finestra, fullscreen, shaders, presets, ...) - GlobalInputs::handle(); + game_over_menu_->update(); + fade_->update(dt_s); - // Actualiza la lógica del menu - game_over_menu_->update(); - - // Actualiza el fade - fade_->update(); - - // Si ha terminado el fade, actua segun se haya operado - if (fade_->hasEnded()) { - switch (game_over_post_fade_) { - case 0: // YES - section_->name = SECTION_PROG_GAME; - deleteAllVectorObjects(); - init(); - section_->subsection = num_players_ == 1 ? SUBSECTION_GAME_PLAY_1P : SUBSECTION_GAME_PLAY_2P; - break; - - case 1: // NO - section_->name = SECTION_PROG_TITLE; - section_->subsection = SUBSECTION_TITLE_1; - break; - - default: - break; - } + if (fade_->hasEnded()) { + switch (game_over_post_fade_) { + case 0: // YES + section_->name = SECTION_PROG_GAME; + deleteAllVectorObjects(); + init(); + section_->subsection = num_players_ == 1 ? SUBSECTION_GAME_PLAY_1P : SUBSECTION_GAME_PLAY_2P; + break; + case 1: // NO + section_->name = SECTION_PROG_TITLE; + section_->subsection = SUBSECTION_TITLE_1; + break; + default: + break; } + } - // Comprueba las entradas para el menu solo si no esta el juego completo - if (!game_completed_) { - game_over_menu_->checkInput(); - - // Comprueba si se ha seleccionado algún item del menú - switch (game_over_menu_->getItemSelected()) { - case 0: // YES - game_over_post_fade_ = 0; - fade_->activateFade(); - break; - - case 1: // NO - game_over_post_fade_ = 1; - fade_->activateFade(); - break; - - default: - break; - } + if (!game_completed_) { + game_over_menu_->checkInput(); + switch (game_over_menu_->getItemSelected()) { + case 0: // YES + game_over_post_fade_ = 0; + fade_->activateFade(); + break; + case 1: // NO + game_over_post_fade_ = 1; + fade_->activateFade(); + break; + default: + break; } } } @@ -3099,23 +3070,24 @@ void Game::initPaths() { } // Actualiza el tramo final de juego, una vez completado -void Game::updateGameCompleted() { - if (game_completed_) { - game_completed_counter_++; - } +void Game::updateGameCompleted(float dt_s) { + if (!game_completed_) { return; } - if (game_completed_counter_ == GAME_COMPLETED_END) { + const int PREV = game_completed_counter_; + game_completed_counter_s_ += dt_s; + game_completed_counter_ = static_cast(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 +// Actualiza las variables de ayuda. De moment cap timer real dins helper_; +// el dt_s queda reservat per a futurs comptadors d'helper. +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 { diff --git a/source/game/game.h b/source/game/game.h index a593ffc..4b88249 100644 --- a/source/game/game.h +++ b/source/game/game.h @@ -38,6 +38,11 @@ class Game { void handleEvent(const SDL_Event *event); // Procesa un evento private: + // Branques de iterate() — separades per a reduir la complexitat cognitiva + void iteratePaused(float dt_s); + void iterateGameOver(float dt_s); + void iteratePlaying(float dt_s); + // Cantidad de elementos a escribir en los ficheros de datos static constexpr int TOTAL_SCORE_DATA = 3; static constexpr int TOTAL_DEMO_DATA = 2000; @@ -141,10 +146,10 @@ class Game { DemoKeys data_file[TOTAL_DEMO_DATA]; // Datos del fichero con los movimientos para la demo }; - void update(); // Actualiza el juego - void render(); // Dibuja el juego - void init(); // Inicializa las variables necesarias para la sección 'Game' - void loadMedia(); // Carga los recursos necesarios para la sección 'Game' + void update(float dt_s); // Actualiza el juego + void render(); // Dibuja el juego + void init(); // Inicializa las variables necesarias para la sección 'Game' + void loadMedia(); // Carga los recursos necesarios para la sección 'Game' auto loadScoreFile() -> bool; // Carga el fichero de puntos auto loadDemoFile() -> bool; // Carga el fichero de datos para la demo @@ -167,14 +172,14 @@ class Game { static auto updateScoreText(Uint32 num) -> std::string; // Transforma un valor numérico en una cadena de 6 cifras void renderScoreBoard(); // Pinta el marcador en pantalla usando un objeto texto - void updatePlayers(); // Actualiza las variables del jugador - void renderPlayers(); // Dibuja a los jugadores + void updatePlayers(float dt_s); // Actualiza las variables del jugador + void renderPlayers(); // Dibuja a los jugadores - void updateStage(); // Actualiza las variables de la fase - void updateDeath(); // Actualiza el estado de muerte + void updateStage(float dt_s); // Actualiza las variables de la fase + void updateDeath(float dt_s); // Actualiza el estado de muerte void renderDeathFade(int counter); // Renderiza el fade final cuando se acaba la partida - void updateBalloons(); // Actualiza los globos + void updateBalloons(float dt_s); // Actualiza los globos void renderBalloons(); // Pinta en pantalla todos los globos activos auto createBalloon(float x, float y, Uint8 kind, float velx, float speed, Uint16 creationtimer) -> Uint8; // Crea un globo nuevo en el vector de globos void createPowerBall(); // Crea una PowerBall @@ -193,12 +198,12 @@ class Game { void checkBulletBalloonCollision(); // Comprueba la colisión entre las balas y los globos void resolveBulletBalloonHit(Bullet *bullet, Balloon *balloon); // Resuelve un impacto bala-globo (helper de checkBulletBalloonCollision) - void moveBullets(); // Mueve las balas activas + void moveBullets(float dt_s); // Mueve las balas activas void renderBullets(); // Pinta las balas activas void createBullet(int x, int y, Bullet::Kind kind, bool powered_up, int owner); // Crea un objeto bala void freeBullets(); // Vacia el vector de balas - void updateItems(); // Actualiza los items + void updateItems(float dt_s); // Actualiza los items void renderItems(); // Pinta los items activos auto dropItem() -> Item::Id; // Devuelve un item en función del azar void createItem(Item::Id kind, float x, float y); // Crea un objeto item @@ -207,11 +212,11 @@ class Game { void createItemScoreSprite(int x, int y, const SmartSprite *sprite); // Crea un objeto SmartSprite void freeSmartSprites(); // Vacia el vector de smartsprites - void renderFlashEffect(); // Dibuja el efecto de flash - void updateShakeEffect(); // Actualiza el efecto de agitar la pantalla - void throwCoffee(int x, int y); // Crea un SmartSprite para arrojar el item café al recibir un impacto - void updateSmartSprites(); // Actualiza los SmartSprites - void renderSmartSprites(); // Pinta los SmartSprites activos + void renderFlashEffect(); // Dibuja el efecto de flash + void updateShakeEffect(float dt_s); // Actualiza el efecto de agitar la pantalla + void throwCoffee(int x, int y); // Crea un SmartSprite para arrojar el item café al recibir un impacto + void updateSmartSprites(float dt_s); // Actualiza los SmartSprites + void renderSmartSprites(); // Pinta los SmartSprites activos void killPlayer(Player *player); // Acciones a realizar cuando el jugador muere void evaluateAndSetMenace(); // Calcula y establece el valor de amenaza en funcion de los globos activos @@ -222,11 +227,11 @@ class Game { void setTimeStoppedCounter(Uint16 value); // Establece el valor de la variable void incTimeStoppedCounter(Uint16 value); // Incrementa el valor de la variable - void updateEnemyDeployCounter(); // Actualiza la variable EnemyDeployCounter - void updateTimeStoppedCounter(); // Actualiza y comprueba el valor de la variable - void updateMenace(); // Gestiona el nivel de amenaza - void updateBackground(); // Actualiza el fondo - void renderBackground(); // Dibuja el fondo + void updateEnemyDeployCounter(float dt_s); // Actualiza la variable EnemyDeployCounter + void updateTimeStoppedCounter(float dt_s); // Actualiza y comprueba el valor de la variable + void updateMenace(); // Gestiona el nivel de amenaza + void updateBackground(float dt_s); // Actualiza el fondo + void renderBackground(); // Dibuja el fondo void checkGameInput(); // Gestiona la entrada durante el juego void processDemoInput(); // Helper de checkGameInput @@ -241,21 +246,21 @@ class Game { [[nodiscard]] auto isDeathShaking() const -> bool; // Indica si el efecto de agitación intensa está activo void updateDeathSequence(); // Actualiza la secuencia de muerte del jugador - void updatePausedGame(); // Actualiza las variables del menu de pausa del juego - void updateLeavingPauseMenu(); // Helper de updatePausedGame - void updatePauseMenuUI(); // Helper de updatePausedGame - void renderPausedGame(); // Dibuja el menu de pausa del juego - void enterPausedGame(); // Inicializa el estado de pausa del juego + void updatePausedGame(float dt_s); // Actualiza el menu de pausa + void updateLeavingPauseMenu(float dt_s); // Helper + void updatePauseMenuUI(float dt_s); // Helper + void renderPausedGame(); // Dibuja el menu de pausa del juego + void enterPausedGame(); // Inicializa el estado de pausa del juego - void updateGameOverScreen(); // Actualiza los elementos de la pantalla de game over - void renderGameOverScreen(); // Dibuja los elementos de la pantalla de game over - void enterGameOverScreen(); // Inicializa el estado de game over + void updateGameOverScreen(float dt_s); // Actualiza game over + void renderGameOverScreen(); // Dibuja los elementos de la pantalla de game over + void enterGameOverScreen(); // Inicializa el estado de game over auto canPowerBallBeCreated() -> bool; // Indica si se puede crear una powerball auto calculateScreenPower() -> int; // Calcula el poder actual de los globos en pantalla void initPaths(); // Inicializa las variables que contienen puntos de ruta para mover objetos - void updateGameCompleted(); // Actualiza el tramo final de juego, una vez completado - void updateHelper(); // Actualiza las variables de ayuda + void updateGameCompleted(float dt_s); // Actualiza el tramo final de juego + void updateHelper(float dt_s); // Actualiza las variables de ayuda auto allPlayersAreDead() -> bool; // Comprueba si todos los jugadores han muerto void deleteAllVectorObjects(); // Elimina todos los objetos contenidos en vectores void setHiScore(); // Establece la máxima puntuación desde fichero o desde las puntuaciones online @@ -337,30 +342,36 @@ class Game { // Variables int num_players_; // Numero de jugadores - Uint32 ticks_; // Contador de ticks para ajustar la velocidad del programa - Uint8 ticks_speed_; // Velocidad a la que se repiten los bucles del programa + float elapsed_s_{0.0F}; // Acumulador global de temps de joc Uint32 hi_score_; // Puntuación máxima bool hi_score_achieved_; // Indica si se ha superado la puntuación máxima std::string hi_score_name_; // Nombre del jugador que ostenta la máxima puntuación Stage stage_[10]; // Variable con los datos de cada pantalla Uint8 current_stage_; // Indica la fase actual - Uint8 stage_bitmap_counter_; // Contador para el tiempo visible del texto de Stage + Uint8 stage_bitmap_counter_; // Contador para el tiempo visible del texto de Stage (frame-based) + float stage_bitmap_counter_s_{0.0F}; // Contador (time-based) float stage_bitmap_path_[STAGE_COUNTER]; // Vector con los puntos Y por donde se desplaza el texto float get_ready_bitmap_path_[STAGE_COUNTER]; // Vector con los puntos X por donde se desplaza el texto - Uint16 death_counter_; // Contador para la animación de muerte del jugador + Uint16 death_counter_; // Contador para la animación de muerte del jugador (frame-based) + float death_counter_s_{0.0F}; // Contador (time-based) Uint8 menace_current_; // Nivel de amenaza actual Uint8 menace_threshold_; // Umbral del nivel de amenaza. Si el nivel de amenaza cae por debajo del umbral, se generan más globos. Si el umbral aumenta, aumenta el numero de globos bool time_stopped_; // Indica si el tiempo está detenido - Uint16 time_stopped_counter_; // Temporizador para llevar la cuenta del tiempo detenido - Uint32 counter_; // Contador para el juego + Uint16 time_stopped_counter_; // Temporizador (frame-based) + float time_stopped_counter_s_{0.0F}; // Temporizador (time-based) + Uint32 counter_; // Contador para el juego (frame-based, derivat de elapsed_s_*60 en time-based) Uint32 score_data_file_[TOTAL_SCORE_DATA]; // Datos del fichero de puntos SDL_Rect sky_colors_rect_[4]; // Vector con las coordenadas de los 4 colores de cielo Uint16 balloons_popped_; // Lleva la cuenta de los globos explotados Uint8 last_enemy_deploy_; // Guarda cual ha sido la última formación desplegada para no repetir; - int enemy_deploy_counter_; // Cuando se lanza una formación, se le da un valor y no sale otra hasta que llegue a cero + int enemy_deploy_counter_; // Cuando se lanza una formación, se le da un valor y no sale otra hasta que llegue a cero (frame-based) + float enemy_deploy_counter_s_{0.0F}; // Comptador (time-based) float enemy_speed_; // Velocidad a la que se mueven los enemigos float default_enemy_speed_; // Velocidad base de los enemigos, sin incrementar Effect effect_; // Variable para gestionar los efectos visuales + float shake_phase_s_{0.0F}; // Acumulador per decrementar shake_counter a 60Hz (time-based) + float helper_counter_s_{0.0F}; // Acumulador per al comptador helper_.counter (time-based) + float enemy_deploy_phase_s_{0.0F}; // Acumulador per al decrement de enemy_deploy_counter_ (time-based) DeathShake death_shake_; // Variable para gestionar el efecto de agitación intensa DeathSequence death_sequence_; // Variable para gestionar la secuencia de muerte Helper helper_; // Variable para gestionar las ayudas @@ -368,7 +379,8 @@ class Game { Uint8 power_ball_counter_; // Contador de formaciones enemigas entre la aparicion de una PowerBall y otra bool coffee_machine_enabled_; // Indica si hay una máquina de café en el terreno de juego bool game_completed_; // Indica si se ha completado la partida, llegando al final de la ultima pantalla - int game_completed_counter_; // Contador para el tramo final, cuando se ha completado la partida y ya no aparecen más enemigos + int game_completed_counter_; // Contador per al tram final (frame-based) + float game_completed_counter_s_{0.0F}; // Comptador (time-based) Uint8 difficulty_; // Dificultad del juego float difficulty_score_multiplier_; // Multiplicador de puntos en función de la dificultad Color difficulty_color_; // Color asociado a la dificultad @@ -379,7 +391,8 @@ class Game { Demo demo_; // Variable con todas las variables relacionadas con el modo demo int total_power_to_complete_game_; // La suma del poder necesario para completar todas las fases int clouds_speed_{0}; // Velocidad a la que se desplazan las nubes - int pause_counter_; // Contador para salir del menu de pausa y volver al juego + int pause_counter_; // Contador per a sortir del menu de pausa (frame-based, frames) + float pause_counter_phase_s_{0.0F}; // Acumulador de fase per decrementar pause_counter_ a 60Hz (time-based) bool leaving_pause_menu_; // Indica si esta saliendo del menu de pausa para volver al juego bool pause_initialized_; // Indica si la pausa ha sido inicializada bool game_over_initialized_; // Indica si el game over ha sido inicializado diff --git a/source/game/scenes/instructions.cpp b/source/game/scenes/instructions.cpp index 1cb8a73..4f6fb79 100644 --- a/source/game/scenes/instructions.cpp +++ b/source/game/scenes/instructions.cpp @@ -97,6 +97,31 @@ void Instructions::update() { } } +// Time-based. counter_ es deriva de elapsed_s_*60 (cadència de referència 60Hz) +// per a no haver de refactoritzar render() — la geometria del scroll, la +// pulsació dels sprites i el blink de manual segueixen llegint counter_. +void Instructions::update(float dt_s) { + Audio::update(); + checkInput(); + + elapsed_s_ += dt_s; + constexpr float FRAMES_PER_S = 60.0F; + + if (mode_ == Mode::AUTO) { + counter_ = static_cast(elapsed_s_ * FRAMES_PER_S); + if (elapsed_s_ >= SCENE_DURATION_S) { + finished_ = true; + } + } else { + // counter_ acotat al rang original 0..59999 per no trencar les expressions + // de blink i sprite-cycling que en depenen. + counter_ = static_cast(static_cast(elapsed_s_ * FRAMES_PER_S) % 60000); + if (manual_quit_) { + finished_ = true; + } + } +} + // Pinta en pantalla void Instructions::render() { // Pinta en pantalla @@ -252,6 +277,7 @@ void Instructions::start(Mode mode) { manual_quit_ = false; counter_ = 0; ticks_ = 0; + elapsed_s_ = 0.0F; } // Indica si las instrucciones han terminado diff --git a/source/game/scenes/instructions.h b/source/game/scenes/instructions.h index 2471344..26a7049 100644 --- a/source/game/scenes/instructions.h +++ b/source/game/scenes/instructions.h @@ -23,11 +23,12 @@ class Instructions { Instructions(const Instructions &) = delete; auto operator=(const Instructions &) -> Instructions & = delete; - void run(Mode mode); // Bucle principal - void start(Mode mode); // Inicia las instrucciones (sin bucle) - void update(); // Actualiza las variables - void render(); // Pinta en pantalla - void checkEvents(); // Comprueba los eventos + void run(Mode mode); // Bucle principal + void start(Mode mode); // Inicia las instrucciones (sin bucle) + void update(); // Actualiza las variables (frame-based) + void update(float dt_s); // Actualiza las variables (time-based) + void render(); // Pinta en pantalla + void checkEvents(); // Comprueba los eventos [[nodiscard]] auto hasFinished() const -> bool; // Indica si las instrucciones han terminado [[nodiscard]] auto isQuitRequested() const -> bool; // Indica si se ha solicitado salir de la aplicación @@ -43,14 +44,20 @@ class Instructions { Section *section_; // Estado del bucle principal para saber si continua o se sale // Variables - Uint16 counter_; // Contador + Uint16 counter_; // Contador (derivat de elapsed_s_ * 60 en mode time-based) Uint16 counter_end_; // Valor final para el contador - Uint32 ticks_; // Contador de ticks para ajustar la velocidad del programa - Uint32 ticks_speed_; // Velocidad a la que se repiten los bucles del programa + Uint32 ticks_; // Contador de ticks para ajustar la velocidad del programa (frame-based) + Uint32 ticks_speed_; // Velocidad a la que se repiten los bucles del programa (frame-based) + float elapsed_s_{0.0F}; // Acumulador de temps (time-based) bool manual_quit_; // Indica si se quiere salir del modo manual Mode mode_{Instructions::Mode::AUTO}; // Modo en el que se van a ejecutar las instrucciones bool finished_; // Indica si las instrucciones han terminado bool quit_requested_; // Indica si se ha solicitado salir de la aplicación + // Time-based: durada total de la escena en mode AUTO (600 frames a 60Hz). + static constexpr float SCENE_DURATION_S = 10.0F; + // Time-based: temps mínim al mode MANUAL abans de poder sortir (30 frames a 60Hz). + static constexpr float MANUAL_QUIT_DELAY_S = 0.5F; + void checkInput(); // Comprueba las entradas }; diff --git a/source/game/scenes/intro.cpp b/source/game/scenes/intro.cpp index 4955887..605d567 100644 --- a/source/game/scenes/intro.cpp +++ b/source/game/scenes/intro.cpp @@ -12,9 +12,29 @@ #include "core/rendering/smartsprite.h" // for SmartSprite #include "core/rendering/writer.h" // for Writer #include "core/resources/resource.h" +#include "core/system/delta_time.hpp" #include "game/defaults.hpp" // for GAMECANVAS_CENTER_X, GAMECANVAS_FIRST_QU... #include "utils/utils.h" // for Section, Color +// =========================================================================== +// Time-based. Tots els valors precalculats: velocitats en px/s, acceleracions +// en px/s^2, durades en segons. La cadència real de la versió frame-based era +// empíricament ~60 Hz (el gate `> 15 ms` esperava al següent múltiple del +// refresh del SO, típicament 16.67 ms). Per això la conversió és: +// vel px/tick → vel * 60 = px/s +// acc px/tick² → acc * 3600 = px/s² +// counter frames → counter/60 = segons +// =========================================================================== + +namespace { + // Durades comunes (segons). Arrodonides amunt respecte a la conversió + // exacta des de frames per a tenir valors més "bonics" i una mica més + // de respir visual. + constexpr float BITMAP_REMAINING_TIME_S = 0.5F; // de 0.333 (20/60) ⇒ 0.5 + constexpr float BITMAP_FALLING_REMAINING_TIME_S = 5.0F; // de 4.167 (250/60) ⇒ 5.0 + constexpr float TEXT_REMAINING_TIME_S = 3.0F; // 180/60 ⇒ ja és exacte +} // namespace + // Constructor Intro::Intro(SDL_Renderer *renderer, Section *section) { // Copia los punteros @@ -30,8 +50,6 @@ Intro::Intro(SDL_Renderer *renderer, Section *section) { // Inicializa variables section->name = SECTION_PROG_INTRO; section->subsection = 0; - ticks_ = 0; - ticks_speed_ = 15; scene_ = 1; // Inicializa los bitmaps de la intro @@ -40,62 +58,69 @@ Intro::Intro(SDL_Renderer *renderer, Section *section) { auto *ss = new SmartSprite(texture_, renderer); ss->setWidth(128); ss->setHeight(96); - ss->setEnabledCounter(20); + ss->setRemainingTime(BITMAP_REMAINING_TIME_S); ss->setDestX(GAMECANVAS_CENTER_X - 64); ss->setDestY(GAMECANVAS_FIRST_QUARTER_Y - 24); bitmaps_.push_back(ss); } + // bitmap 0: entra des de l'esquerra, accelerant cap a la dreta bitmaps_[0]->setPosX(-128); bitmaps_[0]->setPosY(GAMECANVAS_FIRST_QUARTER_Y - 24); bitmaps_[0]->setVelX(0.0F); bitmaps_[0]->setVelY(0.0F); - bitmaps_[0]->setAccelX(0.6F); + bitmaps_[0]->setAccelX(2160.0F); // 0.6 px/tick² ⇒ 0.6 * 3600 px/s² bitmaps_[0]->setAccelY(0.0F); bitmaps_[0]->setSpriteClip(0, 0, 128, 96); + // bitmap 1: entra des de la dreta amb velocitat negativa i accelera més bitmaps_[1]->setPosX(GAMECANVAS_WIDTH); bitmaps_[1]->setPosY(GAMECANVAS_FIRST_QUARTER_Y - 24); - bitmaps_[1]->setVelX(-1.0F); + bitmaps_[1]->setVelX(-60.0F); // -1 px/tick ⇒ -60 px/s bitmaps_[1]->setVelY(0.0F); - bitmaps_[1]->setAccelX(-0.3F); + bitmaps_[1]->setAccelX(-1080.0F); // -0.3 px/tick² ⇒ -1080 px/s² bitmaps_[1]->setAccelY(0.0F); bitmaps_[1]->setSpriteClip(128, 0, 128, 96); + // bitmap 2: cau des de dalt; queda visible més temps (escena "GRITO") bitmaps_[2]->setPosX(GAMECANVAS_CENTER_X - 64); bitmaps_[2]->setPosY(-96); bitmaps_[2]->setVelX(0.0F); - bitmaps_[2]->setVelY(3.0F); - bitmaps_[2]->setAccelX(0.1F); - bitmaps_[2]->setAccelY(0.3F); + bitmaps_[2]->setVelY(180.0F); // 3 px/tick ⇒ 180 px/s + bitmaps_[2]->setAccelX(360.0F); // 0.1 px/tick² ⇒ 360 px/s² + bitmaps_[2]->setAccelY(1080.0F); // 0.3 px/tick² ⇒ 1080 px/s² bitmaps_[2]->setSpriteClip(0, 96, 128, 96); - bitmaps_[2]->setEnabledCounter(250); + bitmaps_[2]->setRemainingTime(BITMAP_FALLING_REMAINING_TIME_S); + // bitmap 3: puja lentament des de baix (reflexió) bitmaps_[3]->setPosX(GAMECANVAS_CENTER_X - 64); bitmaps_[3]->setPosY(GAMECANVAS_HEIGHT); bitmaps_[3]->setVelX(0.0F); - bitmaps_[3]->setVelY(-0.7F); + bitmaps_[3]->setVelY(-42.0F); // -0.7 px/tick ⇒ -42 px/s bitmaps_[3]->setAccelX(0.0F); bitmaps_[3]->setAccelY(0.0F); bitmaps_[3]->setSpriteClip(128, 96, 128, 96); + // bitmap 4: cau des de dalt (mateix que bitmap 2, sense temps allargat) bitmaps_[4]->setPosX(GAMECANVAS_CENTER_X - 64); bitmaps_[4]->setPosY(-96); bitmaps_[4]->setVelX(0.0F); - bitmaps_[4]->setVelY(3.0F); - bitmaps_[4]->setAccelX(0.1F); - bitmaps_[4]->setAccelY(0.3F); + bitmaps_[4]->setVelY(180.0F); + bitmaps_[4]->setAccelX(360.0F); + bitmaps_[4]->setAccelY(1080.0F); bitmaps_[4]->setSpriteClip(0, 192, 128, 96); + // bitmap 5: entra des de la dreta lentament bitmaps_[5]->setPosX(GAMECANVAS_WIDTH); bitmaps_[5]->setPosY(GAMECANVAS_FIRST_QUARTER_Y - 24); - bitmaps_[5]->setVelX(-0.7F); + bitmaps_[5]->setVelX(-42.0F); // -0.7 px/tick ⇒ -42 px/s bitmaps_[5]->setVelY(0.0F); bitmaps_[5]->setAccelX(0.0F); bitmaps_[5]->setAccelY(0.0F); bitmaps_[5]->setSpriteClip(128, 192, 128, 96); - // Inicializa los textos de la intro + // Inicializa los textos de la intro. Time-based: setSecondsPerChar. + // Conversió: frames_per_char / 60 = segons_per_char. const int TOTAL_TEXTS = 9; for (int i = 0; i < TOTAL_TEXTS; ++i) { auto *w = new Writer(text_); @@ -103,51 +128,53 @@ Intro::Intro(SDL_Renderer *renderer, Section *section) { w->setPosY(GAMECANVAS_HEIGHT - (BLOCK * 6)); w->setKerning(-1); w->setEnabled(false); - w->setEnabledCounter(180); + w->setRemainingTime(TEXT_REMAINING_TIME_S); texts_.push_back(w); } // Un dia qualsevol de l'any 2000 texts_[0]->setCaption(Lang::get()->getText(27)); - texts_[0]->setSpeed(8); + texts_[0]->setSecondsPerChar(0.15F); // de 0.1333 (8/60) ⇒ 0.15 // Tot esta tranquil a la UPV texts_[1]->setCaption(Lang::get()->getText(28)); - texts_[1]->setSpeed(8); + texts_[1]->setSecondsPerChar(0.15F); // Fins que un desaprensiu... texts_[2]->setCaption(Lang::get()->getText(29)); - texts_[2]->setSpeed(12); + texts_[2]->setSecondsPerChar(0.2F); // 12/60 ⇒ ja és 0.2 // HEY! ME ANE A FERME UN CORTAET... texts_[3]->setCaption(Lang::get()->getText(30)); - texts_[3]->setSpeed(8); + texts_[3]->setSecondsPerChar(0.15F); // UAAAAAAAAAAAAA!!! texts_[4]->setCaption(Lang::get()->getText(31)); - texts_[4]->setSpeed(1); + texts_[4]->setSecondsPerChar(0.02F); // de 0.0167 (1/60) ⇒ 0.02 // Espera un moment... texts_[5]->setCaption(Lang::get()->getText(32)); - texts_[5]->setSpeed(16); + texts_[5]->setSecondsPerChar(0.3F); // de 0.2667 (16/60) ⇒ 0.3 // Si resulta que no tinc solt! texts_[6]->setCaption(Lang::get()->getText(33)); - texts_[6]->setSpeed(2); + texts_[6]->setSecondsPerChar(0.05F); // de 0.0333 (2/60) ⇒ 0.05 // MERDA DE MAQUINA! texts_[7]->setCaption(Lang::get()->getText(34)); - texts_[7]->setSpeed(3); + texts_[7]->setSecondsPerChar(0.05F); // 3/60 ⇒ ja és 0.05 // Blop... blop... blop... texts_[8]->setCaption(Lang::get()->getText(35)); - texts_[8]->setSpeed(16); + texts_[8]->setSecondsPerChar(0.3F); for (auto *t : texts_) { t->center(GAMECANVAS_CENTER_X); } Audio::get()->playMusic(music_, 0); + + DeltaTime::reset(); } // Destructor @@ -332,26 +359,19 @@ void Intro::updateScene6() { } // Actualiza las variables del objeto -void Intro::update() { +void Intro::update(float dt_s) { Audio::update(); checkInput(); - if (SDL_GetTicks() - ticks_ > ticks_speed_) { - // Actualiza el contador de ticks - ticks_ = SDL_GetTicks(); - - // Actualiza los objetos - for (auto *bitmap : bitmaps_) { - bitmap->update(); - } - - for (auto *t : texts_) { - t->update(); - } - - // Actualiza las escenas de la intro - updateScenes(); + for (auto *bitmap : bitmaps_) { + bitmap->update(dt_s); } + + for (auto *t : texts_) { + t->update(dt_s); + } + + updateScenes(); } // Dibuja el objeto en pantalla @@ -386,7 +406,8 @@ void Intro::run() { // Ejecuta un frame void Intro::iterate() { - update(); + const float DELTA_TIME_S = DeltaTime::tick(); + update(DELTA_TIME_S); render(); } diff --git a/source/game/scenes/intro.h b/source/game/scenes/intro.h index 99ac45f..5f856ac 100644 --- a/source/game/scenes/intro.h +++ b/source/game/scenes/intro.h @@ -36,15 +36,13 @@ class Intro { Section *section_; // Estado del bucle principal para saber si continua o se sale // Variables - Uint32 ticks_; // Contador de ticks para ajustar la velocidad del programa - Uint8 ticks_speed_; // Velocidad a la que se repiten los bucles del programa Ja::Music *music_; // Musica para la intro - int scene_; // Indica que escena está activa + int scene_; // Indica que escena está activa - void update(); // Actualiza las variables del objeto - void render(); // Dibuja el objeto en pantalla - void checkInput(); // Comprueba las entradas - void updateScenes(); // Actualiza las escenas de la intro + void update(float dt_s); // Actualiza las variables del objeto (time-based) + void render(); // Dibuja el objeto en pantalla + void checkInput(); // Comprueba las entradas + void updateScenes(); // Actualiza las escenas de la intro // Helpers de updateScenes, uno por cada escena void updateScene1(); diff --git a/source/game/scenes/logo.cpp b/source/game/scenes/logo.cpp index ea30fa4..d271909 100644 --- a/source/game/scenes/logo.cpp +++ b/source/game/scenes/logo.cpp @@ -11,12 +11,16 @@ #include "core/rendering/screen.h" // for Screen #include "core/rendering/sprite.h" // for Sprite #include "core/resources/resource.h" -#include "game/defaults.hpp" // for bgColor, SECTION_PROG_LOGO, SECTION_PROG... -#include "utils/utils.h" // for Section, Color +#include "core/system/delta_time.hpp" // for DeltaTime::reset / tick +#include "game/defaults.hpp" // for bgColor, SECTION_PROG_LOGO, SECTION_PROG... +#include "utils/utils.h" // for Section, Color -// Valores de inicialización y fin -constexpr int INIT_FADE = 100; -constexpr int END_LOGO = 200; +// Durades de l'escena (segons). Time-based: ja no comptem frames. Valors +// equivalents al comportament anterior (frame counter a 15ms): 100 i 200 +// frames ⇒ 1.5s i 3.0s; fi a 220 frames ⇒ 3.3s. +constexpr float FADE_START_S = 1.5F; +constexpr float FADE_END_S = 3.0F; +constexpr float SCENE_END_S = 3.3F; // Constructor Logo::Logo(SDL_Renderer *renderer, Section *section) { @@ -30,13 +34,14 @@ Logo::Logo(SDL_Renderer *renderer, Section *section) { sprite_ = new Sprite(14, 75, 226, 44, texture_, renderer); // Inicializa variables - counter_ = 0; section->name = SECTION_PROG_LOGO; section->subsection = 0; - ticks_ = 0; - ticks_speed_ = 15; Audio::get()->stopMusic(); + + // Reset del rellotge: la primera crida a tick() retornarà ~0 i no un + // delta gegant arrossegat des del boot o l'escena anterior. + DeltaTime::reset(); } // Destructor @@ -48,7 +53,7 @@ Logo::~Logo() { // Comprueba si ha terminado el logo void Logo::checkLogoEnd() { - if (counter_ >= END_LOGO + 20) { + if (elapsed_time_s_ >= SCENE_END_S) { section_->name = SECTION_PROG_INTRO; section_->subsection = 0; } @@ -68,9 +73,8 @@ void Logo::checkInput() { // Dibuja el fade void Logo::renderFade() { - // Dibuja el fade - if (counter_ >= INIT_FADE) { - const float STEP = (float)(counter_ - INIT_FADE) / (float)(END_LOGO - INIT_FADE); + if (elapsed_time_s_ >= FADE_START_S) { + const float STEP = (elapsed_time_s_ - FADE_START_S) / (FADE_END_S - FADE_START_S); const int ALPHA = std::min((int)(255 * STEP), 255); SDL_SetRenderDrawColor(renderer_, BG_COLOR.r, BG_COLOR.g, BG_COLOR.b, ALPHA); SDL_RenderFillRect(renderer_, nullptr); @@ -78,20 +82,12 @@ void Logo::renderFade() { } // Actualiza las variables del objeto -void Logo::update() { +void Logo::update(float delta_time_s) { Audio::update(); checkInput(); - if (SDL_GetTicks() - ticks_ > ticks_speed_) { - // Actualiza el contador de ticks - ticks_ = SDL_GetTicks(); - - // Actualiza el contador - counter_++; - - // Comprueba si ha terminado el logo - checkLogoEnd(); - } + elapsed_time_s_ += delta_time_s; + checkLogoEnd(); } // Dibuja el objeto en pantalla @@ -123,7 +119,8 @@ void Logo::run() { // Ejecuta un frame void Logo::iterate() { - update(); + const float DELTA_TIME_S = DeltaTime::tick(); + update(DELTA_TIME_S); render(); } diff --git a/source/game/scenes/logo.h b/source/game/scenes/logo.h index caef369..82184ac 100644 --- a/source/game/scenes/logo.h +++ b/source/game/scenes/logo.h @@ -26,14 +26,12 @@ class Logo { Sprite *sprite_; // Sprite con la textura del logo Section *section_; // Estado del bucle principal para saber si continua o se sale - // Variables - Uint32 ticks_; // Contador de ticks para ajustar la velocidad del programa - Uint32 ticks_speed_; // Velocidad a la que se repiten los bucles del programa - int counter_; // Contador + // Temps acumulat de l'escena (segons). Time-based: no comptem frames. + float elapsed_time_s_{0.0F}; - void update(); // Actualiza las variables del objeto - void render(); // Dibuja el objeto en pantalla - void checkLogoEnd(); // Comprueba si ha terminado el logo - void checkInput(); // Comprueba las entradas - void renderFade(); // Dibuja el fade + void update(float delta_time_s); // Actualiza las variables del objeto + void render(); // Dibuja el objeto en pantalla + void checkLogoEnd(); // Comprueba si ha terminado el logo + void checkInput(); // Comprueba las entradas + void renderFade(); // Dibuja el fade }; diff --git a/source/game/scenes/title.cpp b/source/game/scenes/title.cpp index b1f157a..dd2f73a 100644 --- a/source/game/scenes/title.cpp +++ b/source/game/scenes/title.cpp @@ -2,9 +2,10 @@ #include -#include // for rand -#include // for basic_ostream, operator<<, basic_ostrea... -#include // for basic_string, operator+, char_traits +#include // for min +#include // for rand +#include // for basic_ostream, operator<<, basic_ostrea... +#include // for basic_string, operator+, char_traits #include "core/audio/audio.hpp" // for Audio #include "core/input/global_inputs.hpp" // for GlobalInputs::handle @@ -19,10 +20,11 @@ #include "core/rendering/texture.h" // for Texture #include "core/resources/asset.h" // for Asset #include "core/resources/resource.h" -#include "game/defaults.hpp" // for GAMECANVAS_CENTER_X, SECTION_PROG_QUIT -#include "game/game.h" // for Game -#include "game/options.hpp" // for Options -#include "game/ui/menu.h" // for Menu +#include "core/system/delta_time.hpp" // for DeltaTime::reset / tick +#include "game/defaults.hpp" // for GAMECANVAS_CENTER_X, SECTION_PROG_QUIT +#include "game/game.h" // for Game +#include "game/options.hpp" // for Options +#include "game/ui/menu.h" // for Menu // Constructor Title::Title(SDL_Renderer *renderer, Section *section) { @@ -70,6 +72,9 @@ Title::Title(SDL_Renderer *renderer, Section *section) { // Inicializa los valores init(); + + // Reset del rellotge: la primera crida a tick() retornarà ~0. + DeltaTime::reset(); } // Destructor @@ -92,18 +97,19 @@ Title::~Title() { void Title::init() { // Inicializa variables section_->subsection = SUBSECTION_TITLE_1; - counter_ = COUNTER; - background_counter_ = 0; + demo_remaining_s_ = DEMO_TIMEOUT_S; + bg_scroll_x_s_ = 0.0F; + bg_scroll_y_s_ = 0.0F; + bg_phase_s_ = 0.0F; + blink_phase_s_ = 0.0F; background_mode_ = rand() % 2; menu_visible_ = false; menu_.active = menu_.title; next_section_.name = SECTION_PROG_GAME; post_fade_ = 0; - ticks_ = 0; - ticks_speed_ = 15; fade_->init(0x17, 0x17, 0x26); demo_ = true; - vibration_step_ = 0; + vibration_elapsed_s_ = 0.0F; vibration_initialized_ = false; instructions_active_ = false; demo_game_active_ = false; @@ -148,12 +154,12 @@ void Title::init() { coffee_bitmap_->setWidth(167); coffee_bitmap_->setHeight(46); coffee_bitmap_->setVelX(0.0F); - coffee_bitmap_->setVelY(2.5F); + coffee_bitmap_->setVelY(150.0F); // 2.5 px/tick ⇒ 150 px/s coffee_bitmap_->setAccelX(0.0F); - coffee_bitmap_->setAccelY(0.1F); + coffee_bitmap_->setAccelY(360.0F); // 0.1 px/tick² ⇒ 360 px/s² coffee_bitmap_->setSpriteClip(0, 0, 167, 46); coffee_bitmap_->setEnabled(true); - coffee_bitmap_->setEnabledCounter(0); + coffee_bitmap_->setRemainingTime(0.0F); coffee_bitmap_->setDestX(45); coffee_bitmap_->setDestY(11); @@ -164,12 +170,12 @@ void Title::init() { crisis_bitmap_->setWidth(137); crisis_bitmap_->setHeight(46); crisis_bitmap_->setVelX(0.0F); - crisis_bitmap_->setVelY(-2.5F); + crisis_bitmap_->setVelY(-150.0F); // -2.5 px/tick ⇒ -150 px/s crisis_bitmap_->setAccelX(0.0F); - crisis_bitmap_->setAccelY(-0.1F); + crisis_bitmap_->setAccelY(-360.0F); // -0.1 px/tick² ⇒ -360 px/s² crisis_bitmap_->setSpriteClip(0, 0, 137, 46); crisis_bitmap_->setEnabled(true); - crisis_bitmap_->setEnabledCounter(0); + crisis_bitmap_->setRemainingTime(0.0F); crisis_bitmap_->setDestX(60); crisis_bitmap_->setDestY(57); @@ -209,25 +215,20 @@ void Title::init() { updateMenuLabels(); } -// Actualiza las variables del objeto -void Title::update() { +// Actualiza las variables del objeto (time-based) +void Title::update(float dt_s) { Audio::update(); checkInput(); - if (SDL_GetTicks() - ticks_ <= ticks_speed_) { - return; - } - ticks_ = SDL_GetTicks(); - switch (section_->subsection) { case SUBSECTION_TITLE_1: - updateTitle1(); + updateTitle1(dt_s); break; case SUBSECTION_TITLE_2: - updateTitle2(); + updateTitle2(dt_s); break; case SUBSECTION_TITLE_3: - updateTitle3(); + updateTitle3(dt_s); break; default: break; @@ -235,9 +236,9 @@ void Title::update() { } // Sección 1 - Titulo desplazandose -void Title::updateTitle1() { - coffee_bitmap_->update(); - crisis_bitmap_->update(); +void Title::updateTitle1(float dt_s) { + coffee_bitmap_->update(dt_s); + crisis_bitmap_->update(dt_s); // Si los objetos han llegado a su destino, cambiamos de Sección if (coffee_bitmap_->hasFinished() && crisis_bitmap_->hasFinished()) { @@ -257,7 +258,7 @@ void Title::updateTitle1() { } // Sección 2 - Titulo vibrando -void Title::updateTitle2() { +void Title::updateTitle2(float dt_s) { // Captura las posiciones base la primera vez if (!vibration_initialized_) { vibration_coffee_base_x_ = coffee_bitmap_->getPosX(); @@ -265,37 +266,44 @@ void Title::updateTitle2() { vibration_initialized_ = true; } + // Patró d'offset horitzontal (15 valors, una vegada cadascun cada 3 frames) const int V[] = {-1, 1, -1, 1, -1, 1, -1, 1, -1, 1, -1, 1, -1, 1, 0}; - coffee_bitmap_->setPosX(vibration_coffee_base_x_ + V[vibration_step_ / 3]); - crisis_bitmap_->setPosX(vibration_crisis_base_x_ + V[vibration_step_ / 3]); - dust_bitmap_right_->update(); - dust_bitmap_left_->update(); + constexpr int V_SIZE = static_cast(sizeof(V) / sizeof(V[0])); + const int IDX = std::min(static_cast(vibration_elapsed_s_ / VIBRATION_STEP_DURATION_S), V_SIZE - 1); + coffee_bitmap_->setPosX(vibration_coffee_base_x_ + V[IDX]); + crisis_bitmap_->setPosX(vibration_crisis_base_x_ + V[IDX]); + dust_bitmap_right_->update(dt_s); + dust_bitmap_left_->update(dt_s); - vibration_step_++; + vibration_elapsed_s_ += dt_s; - if (vibration_step_ >= 33) { + if (vibration_elapsed_s_ >= VIBRATION_DURATION_S) { section_->subsection = SUBSECTION_TITLE_3; - vibration_step_ = 0; + vibration_elapsed_s_ = 0.0F; vibration_initialized_ = false; } } // Sección 3 - La pantalla de titulo con el menú y la música -void Title::updateTitle3() { - if (counter_ > 0) { +void Title::updateTitle3(float dt_s) { + if (demo_remaining_s_ > 0.0F) { if (Audio::getRealMusicState() == Audio::MusicState::STOPPED) { Audio::get()->playMusic(title_music_); } - dust_bitmap_right_->update(); - dust_bitmap_left_->update(); - fade_->update(); + dust_bitmap_right_->update(dt_s); + dust_bitmap_left_->update(dt_s); + fade_->update(dt_s); if (fade_->hasEnded()) { handlePostFadeAction(); } - updateBG(); + updateBG(dt_s); + blink_phase_s_ += dt_s; + if (blink_phase_s_ >= PRESS_ANY_KEY_PERIOD_S) { + blink_phase_s_ -= PRESS_ANY_KEY_PERIOD_S; + } if (menu_visible_ && !fade_->isEnabled()) { menu_.active->update(); @@ -312,9 +320,9 @@ void Title::updateTitle3() { } if (menu_.active->getName() == "TITLE") { - counter_--; + demo_remaining_s_ -= dt_s; } - } else if (counter_ == 0) { + } else { if (demo_) { demo_then_instructions_ = true; runDemoGame(); @@ -351,7 +359,7 @@ void Title::handlePostFadeAction() { break; case 3: // TIME OUT - counter_ = COUNTER; + demo_remaining_s_ = DEMO_TIMEOUT_S; menu_.active->reset(); if (demo_) { demo_then_instructions_ = true; @@ -602,8 +610,8 @@ void Title::render() { dust_bitmap_right_->render(); dust_bitmap_left_->render(); - // PRESS ANY KEY! - if ((counter_ % 50 > 14) && (!menu_visible_)) { + // PRESS ANY KEY! Blink: visible quan la fase passa l'umbral "off". + if ((blink_phase_s_ > PRESS_ANY_KEY_OFF_S) && (!menu_visible_)) { text1_->writeDX(Text::FLAG_CENTER | Text::FLAG_SHADOW, GAMECANVAS_CENTER_X, PLAY_AREA_THIRD_QUARTER_Y + BLOCK, Lang::get()->getText(23), 1, NO_COLOR, 1, SHADOW_COLOR); } @@ -626,15 +634,26 @@ void Title::checkInput() { GlobalInputs::handle(); } -// Actualiza el tileado de fondo -void Title::updateBG() { - if (background_mode_ == 0) { // El tileado de fondo se desplaza en diagonal - ++background_window_.x %= 64; - ++background_window_.y %= 64; - } else { // El tileado de fondo se desplaza en circulo - ++background_counter_ %= 360; - background_window_.x = 128 + (int(sin_[(background_counter_ + 270) % 360] * 128)); - background_window_.y = 96 + (int(sin_[(360 - background_counter_) % 360] * 96)); +// Actualiza el tileado de fondo (time-based). +void Title::updateBG(float dt_s) { + if (background_mode_ == 0) { + // Diagonal: 60 px/s a 60Hz ⇒ un cicle de 64 px cada 64/60 = 1.067 s. + // Ancorat a la posició inicial (128, 96): t=0 dona la mateixa vista que + // l'estàtic dels Title1/Title2, sense salt visual a l'entrada del Title3. + constexpr float SCROLL_PERIOD_S = 64.0F / BG_SCROLL_SPEED_PX_PER_S; + bg_scroll_x_s_ += dt_s; + bg_scroll_y_s_ += dt_s; + if (bg_scroll_x_s_ >= SCROLL_PERIOD_S) { bg_scroll_x_s_ -= SCROLL_PERIOD_S; } + if (bg_scroll_y_s_ >= SCROLL_PERIOD_S) { bg_scroll_y_s_ -= SCROLL_PERIOD_S; } + background_window_.x = 128 + static_cast(bg_scroll_x_s_ * BG_SCROLL_SPEED_PX_PER_S); + background_window_.y = 96 + static_cast(bg_scroll_y_s_ * BG_SCROLL_SPEED_PX_PER_S); + } else { + // Cercle: 360 graus en BG_CIRCLE_PERIOD_S segons. + bg_phase_s_ += dt_s; + if (bg_phase_s_ >= BG_CIRCLE_PERIOD_S) { bg_phase_s_ -= BG_CIRCLE_PERIOD_S; } + const int ANGLE = static_cast((bg_phase_s_ / BG_CIRCLE_PERIOD_S) * 360.0F) % 360; + background_window_.x = 128 + (int(sin_[(ANGLE + 270) % 360] * 128)); + background_window_.y = 96 + (int(sin_[(360 - ANGLE) % 360] * 96)); } } @@ -834,9 +853,11 @@ void Title::applyOptions() { // Ejecuta un frame void Title::iterate() { + const float DELTA_TIME_S = DeltaTime::tick(); + // Si las instrucciones están activas, delega el frame if (instructions_active_) { - instructions_->update(); + instructions_->update(DELTA_TIME_S); instructions_->render(); if (instructions_->hasFinished()) { @@ -855,6 +876,8 @@ void Title::iterate() { section_->name = SECTION_PROG_TITLE; section_->subsection = SUBSECTION_TITLE_3; } + // Reset del rellotge per evitar un dt enorme al tornar al Title. + DeltaTime::reset(); } return; } @@ -884,6 +907,8 @@ void Title::iterate() { section_->name = SECTION_PROG_TITLE; section_->subsection = SUBSECTION_TITLE_1; } + // Reset del rellotge per evitar un dt enorme al tornar al Title. + DeltaTime::reset(); } else { // Restaura section para que Director no transicione fuera de Title section_->name = SECTION_PROG_TITLE; @@ -892,7 +917,7 @@ void Title::iterate() { } // Ejecución normal del título - update(); + update(DELTA_TIME_S); render(); } @@ -918,7 +943,7 @@ void Title::handleEvent(const SDL_Event *event) { if (section_->subsection == SUBSECTION_TITLE_3) { if ((event->type == SDL_EVENT_KEY_UP) || (event->type == SDL_EVENT_JOYSTICK_BUTTON_UP)) { menu_visible_ = true; - counter_ = COUNTER; + demo_remaining_s_ = DEMO_TIMEOUT_S; } } } diff --git a/source/game/scenes/title.h b/source/game/scenes/title.h index b8d5a21..0b303c4 100644 --- a/source/game/scenes/title.h +++ b/source/game/scenes/title.h @@ -34,7 +34,21 @@ class Title { private: static constexpr const char *COPYRIGHT = "@2020 JailDesigner (v2.3.4)"; - static constexpr int COUNTER = 800; + // Time-based: temps màxim a la pantalla del títol abans de tornar al + // logo o llançar el demo. 800 frames a 60Hz ⇒ 13.333 s. + static constexpr float DEMO_TIMEOUT_S = 13.333F; + // Període i fracció "off" del blink del text "PRESS ANY KEY". + // Original: counter_ % 50 > 14 ⇒ 50 frames de període, 14 frames off. + static constexpr float PRESS_ANY_KEY_PERIOD_S = 50.0F / 60.0F; + static constexpr float PRESS_ANY_KEY_OFF_S = 14.0F / 60.0F; + // Vibració post-impacte (SUBSECTION_TITLE_2): 33 frames a 60Hz ⇒ 0.55 s, + // 11 valors del patró V[] consumits a `step/3` (3 frames per pas). + static constexpr float VIBRATION_DURATION_S = 33.0F / 60.0F; + static constexpr float VIBRATION_STEP_DURATION_S = 3.0F / 60.0F; + // BG mode 0: scroll diagonal 1 px/tick a 60Hz ⇒ 60 px/s. + static constexpr float BG_SCROLL_SPEED_PX_PER_S = 60.0F; + // BG mode 1: cicle de 360 frames a 60Hz ⇒ 6 s per volta. + static constexpr float BG_CIRCLE_PERIOD_S = 6.0F; struct MenuData { Menu *title; // Menu de la pantalla de título @@ -72,19 +86,20 @@ class Title { Fade *fade_; // Objeto para realizar fundidos en pantalla // Variables - Ja::Music *title_music_; // Musica para el titulo - Ja::Sound *crash_sound_; // Sonido con el impacto del título - int background_counter_; // Temporizador para el fondo de tiles de la pantalla de titulo - int counter_; // Temporizador para la pantalla de titulo - Uint32 ticks_; // Contador de ticks para ajustar la velocidad del programa - Uint8 background_mode_; // Variable para almacenar el tipo de efecto que hará el fondo de la pantalla de titulo - float sin_[360]; // Vector con los valores del seno precalculados - bool menu_visible_; // Indicador para saber si se muestra el menu del titulo o la frase intermitente - bool demo_; // Indica si el modo demo estará activo - Section next_section_; // Indica cual es la siguiente sección a cargar cuando termine el contador del titulo - Uint32 ticks_speed_; // Velocidad a la que se repiten los bucles del programa - Uint8 post_fade_; // Opción a realizar cuando termina el fundido - MenuData menu_; // Variable con todos los objetos menus y sus variables + Ja::Music *title_music_; // Musica para el titulo + Ja::Sound *crash_sound_; // Sonido con el impacto del título + float bg_scroll_x_s_{0.0F}; // Acumulador d'scroll horitzontal (segons) per al BG mode 0 + float bg_scroll_y_s_{0.0F}; // Acumulador d'scroll vertical (segons) per al BG mode 0 + float bg_phase_s_{0.0F}; // Fase del cicle del BG mode 1 (0..BG_CIRCLE_PERIOD_S) + float demo_remaining_s_; // Temps que queda al títol abans de tornar al logo / demo + float blink_phase_s_{0.0F}; // Fase del blink de "PRESS ANY KEY" + Uint8 background_mode_; // Variable para almacenar el tipo de efecto que hará el fondo de la pantalla de titulo + float sin_[360]; // Vector con los valores del seno precalculados + bool menu_visible_; // Indicador para saber si se muestra el menu del titulo o la frase intermitente + bool demo_; // Indica si el modo demo estará activo + Section next_section_; // Indica cual es la siguiente sección a cargar cuando termine el contador del titulo + Uint8 post_fade_; // Opción a realizar cuando termina el fundido + MenuData menu_; // Variable con todos los objetos menus y sus variables // Snapshot per a permetre CANCEL al menú d'opcions. Options::Video prev_video_; Options::Window prev_window_; @@ -94,10 +109,10 @@ class Title { std::vector device_index_; // Indice para el jugador [i] del vector de dispositivos de entrada disponibles // Variables para la vibración del título (SUBSECTION_TITLE_2) - int vibration_step_; // Paso actual de la vibración - int vibration_coffee_base_x_{0}; // Posición X base del bitmap Coffee - int vibration_crisis_base_x_{0}; // Posición X base del bitmap Crisis - bool vibration_initialized_; // Indica si se han capturado las posiciones base + float vibration_elapsed_s_{0.0F}; // Temps transcorregut des de l'inici de la vibració + int vibration_coffee_base_x_{0}; // Posición X base del bitmap Coffee + int vibration_crisis_base_x_{0}; // Posición X base del bitmap Crisis + bool vibration_initialized_; // Indica si se han capturado las posiciones base // Variables para sub-estados delegados (instrucciones y demo) bool instructions_active_; // Indica si las instrucciones están activas @@ -106,20 +121,20 @@ class Title { bool demo_then_instructions_; // Indica si tras la demo hay que mostrar instrucciones void init(); // Inicializa los valores - void update(); // Actualiza las variables del objeto + void update(float dt_s); // Actualiza las variables del objeto (time-based) void render(); // Dibuja el objeto en pantalla static void checkInput(); // Comprueba las entradas (només delega a GlobalInputs) // Helpers de update, uno por cada subsección y por cada switch dentro del título 3 - void updateTitle1(); - void updateTitle2(); - void updateTitle3(); + void updateTitle1(float dt_s); + void updateTitle2(float dt_s); + void updateTitle3(float dt_s); void handlePostFadeAction(); void handleTitleMenuSelection(); void handlePlayerSelectMenuSelection(); void handleOptionsMenuSelection(); - void updateBG(); // Actualiza el tileado de fondo + void updateBG(float dt_s); // Actualiza el tileado de fondo (time-based) static void switchFullScreenModeVar(); // Cambia el valor de la variable de modo de pantalla completa void updateMenuLabels() const; // Actualiza los elementos de los menus void applyOptions(); // Aplica las opciones de menu seleccionadas