22 KiB
Arquitectura de Aventures en Egipte (AEE)
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.
Aventures en Egipte es un juego de aventura/plataformas retro (el protagonista "Sam" explora pirámides esquivando momias y recogiendo tesoros), escrito en C++ + SDL3. Resolución de juego 320×200. El rasgo que más condiciona el código es que está en plena migración desde un motor legacy propio ("Jail") hacia C++ moderno (ver §3). Los comentarios del código están en valenciano/español; este documento está en castellano.
Existe además un
docs/scenes-migration-plan.md(histórico) sobre la migración del sistema de escenas; aquí se documenta el estado actual del código, no el plan.
Índice
- Visión general
- Punto de entrada y bucle principal
- El motor legacy "Jail"
- Escenas y máquina de estados
- Renderizado: de la lógica al píxel
- Entrada
- Gameplay:
ModuleGamey entidades - Sistema de escenas cinemáticas
- Recursos
- Audio y localización
- Configuración y persistencia
- Modo demo / IA
- Convenciones y patrones recurrentes
- Guía de navegación: "si quieres tocar X, mira Y"
1. Visión general
El árbol source/ separa motor y juego, pero con una particularidad:
el motor tiene dos generaciones conviviendo (legacy "Jail" + capas nuevas).
source/core/jail/— el motor legacy "Jail" (APIs C planas):Jg(jgame),Jd8(jdraw8),Ji(jinput),Jf(jfile). En modernización (§3).source/core/(resto) — capas nuevas:rendering(Screen,Overlay,Text,Menu,sdl3gpu),input,system(Director),resources,audio,locale.source/game/— el juego: gameplay legacy en la raíz (modulegame,prota,momia,engendro,bola,mapa,marcador,info,sprite) y el sistema de escenas nuevo enscenes/.source/utils/—easing,utils.source/external/— vendorizado (stb, fkyaml).
~117 ficheros C++, ~40.000 líneas.
El CLAUDE.md define una frontera explícita "Original vs New Code" (§79):
core/jail/ y game/*.cpp son legacy en modernización (modificar con
cuidado, preservando comportamiento); core/rendering, core/input,
utils/, options/defines/defaults son código nuevo. Interiorizar esa
frontera es lo primero para no romper invariantes del juego original.
Ideas-fuerza:
- Un solo hilo, tick-based, vía callbacks de SDL3 (§2).
- El render es software paletizado 8-bit (
Jd8, 320×200) que al final se convierte a ARGB y pasa por shaders GPU (§5). - El flujo de pantallas es una máquina de estados apoyada en
Info::ctx.num_piramide, con unSceneRegistryque va sustituyendo cinemáticas legacy por escenas nuevas (§4).
graph TD
SDL[SDL3 callbacks · main.cpp] --> DIR[Director]
DIR -->|game_state_ + Info::ctx| DISP{createNextScene}
DISP -->|==0| MG["ModuleGame (gameplay)"]
DISP -->|==1| REG["SceneRegistry::tryCreate(num_piramide)"]
REG --> CINE["intro / banner / slides / mort / secreta / credits…"]
MG --> MAPA[Mapa] & PROTA[Prota] & MOMIA[Momia] & BOLA[Bola] & MARC[Marcador]
DIR --> INP["KeyConfig / GlobalInputs / Gamepad / KeyRemap / Mouse"]
MG -->|dibuja índices| JD8["Jd8 (8-bit 320×200)"]
CINE --> JD8
JD8 -->|JD8_Flip → ARGB| SCREEN[Screen]
OVL[Overlay] --> SCREEN
SCREEN --> GPU["sdl3gpu PostFX / CrtPi / upscale"] --> WIN[Ventana]
RES["Resource::Cache / List"] -.-> MG & CINE
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 (clave
para el port a Emscripten). SDL_AppInit monta todo y SDL_AppIterate llama a
Director::iterate().
El arranque en SDL_AppInit inicializa, en orden: carpeta de config (Jf),
sistema de recursos (resources.pack + fallback en Debug/WASM), Options,
KeyConfig, Locale, presets de shaders, y luego el motor: Jg::init,
Screen::init, Jd8::init, Audio::init, Overlay::init, Menu::init,
Resource::List/Cache (con beginLoad()), y Director::init + setup().
2.2. El Director
source/core/system/director.{hpp,cpp}. Es el orquestador singleton, único
hilo del runtime (sin fibers, mutex ni condition variables; el comentario de
director.hpp:11 lo subraya). Posee el estado de escena como miembros:
current_scene_ (std::unique_ptr<Scenes::Scene>), game_state_,
last_tick_ms_, y dos buffers de frame [320*200] (game_frame_,
presentation_buffer_).
Cada iterate() (modelo documentado en CLAUDE.md:161):
Gamepad/KeyRemap/GlobalInputs/Mouse::update
JA_Update() ← bombeo de audio
if (!paused_) {
if (scene && (scene->done() || JG_Quitting())) game_state_ = scene->nextState(); scene.reset();
if (!scene) scene = createNextScene(); scene->onEnter(); ← ModuleGame o SceneRegistry
JI_Update()
scene->tick(now - last_tick_ms_)
JD8_Flip() ← índices → ARGB
memcpy → game_frame
}
memcpy game_frame → presentation_buffer
Overlay::render(presentation_buffer)
Screen::present(presentation_buffer)
SDL_Delay(frame_target - elapsed) ← cap de FPS
2.3. Tiempo, pausa y salida
Las escenas reciben delta_ms real (time-based). El gameplay (ModuleGame)
gatea su Update() a 10 ms fijos vía Jg::shouldUpdate() (ticker del motor
legacy, ya sin yield). La pausa (F11) simplemente salta el bloque de
tick(): el overlay y el present siguen, re-presentando el último frame
congelado. La salida se solicita con requestQuit() (doble ESC o
SDL_QUIT) y requestRestart() hace un reinicio suave (para audio, resetea
Info::ctx, vuelve a la intro).
3. El motor legacy "Jail"
source/core/jail/ es el sustrato heredado, APIs C planas con prefijo por
subsistema (sin clases), que se está convirtiendo progresivamente a C++
idiomático manteniendo los nombres externos estables para no romper los
call sites (CLAUDE.md:92). Es el rasgo más distintivo del proyecto.
| Subsistema | Fichero | Qué hace |
|---|---|---|
Jg (jgame) |
jail/jgame.* |
Timing: init/finalize, fixed-timestep (Jg::shouldUpdate() a 10 ms), flag de salida (JG_Quitting). |
Jd8 (jdraw8) |
jail/jdraw8.* |
Renderer software 8-bit paletizado, buffer de pantalla 320×200 (Jd8::Surface = Uint8*), blits con color key, fades no bloqueantes (máquina de estados), flip() (paleta→ARGB) → Screen::present. |
Ji (jinput) |
jail/jinput.* |
Sondeo de teclado, debounce, códigos cheat. Filtra las teclas de GUI del juego y llama a GlobalInputs/Mouse cada update. |
Jf (jfile) |
jail/jfile.* |
I/O de ficheros: carpeta data/ o pack; carpeta de config en ~/.config/jailgames/aee/. |
Jd8 en detalle (el corazón del render)
jail/jdraw8.hpp. API representativa:
using Surface = Uint8*(búfer de índices de paleta),using Palette = Color*.loadSurface/newSurface/freeSurface,loadPalette/setScreenPalette.- Blits:
blit,blitCK(con color key/transparencia),blitCKCut,blitCKScroll,blitToSurface/blitCKToSurface(blit entre surfaces). fillRect/fillSquare/putPixel/getPixel.- Fade no bloqueante (
fadeStart*+fadeTickStepque devuelvetrueal acabar): sustituye losJD8_FadeOutbloqueantes del código original. flip()convierte el buffer indexado + paleta a ARGB y delega enScreen::present;getFramebuffer()devuelve elUint32*resultante.
Mentalidad clave: todo el dibujo del juego ocurre sobre índices de 8 bits en un buffer 320×200; el color real y los shaders entran solo al final.
4. Escenas y máquina de estados
4.1. La base Scene
source/game/scenes/scene.hpp (namespace Scenes):
class Scene {
public:
virtual void onEnter() {} // una vez, antes del primer tick
virtual void tick(int delta_ms) = 0; // no bloquea, NO llama a Jd8::flip
virtual auto done() const -> bool = 0;
virtual auto nextState() const -> int { return 1; } // 1=siguiente, 0=gameplay, -1=salir
};
El Director hace avanzar la escena hasta que done() es cierto, consulta
nextState() y construye la siguiente.
4.2. El despachador: game_state_ + Info::ctx
Director::createNextScene() decide la siguiente escena:
game_state_ == 0→new ModuleGame(gameplay puro, §7).game_state_ == 1→SceneRegistry::tryCreate(Info::ctx.num_piramide)(game/scenes/scene_registry.hpp): un mapaint → factoryque devuelve la escena registrada para ese estado, onullptrpara caer al path legacy. Replica el viejoModuleSequence::Go, incluido el redirect heredadonum_piramide == 6 && diners < 200 → 7(CLAUDE.md:104).game_state_ == -1→ salir.
4.3. Info::ctx: el estado del juego
source/game/info.hpp — Info::GameContext ctx (singleton inline) es la fuente
de verdad del estado:
struct GameContext {
int num_piramide, num_habitacio, diners, diamants, vida, momies, engendros;
bool nou_personatge, pepe_activat;
void reset();
};
num_piramide es la pieza central: hace de selector tanto de cinemáticas como
de nivel jugable. El SceneRegistry va migrando cada estado de cinemática a
una Scene nueva; lo que no esté registrado cae a legacy. Por eso el registro
"crece" a medida que avanza la modernización.
graph LR
SC[Scene activa] -->|done()| NS{nextState}
NS -->|0| MG[ModuleGame]
NS -->|1| REG["SceneRegistry(num_piramide)"]
NS -->|-1| QUIT[salir]
MG -->|muta Info::ctx| REG
REG --> SC
5. Renderizado: de la lógica al píxel
El pipeline tiene dos mitades: software paletizado (Jd8) y GPU
(Screen + shaders).
5.1. Software: dibujar índices
Escenas y ModuleGame dibujan sobre el buffer screen de Jd8 (índices 8-bit,
320×200) con blits y color key. JD8_Flip() aplica la paleta y produce un
buffer ARGB (formato ABGR8888: 0xFF000000 + R + (G<<8) + (B<<16);
CLAUDE.md:220).
5.2. Overlay: sobre el buffer ARGB
source/core/rendering/overlay.* pinta directamente sobre el buffer ARGB
antes de presentar: notificaciones (slide-in), info de render animada (4
segmentos), indicador de PAUSA y la lógica de doble-ESC para salir.
5.3. Screen: a la ventana (GPU o fallback)
source/core/rendering/screen.* (singleton). Doble camino
(Screen::present, CLAUDE.md:204):
- GPU con shaders (primario,
sdl3gpu/): sube los píxeles a una textura de escena 320×200; opcionalmente upscale/supersampling (3×/6×/9× con downscale Lanczos) y estiramiento 4:3 fusionado en el upscale; luego el shader PostFX o CRT-Pi (con presets) a la swapchain con letterboxing. - GPU sin shaders: subida limpia a la swapchain.
- Fallback:
SDL_UpdateTexture+SDL_RenderPresent(SDL_Renderer).
Screen gestiona ventana, fullscreen, zoom, 4:3, integer scaling, VSync, FPS.
Apoyos de UI: Text (fuentes bitmap .fnt+.gif sobre el buffer ARGB, con
clipping 2D) y Menu (menú flotante de opciones con navegación por páginas,
animaciones y captura de teclas para remapping).
6. Entrada
Toda la entrada de UI/sistema converge en KeyConfig como fuente única de
verdad de las teclas (source/core/input/key_config.*): carga
data/input/keys.yaml (F1–F10 GlobalInputs + F11 pausa + F12 menú) y aplica
overrides del usuario; expone scancode("id"), isGuiKey(sc) (para que el
Director no propague teclas de UI al juego), y persiste solo lo que difiere
del default.
GlobalInputs(global_inputs.*) — mapea las F-keys a acciones de presentación (zoom, fullscreen, shaders, 4:3, supersampling, filtro, render info, pausa, menú). Tabla completa enCLAUDE.md:139.Gamepad(gamepad.*) — primer mando con hot-plug y notificación; D-pad/stick → flechas virtuales; botones frontales → Enter sintético; SELECT → menú, START → pausa. Cargagamecontrollerdb.txt.KeyRemap(key_remap.*) — cada frame copiaOptions::keys_game.*al estado virtual de scancodes estándar (UP/DOWN/LEFT/RIGHT). Permite remapear el movimiento sin tocar el código legacy deprota.cpp/mapa.cpp.Mouse(mouse.*) — auto-oculta el cursor tras 3 s de inactividad.
Reparto deliberado: las teclas de UI/sistema viven en
KeyConfig(keys.yaml); las de movimiento del jugador enOptions::keys_game(config.yaml, seccióncontrols:).
7. Gameplay: ModuleGame y entidades
source/game/modulegame.{hpp,cpp} es la escena de gameplay puro
(game_state_ == 0). Hereda de Scenes::Scene y sustituye el viejo Go()
bloqueante por un tick(). Tres fases internas (Phase):
FADING_IN → PLAYING → FADING_OUT → DONE (modulegame.hpp:46), con un
PaletteFade para los fundidos no bloqueantes.
Coordina las entidades del nivel, todas con una Jd8::Surface gfx_ compartida:
Mapa(game/mapa.*) — la sala/pirámide: tiles con contenido (CONTE_RES/CONTE_TRESOR/CONTE_FARAO…), colisión y dibujo del escenario.Prota(game/prota.*) — el protagonista "Sam" (hereda deSprite), movimiento y colisión.Momia(game/momia.*) — enemigo que persigue alProta(recibeProta*); variantedimoni(demonio).Bola(game/bola.*) — bola/roca que interactúa con elProta.Engendro(game/engendro.*) — entidad generada (suupdate()devuelvebool);Info::ctx.engendroslleva la cuenta.Marcador(game/marcador.*) — el marcador (vidas, dinero, diamantes), lee delProta/Info::ctx.
ModuleGame::tick() hace draw() (a screen, sin flip — lo hace el
Director) y update() (gateado por Jg::shouldUpdate). Cuando la partida
acaba (final_ != 0: muerte o cambio de sala), applyFinalTransitions() muta
Info::ctx y nextState() devuelve el estado siguiente.
Todas las entidades de gameplay derivan de Sprite (game/sprite.*),
clase base con draw()/update() sobre Jd8.
8. Sistema de escenas cinemáticas
Las pantallas no jugables (game_state_ == 1) son Scenes registradas en el
SceneRegistry. Las concretas viven en source/game/scenes/: boot_loader
(carga incremental, §9), banner, intro, intro_new_logo, intro_sprites,
slides, menu (menu_scene), mort (secuencia de muerte), secreta
(pantalla secreta) y credits.
Sobre ellas hay un conjunto de utilidades de animación reutilizables —el andamiaje "cinematográfico" del juego—:
Timeline(scenes/timeline.*) — secuenciación temporal de eventos.FrameAnimator(scenes/frame_animator.*) — animación por fotogramas.SpriteMover(scenes/sprite_mover.*) — movimiento interpolado de sprites (coneasing).PaletteFade(scenes/palette_fade.*) — fundidos por paleta no bloqueantes (también usado porModuleGame).SurfaceHandle(scenes/surface_handle.*) — propiedad RAII de unaJd8::Surface(las escenas poseen sus assets y los liberan en el destructor).scene_utils— helpers compartidos.
9. Recursos
Resource::List(core/resources/resource_list.*) — registro de rutas de asset desdedata/config/assets.yaml, con consulta O(1).Resource::Cache(core/resources/resource_cache.*) — caché de assets con carga incremental:beginLoad()(enSDL_AppInit) + la escenaBootLoaderSceneque elDirectorarranca automáticamente mientrasResource::Cache::isLoadDone()sea falso (una barra de progreso con la ventana viva).- Pack y fallback:
resource_pack.*+resource_helper.*sirven desderesources.pack(formato propio "AEE1",CLAUDE.md:237); en release nativo es estricto (solo pack), en Debug/WASM hay fallback adata/. - Formatos: GIF/paletas para gráficos (vía
Jd8), fuentes.fnt+.gif, OGG/WAV para audio, GLSL para shaders.
10. Audio y localización
- Audio:
core/audio/audio.*(singleton, aplicaOptions::audio) sobrejail_audio(Ja,jail/jail_audio.hpp/core/audio/jail_audio.hpp), mezcla propia con streams SDL3 (OGG víastb_vorbis, WAV).JA_Update()se bombea cada frame desde elDirector; pausa/resume con F11. - Localización:
core/locale/locale.*— mapa plano clave→cadena cargado de YAML (data/locale/ca.yaml, valenciano por defecto). Claves con notación de puntos (menu.items.zoom); si falta una clave, devuelve la propia clave (fallback visible para depurar).
11. Configuración y persistencia
Options(source/game/options.*) — namespace con globalsinliney carga/guardado YAML. Structs:KeysGame(movimiento),Video,RenderInfo,Audio,Window,Game,PostFXPreset,CrtPiPreset.defines.hpp— constantes del juego:Texts::WINDOW_TITLE/VERSION,GameScreen::WIDTH=320/HEIGHT=200/BUFFER_SIZE=64000.defaults.hpp— valores por defecto (Defaults::KeysGame,Video,Audio,Window,Game).KeyConfig— teclas de UI (§6).
Ficheros persistentes en ~/.config/jailgames/aee/ (CLAUDE.md:224):
config.yaml (vídeo/audio/ventana/render_info/controles), keys.yaml
(overrides de UI), postfx.yaml (6 presets), crtpi.yaml (4 presets), y en
Debug debug.yaml (estado inicial de gameplay para pruebas).
Builds condicionales: NDEBUG (release: pack estricto, sin debug.yaml),
__EMSCRIPTEN__ (WASM: fuerza ventana/zoom/4:3, mantiene fallback de assets,
sin persistencia MEMFS).
12. Modo demo / IA
No he encontrado un modo demo, attract mode ni IA de demostración en este
proyecto. Lo he buscado explícitamente: la única aparición de "replay" es un
comentario en jail_audio.hpp sobre re-reproducción de pistas de audio, sin
relación con un modo demo.
A diferencia de los proyectos hermanos coffee_crisis (playback de input
grabado) o jaildoctors_dilemma (tour de habitaciones), aquí el flujo de
atracción —si lo hubiera— se construiría sobre el SceneRegistry y las escenas
cinemáticas (§8), pero hoy no existe tal cosa implementada.
13. Convenciones y patrones recurrentes
- Frontera legacy/nuevo (
CLAUDE.md:79): lo más importante.core/jail/ygame/*.cppson legacy en modernización (preservar comportamiento); el resto es código nuevo. Los assets dedata/gfx/data/musicson intocables. - Namespaces legacy abreviados
Jg/Jd8/Ji/Jf/Ja(APIs C planas), con nombres externos estables durante la transición. - Single-thread, tick-based: sin fibers/mutex; las escenas no bloquean ni
llaman a
Jd8::flip(lo hace elDirector). - Render paletizado 8-bit sobre buffer 320×200; color y shaders solo al
final (
JD8_Flip→Screen). - Singletons con
init()/destroy()/get()(Screen,Audio,Director,Menu,Overlay,Resource::*,Jd8/Jgcomo módulos), creados/destruidos en orden explícito enSDL_AppInit/SDL_AppQuit. - Máquina de estados por
Info::ctx.num_piramide+SceneRegistryque migra cinemáticas legacy aScenes nuevas de forma incremental. - Fades y timing no bloqueantes: lo que antes eran bucles bloqueantes
(
JD8_FadeOut) ahora son máquinas de estados que avanzan un paso por tick. - Comentarios en valenciano/español; muchos
#includecon comentario de justificación (estilo IWYU).
14. Guía de navegación: "si quieres tocar X, mira Y"
| Quiero… | Empieza por… |
|---|---|
| Entender el arranque | source/main.cpp (SDL_AppInit) + core/system/director.cpp |
| El bucle / orden del frame | Director::iterate() (director.cpp) |
| Tocar el motor de dibujo | core/jail/jdraw8.* (Jd8) |
| Timing / fixed-timestep | core/jail/jgame.* (Jg::shouldUpdate) |
| Añadir/editar una pantalla | game/scenes/ (hereda de Scenes::Scene) + SceneRegistry::registerScene |
| Cómo se decide la siguiente pantalla | Director::createNextScene + Info::ctx.num_piramide + scene_registry.* |
| El estado del juego | game/info.hpp (Info::ctx) |
| Gameplay | game/modulegame.* (fases) + entidades prota/momia/bola/engendro/mapa/marcador |
| Animaciones de cinemáticas | game/scenes/{timeline,frame_animator,sprite_mover,palette_fade,surface_handle}.* |
| Composición final / shaders | core/rendering/screen.* + sdl3gpu/ + presets postfx.yaml/crtpi.yaml |
| Overlays (notificaciones/PAUSA/info) | core/rendering/overlay.* |
| Texto / menú | core/rendering/text.*, core/rendering/menu.* |
| Teclas de UI / F1–F12 | core/input/key_config.* + global_inputs.* + data/input/keys.yaml |
| Remapear movimiento | core/input/key_remap.* + Options::keys_game |
| Mando | core/input/gamepad.* |
| Cargar recursos / barra de carga | core/resources/resource_cache.* + resource_list.* + scenes/boot_loader_scene.* |
| Audio | core/audio/audio.* + jail_audio.hpp (Ja) |
| Idiomas | core/locale/locale.* + data/locale/ |
| Opciones / constantes | game/options.*, game/defines.hpp, game/defaults.hpp |
| Carpetas y pack | core/jail/jfile.* (Jf) + core/resources/resource_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.