Aggiunto README e aggiornati file del progetto
This commit is contained in:
@@ -0,0 +1,78 @@
|
||||
# gioco carino (Cute Isometric Game Boy Maze)
|
||||
|
||||
Un motore isometrico sperimentale per Game Boy (DMG/CGB) scritto in C con **GBDK-2020**. Genera un labirinto casuale ed esegue il rendering in proiezione isometrica con autotiling dinamico, movimento interpolato del personaggio, e un set di test automatizzati headless tramite l'emulatore **PyBoy** ed elaborazione d'immagine con **OpenCV**.
|
||||
|
||||
---
|
||||
|
||||
## 🎮 Caratteristiche Principali
|
||||
|
||||
* **Proiezione Isometrica**: Rendering di una mappa 2.5D su schermo Game Boy (tile 32x16 pixel disegnate a diamante).
|
||||
* **Labirinto Casuale Dinamico**: Algoritmo di backtracking iterativo (con stack in WRAM per evitare l'overflow dello stack hardware) che genera ogni volta un percorso 7x7 unico.
|
||||
* **Movimento Interpolato (Lerp)**: Spostamenti fluidi del personaggio e della telecamera interpolati linearmente su 16 tick macchina.
|
||||
* **Delayed Auto Shift (DAS)**: Controlli reattivi e confortevoli con delay iniziale di 12 frame e ripetizione ogni 6 frame per il movimento continuo tenendo premuto il D-Pad.
|
||||
* **Autotiling Intelligente**: Calcolo dei vicini (maschera a 4 bit) per selezionare automaticamente il bordo e gli angoli di ciascuna tessera del pavimento (16 varianti per ognuno dei 2 stili di pavimento alternati a scacchiera).
|
||||
* **Pipeline di Asset Ottimizzata**: Generazione procedurale di tile e sprite da script Python (`generate_assets.py`) e compilazione in C tramite `png2asset`.
|
||||
* **Test Headless & Computer Vision**:
|
||||
* Simulazione dell'input e verifica dello stato direttamente leggendo i registri WRAM del Game Boy in esecuzione.
|
||||
* Rilevamento di glitch grafici (pixel neri o spazi vuoti non allineati) sulla ROM renderizzata usando filtri morfologici di **OpenCV**.
|
||||
|
||||
---
|
||||
|
||||
## 🛠️ Dettagli Tecnici
|
||||
|
||||
### Architettura dei File
|
||||
* [main.c](file:///home/enne2/dev/gameboy-hello/iso_test/main.c): Punto di ingresso del gioco. Esegue l'inizializzazione del ciclo macchina e del joypad e si sincronizza con l'intervallo di VBlank (`wait_vbl_done()`).
|
||||
* [engine.c](file:///home/enne2/dev/gameboy-hello/iso_test/engine.c) / [engine.h](file:///home/enne2/dev/gameboy-hello/iso_test/engine.h): Core del motore isometrico. Contiene la logica del labirinto, l'autotiling, lo scorrimento della telecamera, l'interpolazione del movimento e il supporto DAS.
|
||||
* [player.c](file:///home/enne2/dev/gameboy-hello/iso_test/player.c) / [player.h](file:///home/enne2/dev/gameboy-hello/iso_test/player.h) & [tiles.c](file:///home/enne2/dev/gameboy-hello/iso_test/tiles.c) / [tiles.h](file:///home/enne2/dev/gameboy-hello/iso_test/tiles.h): Asset grafici compilati (metasprite per il giocatore in 4 direzioni e varianti di piastrelle).
|
||||
* [generate_assets.py](file:///home/enne2/dev/gameboy-hello/iso_test/generate_assets.py): Script Python PIL per generare le texture di tiles e sprite a partire da matrici di pixel.
|
||||
|
||||
### Formato delle Coordinate Isometriche
|
||||
Le coordinate logiche del labirinto $2D$ `(lx, ly)` vengono convertite in coordinate dello schermo Game Boy `(iso_x, iso_y)` per i background tiles tramite la seguente formula:
|
||||
$$iso\_x = (lx - ly) \times 2 + 12$$
|
||||
$$iso\_y = (lx + ly) \times 1 + 2$$
|
||||
Questo permette di mappare una griglia ruotata a diamante perfettamente centrata nello spazio di visualizzazione.
|
||||
|
||||
---
|
||||
|
||||
## 🚀 Requisiti e Build
|
||||
|
||||
### Prerequisiti
|
||||
1. **GBDK-2020**: Installato in `/home/enne2/.local/gbdk`.
|
||||
2. **Python 3**: Con i seguenti pacchetti installati per i test e la rigenerazione degli asset:
|
||||
```bash
|
||||
pip install --user Pillow pyboy opencv-python numpy
|
||||
```
|
||||
|
||||
### Compilazione
|
||||
Per compilare la ROM ed esportare `hello_iso.gb`:
|
||||
```bash
|
||||
make clean && make
|
||||
```
|
||||
Questo comando:
|
||||
1. Esegue `generate_assets.py` per creare `tiles.png` e `player.png`.
|
||||
2. Usa `png2asset` per convertire le PNG in codice sorgente C.
|
||||
3. Usa il compilatore `lcc` di GBDK per compilare e linkare tutti i file sorgente C nella ROM finale.
|
||||
|
||||
---
|
||||
|
||||
## 🧪 Test e Analisi Automatica
|
||||
|
||||
Il progetto include tre livelli di verifica headless per testare la correttezza logica e visuale senza avviare manualmente un emulatore grafico.
|
||||
|
||||
1. **Generazione Screenshot**:
|
||||
```bash
|
||||
python3 test_pyboy.py
|
||||
```
|
||||
Avvia la ROM in PyBoy per 120 frame e salva un'immagine `hello_iso_gb.png` del display.
|
||||
|
||||
2. **Test di Movimento in WRAM**:
|
||||
```bash
|
||||
python3 test_movement.py
|
||||
```
|
||||
Carica la ROM in PyBoy, legge lo stato della griglia del labirinto in WRAM (a partire dall'indirizzo `0xC0B1`) e simula la pressione dei tasti direzionali, verificando che la posizione del player (indirizzi `0xC4F4` e `0xC4F5`) cambi correttamente secondo le collisioni calcolate.
|
||||
|
||||
3. **Rilevamento Glitch con OpenCV**:
|
||||
```bash
|
||||
python3 opencv_analyze_tiles.py
|
||||
```
|
||||
Utilizza OpenCV per esaminare lo screenshot generato, cercando disallineamenti o buchi neri tra le giunzioni delle piastrelle isometriche, segnalando eventuali problemi di rendering.
|
||||
@@ -16,10 +16,17 @@ static uint8_t map_buffer[32 * 32];
|
||||
uint8_t player_lx = 1;
|
||||
uint8_t player_ly = 1;
|
||||
uint8_t player_dir = 0; // 0=DR, 1=DL, 2=UL, 3=UR
|
||||
uint8_t walk_timer = 0;
|
||||
uint8_t scroll_x = 0;
|
||||
uint8_t scroll_y = 0;
|
||||
|
||||
// Movement transition variables
|
||||
uint8_t is_moving = 0;
|
||||
uint8_t move_progress = 0;
|
||||
int8_t start_lx, start_ly;
|
||||
int8_t target_lx, target_ly;
|
||||
int16_t start_px, start_py;
|
||||
int16_t target_px, target_py;
|
||||
|
||||
// DAS variables
|
||||
uint8_t das_timer = 0;
|
||||
uint8_t das_active = 0;
|
||||
@@ -143,7 +150,7 @@ static void update_camera(void) {
|
||||
}
|
||||
|
||||
static void update_player_sprite(void) {
|
||||
uint8_t frame_offset = (walk_timer > 0) ? ((walk_timer >> 2) & 1) : 0;
|
||||
uint8_t frame_offset = is_moving ? ((move_progress >> 2) & 1) : 0;
|
||||
move_metasprite(player_metasprites[player_dir * 2 + frame_offset], 0, 0, 88, 88);
|
||||
}
|
||||
|
||||
@@ -174,16 +181,40 @@ void engine_init(void) {
|
||||
}
|
||||
|
||||
void engine_update(uint8_t keys, uint8_t prev_keys) {
|
||||
if (walk_timer > 0) {
|
||||
walk_timer--;
|
||||
if (is_moving) {
|
||||
move_progress++;
|
||||
|
||||
// Interpolate camera position
|
||||
int16_t px = start_px + (((target_px - start_px) * (int16_t)move_progress) >> 4);
|
||||
int16_t py = start_py + (((target_py - start_py) * (int16_t)move_progress) >> 4);
|
||||
|
||||
scroll_x = px - 64;
|
||||
scroll_y = py - 72;
|
||||
move_bkg(scroll_x, scroll_y);
|
||||
|
||||
// Update walk animation frame (alternate frame every 4 ticks)
|
||||
update_player_sprite();
|
||||
|
||||
if (move_progress == 16) {
|
||||
is_moving = 0;
|
||||
player_lx = target_lx;
|
||||
player_ly = target_ly;
|
||||
|
||||
// Final snap to target position to avoid any rounding errors
|
||||
int16_t final_px = (player_lx - player_ly) * 16 + 96;
|
||||
int16_t final_py = (player_lx + player_ly) * 8 + 16;
|
||||
scroll_x = final_px - 64;
|
||||
scroll_y = final_py - 72;
|
||||
move_bkg(scroll_x, scroll_y);
|
||||
|
||||
// Set player to idle stand frame
|
||||
update_player_sprite();
|
||||
}
|
||||
return; // Ignore other inputs while moving
|
||||
}
|
||||
|
||||
uint8_t keys_pressed = keys & ~prev_keys;
|
||||
|
||||
int8_t move_lx = 0;
|
||||
int8_t move_ly = 0;
|
||||
|
||||
if (keys_pressed) {
|
||||
das_timer = DAS_DELAY;
|
||||
das_active = 1;
|
||||
@@ -197,6 +228,9 @@ void engine_update(uint8_t keys, uint8_t prev_keys) {
|
||||
das_active = 0;
|
||||
}
|
||||
|
||||
int8_t move_lx = 0;
|
||||
int8_t move_ly = 0;
|
||||
|
||||
if (keys_pressed & J_RIGHT) {
|
||||
move_lx = 1; player_dir = 0; // DR
|
||||
} else if (keys_pressed & J_LEFT) {
|
||||
@@ -214,12 +248,26 @@ void engine_update(uint8_t keys, uint8_t prev_keys) {
|
||||
// Bounds check & wall/empty area collision check
|
||||
if (new_lx >= 0 && new_lx < MAP_SIZE && new_ly >= 0 && new_ly < MAP_SIZE) {
|
||||
if (maze[new_ly][new_lx] == 1) {
|
||||
player_lx = new_lx;
|
||||
player_ly = new_ly;
|
||||
walk_timer = 12; // 12 frames of walk cycle
|
||||
}
|
||||
}
|
||||
is_moving = 1;
|
||||
move_progress = 0;
|
||||
start_lx = player_lx;
|
||||
start_ly = player_ly;
|
||||
target_lx = new_lx;
|
||||
target_ly = new_ly;
|
||||
|
||||
start_px = (start_lx - start_ly) * 16 + 96;
|
||||
start_py = (start_lx + start_ly) * 8 + 16;
|
||||
target_px = (target_lx - target_ly) * 16 + 96;
|
||||
target_py = (target_lx + target_ly) * 8 + 16;
|
||||
|
||||
update_player_sprite();
|
||||
update_camera();
|
||||
} else {
|
||||
// If collision blocks, just update direction look frame
|
||||
update_player_sprite();
|
||||
}
|
||||
} else {
|
||||
// Out of bounds: update direction look frame
|
||||
update_player_sprite();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 1.5 KiB |
+5
-5
@@ -8,9 +8,9 @@ def main():
|
||||
pyboy.tick()
|
||||
|
||||
# Read player position from RAM
|
||||
# player_lx at 0xC4E8, player_ly at 0xC4E9
|
||||
lx = pyboy.memory[0xC4E8]
|
||||
ly = pyboy.memory[0xC4E9]
|
||||
# player_lx at 0xC4F4, player_ly at 0xC4F5
|
||||
lx = pyboy.memory[0xC4F4]
|
||||
ly = pyboy.memory[0xC4F5]
|
||||
print(f"Initial player coordinates in RAM: lx={lx}, ly={ly}")
|
||||
|
||||
# Read maze array from RAM (7x7 starting at 0xC0B1)
|
||||
@@ -42,7 +42,7 @@ def main():
|
||||
pyboy.send_input(WindowEvent.RELEASE_ARROW_DOWN)
|
||||
for _ in range(30):
|
||||
pyboy.tick()
|
||||
print(f"Player coordinates after DOWN: lx={pyboy.memory[0xC4E8]}, ly={pyboy.memory[0xC4E9]}")
|
||||
print(f"Player coordinates after DOWN: lx={pyboy.memory[0xC4F4]}, ly={pyboy.memory[0xC4F5]}")
|
||||
|
||||
# Try pressing Right (Down-Right)
|
||||
print("\nSimulating pressing RIGHT...")
|
||||
@@ -51,7 +51,7 @@ def main():
|
||||
pyboy.send_input(WindowEvent.RELEASE_ARROW_RIGHT)
|
||||
for _ in range(30):
|
||||
pyboy.tick()
|
||||
print(f"Player coordinates after RIGHT: lx={pyboy.memory[0xC4E8]}, ly={pyboy.memory[0xC4E9]}")
|
||||
print(f"Player coordinates after RIGHT: lx={pyboy.memory[0xC4F4]}, ly={pyboy.memory[0xC4F5]}")
|
||||
|
||||
pyboy.stop()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user