Collisioni

Potersi muovere è fantastico, ma c’è ancora un oggetto di cui abbiamo bisogno per questo gioco: una palla! Come per la racchetta, il primo passo è creare un riquadro per la palla e caricarlo nella VRAM.

Grafica

Add this to the bottom of your file along with the other graphics:

Ball:
	dw `00033000
	dw `00322300
	dw `03222230
	dw `03222230
	dw `00322300
	dw `00033000
	dw `00000000
	dw `00000000
BallEnd:

Now copy it to VRAM somewhere in your initialization code, e.g. after copying the paddle’s tile.

	; Copy the ball tile
	ld de, Ball
	ld hl, $8010
	ld bc, BallEnd - Ball
	call Memcopy

In addition, we need to initialize an entry in OAM, following the code that initializes the paddle.

	; Initialize the paddle sprite in OAM
	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
	; Now initialize the ball sprite
	ld a, 100 + 16
	ld [hli], a
	ld a, 32 + 8
	ld [hli], a
	ld a, 1
	ld [hli], a
	ld a, 0
	ld [hli], a

As the ball bounces around the screen its momentum will change, sending it in different directions. Let’s create two new variables to track the ball’s momentum in each axis: wBallMomentumX and wBallMomentumY.

SECTION "Counter", WRAM0
wFrameCounter: db

SECTION "Input Variables", WRAM0
wCurKeys: db
wNewKeys: db

SECTION "Ball Data", WRAM0
wBallMomentumX: db
wBallMomentumY: db

We will need to initialize these before entering the game loop, so let’s do so right after we write the ball to OAM. By setting the X momentum to 1, and the Y momentum to -1, the ball will start out by going up and to the right.

	; Now initialize the ball sprite
	ld a, 100 + 16
	ld [hli], a
	ld a, 32 + 8
	ld [hli], a
	ld a, 1
	ld [hli], a
	ld a, 0
	ld [hli], a

	; The ball starts out going up and to the right
	ld a, 1
	ld [wBallMomentumX], a
	ld a, -1
	ld [wBallMomentumY], a

Lavoro di preparazione

Now for the fun part! Add a bit of code at the beginning of your main loop that adds the momentum to the OAM positions. Notice that since this is the second OAM entry, we use + 4 for Y and + 5 for X. This can get pretty confusing, but luckily we only have two objects to keep track of. In the future, we’ll go over a much easier way to use OAM.

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

	; Add the ball's momentum to its position in OAM.
	ld a, [wBallMomentumX]
	ld b, a
	ld a, [_OAMRAM + 5]
	add a, b
	ld [_OAMRAM + 5], a

	ld a, [wBallMomentumY]
	ld b, a
	ld a, [_OAMRAM + 4]
	add a, b
	ld [_OAMRAM + 4], a

Si consiglia di compilare nuovamente il gioco per vedere cosa fa. Se lo fate, dovreste vedere la palla muoversi, ma passerà attraverso i muri e poi volerà fuori dallo schermo.

Per risolvere questo problema, dobbiamo aggiungere il rilevamento delle collisioni, in modo che la palla possa rimbalzare. Dovremo ripetere il controllo delle collisioni un paio di volte, quindi utilizzeremo due funzioni per farlo.

Non perdetevi nei dettagli di questa funzione, perché utilizza alcune tecniche e istruzioni che non abbiamo ancora discusso. L’idea di base è che converte la posizione dello sprite in una posizione sulla mappa delle piastrelle. In questo modo, possiamo controllare quale piastrella sta toccando la nostra palla, in modo da sapere quando rimbalzare!

; Convert a pixel position to a tilemap address
; hl = $9800 + X + Y * 32
; @param b: X
; @param c: Y
; @return hl: tile address
GetTileByPixel:
	; First, we need to divide by 8 to convert a pixel position to a tile position.
	; After this we want to multiply the Y position by 32.
	; These operations effectively cancel out so we only need to mask the Y value.
	ld a, c
	and a, %11111000
	ld l, a
	ld h, 0
	; Now we have the position * 8 in hl
	add hl, hl ; position * 16
	add hl, hl ; position * 32
	; Convert the X position to an offset.
	ld a, b
	srl a ; a / 2
	srl a ; a / 4
	srl a ; a / 8
	; Add the two offsets together.
	add a, l
	ld l, a
	adc a, h
	sub a, l
	ld h, a
	; Add the offset to the tilemap's base address, and we are done!
	ld bc, $9800
	add hl, bc
	ret

