Files

29 KiB
Raw Permalink Blame History

Arquitectura de Coffee Crisis Arcade Edition

Guía de orientación para un desarrollador nuevo en el proyecto.

Cada afirmación está anclada a código real: se cita el fichero (y, cuando ayuda, la función o el número de línea) que la respalda. Donde no he encontrado algo, lo digo explícitamente en lugar de inventarlo.

Coffee Crisis Arcade Edition es un shooter arcade cooperativo de 2 jugadores escrito en C++20 sobre SDL3: los jugadores defienden el café de globos gigantes. Apunta a Windows, Linux, macOS (Intel/Apple Silicon), Raspberry Pi, Anbernic y web (Emscripten). Los comentarios del código están en español/valenciano; este documento está en castellano.


Índice

  1. Visión general
  2. Punto de entrada y bucle principal
  3. Secciones y flujo de la aplicación
  4. Renderizado: de la lógica al píxel
  5. Entrada
  6. Lógica del juego: la clase Game
  7. Entidades y managers de gameplay
  8. Modo demo y attract mode
  9. Recursos
  10. Audio
  11. Configuración, parámetros y constantes
  12. Localización
  13. Convenciones y patrones recurrentes
  14. Guía de navegación: "si quieres tocar X, mira Y"

1. Visión general

El árbol source/ separa motor y juego:

  • source/core/ — motor genérico: system (arranque, secciones, demo, eventos globales), rendering (+ sdl3gpu, sprite), input, resources, audio, locale.
  • source/game/ — el juego concreto: scenes (las secciones), gameplay (managers), entities (jugador, globos, balas, ítems…), ui, y options.
  • source/utils/color, param, utils, defines.
  • source/external/ — vendorizado: nlohmann/json, fkyaml, stb_image, stb_vorbis.

Los #include son absolutos respecto a source/ (p.ej. #include "core/audio/audio.hpp"); CMake añade un único -I.../source (ver CLAUDE.md). ~150 ficheros C++, ~32.000 líneas.

Cuatro ideas-fuerza que conviene interiorizar:

  1. El flujo de la aplicación se conduce con una variable global (Section::name), no con objetos de transición. El Director reacciona a sus cambios (§3).
  2. El render usa texturas GPU vía SDL_Renderer dibujadas sobre una render-target texture, con post-procesado opcional vía un backend SDL3 GPU. No es un blitter de software (§4).
  3. El gameplay se organiza con managers/pools (BalloonManager, BulletManager, StageManager) coordinados por una clase Game muy grande (§6, §7).
  4. Sí hay modo demo (attract mode): es reproducción de input grabado, no IA (§8).
graph TD
    SDL[SDL3 callbacks · main.cpp] --> DIR[Director]
    DIR -->|lee Section::name| ST{handleSectionTransition}
    ST --> SEC["Section activa (Logo/Intro/Title/Game/…)"]
    DIR --> GE[GlobalEvents] --> INP[Input] & SVC[ServiceMenu]
    SEC --> GAME[Game]
    GAME --> BM[BalloonManager] & BLM[BulletManager] & STG[StageManager]
    GAME --> PL[Players] -->|Input::Action| INP
    GAME --> DEMO["Demo (playback de input grabado)"] -.->|setInput| PL
    GAME -->|SDL_RenderTexture| CV["game_canvas_ (render target)"]
    CV -->|RenderReadPixels → uploadPixels| SB[ShaderBackend SDL3 GPU] --> WIN[Ventana]
    RES["Resource / Asset"] -.-> GAME & SEC

2. Punto de entrada y bucle principal

2.1. SDL conduce el bucle (callbacks)

source/main.cpp define SDL_MAIN_USE_CALLBACKS: no hay while propio. SDL llama a cuatro funciones, todas delegando en el Director:

SDL_AppInit    *appstate = new Director(argc, argv);   // main.cpp:15
SDL_AppIterate Director::iterate();                     // un frame
SDL_AppEvent   Director::handleEvent(event);            // un evento
SDL_AppQuit    delete Director;

2.2. El Director

source/core/system/director.{hpp,cpp} es el orquestador. Mantiene un unique_ptr por cada sección (preload_, logo_, intro_, title_, game_, …) de los que solo uno está vivo a la vez (director.hpp:60).

