init: Architecture

2026-07-01 10:21:54 +00:00
parent a56edd0398
commit 2c90bc6821
+177
@@ -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.