Refactoring: Project structure, sprite stairs & fog masks rendering fix
This commit is contained in:
@@ -0,0 +1,29 @@
|
||||
# Intelligenza Artificiale (Il Fantasma)
|
||||
|
||||
Il gioco utilizza un inseguitore, soprannominato "Il Fantasma", che bracca il giocatore all'interno del labirinto. Poiché il giocatore si muove velocemente, l'AI del fantasma richiede limiti artificiali per risultare un nemico superabile e bilanciato.
|
||||
|
||||
## Pathfinding: Perché "Greedy" e non A*?
|
||||
|
||||
Sui sistemi moderni o su griglie piccole, l'A-Star (A*) è l'algoritmo standard per il pathfinding dei nemici. Calcola il percorso perfetto evitando vicoli ciechi.
|
||||
Sul Game Boy (CPU 4 MHz), gestire la memoria (heap, nodi) richiesta da A* per un array 7x7 genererebbe ritardi nei frame (frame drops) inammissibili per un gioco real-time.
|
||||
|
||||
La soluzione implementata è un approccio **Greedy (Goloso)**:
|
||||
Ad ogni passo, il fantasma verifica le 4 celle a lui adiacenti (Nord, Sud, Est, Ovest). Tra quelle calpestabili (non muri), sceglie quella la cui coordinata rende **minima la distanza spaziale al quadrato verso il giocatore** (`dx^2 + dy^2`).
|
||||
Non usiamo la radice quadrata (necessaria per calcolare la distanza esatta euclidea) perché troppo costosa per la CPU; dal momento che stiamo solo comparando quale distanza sia minore, i quadrati bastano e avanzano matematicamente (`A < B <=> A^2 < B^2`).
|
||||
|
||||
Un algoritmo Greedy tende a incastrarsi in ostacoli a forma di U, ma essendo questo un labirinto dinamico pieno di loop a corridoio singolo, questo "difetto" si trasforma in una dinamica di gioco: i giocatori astuti possono sfruttare il level design per confondere l'AI e aggirarla.
|
||||
|
||||
## Attivazione e Cooldown
|
||||
|
||||
Il fantasma non parte immediatamente alla carica. Usa la distanza di Chebyshev per "vedere":
|
||||
```c
|
||||
if (dist <= 2) { // Inizia a muoversi }
|
||||
```
|
||||
Ovvero, inizia l'inseguimento solo quando entra nel riquadro visivo del giocatore (5x5, distanza <= 2).
|
||||
|
||||
Inoltre, il fantasma impiega 16 frame per muoversi da un blocco all'altro (come il player), ma è limitato dalla variabile `enemy_cooldown = 60`. Dopo ogni singolo passo, rimane congelato per 1 secondo intero prima di poterne fare un altro. Questo garantisce che il fantasma sia più lento del giocatore e ti costringe, qualora commettessi un errore, a usare rapidamente la meccanica di Salto Evasivo e seminarlo.
|
||||
|
||||
## Hitbox Pixel-Perfect
|
||||
|
||||
Nonostante la mappa sia calcolata su una pura griglia logica bidimensionale (1 passo = 1 casella X,Y), la condizione di "Sconfitta" non si basa sul fatto che il giocatore e il fantasma occupino la medesima casella logica contemporaneamente. Si controlla invece che le coordinate fisiche (i veri e propri **pixel** renderizzati sullo schermo del Game Boy) si sovrappongano con un margine di errore basso (12 pixel in orizzontale e 6 in verticale).
|
||||
Questo rende la morte "giusta" agli occhi dell'utente e permette scambi per il rotto della cuffia!
|
||||
@@ -0,0 +1,25 @@
|
||||
# Architettura di Base
|
||||
|
||||
## Organizzazione del Progetto
|
||||
|
||||
Il progetto è strutturato in modo da separare logicamente gli asset visivi dal codice sorgente:
|
||||
- `assets/`: Contiene le immagini PNG (sprite, tileset) create dagli script.
|
||||
- `scripts/`: Script Python per la generazione procedurale di grafica (es. metasprite del fantasma o del game over).
|
||||
- `src/`: Tutto il codice sorgente C e gli header generati.
|
||||
- `build/`: Risultato della compilazione (i file `.o` e il ROM `.gb` finale).
|
||||
|
||||
## Modularizzazione del Codice C
|
||||
|
||||
Inizialmente, l'intero gioco risiedeva in un unico file `engine.c` di oltre 1000 righe. Per facilitare la manutenzione, è stato rifattorizzato in moduli a singola responsabilità:
|
||||
|
||||
- `maze.c`: Gestisce puramente i dati della mappa e la loro generazione.
|
||||
- `sound.c`: Isola tutte le interazioni con l'hardware audio (APU).
|
||||
- `render.c`: Isola l'hardware visivo (PPU), la memoria video (VRAM) e lo scrolling (SCX/SCY).
|
||||
- `player_logic.c` ed `enemy_logic.c`: Contengono le regole fisiche e l'AI.
|
||||
- `engine.c`: Agisce da "Direttore d'Orchestra", richiamando i vari `update` in sequenza.
|
||||
|
||||
## Gestione dello Stato Globale (`globals.h`)
|
||||
|
||||
Un problema ricorrente nei giochi C in più moduli sono le "dipendenze circolari" (es. il render ha bisogno delle coordinate del player, ma il player ha bisogno della mappa per le collisioni).
|
||||
Per risolvere questo, lo stato mutabile del gioco è centralizzato in `globals.c` ed esposto tramite `globals.h`.
|
||||
Tutti i moduli includono `globals.h` e leggono/scrivono sulle variabili `extern` (come `stamina`, `game_over`, `player_lx`), evitando grovigli di include e rendendo il passaggio dei dati estremamente leggero e globale, un approccio molto comune ed efficiente nello sviluppo retro-console.
|
||||
@@ -0,0 +1,34 @@
|
||||
# Sintesi Audio e Musica
|
||||
|
||||
Tutto l'audio del gioco è prodotto internamente manipolando a basso livello i 4 canali audio (APU) registrati direttamente in memoria hardware del Game Boy (da `NR10_REG` a `NR52_REG`).
|
||||
|
||||
L'architettura separa completamente la logica audio da `engine.c` nel file isolato `sound.c`.
|
||||
|
||||
## Hardware Audio Channels (APU)
|
||||
Il Game Boy dispone di 4 canali dedicati:
|
||||
1. **Canale 1 (Pulse 1)**: Onda quadra con sweep (glissando automatico del pitch).
|
||||
2. **Canale 2 (Pulse 2)**: Onda quadra senza sweep. Ideale per la base musicale.
|
||||
3. **Canale 3 (Wave)**: Onda personalizzabile definita in RAM (qui non utilizzata).
|
||||
4. **Canale 4 (Noise)**: Generatore di rumore pseudo-casuale bianco, usato per batterie ed effetti.
|
||||
|
||||
## Il Sequencer via Interrupt VBL
|
||||
L'intera colonna sonora viene eseguita senza bloccare mai l'esecuzione del codice di rendering o della CPU.
|
||||
La funzione `play_music_tick` viene agganciata al **Vertical Blanking (VBL) Interrupt** (`add_VBL()`), che scatta magicamente tra il disegno di un frame video e il successivo sul display a circa 60 Hz.
|
||||
Ogni chiamata incrementa un timer logico; quando il timer supera la durata della nota impostata, il sequencer aggiorna l'indice dell'array passandolo ai registri e suonando fisicamente il suono.
|
||||
|
||||
## Illusioni Audio (Arpeggi e Polifonia Fittizia)
|
||||
Il chip GBDK può suonare solo un singolo tono per canale alla volta.
|
||||
Per la "Fanfara di Vittoria" e i momenti di forte tensione, per imitare accordi (che richiederebbero 3 canali), usiamo gli **Arpeggi Veloci**.
|
||||
Una melodia arpeggiata è un elenco di tre note (Es: Do-Mi-Sol) suonate in un loop talmente veloce che l'orecchio umano le fonde percependo un accordo polifonico. Nel codice, le note `N_C4`, `N_E4`, `N_G4` si susseguono ogni pochi frame (timer = 8 frame/tick).
|
||||
|
||||
## Il Rumore come Percussione
|
||||
Durante la triste sequenza in tonalità minore del "Game Over", il Canale 4 del Rumore viene attivato sincopatamente:
|
||||
```c
|
||||
if (step % 8 == 0) {
|
||||
NR41_REG = 0x01;
|
||||
NR42_REG = 0xB2; // Decadimento rapidissimo per dare un colpo percussivo
|
||||
NR43_REG = (step < 64) ? 0x68 : 0x42; // Varia la frequenza: Thud basso vs Piatto (Crash) acuto
|
||||
NR44_REG = 0x80;
|
||||
}
|
||||
```
|
||||
Questo simula il battito di un tamburo e i piatti, donando drammaticità alla scena.
|
||||
@@ -0,0 +1,29 @@
|
||||
# Fisica e Controlli del Giocatore
|
||||
|
||||
Il movimento nel gioco è rigidamente vincolato alla griglia (Grid-Based), come nei vecchi RPG (Zelda, Pokémon), per semplificare le collisioni e la leggibilità degli ostacoli.
|
||||
|
||||
## State Machine (Blocco Input)
|
||||
|
||||
In `player_logic.c`, l'interpolazione fluida del movimento (il passaggio da una casella all'altra) richiede 16 frame.
|
||||
Durante questo lasso di tempo, la variabile `is_moving` è `true`. Qualsiasi input direzionale proveniente dai tasti (D-Pad) o dal tasto A viene totalmente ignorato finché il movimento sub-tile non si azzera (`move_progress == 16`). Questo impedisce movimenti diagonali non voluti o "scivolamenti" fuori griglia.
|
||||
|
||||
## Delayed Auto-Shift (DAS)
|
||||
|
||||
Leggere banalmente il D-Pad a 60 FPS renderebbe il personaggio incontrollabile (si muoverebbe di 4 caselle con una pressione leggermente prolungata). Leggere solo la "singola pressione" (keys_pressed) obbligherebbe a martellare il tasto per avanzare.
|
||||
|
||||
Abbiamo implementato un DAS, una tecnica tipica del Tetris:
|
||||
- Al primo tocco, il giocatore si muove.
|
||||
- Se si continua a tenere premuto, l'input viene ignorato per `DAS_DELAY` frames.
|
||||
- Superato il delay, l'input si ripete in automatico ogni `DAS_REPEAT` frames.
|
||||
Questo rende la navigazione dei corridoi estremamente fluida e confortevole.
|
||||
|
||||
## Il Salto e il Costo in Stamina
|
||||
|
||||
Tenendo premuto il tasto `A` assieme a una direzione direzionale, il giocatore scavalca il blocco adiacente atterrando due tile più in là.
|
||||
Condizioni per il salto:
|
||||
1. La casella di destinazione (`X+2`, `Y+2`) deve essere libera.
|
||||
2. La casella intermedia saltata (`X+1`, `Y+1`) DEVE essere un muro (non si può saltare a vuoto sui corridoi).
|
||||
3. Il giocatore deve avere almeno 60 punti Stamina.
|
||||
|
||||
La stamina si ricarica di 1 punto ogni secondo. Eseguire un salto costa 60 punti, disabilitando ulteriori balzi per un minuto intero. Questo lo rende una manovra evasiva salva-vita estrema e non uno strumento da spam abusivo.
|
||||
Visivamente, l'altezza del salto non altera la logica (sempre 2D Grid), ma solo il rendering in `update_player_sprite()`, in cui sottraiamo al posizionamento verticale la formula parabolica `x * (16 - x) / 4`.
|
||||
@@ -0,0 +1,23 @@
|
||||
# Generazione Procedurale del Livello
|
||||
|
||||
## Algoritmo Depth-First Search (DFS)
|
||||
|
||||
Per garantire che ogni sessione di gioco sia unica, il labirinto viene generato proceduralmente ad ogni avvio (usando `DIV_REG` hardware del Game Boy come seed pseudo-casuale).
|
||||
|
||||
L'algoritmo utilizzato in `maze.c` è un **Depth-First Search (DFS)** con Backtracking, modellato per creare un "Perfect Maze" (labirinto perfetto in cui ogni cella è raggiungibile tramite un unico percorso possibile e senza cicli).
|
||||
La mappa è concettualmente divisa in:
|
||||
- Celle dispari (es. `1,1`, `1,3`): "Stanze"
|
||||
- Celle pari: "Muri Divisori"
|
||||
|
||||
L'algoritmo parte da `1,1`, sceglie un vicino dispari casuale non visitato, abbatte il muro divisorio pari nel mezzo e avanza, salvando le posizioni in uno stack per poter tornare indietro (backtracking) quando finisce in un vicolo cieco.
|
||||
|
||||
## Rottura del Perfect Maze (Loop Generation)
|
||||
|
||||
Un "Perfect Maze" è frustrante in un gioco di inseguimento, perché intrappola il giocatore nei vicoli ciechi senza via di scampo.
|
||||
Pertanto, è stata aggiunta una *Fase 2* alla generazione: l'algoritmo scansiona i muri rimanenti e, se un muro divide due stanze adiacenti, lo abbatte con una probabilità del 15%.
|
||||
Questo genera anelli (loop) all'interno del labirinto, permettendo al giocatore tattiche evasive per aggirare il fantasma.
|
||||
|
||||
## Posizionamento del Traguardo (Vittoria)
|
||||
|
||||
La casella traguardo (il "Portale") deve essere logicamente il punto più lontano dalla partenza (situata in alto a sinistra a `1,1`).
|
||||
Per trovarla, l'algoritmo calcola la **Distanza di Manhattan** (`|X1 - X2| + |Y1 - Y2|`) di ogni cella percorribile rispetto a `1,1`. La cella con il valore più alto viene designata con ID 2, trasformandola graficamente nel portale di fine livello.
|
||||
@@ -0,0 +1,41 @@
|
||||
# Motore Grafico e Rendering
|
||||
|
||||
## Proiezione Isometrica
|
||||
|
||||
Il Game Boy possiede un hardware progettato per griglie 2D piatte (scrolling orizzontale/verticale). Per ottenere un effetto isometrico 3D, trasformiamo matematicamente le coordinate logiche della griglia `(X, Y)` in pixel schermo `(iso_x, iso_y)`.
|
||||
|
||||
La formula utilizzata in `render.c` è:
|
||||
```c
|
||||
int8_t iso_x = (lx - ly) * 2 + 12;
|
||||
int8_t iso_y = (lx + ly) * 1 + 2;
|
||||
```
|
||||
Questa trasformazione ruota la mappa di 45 gradi e la appiattisce sull'asse Y (rapporto 2:1, tipico dell'isometrico pixel-art).
|
||||
|
||||
## Ottimizzazione della Mappa
|
||||
|
||||
Aggiornare l'intera background map hardware del Game Boy (32x32 tiles, 1024 bytes) ad ogni "passo" del giocatore causerebbe gravissimi cali di framerate (lag) e sfarfallii visivi.
|
||||
La soluzione implementata prevede di aggiornare *solo* le 16 righe visibili sullo schermo (160x144 pixel = 20x18 tiles) durante la funzione `draw_map`:
|
||||
```c
|
||||
set_bkg_tiles(0, 2, 32, 16, &map_buffer[2 * 32]);
|
||||
```
|
||||
|
||||
## Fog of War e Distanza di Chebyshev
|
||||
|
||||
Per aumentare la tensione e oscurare il labirinto, il gioco disegna solo i pavimenti vicini al giocatore. Invece di calcolare un cerchio reale (distanza euclidea) che richiederebbe una CPU avida di risorse matematiche (radici quadrate o lookup tables), usiamo la **Distanza di Chebyshev**:
|
||||
```c
|
||||
int8_t dist = max(abs(dx), abs(dy));
|
||||
if (dist > 2) continue; // Nascondi
|
||||
```
|
||||
Questo crea un quadrato di visibilità (5x5 celle) centrato sul giocatore, perfetto per le dinamiche a griglia.
|
||||
|
||||
## Auto-Tiling e Illuminazione
|
||||
|
||||
Il modulo di rendering controlla i vicini logici (Nord, Sud, Est, Ovest) di ogni casella per applicare la grafica giusta (es. muri arrotondati e non tagliati). Questo avviene tramite una maschera di bit (`mask`).
|
||||
Inoltre, le celle a distanza Chebyshev pari a 2 (i bordi della visibilità) subiscono un cambio di offset grafico (`v = 32 + mask` o `v = 48 + mask`), prelevando dal tileset varianti più scure ("Tile Dark"). Questo simula l'affievolirsi della luce prima dell'oscurità totale.
|
||||
|
||||
## Gestione Overlapping Isometrico (Multi-Pass Rendering)
|
||||
|
||||
Nei giochi isometrici basati su tile, la proiezione genera sovrapposizioni (overlapping) tra i macro-blocchi. Dato che il background del Game Boy non supporta vere trasparenze hardware (un tile sovrascrive interamente quello sottostante), i tile disegnati per ultimi "tagliano" quelli precedenti con i loro angoli piatti.
|
||||
Per preservare la complessa grafica 3D del traguardo (le scale) ed evitare l'esaurimento della ristrettissima VRAM (limite di 256 tile) tipico degli scenari pre-renderizzati combinatori, l'engine utilizza una strategia ibrida:
|
||||
1. **Bitmasking:** I pavimenti piatti ricolorano dinamicamente i propri angoli trasparenti in base al colore dei vicini, creando l'illusione di un ritaglio perfetto.
|
||||
2. **Painter's Algorithm (Pass Multiplo):** I pavimenti vengono renderizzati nel Pass 1, mentre gli oggetti complessi (come la casella di Vittoria) vengono disegnati rigorosamente in un Pass 2 successivo. Disegnandola per ultima, la scala sovrascrive i pavimenti frontali ma, grazie al Bitmasking dei propri angoli inferiori, si fonde con essi in modo invisibile. Ciò preserva interamente il disegno 3D senza richiedere sprite hardware addizionali (i quali causerebbero "flickering" per il rigido limite hardware di 10 sprite per scanline orizzontale).
|
||||
@@ -0,0 +1,25 @@
|
||||
# Game Boy Iso-Engine: Documentazione Tecnica
|
||||
|
||||
Benvenuto nella documentazione tecnica del progetto. Questo motore per Game Boy a 8-bit è stato progettato per dimostrare come tecniche avanzate (prospettiva isometrica, fog of war, generazione procedurale e intelligenza artificiale) possano essere implementate sulle ristrette risorse hardware del Game Boy (CPU Sharp LR35902 a 4.19 MHz, 8KB WRAM).
|
||||
|
||||
## Indice dei Capitoli
|
||||
|
||||
La documentazione è suddivisa in file separati, ognuno dedicato a un aspetto specifico dell'engine.
|
||||
|
||||
1. **[Architettura di Base](architecture.md)**
|
||||
Struttura del codice, organizzazione in moduli (src, assets, scripts) e gestione dello stato globale per evitare dipendenze circolari in C.
|
||||
|
||||
2. **[Motore Grafico e Rendering](graphics.md)**
|
||||
La matematica dietro la proiezione isometrica, l'ottimizzazione dell'auto-tiling, il sistema "Fog of War" basato sulla distanza di Chebyshev e la gestione dei metasprite.
|
||||
|
||||
3. **[Generazione Procedurale del Livello](generation.md)**
|
||||
Come l'algoritmo Depth-First Search (DFS) crea un labirinto perfetto, come vengono generati i loop per il gameplay e come la distanza di Manhattan posiziona il traguardo.
|
||||
|
||||
4. **[Fisica e Controlli del Giocatore](gameplay.md)**
|
||||
Il sistema di input con Delayed Auto-Shift (DAS), la State Machine rigida per l'allineamento alla griglia, e la complessa meccanica del salto con traiettoria parabolica e costo in Stamina.
|
||||
|
||||
5. **[Intelligenza Artificiale (Il Fantasma)](ai.md)**
|
||||
Perché il nemico usa un algoritmo Greedy invece di A*, il sistema di cooldown dei movimenti per bilanciare il gameplay, e la hitbox "Pixel-Perfect" che scatena il Game Over.
|
||||
|
||||
6. **[Sintesi Audio e Musica](audio.md)**
|
||||
Come l'engine manipola direttamente i registri hardware APU (Audio Processing Unit) per creare arpeggi finti, percussioni bianche (noise channel) e melodie atmosferiche.
|
||||
Reference in New Issue
Block a user