Merge branch 'feat/title-intro-sequence': intro coreografiada al títol

This commit is contained in:
2026-05-22 14:05:57 +02:00
4 changed files with 166 additions and 44 deletions
+29 -4
View File
@@ -81,7 +81,7 @@ namespace Defaults::Title {
// Durades de animación // Durades de animación
constexpr float ENTRY_DURATION = 2.0F; // Entrada (segons) constexpr float ENTRY_DURATION = 2.0F; // Entrada (segons)
constexpr float EXIT_DURATION = 1.0F; // Salida (segons) constexpr float EXIT_DURATION = 1.5F; // Salida (segons)
// Flotació (oscil·lació reduïda y diferenciada per ship) // Flotació (oscil·lació reduïda y diferenciada per ship)
constexpr float FLOAT_AMPLITUDE_X = 4.0F; // Amplitud X (píxels) constexpr float FLOAT_AMPLITUDE_X = 4.0F; // Amplitud X (píxels)
@@ -96,9 +96,6 @@ namespace Defaults::Title {
constexpr float P1_ENTRY_DELAY = 0.0F; // P1 entra immediatament constexpr float P1_ENTRY_DELAY = 0.0F; // P1 entra immediatament
constexpr float P2_ENTRY_DELAY = 0.5F; // P2 entra 0.5s después constexpr float P2_ENTRY_DELAY = 0.5F; // P2 entra 0.5s después
// Delay global antes de start l'animación de entrada al state MAIN
constexpr float ENTRANCE_DELAY = 5.0F; // Temps de espera antes que las naves entrin
// Multiplicadors de freqüència para cada ship (variació sutil ±12%) // Multiplicadors de freqüència para cada ship (variació sutil ±12%)
constexpr float P1_FREQUENCY_MULTIPLIER = 0.88F; // 12% més lenta constexpr float P1_FREQUENCY_MULTIPLIER = 0.88F; // 12% més lenta
constexpr float P2_FREQUENCY_MULTIPLIER = 1.12F; // 12% més ràpida constexpr float P2_FREQUENCY_MULTIPLIER = 1.12F; // 12% més ràpida
@@ -128,6 +125,34 @@ namespace Defaults::Title {
constexpr float TEXT_SPACING = 2.0F; constexpr float TEXT_SPACING = 2.0F;
} // namespace Layout } // namespace Layout
// Coreografia de la seqüència d'entrada al state MAIN.
// Tots els elements (logo, footer, naus, press start) entren ordenadament
// segons aquests thresholds. Vegeu title_scene.cpp/updateMainState.
//
// Per al logo i el footer, l'efecte simula un moviment 3D des de l'usuari
// cap al VP: el text arrenca gran i a la posició projectada extrema (com
// si estigués prop de la càmera, fora de pantalla) i acaba a la seva
// posició final amb escala normal (com si hagués aterrat al VP). Pivot:
// centre de pantalla (= projecció del VP 3D).
namespace Sequence {
// Factor d'escala inicial. >1 = sprite gran a l'inici (prop de l'usuari).
// La posició inicial es deriva: pivot=centre, delta multiplicat per aquest factor.
constexpr float LOGO_INTRO_SCALE_START = 2.5F;
constexpr float FOOTER_INTRO_SCALE_START = 2.5F;
// Durades de les animacions d'entrada (segons).
constexpr float LOGO_ENTRY_DURATION = 1.2F;
constexpr float JAILGAMES_ENTRY_DURATION = 0.7F;
constexpr float COPYRIGHT_ENTRY_DURATION = 0.7F;
// Stagger "pam-pam" entre l'arrencada de JAILGAMES i la de COPYRIGHT.
constexpr float COPYRIGHT_STAGGER = 0.18F;
// Delays entre etapes.
constexpr float SHIPS_DELAY_AFTER_FOOTER = 0.20F;
constexpr float PRESS_START_DELAY_AFTER_SHIPS = 0.40F;
} // namespace Sequence
// Paleta neon de l'escena de títol (cian + magenta synthwave). // Paleta neon de l'escena de títol (cian + magenta synthwave).
// alpha = 255 (sentinela "color vàlid") fa que el pipeline ignori // alpha = 255 (sentinela "color vàlid") fa que el pipeline ignori
// el color global de l'oscil·lador per a aquesta crida. // el color global de l'oscil·lador per a aquesta crida.
+126 -37
View File
@@ -14,6 +14,7 @@
#include "core/defaults.hpp" #include "core/defaults.hpp"
#include "core/graphics/shape_loader.hpp" #include "core/graphics/shape_loader.hpp"
#include "core/input/input.hpp" #include "core/input/input.hpp"
#include "core/math/easing.hpp"
#include "core/rendering/shape_renderer.hpp" #include "core/rendering/shape_renderer.hpp"
#include "core/system/scene_context.hpp" #include "core/system/scene_context.hpp"
#include "project.h" #include "project.h"
@@ -68,13 +69,9 @@ TitleScene::TitleScene(SDLManager& sdl, SceneContext& context)
ship_animator_ = std::make_unique<Title::ShipAnimator>(sdl_.getRenderer(), camera_.get()); ship_animator_ = std::make_unique<Title::ShipAnimator>(sdl_.getRenderer(), camera_.get());
ship_animator_->init(); ship_animator_->init();
// Les naus comencen invisibles; updateMainState() les dispara al moment
if (estat_actual_ == TitleState::MAIN) { // correcte de la intro coreografiada (també quan venim de JUMP_TO_TITLE_MAIN).
ship_animator_->setVisible(true); ship_animator_->setVisible(false);
ship_animator_->startEntryAnimation();
} else {
ship_animator_->setVisible(false);
}
initTitle(); initTitle();
inicialitzarJailgames(); inicialitzarJailgames();
@@ -251,8 +248,23 @@ void TitleScene::inicialitzarJailgames() {
} }
void TitleScene::dibuixarPeuTitol(float spacing) const { void TitleScene::dibuixarPeuTitol(float spacing) const {
namespace S = Defaults::Title::Sequence;
// Pivot al centre de pantalla (= projecció VP). Cada element s'expandeix
// des d'aquí mentre s_factor passa de SCALE_START (gran, prop de l'usuari)
// a 1.0 (a la mida i posició finals, "lluny" al VP).
const float SCREEN_CENTRE_X = Defaults::Game::WIDTH / 2.0F;
const float SCREEN_CENTRE_Y = Defaults::Game::HEIGHT / 2.0F;
const float JAILGAMES_S = std::lerp(S::FOOTER_INTRO_SCALE_START, 1.0F, Easing::easeOutQuad(intro_jailgames_progress_));
const float COPYRIGHT_S = std::lerp(S::FOOTER_INTRO_SCALE_START, 1.0F, Easing::easeOutQuad(intro_copyright_progress_));
const float JAILGAMES_RENDER_SCALE = Defaults::Title::Layout::JAILGAMES_SCALE * JAILGAMES_S;
for (const auto& lletra : lletres_jailgames_) { for (const auto& lletra : lletres_jailgames_) {
Rendering::renderShape(sdl_.getRenderer(), lletra.shape, lletra.position, 0.0F, Defaults::Title::Layout::JAILGAMES_SCALE, 1.0F, 1.0F, Defaults::Title::Colors::JAILGAMES_LOGO); const Vec2 POS{
.x = SCREEN_CENTRE_X + (JAILGAMES_S * (lletra.position.x - SCREEN_CENTRE_X)),
.y = SCREEN_CENTRE_Y + (JAILGAMES_S * (lletra.position.y - SCREEN_CENTRE_Y)),
};
Rendering::renderShape(sdl_.getRenderer(), lletra.shape, POS, 0.0F, JAILGAMES_RENDER_SCALE, 1.0F, 1.0F, Defaults::Title::Colors::JAILGAMES_LOGO);
} }
std::string copyright = Project::COPYRIGHT; std::string copyright = Project::COPYRIGHT;
for (char& c : copyright) { for (char& c : copyright) {
@@ -260,9 +272,11 @@ void TitleScene::dibuixarPeuTitol(float spacing) const {
c = static_cast<char>(c - 32); c = static_cast<char>(c - 32);
} }
} }
const float Y_COPY = Defaults::Game::HEIGHT * Defaults::Title::Layout::COPYRIGHT1_POS; const float Y_COPY_FINAL = Defaults::Game::HEIGHT * Defaults::Title::Layout::COPYRIGHT1_POS;
const float CENTRE_X = Defaults::Game::WIDTH / 2.0F; const float COPY_X = SCREEN_CENTRE_X; // ja al centre
text_.renderCentered(copyright, {.x = CENTRE_X, .y = Y_COPY}, Defaults::Title::Layout::COPYRIGHT_SCALE, spacing, 1.0F, Defaults::Title::Colors::COPYRIGHT); const float COPY_Y = SCREEN_CENTRE_Y + (COPYRIGHT_S * (Y_COPY_FINAL - SCREEN_CENTRE_Y));
const float COPY_RENDER_SCALE = Defaults::Title::Layout::COPYRIGHT_SCALE * COPYRIGHT_S;
text_.renderCentered(copyright, {.x = COPY_X, .y = COPY_Y}, COPY_RENDER_SCALE, spacing, 1.0F, Defaults::Title::Colors::COPYRIGHT);
} }
auto TitleScene::isFinished() const -> bool { auto TitleScene::isFinished() const -> bool {
@@ -326,18 +340,48 @@ void TitleScene::updateStarfieldState(float delta_time) {
void TitleScene::updateMainState(float delta_time) { void TitleScene::updateMainState(float delta_time) {
temps_estat_main_ += delta_time; temps_estat_main_ += delta_time;
if (temps_estat_main_ >= Defaults::Title::Ships::ENTRANCE_DELAY &&
ship_animator_ && !ship_animator_->isVisible()) { namespace S = Defaults::Title::Sequence;
namespace Sh = Defaults::Title::Ships;
// Thresholds derivats de la coreografia (vegeu Defaults::Title::Sequence).
constexpr float T_FOOTER_START = S::LOGO_ENTRY_DURATION;
constexpr float T_COPY_START = T_FOOTER_START + S::COPYRIGHT_STAGGER;
constexpr float T_JAILGAMES_END = T_FOOTER_START + S::JAILGAMES_ENTRY_DURATION;
constexpr float T_COPYRIGHT_END = T_COPY_START + S::COPYRIGHT_ENTRY_DURATION;
constexpr float T_FOOTER_END = std::max(T_JAILGAMES_END, T_COPYRIGHT_END);
constexpr float T_SHIPS_START = T_FOOTER_END + S::SHIPS_DELAY_AFTER_FOOTER;
constexpr float T_SHIPS_LANDED = T_SHIPS_START + Sh::ENTRY_DURATION + Sh::P2_ENTRY_DELAY;
constexpr float T_PRESS_START_VISIBLE = T_SHIPS_LANDED + S::PRESS_START_DELAY_AFTER_SHIPS;
intro_logo_progress_ = std::clamp(temps_estat_main_ / S::LOGO_ENTRY_DURATION, 0.0F, 1.0F);
intro_jailgames_progress_ = std::clamp(
(temps_estat_main_ - T_FOOTER_START) / S::JAILGAMES_ENTRY_DURATION,
0.0F,
1.0F);
intro_copyright_progress_ = std::clamp(
(temps_estat_main_ - T_COPY_START) / S::COPYRIGHT_ENTRY_DURATION,
0.0F,
1.0F);
if (!ships_intro_launched_ && temps_estat_main_ >= T_SHIPS_START &&
ship_animator_ != nullptr) {
ship_animator_->setVisible(true); ship_animator_->setVisible(true);
ship_animator_->startEntryAnimation(); ship_animator_->startEntryAnimation();
ships_intro_launched_ = true;
} }
if (temps_estat_main_ < DELAY_INICI_ANIMACIO) { if (!press_start_visible_ && temps_estat_main_ >= T_PRESS_START_VISIBLE) {
press_start_visible_ = true;
}
// L'oscil·lació suau del logo arrenca quan el logo ha aterrat. Així
// l'amplitud creix gradualment (lerp) durant la resta de la intro.
if (temps_estat_main_ < S::LOGO_ENTRY_DURATION) {
factor_lerp_ = 0.0F; factor_lerp_ = 0.0F;
animacio_activa_ = false; animacio_activa_ = false;
} else if (temps_estat_main_ < DELAY_INICI_ANIMACIO + DURACIO_LERP) { } else if (temps_estat_main_ < S::LOGO_ENTRY_DURATION + DURACIO_LERP) {
const float TEMPS_LERP = temps_estat_main_ - DELAY_INICI_ANIMACIO; factor_lerp_ = (temps_estat_main_ - S::LOGO_ENTRY_DURATION) / DURACIO_LERP;
factor_lerp_ = TEMPS_LERP / DURACIO_LERP;
animacio_activa_ = true; animacio_activa_ = true;
} else { } else {
factor_lerp_ = 1.0F; factor_lerp_ = 1.0F;
@@ -382,13 +426,36 @@ void TitleScene::handleSkipInput() {
} }
estat_actual_ = TitleState::MAIN; estat_actual_ = TitleState::MAIN;
starfield_->setBrightness(BRIGHTNESS_STARFIELD); starfield_->setBrightness(BRIGHTNESS_STARFIELD);
temps_estat_main_ = 0.0F;
// Saltar la intro coreografiada: deixar tots els elements ja in-place.
namespace S = Defaults::Title::Sequence;
namespace Sh = Defaults::Title::Ships;
constexpr float T_FOOTER_END = std::max(
S::LOGO_ENTRY_DURATION + S::JAILGAMES_ENTRY_DURATION,
S::LOGO_ENTRY_DURATION + S::COPYRIGHT_STAGGER + S::COPYRIGHT_ENTRY_DURATION);
constexpr float T_PRESS_START_VISIBLE = T_FOOTER_END + S::SHIPS_DELAY_AFTER_FOOTER +
Sh::ENTRY_DURATION + Sh::P2_ENTRY_DELAY + S::PRESS_START_DELAY_AFTER_SHIPS;
temps_estat_main_ = T_PRESS_START_VISIBLE;
intro_logo_progress_ = 1.0F;
intro_jailgames_progress_ = 1.0F;
intro_copyright_progress_ = 1.0F;
press_start_visible_ = true;
ships_intro_launched_ = true;
if (ship_animator_ != nullptr) {
ship_animator_->setVisible(true);
ship_animator_->startEntryAnimation();
}
} }
void TitleScene::handleStartInput() { void TitleScene::handleStartInput() {
if (estat_actual_ != TitleState::MAIN) { if (estat_actual_ != TitleState::MAIN) {
return; return;
} }
// No acceptar START fins que la intro coreografiada haja conclòs i el
// text "PRESS START TO PLAY" siga visible.
if (!press_start_visible_) {
return;
}
const bool P1_ABANS = match_config_.jugador1_actiu; const bool P1_ABANS = match_config_.jugador1_actiu;
const bool P2_ABANS = match_config_.jugador2_actiu; const bool P2_ABANS = match_config_.jugador2_actiu;
@@ -466,6 +533,14 @@ void TitleScene::draw() {
return; return;
} }
// Factor d'escala+posició per simular un moviment 3D des de l'usuari (prop,
// sprite gran i posició projectada extrema) cap al VP (lluny, sprite a la
// seva mida i posició finals). Pivot: centre de pantalla (= projecció VP).
const float LOGO_S = std::lerp(Defaults::Title::Sequence::LOGO_INTRO_SCALE_START, 1.0F, Easing::easeOutQuad(intro_logo_progress_));
const float SCREEN_CENTRE_X = Defaults::Game::WIDTH / 2.0F;
const float SCREEN_CENTRE_Y = Defaults::Game::HEIGHT / 2.0F;
const float LOGO_RENDER_SCALE = Defaults::Title::Layout::LOGO_SCALE * LOGO_S;
if (animacio_activa_) { if (animacio_activa_) {
float temps_shadow = std::max(0.0F, temps_animacio_ - SHADOW_DELAY); float temps_shadow = std::max(0.0F, temps_animacio_ - SHADOW_DELAY);
const float TWO_PI = 2.0F * Defaults::Math::PI; const float TWO_PI = 2.0F * Defaults::Math::PI;
@@ -473,41 +548,55 @@ void TitleScene::draw() {
const float SHADOW_OY = (ORBIT_AMPLITUDE_Y * std::sin((TWO_PI * ORBIT_FREQUENCY_Y * temps_shadow) + ORBIT_PHASE_OFFSET)) + SHADOW_OFFSET_Y; const float SHADOW_OY = (ORBIT_AMPLITUDE_Y * std::sin((TWO_PI * ORBIT_FREQUENCY_Y * temps_shadow) + ORBIT_PHASE_OFFSET)) + SHADOW_OFFSET_Y;
for (std::size_t i = 0; i < lletres_orni_.size(); ++i) { for (std::size_t i = 0; i < lletres_orni_.size(); ++i) {
const float BASE_X = posicions_originals_orni_[i].x + std::round(SHADOW_OX);
const float BASE_Y = posicions_originals_orni_[i].y + std::round(SHADOW_OY);
const Vec2 POS_SHADOW{ const Vec2 POS_SHADOW{
.x = posicions_originals_orni_[i].x + std::round(SHADOW_OX), .x = SCREEN_CENTRE_X + (LOGO_S * (BASE_X - SCREEN_CENTRE_X)),
.y = posicions_originals_orni_[i].y + std::round(SHADOW_OY), .y = SCREEN_CENTRE_Y + (LOGO_S * (BASE_Y - SCREEN_CENTRE_Y)),
}; };
Rendering::renderShape(sdl_.getRenderer(), lletres_orni_[i].shape, POS_SHADOW, 0.0F, Defaults::Title::Layout::LOGO_SCALE, 1.0F, SHADOW_BRIGHTNESS, Defaults::Title::Colors::LOGO_SHADOW); Rendering::renderShape(sdl_.getRenderer(), lletres_orni_[i].shape, POS_SHADOW, 0.0F, LOGO_RENDER_SCALE, 1.0F, SHADOW_BRIGHTNESS, Defaults::Title::Colors::LOGO_SHADOW);
} }
for (std::size_t i = 0; i < lletres_attack_.size(); ++i) { for (std::size_t i = 0; i < lletres_attack_.size(); ++i) {
const float BASE_X = posicions_originals_attack_[i].x + std::round(SHADOW_OX);
const float BASE_Y = posicions_originals_attack_[i].y + std::round(SHADOW_OY);
const Vec2 POS_SHADOW{ const Vec2 POS_SHADOW{
.x = posicions_originals_attack_[i].x + std::round(SHADOW_OX), .x = SCREEN_CENTRE_X + (LOGO_S * (BASE_X - SCREEN_CENTRE_X)),
.y = posicions_originals_attack_[i].y + std::round(SHADOW_OY), .y = SCREEN_CENTRE_Y + (LOGO_S * (BASE_Y - SCREEN_CENTRE_Y)),
}; };
Rendering::renderShape(sdl_.getRenderer(), lletres_attack_[i].shape, POS_SHADOW, 0.0F, Defaults::Title::Layout::LOGO_SCALE, 1.0F, SHADOW_BRIGHTNESS, Defaults::Title::Colors::LOGO_SHADOW); Rendering::renderShape(sdl_.getRenderer(), lletres_attack_[i].shape, POS_SHADOW, 0.0F, LOGO_RENDER_SCALE, 1.0F, SHADOW_BRIGHTNESS, Defaults::Title::Colors::LOGO_SHADOW);
} }
} }
for (const auto& lletra : lletres_orni_) { for (const auto& lletra : lletres_orni_) {
Rendering::renderShape(sdl_.getRenderer(), lletra.shape, lletra.position, 0.0F, Defaults::Title::Layout::LOGO_SCALE, 1.0F, 1.0F, Defaults::Title::Colors::LOGO_MAIN); const Vec2 POS{
.x = SCREEN_CENTRE_X + (LOGO_S * (lletra.position.x - SCREEN_CENTRE_X)),
.y = SCREEN_CENTRE_Y + (LOGO_S * (lletra.position.y - SCREEN_CENTRE_Y)),
};
Rendering::renderShape(sdl_.getRenderer(), lletra.shape, POS, 0.0F, LOGO_RENDER_SCALE, 1.0F, 1.0F, Defaults::Title::Colors::LOGO_MAIN);
} }
for (const auto& lletra : lletres_attack_) { for (const auto& lletra : lletres_attack_) {
Rendering::renderShape(sdl_.getRenderer(), lletra.shape, lletra.position, 0.0F, Defaults::Title::Layout::LOGO_SCALE, 1.0F, 1.0F, Defaults::Title::Colors::LOGO_MAIN); const Vec2 POS{
.x = SCREEN_CENTRE_X + (LOGO_S * (lletra.position.x - SCREEN_CENTRE_X)),
.y = SCREEN_CENTRE_Y + (LOGO_S * (lletra.position.y - SCREEN_CENTRE_Y)),
};
Rendering::renderShape(sdl_.getRenderer(), lletra.shape, POS, 0.0F, LOGO_RENDER_SCALE, 1.0F, 1.0F, Defaults::Title::Colors::LOGO_MAIN);
} }
const float SPACING = Defaults::Title::Layout::TEXT_SPACING; const float SPACING = Defaults::Title::Layout::TEXT_SPACING;
bool mostrar_text = true; if (press_start_visible_) {
if (estat_actual_ == TitleState::PLAYER_JOIN_PHASE) { bool mostrar_text = true;
const float FASE = temps_acumulat_ * BLINK_FREQUENCY * 2.0F * std::numbers::pi_v<float>; if (estat_actual_ == TitleState::PLAYER_JOIN_PHASE) {
mostrar_text = (std::sin(FASE) > 0.0F); const float FASE = temps_acumulat_ * BLINK_FREQUENCY * 2.0F * std::numbers::pi_v<float>;
} mostrar_text = (std::sin(FASE) > 0.0F);
if (mostrar_text) { }
const std::string MAIN_TEXT = "PRESS START TO PLAY"; if (mostrar_text) {
const float MAIN_SCALE = Defaults::Title::Layout::PRESS_START_SCALE; const std::string MAIN_TEXT = "PRESS START TO PLAY";
const float CENTRE_X = Defaults::Game::WIDTH / 2.0F; const float MAIN_SCALE = Defaults::Title::Layout::PRESS_START_SCALE;
const float CENTRE_Y = Defaults::Game::HEIGHT * Defaults::Title::Layout::PRESS_START_POS; const float CENTRE_X = Defaults::Game::WIDTH / 2.0F;
text_.renderCentered(MAIN_TEXT, {.x = CENTRE_X, .y = CENTRE_Y}, MAIN_SCALE, SPACING, 1.0F, Defaults::Title::Colors::PRESS_START); const float CENTRE_Y = Defaults::Game::HEIGHT * Defaults::Title::Layout::PRESS_START_POS;
text_.renderCentered(MAIN_TEXT, {.x = CENTRE_X, .y = CENTRE_Y}, MAIN_SCALE, SPACING, 1.0F, Defaults::Title::Colors::PRESS_START);
}
} }
dibuixarPeuTitol(SPACING); dibuixarPeuTitol(SPACING);
+7 -1
View File
@@ -80,6 +80,13 @@ class TitleScene final : public Scene {
bool animacio_activa_{false}; bool animacio_activa_{false};
float factor_lerp_{0.0F}; float factor_lerp_{0.0F};
// Progresos de la intro coreografiada al state MAIN.
float intro_logo_progress_{0.0F};
float intro_jailgames_progress_{0.0F};
float intro_copyright_progress_{0.0F};
bool press_start_visible_{false};
bool ships_intro_launched_{false};
static constexpr float BRIGHTNESS_STARFIELD = 1.2F; static constexpr float BRIGHTNESS_STARFIELD = 1.2F;
static constexpr float DURACIO_FADE_IN = 3.0F; static constexpr float DURACIO_FADE_IN = 3.0F;
static constexpr float DURACIO_INIT = 4.0F; static constexpr float DURACIO_INIT = 4.0F;
@@ -100,7 +107,6 @@ class TitleScene final : public Scene {
static constexpr float SHADOW_OFFSET_X = 2.0F; static constexpr float SHADOW_OFFSET_X = 2.0F;
static constexpr float SHADOW_OFFSET_Y = 2.0F; static constexpr float SHADOW_OFFSET_Y = 2.0F;
static constexpr float DELAY_INICI_ANIMACIO = 10.0F;
static constexpr float DURACIO_LERP = 2.0F; static constexpr float DURACIO_LERP = 2.0F;
// Càmera 3D: FOV vertical en radians. // Càmera 3D: FOV vertical en radians.
+4 -2
View File
@@ -88,8 +88,10 @@ namespace Title {
// Mida visual i animació. // Mida visual i animació.
constexpr float SHIP_FLOAT_SCALE = 2.0F; constexpr float SHIP_FLOAT_SCALE = 2.0F;
constexpr float SHIP_ENTRY_SCALE = 2.0F; // Mida mundial idèntica; la perspectiva fa la resta constexpr float SHIP_ENTRY_SCALE = 2.0F; // Mida mundial idèntica; la perspectiva fa la resta
constexpr float ENTRY_DURATION = 2.0F; // ENTRY_DURATION viu a Defaults::Title::Ships::ENTRY_DURATION (compartit
constexpr float EXIT_DURATION = 1.5F; // amb title_scene.cpp per calcular el threshold T_SHIPS_LANDED).
constexpr float ENTRY_DURATION = Defaults::Title::Ships::ENTRY_DURATION;
constexpr float EXIT_DURATION = Defaults::Title::Ships::EXIT_DURATION;
// Oscil·lació en unitats mundials (al voltant del target_position). // Oscil·lació en unitats mundials (al voltant del target_position).
constexpr float FLOAT_AMPLITUDE_X = 1.5F; constexpr float FLOAT_AMPLITUDE_X = 1.5F;