diff --git a/Architecture.md b/Architecture.md new file mode 100644 index 0000000..0ab3eec --- /dev/null +++ b/Architecture.md @@ -0,0 +1,177 @@ +# Architecture + +Approfondimento tecnico sul motore di gioco. + +## Layout della memoria + +Il ROM è 32 KB flat (nessun MBC, no banking). Lo schema è: + +``` +0x0000 - 0x00FF Header entry points + interrupt vectors +0x0100 - 0x014F Cartridge header (Nintendo logo, titolo, tipo ROM) +0x0150 - 0x3FFF Code + data +``` + +WRAM (8 KB) è partizionata come: + +| Indirizzo | Contenuto | +|---|---| +| `0xC000-0xC0FF` | map[21][21] (labirinto corrente) | +| `0xC100-0xC140` | map_buffer[32][32] (backbuffer isometrico) | +| `0xC200-0xC2A0` | enemy_lx/ly, enemy_is_moving, ... (8 fantasmi) | +| `0xC300+` | stato globale (player, stamina, livello, ...) | + +## Moduli + +| File | Responsabilità | +|---|---| +| `main.c` | entry point, loop VBL, `app_state` | +| `engine.c` | orchestrazione, init, schermate | +| `globals.c` / `globals.h` | stato globale, defines | +| `maze.c` | generazione labirinto (DFS + loop) | +| `player_logic.c` | DAS, camminata, corsa, salto | +| `enemy_logic.c` | AI multi-entity | +| `render.c` | proiezione isometrica, autotiling, fog | +| `sound.c` | sequencer VBL, 5 tracce musicali | +| `screens/` | schermate testuali (intro, istruzioni, death, ...) | + +## Proiezione isometrica + +Il rendering isometrico "2.5D" proietta coordinate logiche `(lx, ly)` su schermo in due passi: + +``` +// Coordinate iso (tile) +iso_x = (lx - ly) * 2 + 12 +iso_y = (lx + ly) + 2 + +// Coordinate pixel +px = (lx - ly) * 16 + 96 +py = (lx + ly) * 8 + 16 +``` + +Ogni tile è un diamante 32×16 px (`4 tile 8×8` in larghezza, `2 tile` in altezza). + +La camera è centrata sul player: + +```c +scroll_x = px - 64; // schermo 160px, centro = 80 +scroll_y = py - 72; // schermo 144px, centro = 72 +move_bkg(scroll_x, scroll_y); +``` + +## Generazione del labirinto + +Algoritmo: **DFS iterativo con stack in WRAM**. + +```c +void generate_maze(void) { + // 1. Inizializza tutto a muro + memset(maze, 0, sizeof(maze)); + + // 2. Scegli una cella di partenza (1,1) + uint8_t cx = 1, cy = 1; + maze[cy][cx] = 1; + + // 3. DFS iterativo + // Per ogni cella, scegli un vicino casuale non visitato + // e "scava" il muro tra i due + // Backtrack quando non ci sono vicini + + // 4. Rimuovi il 15% dei muri per creare loop + for (uint16_t i = 0; i < (map_size * map_size) / 7; i++) { + // Trova un muro tra due corridoi e rimuovilo + } + + // 5. Piazza la botola in una cella lontana dal player + place_hatch_far_from_player(); +} +``` + +`map_size` cresce di 2 per livello: + +| Livello | map_size | +|---|---| +| 1 | 7 | +| 2 | 9 | +| ... | ... | +| 8 | 21 (cap) | + +## Fog of War + +Distanza di **Chebyshev** (`max(|dx|, |dy|)`) — un quadrato, non un cerchio. + +```c +int8_t dist = (dx > dy) ? dx : dy; +if (dist > fog_radius) return 0; // buio totale +if (dist == fog_radius) return 2; // "bordo" della nebbia +return 1; // cella visibile +``` + +`fog_radius = 2` (5×5) per livelli 1-6, `fog_radius = 1` (3×3) dal livello 7. + +## Multi-pass rendering + +Il rendering isometrico viene fatto in **due passate** per gestire correttamente le scale (tile di "vittoria"): + +1. **Passata 1**: tutti i pavimenti. Ordinamento per `iso_y` → disegnati in alto-basso, dx per disambiguare. +2. **Passata 2**: solo la botola (`maze[y][x] == 2`). Disegnata per ultima, così i suoi bordi trasparenti si fondono coi pavimenti sotto. + +Se disegnassimo scale e pavimenti nello stesso loop, i pavimenti "coprirebbero" le scale perché hanno angoli di disegno identici ma con maschere diverse. + +## Autotiling + +Ogni cella guarda i 4 vicini cardinali nella griglia logica: + +```c +state_tl = is_visible(lx - 1, ly) ? 1 : 0; +state_tr = is_visible(lx, ly - 1) ? 1 : 0; +mask = state_tl + state_tr * 3; // 0..3 +``` + +La maschera seleziona una delle 4 varianti di tile tra 0 e 3, dando un'autotiling 2D-free. + +## Sistema di coordinate nemico + +8 fantasmi (`MAX_ENEMIES = 8`). Ciascuno ha: + +```c +struct enemy_t { + uint8_t lx, ly; // cella logica + uint8_t is_moving; // in interpolazione? + uint8_t move_progress; // 0..16 + int8_t start_lx, start_ly; // origine interpolazione + int8_t target_lx, target_ly; // destinazione + int16_t start_px, start_py; // pixel origine + int16_t target_px, target_py; // pixel destinazione + uint8_t cooldown; // timer prima del prossimo passo +}; +``` + +L'AI è **greedy Manhattan**: ad ogni step il fantasma si sposta nella direzione che minimizza la distanza Manhattan dal player. Cooldown scalabile: 60 frame al livello 1, 11 al livello 8 (cap minimo 10). + +## Audio + +4 canali APU GB: +- **CH1**: onda quadra + sweep + envelope (melodia) +- **CH2**: onda quadra + envelope (melodia/basso) +- **CH3**: wave RAM (samples) +- **CH4**: noise ( percussioni) + +Il sequencer è in VBL interrupt (`add_VBL(play_music_tick)`), legge step da tabelle statiche di note (frequenza, durata, channel). 5 tracce: +1. Title (112 step) +2. Gameplay "eerie pulse" (96) +3. Game Over (128) +4. Finale (192) +5. Going Deeper (96) + +## Performance + +- 60 FPS (16.67 ms/frame) +- Budget: ~70 000 cicli SM83 per frame +- Render: ~50% del budget +- AI nemico: ~10% +- Audio: ~5% +- Loop overhead: ~5% +- Margine: ~30% per future feature + +Misurato con BGB debugger + profiler PyBoy.