Minimal Game Boy Hello World using GBDK-2020 and PyBoy
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 

7.0 KiB

Loop Principale e Inizializzazione

src/main.c è il punto di ingresso del programma. Contiene la sequenza completa di boot, l'inizializzazione di tutti i sotto-sistemi e il game loop principale.


Sequenza di Avvio

sequenceDiagram
    participant HW as Hardware GB
    participant main as main.c
    participant sub as Sotto-sistemi

    HW->>main: Boot (entry point void main())

    Note over main: === SCHERMATA TITOLO ===
    main->>HW: set_bkg_data(title_bg_tiles)
    main->>HW: set_bkg_tiles(title_bg_map)
    main->>HW: SHOW_BKG + DISPLAY_ON
    main->>sub: play_title_music()
    main->>HW: loop: joypad() → attendi START

    Note over main: === INIZIALIZZAZIONE GIOCO ===
    main->>HW: DISPLAY_OFF (safe VRAM write)
    main->>HW: initrand(DIV_REG)
    main->>HW: set_bkg_data(TileData) — tileset di gioco
    main->>HW: Riempi mappa BG con siepi
    main->>sub: generate_maze()
    main->>HW: Rendering autotile del labirinto
    main->>HW: move_bkg(252, 252) — centra il labirinto
    main->>sub: init_rats()
    main->>sub: init_cursor()
    main->>sub: init_bombs()
    main->>sub: init_music()
    main->>HW: DISPLAY_ON

    Note over main: === GAME LOOP ===
    loop Ogni frame (60 Hz)
        main->>HW: wait_vbl_done()
        main->>sub: update_music()
        main->>sub: update_rats()
        main->>sub: update_cursor()
        main->>sub: update_bombs()
        main->>HW: Aggiorna timer HUD
        main->>main: check: game_over_flag || victory_flag
    end

Seme Casuale da DIV_REG

// src/main.c, riga 65
initrand(DIV_REG);

Il registro DIV_REG (0xFF04) è un timer hardware che si incrementa a ~16384 Hz indipendentemente dalla CPU. Il tempo esatto tra il boot e la pressione del tasto START da parte del giocatore è imprevedibile, rendendo il valore di DIV_REG un ottimo seme entropico per il generatore di numeri pseudo-casuali.

!!! tip "Perché DIV_REG e non una costante?" Se il seme fosse fisso, ogni partita genererebbe esattamente lo stesso labirinto. DIV_REG garantisce che ogni run produca un labirinto diverso.


Caricamento VRAM in Sicurezza (DISPLAY_OFF)

// src/main.c
DISPLAY_OFF;                              // Disattiva PPU → VRAM scrivibile in qualsiasi momento
set_bkg_data(0, 22, TileData);           // Carica tileset di gioco
// ... setup sprite data ...
generate_maze();
// ... rendering del labirinto nella mappa BG ...
DISPLAY_ON;

La VRAM è accessibile solo durante HBlank o VBlank quando il display è acceso. Disattivare la PPU prima di caricare grandi blocchi di dati è il metodo più sicuro per evitare glitch visivi durante l'inizializzazione.


Rendering Autotile del Labirinto

Dopo la generazione, ogni cella del labirinto viene tradotta in un indice tile in base ai suoi vicini:

// src/main.c, righe 94-112
for (y = 0; y < MAZE_HEIGHT; y++) {
    for (x = 0; x < MAZE_WIDTH; x++) {
        uint8_t tile_idx = maze[y][x];

        if (tile_idx == 1) { // Muro: calcola autotile
            uint8_t mask = 0;
            if (y > 0            && maze[y-1][x] == 1) mask |= 8; // Nord
            if (x < MAZE_WIDTH-1 && maze[y][x+1] == 1) mask |= 4; // Est
            if (y < MAZE_HEIGHT-1&& maze[y+1][x] == 1) mask |= 2; // Sud
            if (x > 0            && maze[y][x-1] == 1) mask |= 1; // Ovest
            tile_idx = 6 + mask; // tile 6-21: 16 varianti di cespuglio
        } else {
            // Percorso: variante di pavimento casuale
            uint8_t r = rand() % 10;
            if (r < 5) tile_idx = 0;       // Pavimento liscio (più comune)
            else tile_idx = 1 + (r % 5);   // Variante decorata
        }
        set_bkg_tiles(x, y, 1, 1, &tile_idx);
    }
}

