6 changed files with 823 additions and 0 deletions
@ -0,0 +1,198 @@
|
||||
# Architettura dei Moduli |
||||
|
||||
Il codice di MICE! è organizzato in moduli C indipendenti, ognuno responsabile di una singola area funzionale. Questa separazione è fondamentale su un sistema embedded dove la RAM è un bene rarissimo. |
||||
|
||||
--- |
||||
|
||||
## Grafico delle Dipendenze |
||||
|
||||
```mermaid |
||||
graph TD |
||||
main["main.c\nEntry point + Game Loop"] |
||||
|
||||
maze["maze.c/h\nGenerazione labirinto"] |
||||
rat["rat.c/h\nAI topi + Spawn + Kill"] |
||||
cursor["cursor.c/h\nInput giocatore + DAS"] |
||||
bomb["bomb.c/h\nMeccanica bomba"] |
||||
music["music.c/h\nTracker audio + SFX"] |
||||
|
||||
tiles["tiles.c/h\nTileset grafico"] |
||||
title_bg["title_bg.c/h\nSfondo titolo"] |
||||
rat_bg["rat_bg.c/h\nSfondo game over"] |
||||
victory_bg["victory_bg.c/h\nSfondo vittoria"] |
||||
pause_gfx["pause_gfx.c/h\nSprite P-A-U-S-E"] |
||||
numbers_gfx["numbers_gfx.c/h\nSprite cifre 0-9"] |
||||
bomb_gfx["bomb_gfx.c\nSprite bomba animata"] |
||||
mockup_gfx["mockup_gfx.c/h\nTileset cespugli"] |
||||
|
||||
main --> maze |
||||
main --> rat |
||||
main --> cursor |
||||
main --> bomb |
||||
main --> music |
||||
main --> tiles |
||||
main --> title_bg |
||||
main --> rat_bg |
||||
main --> victory_bg |
||||
main --> pause_gfx |
||||
main --> numbers_gfx |
||||
|
||||
rat --> maze |
||||
rat --> music |
||||
|
||||
cursor --> maze |
||||
cursor --> bomb |
||||
cursor --> music |
||||
cursor --> rat |
||||
|
||||
bomb --> maze |
||||
bomb --> rat |
||||
bomb --> music |
||||
|
||||
bomb --> bomb_gfx |
||||
tiles --> mockup_gfx |
||||
``` |
||||
|
||||
--- |
||||
|
||||
## Descrizione dei Moduli |
||||
|
||||
### `main.c` — Collante e Loop Principale |
||||
|
||||
**Responsabilità**: inizializzazione hardware, sequenza di schermate (titolo → gioco → vittoria/sconfitta), game loop principale. |
||||
|
||||
Non contiene logica di gameplay propria: si limita a inizializzare i sotto-sistemi e richiamarli ogni frame nel corretto ordine. |
||||
|
||||
```c |
||||
// Ordine di chiamata nel game loop |
||||
wait_vbl_done(); // Sincronizza con VBlank |
||||
update_music(); // Avanza il tracker audio |
||||
update_rats(); // AI + movimento + riproduzione |
||||
update_cursor(); // Input giocatore + armi |
||||
update_bombs(); // Stato bomba + esplosioni |
||||
``` |
||||
|
||||
--- |
||||
|
||||
### `maze.c/h` — Generazione Procedurale |
||||
|
||||
Espone: |
||||
- `uint8_t maze[MAZE_HEIGHT][MAZE_PITCH]` — mappa globale in WRAM |
||||
- `void generate_maze(void)` — popola la mappa |
||||
|
||||
Non ha dipendenze esterne. È il modulo più puro. |
||||
|
||||
--- |
||||
|
||||
### `rat.c/h` — AI e Logica Topi |
||||
|
||||
Espone: |
||||
- `void init_rats(void)` — inizializza i 4 topi di partenza |
||||
- `void update_rats(void)` — movimento, riproduzione, collisioni |
||||
- `void kill_rats_at(uint8_t x, uint8_t y)` — elimina topi su una cella |
||||
- `void spawn_rat(uint8_t x, uint8_t y, uint8_t dir)` — spawna un cucciolo |
||||
|
||||
Dipende da `maze.c` (per il pathfinding) e `music.c` (per SFX nascita/morte). |
||||
|
||||
--- |
||||
|
||||
### `cursor.c/h` — Input del Giocatore |
||||
|
||||
Espone: |
||||
- `void init_cursor(void)` — carica il tile del cursore, posizione iniziale |
||||
- `void update_cursor(void)` — legge joypad, gestisce DAS, bomba, fucile, pausa audio |
||||
|
||||
Dipende da `bomb.c`, `music.c`, `rat.c` (per il fucile). |
||||
|
||||
--- |
||||
|
||||
### `bomb.c/h` — Meccanica Bomba |
||||
|
||||
Espone: |
||||
- `void init_bombs(void)` — carica sprite bomba, nasconde tutto |
||||
- `void drop_bomb(uint8_t x, uint8_t y)` — attiva una bomba sulla cella |
||||
- `void update_bombs(void)` — avanza timer, gestisce esplosione propagante |
||||
|
||||
Dipende da `maze.c` (per propagazione), `rat.c` (per kill), `music.c` (per SFX). |
||||
|
||||
--- |
||||
|
||||
### `music.c/h` — Tracker Audio |
||||
|
||||
Espone: |
||||
- `void init_music(void)` — configura registri APU, inizia la musica di gioco |
||||
- `void update_music(void)` — avanza il sequencer di un tick se necessario |
||||
- `void toggle_music(void)` — mute/unmute della musica di sottofondo |
||||
- `void play_sfx_*(void)` — effetti sonori istantanei (esplosione, fucile, ecc.) |
||||
- `void play_game_over_music(void)` / `play_victory_music(void)` / `play_title_music(void)` |
||||
|
||||
Non dipende da nessun altro modulo di gioco. |
||||
|
||||
--- |
||||
|
||||
### Moduli Asset (Sola Lettura) |
||||
|
||||
Tutti questi moduli espongono esclusivamente **array `const`** in ROM, generati da script Python offline. |
||||
|
||||
| Modulo | Contenuto | Dimensione tipica | |
||||
|--------|-----------|------------------| |
||||
| `tiles.c/h` | 22 tile 8×8: pavimenti + 16 varianti autotile cespugli | ~352 byte | |
||||
| `title_bg.c/h` | Tileset + mappa 20×18 dello sfondo titolo | ~8 KB | |
||||
| `rat_bg.c/h` | Tileset + mappa 20×18 dello sfondo game over | ~8 KB | |
||||
| `victory_bg.c/h` | Tileset + mappa 20×18 dello sfondo vittoria | ~8 KB | |
||||
| `pause_gfx.c/h` | 5 tile per le lettere P-A-U-S-E | ~80 byte | |
||||
| `numbers_gfx.c/h` | 10 tile per le cifre 0–9 | ~160 byte | |
||||
| `bomb_gfx.c` | Sprite bomba (3 fasi ticking + centro esplosione + fiamme) | ~176 byte | |
||||
| `mockup_gfx.c/h` | Tileset alternativo cespugli | ~varia | |
||||
|
||||
--- |
||||
|
||||
## Mappa della VRAM Sprite (Tile Indices) |
||||
|
||||
``` |
||||
Indice Contenuto |
||||
───────────────────────────────────────────────────── |
||||
0 Topo: metà destra vista SINISTRA/DESTRA |
||||
1 Topo: metà sinistra vista SINISTRA/DESTRA |
||||
2 Topo: metà superiore vista SU/GIÙ |
||||
3 Topo: metà inferiore vista SU/GIÙ |
||||
4 Cursore (bordo quadrato lampeggiante) |
||||
5 Bomba frame 3 (spenta) |
||||
6 Bomba frame 2 (miccia lenta) |
||||
7 Bomba frame 1 (miccia veloce, lampeggia) |
||||
8 Centro esplosione (stella) |
||||
9 Fiamma orizzontale (braccio est/ovest) |
||||
10 Fiamma verticale (braccio nord/sud) |
||||
11 Lettera P (PAUSE) |
||||
12 Lettera A |
||||
13 Lettera U |
||||
14 Lettera S |
||||
15 Lettera E |
||||
16–24 (non usati o riservati) |
||||
25–34 Cifre 0–9 (timer HUD) |
||||
``` |
||||
|
||||
--- |
||||
|
||||
## Mappa dell'OAM (Sprite Hardware Indices) |
||||
|
||||
``` |
||||
Slot Uso |
||||
────────────────────────────────────────────────────── |
||||
0–1 Topo 0 (metà sinistra, metà destra) |
||||
2–3 Topo 1 |
||||
4–5 Topo 2 |
||||
6–7 Topo 3 |
||||
8–9 Topo 4 |
||||
10–11 Topo 5 |
||||
12–13 Topo 6 |
||||
14–15 Topo 7 |
||||
16–17 Topo 8 |
||||
18–19 Topo 9 |
||||
20–23 Pool esplosione bomba (gruppo A) |
||||
24 Flash sparo fucile |
||||
25–28 Timer HUD (4 cifre) |
||||
29–37 Pool esplosione bomba (gruppo B) |
||||
38 Sprite bomba principale (animato) |
||||
39 Cursore del giocatore |
||||
``` |
||||
@ -0,0 +1,127 @@
|
||||
# Hardware Game Boy DMG |
||||
|
||||
Capire i vincoli dell'hardware è indispensabile per capire ogni scelta architetturale del codice. Il Game Boy DMG del 1989 è una macchina meravigliosamente vincolata. |
||||
|
||||
--- |
||||
|
||||
## CPU: Sharp SM83 |
||||
|
||||
Il processore è una via di mezzo tra lo Z80 e l'Intel 8080, con un set di istruzioni ridotto. |
||||
|
||||
| Proprietà | Valore | |
||||
|-----------|--------| |
||||
| **Architettura** | 8-bit (con registri a 16-bit per indirizzi) | |
||||
| **Clock** | ~4.194304 MHz | |
||||
| **Moltiplicazione hardware** | ❌ Assente | |
||||
| **Divisione hardware** | ❌ Assente | |
||||
| **Virgola mobile (FPU)** | ❌ Assente | |
||||
| **Stack** | 16-bit, in HRAM o WRAM | |
||||
| **Registri generali** | A, B, C, D, E, H, L (più F flags) | |
||||
|
||||
!!! warning "Niente moltiplicazioni!" |
||||
Qualsiasi `a * b` in C viene compilato da SDCC in una subroutine di addizioni ripetute. |
||||
Su questo hardware, `y * 19` può richiedere **20+ cicli di clock**. Il codice di MICE! |
||||
usa esclusivamente potenze di 2 (`y << 5`) per eliminare questo costo. |
||||
|
||||
--- |
||||
|
||||
## Memoria |
||||
|
||||
``` |
||||
0x0000 - 0x3FFF ROM Bank 0 (16 KB, fisso) |
||||
0x4000 - 0x7FFF ROM Bank 1+ (16 KB, switchable via MBC) |
||||
0x8000 - 0x9FFF VRAM (8 KB) |
||||
0x8000 - 0x97FF Tile Data (384 tile × 16 byte = 6144 byte) |
||||
0x9800 - 0x9BFF BG Map 0 (32×32 tile) |
||||
0x9C00 - 0x9FFF BG Map 1 (32×32 tile) |
||||
0xA000 - 0xBFFF External RAM (cartuccia, non usata) |
||||
0xC000 - 0xDFFF WRAM (8 KB) |
||||
0xFE00 - 0xFE9F OAM (160 byte = 40 sprite × 4 byte) |
||||
0xFF00 - 0xFF7F I/O Registers |
||||
0xFF80 - 0xFFFE HRAM (127 byte, velocissima) |
||||
``` |
||||
|
||||
!!! info "VRAM e accesso sicuro" |
||||
La VRAM è accessibile **solo durante HBlank o VBlank**. GBDK gestisce questo |
||||
automaticamente tramite la funzione `wait_vbl_done()` che il game loop chiama |
||||
ogni frame per sincronizzarsi con il VBlank. |
||||
|
||||
--- |
||||
|
||||
## Video: PPU (Pixel Processing Unit) |
||||
|
||||
| Parametro | Valore | |
||||
|-----------|--------| |
||||
| **Risoluzione** | 160×144 pixel | |
||||
| **Colori** | 4 sfumature di grigio (palette software 2bpp) | |
||||
| **Tile size** | 8×8 pixel, 2 bit per pixel | |
||||
| **Tile in VRAM** | 256 (modalità $8000) o 256 (modalità $8800) | |
||||
| **Background map** | 32×32 tile (256×256 px), visibile 20×18 | |
||||
| **Sprite (OAM)** | Max **40 totali**, max **10 per scanline** | |
||||
| **Sprite size** | 8×8 o 8×16 (configurabile via LCDC) | |
||||
| **Layer** | Background + Window + Sprites | |
||||
|
||||
### Palette Hardware |
||||
|
||||
Il Game Boy non ha "colori" — ha 4 livelli di intensità per layer. Le palette sono configurate via registri: |
||||
|
||||
```c |
||||
BGP_REG = 0b11100100; // BG: 3=Nero, 2=Grigio Sc., 1=Grigio Ch., 0=Bianco |
||||
OBP0_REG = 0b11100100; // Sprite palette 0 (topi, cursore) |
||||
OBP1_REG = 0b11000000; // Sprite palette 1 (timer HUD - inverte contrasto) |
||||
``` |
||||
|
||||
Il bit 0 di ogni sprite in OAM seleziona quale palette usare (`S_PALETTE` flag). |
||||
|
||||
--- |
||||
|
||||
## Audio: APU (Audio Processing Unit) |
||||
|
||||
L'APU del Game Boy ha **4 canali** indipendenti, ciascuno controllato da registri hardware mappati in memoria. |
||||
|
||||
| Canale | Tipo | Registri | Uso in MICE! | |
||||
|--------|------|----------|--------------| |
||||
| **CH1** | Square Wave con Sweep | NR10–NR14 | Arpeggio melodico | |
||||
| **CH2** | Square Wave | NR21–NR24 | Melodia principale | |
||||
| **CH3** | Wave RAM (forma d'onda custom) | NR30–NR34 + 0xFF30 | Basso | |
||||
| **CH4** | Noise (LFSR) | NR41–NR44 | Percussioni + SFX | |
||||
|
||||
### Frequenza Note |
||||
|
||||
Le note musicali vengono codificate come valori di frequenza nei registri `NR_3` (low byte) e `NR_4` (high 3 bit): |
||||
|
||||
``` |
||||
f_reg = 2048 - (131072 / Hz) |
||||
``` |
||||
|
||||
Esempio per C4 (261.63 Hz): |
||||
|
||||
```c |
||||
#define N_C4 0x60B // = 2048 - (131072 / 261.63) ≈ 0x60B |
||||
``` |
||||
|
||||
--- |
||||
|
||||
## OAM: Object Attribute Memory |
||||
|
||||
Ogni sprite occupa **4 byte** nell'OAM: |
||||
|
||||
| Byte | Contenuto | |
||||
|------|-----------| |
||||
| 0 | Y position (schermo + 16) | |
||||
| 1 | X position (schermo + 8) | |
||||
| 2 | Tile index (in VRAM sprite tiles) | |
||||
| 3 | Flags (palette, flip X/Y, priorità) | |
||||
|
||||
Uno sprite è "nascosto" posizionandolo a coordinate `(0, 0)` — fuori dall'area visibile |
||||
(lo schermo inizia a Y=16, X=8 in coordinate OAM assolute). |
||||
|
||||
```c |
||||
// Nascondere uno sprite in GBDK |
||||
move_sprite(sprite_index, 0, 0); |
||||
``` |
||||
|
||||
!!! danger "Limite 10 sprite per scanline" |
||||
Se più di 10 sprite si trovano sulla stessa riga orizzontale (scanline), |
||||
gli ultimi vengono semplicemente ignorati dalla PPU. In MICE! i topi sono |
||||
disposti verticalmente nel labirinto per ridurre la probabilità di conflitti. |
||||
@ -0,0 +1,75 @@
|
||||
# MICE! — Documentazione Tecnica |
||||
|
||||
Benvenuto nella documentazione tecnica di **MICE!**, un gioco completo per **Game Boy DMG (1989)** scritto interamente in C con la toolchain [GBDK-2020](https://github.com/gbdk-2020/gbdk-2020). |
||||
|
||||
 |
||||
|
||||
--- |
||||
|
||||
## Cos'è MICE! |
||||
|
||||
MICE! è un gioco di strategia in tempo reale a schermata fissa. Il labirinto viene generato proceduralmente ad ogni partita. Il giocatore controlla un cursore di mira e deve eliminare tutti i topi prima che il tempo scada. I topi si muovono autonomamente, trovano percorsi nel labirinto e si riproducono se si incontrano. |
||||
|
||||
### Meccaniche principali |
||||
|
||||
| Meccanica | Descrizione | |
||||
|-----------|-------------| |
||||
| **Generazione labirinto** | Recursive Backtracker iterativo, sempre risolvibile | |
||||
| **AI topi** | Pathfinding casuale senza backtracking, spatial hash per collisioni | |
||||
| **Riproduzione** | Spawn di un cucciolo a ogni incontro tra topi adulti | |
||||
| **Bomba (A)** | Esplosione a croce che si propaga lungo tutto il corridoio | |
||||
| **Fucile (B)** | Sparo istantaneo con cooldown di 3 secondi | |
||||
| **Timer** | Countdown visualizzato come HUD sprite nella barra inferiore | |
||||
| **Musica** | Tracker chiptune 4 canali nativo, jingle dedicati per vittoria/sconfitta | |
||||
|
||||
--- |
||||
|
||||
## Struttura della Documentazione |
||||
|
||||
| Sezione | Contenuto | |
||||
|---------|-----------| |
||||
| [Hardware Game Boy](hardware.md) | Vincoli fisici: SM83, VRAM, OAM, APU | |
||||
| [Architettura Moduli](architecture.md) | Grafico dei moduli C e dipendenze | |
||||
| [Loop Principale](main_loop.md) | Sequenza di inizializzazione e game loop | |
||||
| [Generazione Labirinto](maze.md) | Algoritmo Recursive Backtracker, MAZE_PITCH | |
||||
| [AI dei Topi](ai.md) | Movimento, pathfinding, riproduzione, spatial hash | |
||||
| [Rendering e Autotiling](rendering.md) | Tile system, bitmask 4-bit, hardware scrolling | |
||||
| [Sprite e OAM](sprites.md) | Allocazione 40 sprite hardware, meta-sprite 16×8 | |
||||
| [Armi e Gameplay](weapons.md) | Cursore DAS, bomba propagante, fucile con cooldown | |
||||
| [Audio](audio.md) | Mini-tracker 4 canali, SFX, Wave RAM | |
||||
| [Pipeline Asset](assets.md) | Script Python, conversione PNG → header C 2bpp | |
||||
| [Ottimizzazioni](optimizations.md) | Riepilogo di tutte le tecniche di ottimizzazione | |
||||
|
||||
--- |
||||
|
||||
## Quick Start |
||||
|
||||
```bash |
||||
# Compila tutto |
||||
make |
||||
|
||||
# Gioca |
||||
pyboy -w SDL2 -s 3 --sound-volume 100 maze.gb |
||||
|
||||
# Test audio interattivo |
||||
pyboy -w SDL2 -s 3 --sound-volume 100 test_audio.gb |
||||
|
||||
# Validazione headless |
||||
python3 tests/test_pyboy.py |
||||
``` |
||||
|
||||
--- |
||||
|
||||
## Dati Tecnici |
||||
|
||||
| Parametro | Valore | |
||||
|-----------|--------| |
||||
| **Target hardware** | Nintendo Game Boy DMG (1989) | |
||||
| **Toolchain** | GBDK-2020 + SDCC | |
||||
| **Linguaggio** | C (SM83/Z80-compatible) | |
||||
| **ROM size** | 32 KB (MBC1 `0x00`, solo ROM) | |
||||
| **Tile in VRAM** | 256 tile 8×8 @ 2bpp | |
||||
| **Sprite OAM** | 40 slot hardware (8×8 px ciascuno) | |
||||
| **RAM disponibile** | 8 KB (WRAM) | |
||||
| **FPS target** | 60 Hz (sincronizzato su VBlank) | |
||||
| **Frequenza CPU** | ~4.19 MHz | |
||||
@ -0,0 +1,199 @@
|
||||
# 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 |
||||
|
||||
```mermaid |
||||
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` |
||||
|
||||
```c |
||||
// 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) |
||||
|
||||
```c |
||||
// 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: |
||||
|
||||
```c |
||||
// 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: |
||||
|
||||
```c |
||||
// 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 |
||||
|
||||
```c |
||||
// 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: |
||||
|
||||
```c |
||||
// 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. |
||||
@ -0,0 +1,166 @@
|
||||
# Generazione Procedurale del Labirinto |
||||
|
||||
**File**: `src/maze.c`, `src/maze.h` |
||||
|
||||
Il labirinto è generato ogni volta che si avvia una partita usando l'algoritmo **Recursive Backtracker** (noto anche come DFS su griglia), implementato in versione **iterativa** per evitare di esaurire lo stack hardware del Game Boy. |
||||
|
||||
--- |
||||
|
||||
## Struttura Dati |
||||
|
||||
```c |
||||
// src/maze.h |
||||
#define MAZE_WIDTH 19 // Larghezza logica (celle dispari per simmetria) |
||||
#define MAZE_HEIGHT 17 // Altezza logica |
||||
#define MAZE_PITCH 32 // Larghezza allocata (potenza di 2 — ottimizzazione critica) |
||||
|
||||
extern uint8_t maze[MAZE_HEIGHT][MAZE_PITCH]; |
||||
``` |
||||
|
||||
La matrice è allocata in **WRAM** (Work RAM), uno dei beni più preziosi del Game Boy. Con `MAZE_PITCH = 32`, occupa `17 × 32 = 544 byte` su un totale di 8192 byte disponibili. |
||||
|
||||
### Perché MAZE_PITCH = 32 e non 19? |
||||
|
||||
L'accesso a `maze[y][x]` viene compilato da SDCC come: |
||||
|
||||
``` |
||||
address = base_address + (y * MAZE_PITCH) + x |
||||
``` |
||||
|
||||
Con `MAZE_PITCH = 19`, SDCC emette una subroutine di moltiplicazione software (~20 cicli). Con `MAZE_PITCH = 32` (potenza di 2), la moltiplicazione diventa un **bit-shift**: |
||||
|
||||
```asm |
||||
; y * 32 = y << 5 |
||||
; Generato da SDCC con MAZE_PITCH = 32: |
||||
LD A, y_reg |
||||
ADD A, A ; ×2 |
||||
ADD A, A ; ×4 |
||||
ADD A, A ; ×8 |
||||
ADD A, A ; ×16 |
||||
ADD A, A ; ×32 ← 5 istruzioni, ~5 cicli |
||||
``` |
||||
|
||||
I 13 byte "sprecati" per riga valgono ampiamente il risparmio in cicli CPU su ogni accesso. |
||||
|
||||
--- |
||||
|
||||
## L'Algoritmo: Recursive Backtracker |
||||
|
||||
L'algoritmo scava i percorsi del labirinto partendo da una cella iniziale ed espandendosi in modo pseudo-casuale, tornando sui propri passi quando si trova in un vicolo cieco. |
||||
|
||||
### Garanzie dell'algoritmo |
||||
|
||||
1. **Labirinto perfetto**: ogni cella è raggiungibile da ogni altra tramite un unico percorso (albero di copertura). |
||||
2. **Nessun ciclo**: non esistono percorsi circolari. |
||||
3. **Nessuna isola**: non esistono sezioni disconnesse. |
||||
|
||||
### Implementazione Iterativa |
||||
|
||||
```c |
||||
// src/maze.c — stack customizzato in WRAM |
||||
uint8_t stack_x[100]; |
||||
uint8_t stack_y[100]; |
||||
uint8_t stack_ptr = 0; |
||||
``` |
||||
|
||||
!!! warning "Perché non la ricorsione?" |
||||
La versione ricorsiva di questo algoritmo richiede uno stack frame per ogni cella visitata. |
||||
Per un labirinto 19×17 = 323 celle, lo stack potrebbe crescere di **300+ livelli**. |
||||
Lo stack del Game Boy è in HRAM (127 byte) o WRAM, ed è condiviso con GBDK. |
||||
Una ricorsione profonda causerebbe uno **stack overflow** e un crash immediato. |
||||
|
||||
### Codice Completo |
||||
|
||||
```c |
||||
// src/maze.c |
||||
void generate_maze(void) { |
||||
uint8_t x, y, nx, ny; |
||||
uint8_t dirs[4]; |
||||
uint8_t count, r; |
||||
|
||||
// Fase 1: Riempi tutto con muri |
||||
for (y = 0; y < MAZE_HEIGHT; y++) |
||||
for (x = 0; x < MAZE_WIDTH; x++) |
||||
maze[y][x] = 1; |
||||
|
||||
// Fase 2: Cella iniziale in (1,1) |
||||
stack_ptr = 0; |
||||
push(1, 1); |
||||
maze[1][1] = 0; |
||||
|
||||
// Fase 3: DFS iterativo |
||||
while (stack_ptr > 0) { |
||||
x = stack_x[stack_ptr - 1]; |
||||
y = stack_y[stack_ptr - 1]; |
||||
|
||||
// Cerca vicini non visitati a distanza 2 |
||||
count = 0; |
||||
if (x >= 2 && maze[y][x-2] == 1) dirs[count++] = 0; // Sinistra |
||||
if (x <= MAZE_WIDTH-3 && maze[y][x+2] == 1) dirs[count++] = 1; // Destra |
||||
if (y >= 2 && maze[y-2][x] == 1) dirs[count++] = 2; // Su |
||||
if (y <= MAZE_HEIGHT-3&& maze[y+2][x] == 1) dirs[count++] = 3; // Giù |
||||
|
||||
if (count > 0) { |
||||
r = rand() & 3; // ← & invece di %, vedi Ottimizzazioni |
||||
while (r >= count) r = rand() & 3; |
||||
|
||||
nx = x; ny = y; |
||||
if (dirs[r] == 0) { nx -= 2; maze[y][x-1] = 0; } // Sinistra |
||||
else if (dirs[r] == 1) { nx += 2; maze[y][x+1] = 0; } // Destra |
||||
else if (dirs[r] == 2) { ny -= 2; maze[y-1][x] = 0; } // Su |
||||
else if (dirs[r] == 3) { ny += 2; maze[y+1][x] = 0; } // Giù |
||||
|
||||
maze[ny][nx] = 0; |
||||
push(nx, ny); |
||||
} else { |
||||
pop(&x, &y); // Backtrack |
||||
} |
||||
} |
||||
} |
||||
``` |
||||
|
||||
--- |
||||
|
||||
## Visualizzazione dell'Algoritmo |
||||
|
||||
```mermaid |
||||
flowchart TD |
||||
A[Inizializza: tutto muri] --> B[Push cella 1,1\nSegna come visitata] |
||||
B --> C{Stack vuoto?} |
||||
C -- No --> D[Leggi cella in cima allo stack] |
||||
D --> E{Vicini non visitati?} |
||||
E -- Sì --> F[Scegli direzione casuale\nDemolisci muro intermedio\nSegna nuova cella\nPush nuova cella] |
||||
E -- No --> G[Pop: backtrack] |
||||
F --> C |
||||
G --> C |
||||
C -- Sì --> H[Labirinto completo!] |
||||
``` |
||||
|
||||
--- |
||||
|
||||
## Convenzione Celle |
||||
|
||||
``` |
||||
1 = Muro (blocco di siepe) |
||||
0 = Percorso (camminabile) |
||||
``` |
||||
|
||||
Il labirinto usa coordinate con **indici dispari** per le celle percorribili e **indici pari** per i muri: |
||||
|
||||
``` |
||||
(1,1) (1,3) (1,5) ... ← Celle percorribili |
||||
(1,2) ← Muro tra (1,1) e (1,3) — può essere abbattuto |
||||
(0,*) ← Sempre muro (bordo) |
||||
``` |
||||
|
||||
Questo garantisce che il bordo esterno (riga 0, colonna 0, ultima riga, ultima colonna) sia sempre un muro solido. |
||||
|
||||
--- |
||||
|
||||
## Entropía: `initrand(DIV_REG)` |
||||
|
||||
```c |
||||
initrand(DIV_REG); // Seme dal timer hardware |
||||
``` |
||||
|
||||
Il registro `DIV_REG` (0xFF04) si incrementa a ~16384 Hz, indipendentemente dalla CPU. Il tempo tra il boot e la pressione di START da parte del giocatore è imprevedibile, garantendo un seme diverso ad ogni partita e quindi un labirinto sempre unico. |
||||
@ -0,0 +1,58 @@
|
||||
site_name: "MICE! — Documentazione Tecnica" |
||||
site_description: "Architettura e implementazione di un gioco completo per Game Boy DMG in C con GBDK-2020" |
||||
site_author: "Matteo" |
||||
docs_dir: docs |
||||
|
||||
theme: |
||||
name: material |
||||
language: it |
||||
palette: |
||||
- scheme: slate |
||||
primary: black |
||||
accent: green |
||||
font: |
||||
text: Roboto Mono |
||||
code: Roboto Mono |
||||
features: |
||||
- navigation.tabs |
||||
- navigation.sections |
||||
- navigation.top |
||||
- search.suggest |
||||
- content.code.annotate |
||||
- content.code.copy |
||||
|
||||
markdown_extensions: |
||||
- admonition |
||||
- pymdownx.details |
||||
- pymdownx.superfences: |
||||
custom_fences: |
||||
- name: mermaid |
||||
class: mermaid |
||||
format: !!python/name:pymdownx.superfences.fence_code_format |
||||
- pymdownx.highlight: |
||||
anchor_linenums: true |
||||
- pymdownx.inlinehilite |
||||
- pymdownx.tabbed: |
||||
alternate_style: true |
||||
- tables |
||||
- toc: |
||||
permalink: true |
||||
|
||||
nav: |
||||
- Home: index.md |
||||
- Hardware Game Boy: |
||||
- Panoramica Hardware: hardware.md |
||||
- Architettura del Codice: |
||||
- Struttura dei Moduli: architecture.md |
||||
- Loop Principale: main_loop.md |
||||
- Generazione del Labirinto: maze.md |
||||
- Intelligenza Artificiale dei Topi: ai.md |
||||
- Rendering e Grafica: |
||||
- Tile System e Autotiling: rendering.md |
||||
- Sprite e OAM Management: sprites.md |
||||
- Armi e Gameplay: |
||||
- Cursore, Bomba e Fucile: weapons.md |
||||
- Audio: |
||||
- Mini-Tracker Musicale: audio.md |
||||
- Pipeline Asset: assets.md |
||||
- Ottimizzazioni: optimizations.md |
||||
Loading…
Reference in new issue