The next function is called IsWallTile, and it’s going to contain a list of tiles which the ball can bounce off of.

; @param a: tile ID
; @return z: set if a is a wall.
IsWallTile:
	cp a, $00
	ret z
	cp a, $01
	ret z
	cp a, $02
	ret z
	cp a, $04
	ret z
	cp a, $05
	ret z
	cp a, $06
	ret z
	cp a, $07
	ret

Questa funzione può sembrare un po’ strana all’inizio. Invece di restituire il risultato in un registro, come a, lo restituisce in un flag: Z! Se in qualsiasi punto una piastrella corrisponde, la funzione ha trovato un muro ed esce con Z impostato. Se l’ID della piastrella di destinazione (in a) corrisponde a uno degli ID delle piastrelle del muro, il corrispondente cp lascerà Z impostato; in tal caso, si ritorna immediatamente (tramite ret z), con Z impostato. Ma se, dopo aver effettuato l’ultimo confronto, Z non viene ancora impostato, sapremo che non abbiamo colpito un muro e non abbiamo bisogno di rimbalzare.

Unificare il tutto

Time to use these new functions to add collision detection! Add the following after the code that updates the ball’s position:

BounceOnTop:
	; Remember to offset the OAM position!
	; (8, 16) in OAM coordinates is (0, 0) on the screen.
	ld a, [_OAMRAM + 4]
	sub a, 16 + 1
	ld c, a
	ld a, [_OAMRAM + 5]
	sub a, 8
	ld b, a
	call GetTileByPixel ; Returns tile address in hl
	ld a, [hl]
	call IsWallTile
	jp nz, BounceOnRight
	ld a, 1
	ld [wBallMomentumY], a

Vedrete che quando carichiamo le posizioni dello sprite, le sottraiamo prima di chiamare GetTileByPixel. Forse ricorderete dall’ultimo capitolo che le posizioni OAM sono leggermente sfalsate; cioè, (0, 0) in OAM è in realtà completamente fuori dallo schermo. Queste istruzioni sub annullano questo offset.

Tuttavia, c’è dell’altro: avrete notato che abbiamo sottratto un pixel in più dalla posizione Y. Questo perché (come suggerisce l’etichetta) questo codice controlla la presenza di una piastrella sopra la palla. Questo perché (come suggerisce l’etichetta), il codice controlla la presenza di una piastrella sopra la palla. In realtà abbiamo bisogno di controllare tutti e quattro i lati della palla, in modo da sapere come cambiare la quantità di moto a seconda del lato che si è scontrato, quindi… aggiungiamo il resto!

BounceOnRight:
	ld a, [_OAMRAM + 4]
	sub a, 16
	ld c, a
	ld a, [_OAMRAM + 5]
	sub a, 8 - 1
	ld b, a
	call GetTileByPixel
	ld a, [hl]
	call IsWallTile
	jp nz, BounceOnLeft
	ld a, -1
	ld [wBallMomentumX], a

BounceOnLeft:
	ld a, [_OAMRAM + 4]
	sub a, 16
	ld c, a
	ld a, [_OAMRAM + 5]
	sub a, 8 + 1
	ld b, a
	call GetTileByPixel
	ld a, [hl]
	call IsWallTile
	jp nz, BounceOnBottom
	ld a, 1
	ld [wBallMomentumX], a

BounceOnBottom:
	ld a, [_OAMRAM + 4]
	sub a, 16 - 1
	ld c, a
	ld a, [_OAMRAM + 5]
	sub a, 8
	ld b, a
	call GetTileByPixel
	ld a, [hl]
	call IsWallTile
	jp nz, BounceDone
	ld a, -1
	ld [wBallMomentumY], a