Questo si traduce in 16 varianti di cespuglio (angolo, bordo N/S/E/O, T-junction, croce, ecc.) che si connettono visivamente in modo coerente.


Centratura con Hardware Scrolling

La mappa del Background hardware è 32×32 tile (256×256 px), ma il labirinto è 19×17 tile (152×136 px). Per centrarlo nella finestra di 160×144:

// src/main.c, riga 115
move_bkg(252, 252);

252 = 256 - 4, dove 4 è il margine di 4 pixel a sinistra. Questo sfrutta il wrap-around modulare della mappa hardware: lo scroll a (252, 252) equivale a iniziare la visualizzazione dal pixel (-4, -4) del tilemap, centrando il labirinto con un bordo di 4 px su tutti i lati.


Game Loop e Transizione agli Stati Finali

// src/main.c, game loop
while (1) {
    wait_vbl_done();        // (1) Sync VBlank — punto sicuro per VRAM
    update_music();         // (2) Audio — priorità alta per continuità sonora
    update_rats();          // (3) AI — logica + sprite
    update_cursor();        // (4) Input + armi
    update_bombs();         // (5) Esplosioni

    // Aggiornamento HUD timer...

    if (game_over_flag || victory_flag) {
        HIDE_SPRITES;       // Nasconde tutti gli sprite
        move_bkg(0, 0);     // Azzera lo scrolling hardware

        // Carica lo sfondo dedicato in VRAM
        if (victory_flag) {
            set_bkg_data(0, 256, victory_bg_tiles);
            set_bkg_tiles(0, 0, 20, 18, victory_bg_map);
            play_victory_music();
        } else {
            set_bkg_data(0, 256, rat_bg_tiles);
            set_bkg_tiles(0, 0, 20, 18, rat_bg_map);
            play_game_over_music();
        }

        SHOW_SPRITES;
        while(1) {          // Loop finale — solo audio e punteggio
            update_music();
            wait_vbl_done();
        }
    }
}

!!! note "Perché HIDE_SPRITES prima di caricare la VRAM?" Quando si carica set_bkg_data(0, 256, ...) si sovrascrivono tutti i tile in VRAM, compresi quelli usati dagli sprite. Se la PPU stesse ancora disegnando gli sprite in quel momento, vedremmo un singolo frame di glitch grafico. HIDE_SPRITES sposta tutti gli sprite a coordinate (0,0) atomicamente prima del caricamento.


Timer HUD con Aritmetica BCD

Il timer usa un array di 4 cifre decimali invece di un singolo intero a 16-bit, evitando la divisione hardware:

// src/main.c
uint8_t timer_digits[4] = {0, 0, 0, 0}; // Rappresenta "0000"
uint8_t timer_frames = 0;

// Ogni 60 frame (1 secondo), incrementa con carry BCD manuale
timer_frames++;
if (timer_frames >= 60) {
    timer_frames = 0;
    timer_digits[3]++;           // Unità secondi
    if (timer_digits[3] >= 10) { timer_digits[3] = 0; timer_digits[2]++; }
    if (timer_digits[2] >= 6)  { timer_digits[2] = 0; timer_digits[1]++; } // Decine secondi
    if (timer_digits[1] >= 10) { timer_digits[1] = 0; timer_digits[0]++; } // Minuti
}

// Render: ogni cifra usa un tile sprite (indice 25+digit)
set_sprite_tile(25, 25 + timer_digits[0]);
set_sprite_tile(26, 25 + timer_digits[1]);
set_sprite_tile(27, 25 + timer_digits[2]);
set_sprite_tile(28, 25 + timer_digits[3]);

Questa tecnica è identica a quella usata nei computer degli anni '80 per i contatori di punteggio, ed elimina completamente le divisioni a 16 bit che sarebbero necessarie per estrarre le singole cifre da un intero.