Oggetti

Lo sfondo è molto utile quando l’intero schermo deve muoversi contemporaneamente, ma non è l’ideale per tutto. Ad esempio, il cursore in un menu, i PNG e il giocatore in un RPG, i proiettili in uno shmup o le palline in un clone di Arkanoid… devono tutti muoversi indipendentemente dallo sfondo. Fortunatamente, il Game Boy ha una funzione perfetta per queste situazioni! In questa lezione parleremo di oggetti (talvolta chiamati “OBJ”).

La descrizione precedente potrebbe avervi fatto pensare al termine “sprite” invece che a “oggetto”. Il termine “sprite” ha molti significati a seconda del contesto, quindi, per evitare confusione, questo tutorial cerca di usare alternative specifiche, come oggetto, metasprite, attore, ecc.

Ogni oggetto permette di disegnare una o due piastrelle (rispettivamente 8×8 o 8×16 pixel) in qualsiasi posizione sullo schermo, a differenza dello sfondo, dove tutte le piastrelle sono disegnate in una griglia. Pertanto, un oggetto è composto dalla sua posizione sullo schermo, da un ID tile (come con la tilemap) e da alcune proprietà extra chiamate “attributi”. Queste proprietà extra consentono, ad esempio, di visualizzare la piastrella capovolta. Ne parleremo più avanti.

Just like how the tilemap is stored in VRAM, objects live in a region of memory called OAM, meaning Object Attribute Memory. Recall from above that an object consists of:

  • Its on-screen position
  • A tile ID
  • The “attributes”

Questi sono memorizzati in 4 byte: uno per la coordinata Y, uno per la coordinata X, uno per l’ID della piastrella e uno per gli attributi. L’OAM è lungo 160 byte e poiché 160 ∕ 4 = 40, il Game Boy memorizza un totale di 40 oggetti in qualsiasi momento.

C’è però un problema: i byte delle coordinate Y e X di un oggetto in OAM non memorizzano la sua posizione sullo schermo! Invece, la posizione X sullo schermo è la posizione X memorizzata meno 8, e la posizione Y sullo schermo è la posizione Y memorizzata meno 16. Per interrompere la visualizzazione di un oggetto, è sufficiente metterlo fuori dallo schermo, ad esempio impostando la sua posizione Y a 0.

Questi offset non sono arbitrari! Si consideri la dimensione massima di un oggetto: 8 x 16 pixel. Questi offset consentono agli oggetti di essere tagliati dai bordi sinistro e superiore dello schermo. Il NES, ad esempio, non dispone di tali offset, per cui si noterà che gli oggetti scompaiono sempre dopo aver toccato il bordo sinistro o superiore dello schermo.

Scopriamo gli oggetti sperimentandoli!

Innanzitutto, all’accensione del Game Boy, l’OAM si riempie di valori semicasuali, che possono coprire lo schermo di spazzatura casuale. Risolviamo questo problema cancellando l’OAM prima di attivare gli oggetti per la prima volta. Aggiungiamo quanto segue subito dopo il ciclo CopyTilemap:

	ld a, 0
	ld b, 160
	ld hl, _OAMRAM
ClearOam:
	ld [hli], a
	dec b
	jp nz, ClearOam

Questo è un buon momento per farlo, poiché proprio come la VRAM, lo schermo deve essere spento per accedere in modo sicuro alla OAM.

Una volta che l’OAM è svuotato, possiamo disegnare un oggetto scrivendo le sue proprietà.

	ld hl, _OAMRAM
	ld a, 128 + 16
	ld [hli], a
	ld a, 16 + 8
	ld [hli], a
	ld a, 0
	ld [hli], a
	ld [hli], a

Si ricordi che ogni oggetto in OAM è composto da 4 byte, nell’ordine Y, X, Tile ID, Attributi. Quindi, il pixel in alto a sinistra dell’oggetto si trova a 128 pixel dalla parte superiore dello schermo e a 16 da quella sinistra. L’ID tessera e gli attributi sono entrambi impostati a 0.

Come si ricorderà dalla lezione precedente, stiamo già usando l’ID 0, che è l’inizio della grafica del nostro sfondo. Tuttavia, per impostazione predefinita, gli oggetti e gli sfondi utilizzano un insieme diverso di piastrelle, almeno per i primi 128 ID. Le mattonelle con ID 128-255 sono condivise da entrambi, il che è utile se si ha una mattonella che viene utilizzata sia dallo sfondo che da un oggetto.