Constructor (director.cpp:82): fija la semilla aleatoria, crea la carpeta de sistema (jailgames/coffee_crisis_arcade_edition), decide la sección inicial según el build (RECORDING → GAME; _DEBUG → lee debug.yaml; Release → LOGO) y llama a init().

init() (director.cpp:131) inicializa en orden: Asset (índice de ficheros), sistema de recursos (resources.pack, con/ sin fallback a disco según build), Input, Options (config.yaml, controllers.json, presets de shaders), parámetros y scores, Lang, Screen, Audio y Resource.

2.3. Arranque NO bloqueante (PRELOAD incremental)

Un detalle importante: si el modo de carga es PRELOAD, no se cargan todos los recursos de golpe. init() redirige el arranque a la sección PRELOAD guardando el destino real en Section::post_preload (director.cpp:191):

Section::post_preload = Section::name;   // destino real
Section::name = Section::Name::PRELOAD;  // mientras tanto, barra de progreso
Resource::get()->beginLoad();

Cada frame, Director::iterate() (director.cpp:489) llama a Resource::loadStep(50 /*ms*/): carga recursos hasta agotar un presupuesto de 50 ms por frame, manteniendo la ventana y el bucle vivos. Cuando termina, finishBoot() inicializa lo que depende de los recursos (ServiceMenu, Notifier, singletons de Screen) y salta a post_preload.

2.4. Orden del frame y gestión del tiempo

Director::iterate() (director.cpp:489):

sequenceDiagram
    participant SDL
    participant Dir as Director::iterate
    participant Sec as Section activa
    SDL->>Dir: SDL_AppIterate
    alt boot_loading_
        Dir->>Dir: Resource::loadStep(50ms) → finishBoot() al terminar
    end
    Dir->>Dir: handleSectionTransition() (destruye/crea sección)
    Dir->>Sec: iterate()  (la sección hace su propio update+render)

El tiempo es time-based: cada sección calcula su propio delta_time. En Game es calculateDeltaTime() (game.hpp:210), usado en todos los update. Los eventos llegan por separado vía Director::handleEventGlobalEvents::handle + reenvío a la sección activa (director.cpp:539).

2.5. Reinicio en caliente

Igual que su proyecto hermano, soporta reinicio real vía execv (Director::relaunch(), director.cpp:254): la sección RESET (p.ej. tras F10 o cambio de idioma) reemplaza el proceso por sí mismo; si no se puede (Emscripten, argv inválido), cae a un reset() interno que recarga recursos y vuelve a LOGO.


3. Secciones y flujo de la aplicación

3.1. Variables globales de estado

source/core/system/section.hpp define el estado del flujo como variables globales inline en el namespace Section:

inline Name name = Name::RESET;            // sección a la que ir
inline Name post_preload = Name::LOGO;     // destino tras PRELOAD
inline Options options = Options::NONE;    // 1P/2P/BOTH, timeouts, etc.
inline AttractMode attract_mode = AttractMode::TITLE_TO_DEMO;

Name enumera: RESET, PRELOAD, LOGO, INTRO, TITLE, GAME, HI_SCORE_TABLE, GAME_DEMO, INSTRUCTIONS, CREDITS, QUIT. Cualquier parte del código cambia el flujo simplemente asignando Section::name = ....

3.2. La transición de secciones

Director::handleSectionTransition() (director.cpp:390) se ejecuta cada frame:

  • Si Section::name == RESET: intenta relaunch(); si vuelve, hace el reset interno.
  • Si Section::name == last_built_section_name_: no hace nada (ya está viva).
  • Si cambió: resetActiveSection() (libera todos los unique_ptr) y un switch construye la nueva sección. Para GAME traduce Section::options a Player::Id (1P/2P/BOTH); para GAME_DEMO construye Game(..., DEMO_ON) con jugador aleatorio (director.cpp:448).

Cada sección (source/game/scenes/) es autónoma y expone iterate() + handleEvent(). Director::iterate despacha al puntero vivo con una cadena de if/else if (director.cpp:517).