BounceDone:

Era molto, ma ora la palla rimbalza sullo schermo! C’è solo un’ultima cosa da fare prima della fine di questo capitolo: la collisione tra palla e paddle.

Rimbalzo della pagaia

A differenza di quanto accade con la tilemap, qui non è necessario effettuare conversioni di posizione, ma solo confronti diretti. Tuttavia, per questi ultimi, avremo bisogno del flag carry. Il flag di riporto è indicato con C, come il flag zero è indicato con Z, ma non bisogna confonderlo con il registro c!

A refresher on comparisons

Proprio come Z, è possibile utilizzare il flag di riporto per fare salti condizionati. Tuttavia, mentre Z viene utilizzato per verificare se due numeri sono uguali, C può essere utilizzato per verificare se un numero è maggiore o minore di un altro. Per esempio, cp a, b imposta C se a < b e lo azzera se a >= b. (Per verificare a <= b o a > b si possono usare Z e C in tandem con due istruzioni jp)

Armed with this knowledge, let’s work through the paddle bounce code:

	; First, check if the ball is low enough to bounce off the paddle.
	ld a, [_OAMRAM]
	ld b, a
	ld a, [_OAMRAM + 4]
	cp a, b
	jp nz, PaddleBounceDone ; If the ball isn't at the same Y position as the paddle, it can't bounce.
	; Now let's compare the X positions of the objects to see if they're touching.
	ld a, [_OAMRAM + 5] ; Ball's X position.
	ld b, a
	ld a, [_OAMRAM + 1] ; Paddle's X position.
	sub a, 8
	cp a, b
	jp nc, PaddleBounceDone
	add a, 8 + 16 ; 8 to undo, 16 as the width.
	cp a, b
	jp c, PaddleBounceDone

	ld a, -1
	ld [wBallMomentumY], a

PaddleBounceDone:

Il controllo della posizione Y è semplice, poiché la nostra racchetta è piatta. Tuttavia, la posizione X ha due controlli che ampliano l’area in cui la palla può rimbalzare. Innanzitutto aggiungiamo 16 alla posizione della pallina; se la pallina si trova a più di 16 pixel a destra della racchetta, non dovrebbe rimbalzare. Poi annulliamo questa operazione sottraendo 16 e, già che ci siamo, sottraiamo altri 8 pixel; se la palla si trova a più di 8 pixel a sinistra della racchetta, non dovrebbe rimbalzare.

jp c, DoNotBounce jp nc, DoNotBounce - 8 + 8 + 16

Paddle width

Ci si potrebbe chiedere perché abbiamo controllato 16 pixel a destra ma solo 8 pixel a sinistra. Ricordate che le posizioni OAM rappresentano l’angolo superiore sinistro di uno sprite, quindi il centro della nostra paletta è in realtà 4 pixel a destra della posizione in OAM. Se si considera questo, in realtà stiamo controllando 12 pixel su entrambi i lati dal centro della paletta.

12 pixel possono sembrare molti, ma danno un po’ di tolleranza al giocatore nel caso in cui il suo posizionamento sia sbagliato. Se si preferisce rendere il tutto più facile o più difficile, è possibile regolare i valori!

BONUS: modifica dell’altezza di rimbalzo

Si può notare che la pallina sembra “affondare” un po’ nella paletta prima di rimbalzare. Questo perché la pallina rimbalza quando la sua riga superiore di pixel si allinea con la riga superiore della paletta (si veda l’immagine sopra). Se volete, provate a regolare questo aspetto in modo che la pallina rimbalzi quando la sua fila di pixel inferiore tocca quella superiore della paletta.

Suggerimento: è possibile farlo con una sola istruzione!

Risposta:
	ld a, [_OAMRAM]
	ld b, a
	ld a, [_OAMRAM + 4]
+	add a, 6
	cp a, b

Alternatively, you can add sub a, 6 just after ld a, [_OAMRAM].

In entrambi i casi, provate a giocare con il valore 6; vedete cosa vi sembra giusto!