From 991afd17498e1d62c643c28bfd86f7185e192a80 Mon Sep 17 00:00:00 2001 From: enne2 Date: Mon, 22 Jun 2026 18:50:25 +0200 Subject: [PATCH] Aggiunto README e aggiornati file del progetto --- README.md | 78 +++++++++++++++++++++++++++++++++++++++++++++++ engine.c | 72 +++++++++++++++++++++++++++++++++++-------- hello_iso_gb.png | Bin 1439 -> 1490 bytes test_movement.py | 10 +++--- 4 files changed, 143 insertions(+), 17 deletions(-) create mode 100644 README.md diff --git a/README.md b/README.md new file mode 100644 index 0000000..685591d --- /dev/null +++ b/README.md @@ -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. diff --git a/engine.c b/engine.c index 6c3786d..743294e 100644 --- a/engine.c +++ b/engine.c @@ -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(); + } else { + // If collision blocks, just update direction look frame + update_player_sprite(); } + } else { + // Out of bounds: update direction look frame + update_player_sprite(); } - update_player_sprite(); - update_camera(); } } diff --git a/hello_iso_gb.png b/hello_iso_gb.png index ee3f2ce8c54d58a741e8dea868f98f6ebdf66abb..a652d87f374c9e3bc04f79bb4afa524a0c7cc8cb 100644 GIT binary patch delta 1278 zcmV3(^aaBte-;L_t(|ob8<5b*nlMM#p!Z(%b?mA*m@T!EPbF`8?z*OV&Xk z1o#_Cd%xtziD6)8K;nxoX7(@u5&x-o0LBK9z-^JRT7Tt_dIwtN^YioR>yM8Q*}k+4 z9k7B_zTEd<9R%D#=z4U&dM;-6Q13*Ga0xyJ1OzDzLMoZ2>FMkH{Vv;=mf;?-0tO++ zfq)=|d%z8Z%kKStpS=XfuR|H!0~Qd(x?ZopAJb9q+$9(h*P#P0kct-w@}(UwmGJxf z`}cEXoPUQ7I73dnSST~I+wEp%W|zz5>Gil%rXb=obijx;@ou-S%seN}H83AqRS=j6v-Fuj!S z?Sz5&k{)koL=lJYfGN5AK+6n5wsgQ$yp$0|M1Q~x@kH~SjP}1!x!2O4vke_|BoPs? zfTgau#FBXN<$bLqTE{XX;08)X%!xNQ8n%V`#kt?@O+*B&pk%%Fyv5wU3jJ)}>p3^L@p zpno0E3DfuWQ2$<<>VRv501rh_&5DzCJo=04oW+|B) ztR2L(AfjslOP@-tbp&%F-<^{ z?|HnOh->2YQh$sK0Y}Z_P19tz+ih075r678wScwHSdYTPNf>Pyx5jZ{;Y-!!6w-^`6J;DUW!FfOF!#Di1eu9VTzrye%Sz4!HCL zV9Q_H=y|->>Ms(oHjlR@>vr_WYxF!`EA=l5cw`=LOIJMl-fHwbp6B{!1w1p4w{#HB zzNsHQkLR)eCjnoX$5YO&jh@HzRDb`)%pS@-o(WIJ=y^O3^R+$_&l!2NwN~!sz@L>zr2g{y{~3gA&7-9oE#0t!GxCU`15kfm{n=@l(mdMI z&$M#jQ)-eo)L&QssT0wj5B!;VMCw0iB08mor7=vHN2LCf9>t~P{*%l5fqy?Mk4XLX z^&b+)@_681m4`Fx-&Mbhe@w5%Lmp8{5A}DkKAsGz6%n-VZL2s+|K9F<2rlOF4D z;u73q9`Q(euHUtQz2p&V8KHic0`{6mEM=tnT?sf!9x=y=^}7&o)I4Hf*IOK1EE&B^+BE$8vn$map~;$eNfmh*UO zn(u%q9@fWGu8-In-3b`*us&W3c|0}U3K-*IeLSyuJT*fI81S$@o-emNo|>Tq40u={ ouh&uSR)!QX;9-3{8&tf&UnbT1V^PRkKmY&$07*qoM6N<$f>y(m%m4rY delta 1225 zcmV;)1UCE93!e*+Btd3LL_t(|ob8-ZcB48F1>5h;(!2$-gk(+067UwXH_u0&a%7DJ zNMPRQ}8h$AV}dRq>*Ww9=^ZbZnAyp8Lj~v;3nia z5D=tr4Ol@q?cQ#;>?t_@9JawV;0A(N*X#B7YkJq4I|U=+I&{DVQt<*ozO>_|5`KSw z|Na{p^MB9*GnB-Og)%dn=h@86E|<&0=W(Y@LBvPsfDvor&GY;#o0-}D{xfvx&)(kN z9)3Umd*N>(;tvQ|5HId_q0_Yz3mLY7h(8Q3U@Bj&cwb*%&D%yot^xl!_;D9ZPo;Z1 zVIV%G$J-fE#Nj(&O6fY#3O6A&9WWIyWkeAXuzx^2(VUaf{uiqBS^EF1qJxekA_8t; zsVgqABwl=aU+ai=Vi^&zf~_K!#LL|c+rs=}?sv~7A_6wBWxg;b$g{PBE(v)y5fN|) z(=^%jdd-Sg>UJ&UriRWUV%GwCNS}m#NX4*?;=RAWH*fn45_UNcSULqN4n){ykfGEC z?SFtynEqW4_3x#t4!Cv`;Ni(a+7V-NLe_M^wQETa_1DscfVI;g9u7o2kGM3=vSn_t z^(LkV5nT(o^{K>KM=&Qc);`Kx60fKFmvAZImONhVwU&pjuvNs8c)i!3!^?#dMz}9E1F-v!mYmdIvgFkxxrkV#VP2N6x zp3KX@k6wSwO9d=l`y6o~mYzU(8Tirb&+(E|aP2fi&4EbYM9Y1q9{ADgU*f3(Zhb1S zB@b8A6}$}m==E>mDFUudGuVzi+|q&YGVr6FDF$~>Mq9eC=0P5qZ?n(ny|wr;rh_3=ht zAJ9V{@z3b%6oBY*A=q>jEm zo`?E-&Li%AEpzns@jTSua~|>Vmy$+bAJ0SmH6n%%sLCTg?X}F&*T?fve-C*?moKIV zmIl73_3=E^?=_G3(#45JUmwpy{oeA3lGU+BUmwpy{a)r)<;$~;ygr_X`b|6r@&zi% zAnM|kjLwb(+L