Clone
1
Architecture
Matteo Benedetto edited this page 2026-07-01 10:21:54 +00:00
This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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"):

  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:

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:

  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.