graph LR
    RESET --> PRELOAD --> LOGO --> INTRO --> TITLE
    TITLE -->|jugar| GAME --> HI_SCORE_TABLE --> TITLE
    TITLE -->|timeout / attract| GAME_DEMO --> TITLE
    TITLE -->|attract alterno| LOGO
    TITLE --> INSTRUCTIONS & CREDITS

3.3. Attract mode

El attract mode alterna entre mostrar la demo y volver al logo. En title.cpp:51 el Title, al construirse, mira Section::attract_mode y fija su next_section_ (a GAME_DEMO o LOGO), invirtiendo el modo para la próxima vez. Cuando el Title agota su timeout sin input, salta a esa sección (ver §8).


4. Renderizado: de la lógica al píxel

A diferencia de un blitter de software, aquí los sprites son texturas GPU compuestas por SDL_Renderer, y el post-procesado se hace en un backend SDL3 GPU.

4.1. Texture y la jerarquía de sprites

  • source/core/rendering/texture.hppTexture envuelve un SDL_Texture*, con loadFromFile, createBlank, setAsRenderTarget y un render(x, y, clip, zoom, angle, …). Es la unidad de dibujo.
  • source/core/rendering/sprite/ — jerarquía sobre Sprite (sprite.hpp:13), que guarda varias texturas (textures_), un sprite_clip_ y posición/zoom:
    • AnimatedSprite — animación por fotogramas (clip que avanza).
    • MovingSprite — con velocidad.
    • PathSprite — sigue rutas precalculadas (Path).
    • SmartSprite — sprite con lógica propia (p.ej. el café que salta al ser golpeado).
    • CardSprite — variante para "cartas"/paneles.

El Player, por ejemplo, compone un AnimatedSprite player_sprite_ y un power_sprite_ para el aura (player.hpp:250).

4.2. Dos render-targets

Hay dos texturas de destino encadenadas:

  1. Game::canvas_ — textura de la zona de juego. Game::fillCanvas() (game.cpp) fija el render-target a canvas_ y dibuja, en este orden: background → smart_sprites → items → balloons → tabe → players → bullets → path_sprites. Ese orden es el z-order del playfield.

  2. Screen::game_canvas_ — textura de pantalla completa (ARGB8888 a la resolución de juego; screen.cpp:92, SDL_TEXTUREACCESS_TARGET). Game::render() hace screen_->start() (que pone el target en game_canvas_), copia canvas_ sobre él, y encima dibuja scoreboard y los fades de entrada/salida; finalmente screen_->render().

4.3. Post-procesado y presentación

Screen::renderPresent() (screen.cpp:156) decide cómo llega a la ventana:

  • Con backend GPU acelerado: lee los píxeles de game_canvas_ con SDL_RenderReadPixels a un pixel_buffer_ CPU, los sube al backend (shader_backend_->uploadPixels(...)) y este los renderiza con el shader activo (shader_backend_->render()).
  • Sin backend (fallback): vuelca game_canvas_ directamente a la ventana con SDL_RenderTexture.

