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:
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.
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.
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"):
- Passata 1: tutti i pavimenti. Ordinamento per
iso_y→ disegnati in alto-basso, dx per disambiguare. - 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:
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:
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:
- Title (112 step)
- Gameplay "eerie pulse" (96)
- Game Over (128)
- Finale (192)
- 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.