init: Architecture
+177
@@ -0,0 +1,177 @@
|
|||||||
|
# Architecture
|
||||||
|
|
||||||
|
Approfondimento tecnico sul motore di gioco.
|
||||||
|
|
||||||
|
## Layout della memoria
|
||||||
|
|
||||||
|
Il ROM è 32 KB flat (nessun MBC, no banking). Lo schema è:
|
||||||
|
|
||||||
|
```
|
||||||
|
0x0000 - 0x00FF Header entry points + interrupt vectors
|
||||||
|
0x0100 - 0x014F Cartridge header (Nintendo logo, titolo, tipo ROM)
|
||||||
|
0x0150 - 0x3FFF Code + data
|
||||||
|
```
|
||||||
|
|
||||||
|
WRAM (8 KB) è partizionata come:
|
||||||
|
|
||||||
|
| Indirizzo | Contenuto |
|
||||||
|
|---|---|
|
||||||
|
| `0xC000-0xC0FF` | map[21][21] (labirinto corrente) |
|
||||||
|
| `0xC100-0xC140` | map_buffer[32][32] (backbuffer isometrico) |
|
||||||
|
| `0xC200-0xC2A0` | enemy_lx/ly, enemy_is_moving, ... (8 fantasmi) |
|
||||||
|
| `0xC300+` | stato globale (player, stamina, livello, ...) |
|
||||||
|
|
||||||
|
## Moduli
|
||||||
|
|
||||||
|
| File | Responsabilità |
|
||||||
|
|---|---|
|
||||||
|
| `main.c` | entry point, loop VBL, `app_state` |
|
||||||
|
| `engine.c` | orchestrazione, init, schermate |
|
||||||
|
| `globals.c` / `globals.h` | stato globale, defines |
|
||||||
|
| `maze.c` | generazione labirinto (DFS + loop) |
|
||||||
|
| `player_logic.c` | DAS, camminata, corsa, salto |
|
||||||
|
| `enemy_logic.c` | AI multi-entity |
|
||||||
|
| `render.c` | proiezione isometrica, autotiling, fog |
|
||||||
|
| `sound.c` | sequencer VBL, 5 tracce musicali |
|
||||||
|
| `screens/` | schermate testuali (intro, istruzioni, death, ...) |
|
||||||
|
|
||||||
|
## Proiezione isometrica
|
||||||
|
|
||||||
|
Il rendering isometrico "2.5D" proietta coordinate logiche `(lx, ly)` su schermo in due passi:
|
||||||
|
|
||||||
|
```
|
||||||
|
// Coordinate iso (tile)
|
||||||
|
iso_x = (lx - ly) * 2 + 12
|
||||||
|
iso_y = (lx + ly) + 2
|
||||||
|
|
||||||
|
// Coordinate pixel
|
||||||
|
px = (lx - ly) * 16 + 96
|
||||||
|
py = (lx + ly) * 8 + 16
|
||||||
|
```
|
||||||
|
|
||||||
|
Ogni tile è un diamante 32×16 px (`4 tile 8×8` in larghezza, `2 tile` in altezza).
|
||||||
|
|
||||||
|
La camera è centrata sul player:
|
||||||
|
|
||||||
|
```c
|
||||||
|
scroll_x = px - 64; // schermo 160px, centro = 80
|
||||||
|
scroll_y = py - 72; // schermo 144px, centro = 72
|
||||||
|
move_bkg(scroll_x, scroll_y);
|
||||||
|
```
|
||||||
|
|
||||||
|
## Generazione del labirinto
|
||||||
|
|
||||||
|
Algoritmo: **DFS iterativo con stack in WRAM**.
|
||||||
|
|
||||||
|
```c
|
||||||
|
void generate_maze(void) {
|
||||||
|
// 1. Inizializza tutto a muro
|
||||||
|
memset(maze, 0, sizeof(maze));
|
||||||
|
|
||||||
|
// 2. Scegli una cella di partenza (1,1)
|
||||||
|
uint8_t cx = 1, cy = 1;
|
||||||
|
maze[cy][cx] = 1;
|
||||||
|
|
||||||
|
// 3. DFS iterativo
|
||||||
|
// Per ogni cella, scegli un vicino casuale non visitato
|
||||||
|
// e "scava" il muro tra i due
|
||||||
|
// Backtrack quando non ci sono vicini
|
||||||
|
|
||||||
|
// 4. Rimuovi il 15% dei muri per creare loop
|
||||||
|
for (uint16_t i = 0; i < (map_size * map_size) / 7; i++) {
|
||||||
|
// Trova un muro tra due corridoi e rimuovilo
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. Piazza la botola in una cella lontana dal player
|
||||||
|
place_hatch_far_from_player();
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
`map_size` cresce di 2 per livello:
|
||||||
|
|
||||||
|
| Livello | map_size |
|
||||||
|
|---|---|
|
||||||
|
| 1 | 7 |
|
||||||
|
| 2 | 9 |
|
||||||
|
| ... | ... |
|
||||||
|
| 8 | 21 (cap) |
|
||||||
|
|
||||||
|
## Fog of War
|
||||||
|
|
||||||
|
Distanza di **Chebyshev** (`max(|dx|, |dy|)`) — un quadrato, non un cerchio.
|
||||||
|
|
||||||
|
```c
|
||||||
|
int8_t dist = (dx > dy) ? dx : dy;
|
||||||
|
if (dist > fog_radius) return 0; // buio totale
|
||||||
|
if (dist == fog_radius) return 2; // "bordo" della nebbia
|
||||||
|
return 1; // cella visibile
|
||||||
|
```
|
||||||
|
|
||||||
|
`fog_radius = 2` (5×5) per livelli 1-6, `fog_radius = 1` (3×3) dal livello 7.
|
||||||
|
|
||||||
|
## Multi-pass rendering
|
||||||
|
|
||||||
|
Il rendering isometrico viene fatto in **due passate** per gestire correttamente le scale (tile di "vittoria"):
|
||||||
|
|
||||||
|
1. **Passata 1**: tutti i pavimenti. Ordinamento per `iso_y` → disegnati in alto-basso, dx per disambiguare.
|
||||||
|
2. **Passata 2**: solo la botola (`maze[y][x] == 2`). Disegnata per ultima, così i suoi bordi trasparenti si fondono coi pavimenti sotto.
|
||||||
|
|
||||||
|
Se disegnassimo scale e pavimenti nello stesso loop, i pavimenti "coprirebbero" le scale perché hanno angoli di disegno identici ma con maschere diverse.
|
||||||
|
|
||||||
|
## Autotiling
|
||||||
|
|
||||||
|
Ogni cella guarda i 4 vicini cardinali nella griglia logica:
|
||||||
|
|
||||||
|
```c
|
||||||
|
state_tl = is_visible(lx - 1, ly) ? 1 : 0;
|
||||||
|
state_tr = is_visible(lx, ly - 1) ? 1 : 0;
|
||||||
|
mask = state_tl + state_tr * 3; // 0..3
|
||||||
|
```
|
||||||
|
|
||||||
|
La maschera seleziona una delle 4 varianti di tile tra 0 e 3, dando un'autotiling 2D-free.
|
||||||
|
|
||||||
|
## Sistema di coordinate nemico
|
||||||
|
|
||||||
|
8 fantasmi (`MAX_ENEMIES = 8`). Ciascuno ha:
|
||||||
|
|
||||||
|
```c
|
||||||
|
struct enemy_t {
|
||||||
|
uint8_t lx, ly; // cella logica
|
||||||
|
uint8_t is_moving; // in interpolazione?
|
||||||
|
uint8_t move_progress; // 0..16
|
||||||
|
int8_t start_lx, start_ly; // origine interpolazione
|
||||||
|
int8_t target_lx, target_ly; // destinazione
|
||||||
|
int16_t start_px, start_py; // pixel origine
|
||||||
|
int16_t target_px, target_py; // pixel destinazione
|
||||||
|
uint8_t cooldown; // timer prima del prossimo passo
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
L'AI è **greedy Manhattan**: ad ogni step il fantasma si sposta nella direzione che minimizza la distanza Manhattan dal player. Cooldown scalabile: 60 frame al livello 1, 11 al livello 8 (cap minimo 10).
|
||||||
|
|
||||||
|
## Audio
|
||||||
|
|
||||||
|
4 canali APU GB:
|
||||||
|
- **CH1**: onda quadra + sweep + envelope (melodia)
|
||||||
|
- **CH2**: onda quadra + envelope (melodia/basso)
|
||||||
|
- **CH3**: wave RAM (samples)
|
||||||
|
- **CH4**: noise ( percussioni)
|
||||||
|
|
||||||
|
Il sequencer è in VBL interrupt (`add_VBL(play_music_tick)`), legge step da tabelle statiche di note (frequenza, durata, channel). 5 tracce:
|
||||||
|
1. Title (112 step)
|
||||||
|
2. Gameplay "eerie pulse" (96)
|
||||||
|
3. Game Over (128)
|
||||||
|
4. Finale (192)
|
||||||
|
5. Going Deeper (96)
|
||||||
|
|
||||||
|
## Performance
|
||||||
|
|
||||||
|
- 60 FPS (16.67 ms/frame)
|
||||||
|
- Budget: ~70 000 cicli SM83 per frame
|
||||||
|
- Render: ~50% del budget
|
||||||
|
- AI nemico: ~10%
|
||||||
|
- Audio: ~5%
|
||||||
|
- Loop overhead: ~5%
|
||||||
|
- Margine: ~30% per future feature
|
||||||
|
|
||||||
|
Misurato con BGB debugger + profiler PyBoy.
|
||||||
Reference in New Issue
Block a user