El backend vive en source/core/rendering/sdl3gpu/ (SDL3 GPU API; shaders SPIR-V compilados offline desde GLSL en data/shaders/, embebidos en cabeceras spv//msl/). Hay dos shaders seleccionables, PostFX y CrtPi, con presets en postfx.yaml/crtpi.yaml (screen.cpp:382). El build NO_SHADERS (Anbernic) desactiva todo el pipeline de shaders.

graph TD
    OBJ["background, balloons, players, bullets…"] -->|SDL_RenderTexture| CANVAS["Game::canvas_ (zona de juego)"]
    CANVAS -->|copiar| GC["Screen::game_canvas_ (pantalla)"]
    SB2[scoreboard] --> GC
    FADE[fades de entrada/salida] --> GC
    GC -->|RenderReadPixels → uploadPixels| SHADER["ShaderBackend (PostFX/CrtPi)"]
    SHADER --> WIN[Ventana]
    GC -.fallback sin GPU.-> WIN

4.4. Efectos de pantalla

Screen integra efectos globales sobre la presentación: shake (ShakeEffect, desplaza el rect), flash (FlashEffect, tinte temporal) y attenuate (atenuación) — todos definidos como structs internos en screen.hpp:108205. Las transiciones entre estados usan source/core/rendering/fade.* (cuadrícula de cuadros).


5. Entrada

5.1. Input singleton

source/core/input/input.hpp. Centraliza teclado y mandos bajo un enum de acciones (Input::Action, alias de InputAction). El mapeo por defecto vive en structs:

  • Keyboard (input.hpp:54): flechas = mover; Q/W/E = disparar izquierda/centro/derecha; Enter = START; F12 = SERVICE; P = PAUSE; ESC = EXIT; F1F11 = ventana/vídeo/audio/idioma/reset/info.
  • Gamepad (input.hpp:99): cruceta = mover; WEST/NORTH/EAST = disparar; START/ BACK = start/service. Cada mando es un shared_ptr<Gamepad> con su propio bindings, nombre y path.

La consulta principal es checkAction(action, repeat, check_keyboard, gamepad) (input.hpp:168). Hay además detección de ejes y triggers como botones, y gestión de configuraciones de mando persistidas (gamepad_config_manager, controllers.json).

5.2. Eventos globales y hotkeys

  • source/core/system/global_events.cppGlobalEvents::handle(event) trata lo común a cualquier sección: SDL_EVENT_QUIT, resize, render target reset, hot-plug de mandos (con notificación tras markStartupComplete), el toggle del menú de servicio y el ratón.
  • source/core/input/global_inputs.cpp — hotkeys de sistema, despachadas con un mapa acción→lambda (global_inputs.cpp:187): fullscreen, zoom ±, audio, autofire, idioma (CHANGE_LANG → Section::RESET, reinicia), VSync, integer scale, info; más PostFX/shader/preset. F10/RESET pone Section::name = RESET (reinicio) y ESC/EXIT pone QUIT.

5.3. Cómo llega la entrada a un jugador

Dentro de Game, checkInput()handlePlayersInput()handleNormalPlayerInput(player) consulta Input y traduce a player->setInput(Input::Action) y disparos (handleFireInput). El Player sabe si usa teclado o un mando concreto (uses_keyboard_, gamepad_; player.hpp:217). En modo demo, esta misma vía se alimenta de datos grabados (§8).


6. Lógica del juego: la clase Game

source/game/scenes/game.{hpp,cpp} es la sección de gameplay y la clase más grande del proyecto. Coordina jugadores, globos, balas, ítems, fases, puntuación y efectos.

6.1. FSM de la partida

Game::State (game.hpp:75): FADE_IN → ENTERING_PLAYER → SHOWING_GET_READY_MESSAGE → PLAYING → COMPLETED / GAME_OVER. checkState() (game.cpp) detecta fin de juego (stage_manager_->isGameCompleted()) o game over (allPlayersAreGameOver()); setState() hace la transición.

6.2. El frame de Game

void Game::iterate() {                 // game.cpp
    const float DELTA_TIME = calculateDeltaTime();
    checkInput();
    update(DELTA_TIME);
    render();
}
void Game::update(float dt) {
    screen_->update(dt);
    Audio::update();
    updateDemo(dt);                    // no-op si no es demo
    updateGameStates(dt);             // dispatch por State
    fillCanvas();                      // dibuja la zona de juego en canvas_
}

updateGameStates despacha a un updateGameState<Estado> por cada valor de la FSM (game.hpp:218).

6.3. Sistemas que orquesta

Game posee (vía unique_ptr) los managers y objetos del nivel (game.hpp:136):

  • StageManager — progresión por "poder": cada fase necesita power_to_complete; el juego se completa al acumular el poder total. También define umbrales de amenaza (menace) por fase. Implementa IStageInfo (stage.hpp:51), interfaz mínima que se inyecta a jugador y globos.
  • BalloonManager — despliega formaciones (deployRandomFormation), globos hijos al explotar uno, power balls, ajusta velocidad (Balloon::GAME_TEMPO) y calcula la amenaza en pantalla (§7).
  • BulletManager — pool de balas; las colisiones se resuelven con callbacks que registra Game (setBalloonCollisionCallback, setTabeCollisionCallback, setOutOfBoundsCallback; bullet_manager.hpp:49), manteniendo la lógica de juego dentro de Game.
  • Background, Fade ×2, Tabe (enemigo especial volador), Scoreboard, PauseManager.
  • Listas de Item (power-ups y puntos), SmartSprite y PathSprite.

6.4. Mecánicas destacadas

  • Ítems / power-ups (game.hpp:268): café (toque extra), máquina de café (power-up), power ball, reloj (= detener el tiempo, enableTimeStopItem), e ítems de puntos. La probabilidad de soltar ítem la decide dropItem() con odds configurables (Helper, game.hpp:100).
  • Sistema de amenaza (updateMenace/setMenace): si la amenaza cae por debajo de un umbral, se generan más globos.
  • Multijugador: players_ es un vector<shared_ptr<Player>> (1 o 2). Un players_draw_list_ separado mantiene el z-order de dibujo (buildPlayerDrawList, sendPlayerToBack/bringPlayerToFront; game.hpp:338).
  • Récords: al perder con buena puntuación, el jugador entra en ENTERING_NAME (enter_name.*, manage_hiscore_table.*).

7. Entidades y managers de gameplay

source/game/entities/:

  • Player (player.hpp) — la entidad más compleja. Hereda de AnimatedSprite. Tiene tres ejes de estado independientes: walking_state_, firing_state_ y playing_state_ (player.hpp:266). El disparo usa un sistema de dos líneas: una funcional (cooldown, canFire) y otra visual (animaciones NORMAL→AIMING→RECOILING→THREAT_POSE, player.hpp:296). Soporta power-up, invulnerabilidad con parpadeo, continue y entrada de nombre. Puede controlarse por teclado o por un Gamepad concreto.
  • Balloon (balloon.hpp) — el enemigo básico; al explotar puede crear globos hijos. Balloon::GAME_TEMPO define velocidades por progreso.
  • Bullet (bullet.hpp) — proyectil con tipo (UP/LEFT/RIGHT) y color (según power-up del jugador).
  • Item — power-ups y objetos de puntos que caen.
  • Explosions — efectos de explosión (lo posee BalloonManager).
  • Tabe — enemigo/objeto especial volador con su propia lógica de impacto.

source/game/gameplay/ contiene los managers y sistemas (no entidades en sí): BalloonManager, BulletManager, StageManager, Difficulty, Scoreboard, balloon_formations (lee formations.txt), cooldown, enter_name, manage_hiscore_table, game_logo.

El patrón general aquí no es un EntityManager polimórfico único (como en el proyecto hermano), sino managers especializados por tipo + listas homogéneas en Game, con callbacks para cruzar responsabilidades sin acoplar (caso de BulletManager).


8. Modo demo y attract mode

A diferencia de muchos juegos, aquí el modo demo SÍ existe — pero NO es IA. Es reproducción de input pregrabado.

8.1. Formato de los datos

source/core/system/demo.hpp: cada fotograma de demo es un DemoKeys con seis banderas (left, right, no_input, fire, fire_left, fire_right). Una demo es un vector<DemoKeys> de TOTAL_DEMO_DATA = 2000 fotogramas. Hay tres ficheros: data/demo/demo{1,2,3}.bin, leídos por loadDemoDataFromFile (demo.cpp:11).

8.2. Reproducción

Cuando Game se construye con DEMO_ON (sección GAME_DEMO), initDemo() (game.cpp) carga todas las demos del Asset y asigna una a cada jugador de forma aleatoria (shuffle), elige una fase de inicio al azar, da cafés aleatorios, pone los marcadores en modo DEMO y silencia los globos. Luego cada frame, demoHandlePlayerInput() lee el fotograma actual y lo inyecta por la misma vía de input que un humano:

// game.cpp — demoHandlePlayerInput
const auto& demo_data = demo_data_vec.at(demo_.index % demo_data_vec.size());
if (demo_data.left == 1)       player->setInput(Input::Action::LEFT);
else if (demo_data.right == 1) player->setInput(Input::Action::RIGHT);
...
if (demo_data.fire == 1)       handleFireInput(player, Bullet::Type::UP);

No hay toma de decisiones: el jugador "demo" repite exactamente las pulsaciones grabadas. demoHandlePassInput() permite salir con cualquier botón, volviendo a TITLE y dejando armado el siguiente attract (TITLE_TO_DEMO).

8.3. Attract mode

El Title alterna TITLE_TO_DEMOTITLE_TO_LOGO (title.cpp:51): tras su timeout, una vez lanza la demo y la siguiente vuelve al logo, en bucle de atracción típico de recreativa.

8.4. Grabación y autoplay

  • #ifdef RECORDING (demo.cpp:43, game.hpp:349): build especial que arranca directamente en GAME y graba las pulsaciones a un .bin con saveDemoFile. Así se generan las demos.
  • autoplay (debug): en _DEBUG, debug_config.autoplay permite alimentar datos de demo a los jugadores en una partida normal (game.hpp:354, initDemo).

9. Recursos

9.1. Resource (singleton)

source/core/resources/resource.hpp. Cachea por nombre texturas, música, sonidos, ficheros de texto/fuentes, animaciones y datos de demo (getDemoData). Dos modos (LoadingMode):

  • PRELOAD — carga incremental al arranque con beginLoad() + loadStep(budget_ms) + renderProgress() (la sección Preload pinta la barra). Es el modo por defecto.
  • LAZY_LOAD — carga bajo demanda (seleccionable vía debug.yaml).

9.2. Asset y el pack

source/core/resources/asset.* mantiene el índice de ficheros a partir del manifiesto data/config/assets.txt (director.cpp:296) y valida que no falte ninguno (Asset::check()). La lectura física pasa por ResourceHelper::loadFile, que sirve desde resources.pack y hace fallback al filesystem en builds de desarrollo (en Release nativo es estricto: solo el pack; director.cpp:143). resource_pack.* y resource_loader.* implementan el formato del pack y la carga.

Formatos: imágenes vía stb_image + gif propio; audio .ogg vía stb_vorbis; texto/JSON vía nlohmann/json; configs vía fkyaml.


10. Audio

source/core/audio/audio.hppAudio singleton sobre jail_audio (primer-party, no librería externa) y audio_adapter. API: playMusic(name, loop, crossfade_ms), playSound(name, Group), control de volumen por grupos (Group::GAME, INTERFACE, …), pause/resume/stop, crossfade y fade out (audio.hpp:49). Audio::update() se llama cada frame desde las secciones (p.ej. Game::update). El build NO_AUDIO compila sin audio.


11. Configuración, parámetros y constantes

El proyecto tiene tres capas de configuración que conviene no confundir:

  1. Options (source/game/options.hpp) — opciones persistentes del usuario, agrupadas en structs (Window, Video, Audio, Settings, Gamepad, Keyboard) más presets de shaders (PostFXPreset, CrtPiPreset). Se guardan en config.yaml / controllers.json / postfx.yaml / crtpi.yaml. Incluye params_preset ∈ {classic, arcade, red} (options.hpp:339).
  2. param / Param (source/utils/param.hpp) — parámetros de gameplay y layout (dimensiones del juego, play_area, ajustes de globos, scoreboard, título, fades, jugador, Tabe…). Se cargan de ficheros de texto param_320x240.txt / param_320x256.txt / classic.txt con loadParamsFromFile (director.cpp:275). El objeto global param lo leen casi todos los subsistemas.
  3. Otros datos de juego: stages.txt (fases, lo lee StageManager), formations.txt (formaciones de globos), assets.txt (manifiesto).

Los valores de fallback compilados viven en source/core/system/defaults.hpp (y source/utils/defines.hpp para constantes base). En Debug, debug.yaml controla sección inicial, opciones, fase, modo de carga, autoplay e invincibility (director.cpp:318).

Builds condicionales (multiplataforma)

El juego se adapta por defines (ver CLAUDE.md): WINDOWS_BUILD / LINUX_BUILD / MACOS_BUILD, RELEASE_BUILD, MACOS_BUNDLE, ANBERNIC (handheld), __EMSCRIPTEN__ (web), NO_SHADERS, NO_AUDIO, RECORDING, ARCADE, DEBUG/VERBOSE. Aparecen sobre todo en Director::init y Screen.


12. Localización

source/core/locale/lang.*Lang carga el idioma desde JSON en data/lang/. Lang::setLanguage(Options::settings.language) se llama en init() y en cada reset(). Cambiar idioma en caliente (F9 / CHANGE_LANG) fuerza un Section::RESET (reinicio del proceso) para recargarlo todo (global_inputs.cpp).


13. Convenciones y patrones recurrentes

  • Naming (de .clang-format/.clang-tidy, resumido en CLAUDE.md): clases CamelCase, métodos camelBack, variables/params snake_case, miembros privados snake_case_ (guion bajo final), constantes UPPER_CASE, namespaces CamelCase, valores de enum UPPER_CASE. clang-tidy trata los warnings como errores.
  • Singletons con init/destroy manual: Screen, Resource, Audio, Input, Asset, ServiceMenu, Notifier, Lang. Se crean/destruyen en orden explícito en Director::init / shutdownSubsystems (director.cpp:227) — no se confía en destructores estáticos.
  • Estado de flujo por variable global (Section::name): patrón central; no hay objetos de transición tipados.
  • Time-based en todo: cada sistema recibe delta_time; las constantes de tiempo se documentan como "N frames a 60fps → segundos" (p.ej. game.hpp:85).
  • Callbacks para desacoplar: BulletManager y StageManager reciben std::function desde Game para no conocer la lógica de colisión/puntuación.
  • Interfaz mínima IStageInfo (stage_interface.hpp): se inyecta a jugador y globos para que consulten amenaza/poder sin ver todo el StageManager.
  • Managers por tipo + listas homogéneas en Game, en vez de un contenedor de entidades polimórfico.
  • Comentarios en español/valenciano; muchos #include llevan comentario "// Para X" indicando qué símbolo justifican (estilo IWYU).
  • Multiplataforma por #ifdef (ver §11). Conviene leerlos al tocar arranque o render.

14. Guía de navegación: "si quieres tocar X, mira Y"

Quiero… Empieza por…
Entender el arranque source/core/system/director.cpp (init, iterate)
Cambiar el flujo de pantallas source/core/system/section.hpp + handleSectionTransition
Añadir/editar una pantalla source/game/scenes/ (crea iterate()+handleEvent()) y un case en director.cpp
La barra de carga / arranque lento Resource::beginLoad/loadStep + scenes/preload.*
Cómo se dibuja todo Game::fillCanvas + Game::render + Screen::renderPresent (screen.cpp:156)
Sprites / animaciones source/core/rendering/sprite/ + texture.hpp
Shaders / CRT / post-FX source/core/rendering/sdl3gpu/ + data/shaders/ + Options presets
Efectos (shake/flash/fade) screen.hpp (structs) + core/rendering/fade.*
Controles / mandos source/core/input/input.hpp (Keyboard/Gamepad/checkAction)
Hotkeys F1F12 / reset source/core/input/global_inputs.cpp
Eventos globales / hot-plug source/core/system/global_events.cpp
Lógica de partida y estados source/game/scenes/game.cpp (updateGameState*)
Globos y formaciones gameplay/balloon_manager.* + entities/balloon.* + formations.txt
Balas y colisiones gameplay/bullet_manager.* (callbacks) + entities/bullet.*
Fases / progresión / dificultad gameplay/stage.* (StageManager) + stages.txt + gameplay/difficulty.*
El jugador (movimiento, disparo) entities/player.* (sistema de disparo de dos líneas)
Ítems y power-ups entities/item.* + Game::dropItem/createItem
Récords / entrada de nombre gameplay/manage_hiscore_table.*, gameplay/enter_name.*
Modo demo / attract core/system/demo.*, Game::initDemo/demoHandle*, scenes/title.cpp
Grabar nuevas demos build con RECORDING + Game::updateRecording
Cargar un recurso core/resources/resource.hpp + asset.* + assets.txt
Audio (música/SFX) core/audio/audio.hpp
Opciones del usuario game/options.hpp (+ config.yaml)
Parámetros de gameplay/layout utils/param.hpp + param_320x*.txt
Valores por defecto core/system/defaults.hpp, utils/defines.hpp
Idiomas core/locale/lang.* + data/lang/
Menú de servicio / notificaciones game/ui/service_menu.*, game/ui/notifier.*
Empaquetar datos tools/ (pack_resources) + make resources.pack

Documento generado a partir de la lectura directa del código en el commit actual de la rama main. Si algo aquí no cuadra con el código, el código manda: actualiza este documento.