If you go to “Tools”, then “Tile Viewer” in Emulicious’ debugger, you should see three distinct sections.

image

Poiché dobbiamo caricarla in un’area diversa, useremo l’indirizzo $8000 e caricheremo una grafica per la paletta del gioco. Lo faremo subito dopo CopyTilemap:

	; Copy the paddle tile
	ld de, Paddle
	ld hl, $8000
	ld bc, PaddleEnd - Paddle
CopyPaddle:
	ld a, [de]
	ld [hli], a
	inc de
	dec bc
	ld a, b
	or a, c
	jp nz, CopyPaddle

E non dimenticate di aggiungere Paddle alla fine del codice.

Paddle:
	dw `13333331
	dw `30000003
	dw `13333331
	dw `00000000
	dw `00000000
	dw `00000000
	dw `00000000
	dw `00000000
PaddleEnd:

Infine, abilitiamo gli oggetti e vediamo il risultato. Gli oggetti devono essere abilitati dal noto registro rLCDC, altrimenti non vengono visualizzati. (Questo è il motivo per cui non abbiamo dovuto cancellare l’OAM nelle lezioni precedenti). Dobbiamo anche inizializzare una delle tavolozze degli oggetti, rOBP0. In realtà ci sono due tavolozze di oggetti, ma ne useremo solo una.

	; Turn the LCD on
	ld a, LCDCF_ON | LCDCF_BGON | LCDCF_OBJON
	ld [rLCDC], a

	; During the first (blank) frame, initialize display registers
	ld a, %11100100
	ld [rBGP], a
	ld a, %11100100
	ld [rOBP0], a

Movimento

Ora che abbiamo un oggetto sullo schermo, spostiamolo. In precedenza, il ciclo Done non faceva nulla; rinominiamolo in Main e usiamolo per spostare il nostro oggetto. Aspetteremo il VBlank prima di cambiare OAM, proprio come abbiamo fatto prima di spegnere lo schermo.

Main:
    ; Wait until it's *not* VBlank
    ld a, [rLY]
    cp 144
    jp nc, Main
WaitVBlank2:
	ld a, [rLY]
	cp 144
	jp c, WaitVBlank2

	; Move the paddle one pixel to the right.
	ld a, [_OAMRAM + 1]
	inc a
	ld [_OAMRAM + 1], a
	jp Main

🤨

In questo caso, si accede all’OAM senza spegnere lo schermo LCD, ma è comunque sicuro. Per spiegarne il motivo è necessaria una spiegazione più approfondita del rendering del Game Boy, quindi per ora ignoriamolo.

Now you should see the paddle moving… very quickly. Because it moves by a pixel every frame, it’s going at a speed of 60 pixels per second! To slow this down, we’ll use a variable.

Finora abbiamo lavorato solo con i registri della CPU, ma è possibile creare anche variabili globali! Per farlo, creiamo un’altra sezione, ma mettiamola in WRAM0 invece che in ROM0. A differenza della ROM (“Read-Only Memory”, memoria di sola lettura), la RAM (“Random-Access Memory”, memoria ad accesso casuale) può essere scritta; quindi, la WRAM, o Work RAM, è il luogo in cui possiamo memorizzare le variabili del nostro gioco.

Aggiungete questo in fondo al vostro file:

SECTION "Counter", WRAM0
wFrameCounter: db

Now we’ll use the wFrameCounter variable to count how many frames have passed since we last moved the paddle. Every 15th frame, we’ll move the paddle by one pixel, slowing it down to 4 pixels per second. Don’t forget that RAM is filled with garbage values when the Game Boy starts, so we need to initialize our variables before first using them.

	; Initialize global variables
	ld a, 0
	ld [wFrameCounter], a

Main:
	ld a, [rLY]
	cp 144
	jp nc, Main
WaitVBlank2:
	ld a, [rLY]
	cp 144
	jp c, WaitVBlank2

	ld a, [wFrameCounter]
	inc a
	ld [wFrameCounter], a
	cp a, 15 ; Every 15 frames (a quarter of a second), run the following code
	jp nz, Main

	; Reset the frame counter back to 0
	ld a, 0
	ld [wFrameCounter], a

	; Move the paddle one pixel to the right.
	ld a, [_OAMRAM + 1]
	inc a
	ld [_OAMRAM + 1], a
	jp Main

Bene! Il prossimo passo è prendere il controllo della tessera.