0x07 Ensamblador ZX Spectrum Pong – Detección de colisiones
En esta nueva entrega de Ensamblador ZX Spectrum Pong vamos a empezar con la detección de colisiones, para que la bola rebote contra las palas.
Tabla de contenidos
- Ensamblador ZX Spectrum Pong – Detección de colisiones
- GetPtrY
- Detección de colisiones
- Enlaces de interés
- Vídeo
Ensamblador ZX Spectrum Pong – Detección de colisiones
Creamos la carpeta Paso07 y copiamos desde la carpeta Paso06 los archivos Controls.asm, Game.asm, Main.asm, Sprite.asm y Video.asm.
A partir de aquí, vamos a utilizar todo lo que hemos implementando hasta ahora, evolucionándolo.
Vamos a implementar la detección de colisiones de la bola con las palas. Para ello necesitamos definir la columna en la que se produce dicha colisión, lo que vamos a hacer en Sprite.asm.
CROSS_LEFT: EQU $01
CROSS_RIGHT: EQU $1d
Para comprobar la colisión en la coordenada X, vamos a usar la columna. Para comprobar la colisión en la coordenada Y vamos a usar tercio, línea y scanline.
GetPtrY
Como hemos visto en entregas anteriores, la composición de la coordenada Y se encuentra en dos bytes distintos (010T TSSS LLLC CCCC), por lo que vamos a implementar una rutina que reciba una posición de memoria de la pantalla y devuelva la coordenada Y (TTLLLSSS).
La rutina la vamos a implementar en Video.asm, tras la rutina Cls, y recibe la posición de memoria de la pantalla en HL y devuelve la coordenada Y obtenida en A.
GetPtrY:
ld a, h
and $18
rlca
rlca
rlca
ld e, a
Cargamos el tercio y scanline en A, LD A, H, nos quedamos con el tercio, AND $18, lo pasamos a los bits 6 y 7, RLCA RLCA RLCA, y cargamos el resultado en E, LD E, A.
ld a, h
and $07
or e
ld e, a
Volvemos a cargar tercio y scanline en A, LD A, H, nos quedamos con el scanline, AND $07, le agregamos el tercio, OR E, y cargamos el resultado en E, LD E, A.
ld a, l
and $e0
rrca
rrca
or e
ret
Cargamos la línea y la columna en A, LD A, L, nos quedamos con la línea, AND $E0, ponemos el valor en los bits 3 a 5, RRCA RRCA, y le añadimos tercio y scanline, OR E.
El aspecto final de la rutina es el siguiente.
; -----------------------------------------------------------------------------
; Obtiene tercio, línea y scanline de una posición de memoria.
; Entrada: HL = Posición de memoria.
; Salida: A = Tercio, línea y scanline obtenido.
; Altera el valor de los registros AF y E.
; -----------------------------------------------------------------------------
GetPtrY:
ld a, h ; Carga en A el valor de H (tercio y scanline)
and $18 ; Se queda con el tercio
rlca
rlca
rlca ; Pasa el valor del tercio a los bits 6 y 7
ld e, a ; Carga el valor en E
ld a, h ; Carga en A el valor de H (tercio y scanline)
and $07 ; Se queda con el scanline
or e ; Lo mezcla con E
ld e, a ; Carga el valor en E TT***SSS
ld a, l ; Carga en A el valor de L (línea y columna)
and $e0 ; Se queda con la línea
rrca
rrca ; Pasa el valor a los bits 3 a 5
or e ; Lo mezcla con E (TTLLLSSS)
ret
Este tipo de conversión ya la hicimos en la rutina checkVerticalLimit, pero al ser necesaria en más de una rutina, la hemos implementado como una rutina aparte.
Para probarla, vamos a modificar la rutina checkVerticalLimit, sustituyendo casi toda ella por una llamada a GetPtrY, quedando de la siguiente manera.
; -----------------------------------------------------------------------------
; Evalúa si se ha alcanzado el límite vertical.
; Entrada: A = Límite vertical (TTLLLSSS).
; HL = Posición actual (010TTSSS LLLCCCCC).
; Altera el valor de los registros AF y BC.
; ----------------------------------------------------------------------------
checkVerticalLimit:
ld b, a ; Guarda el valor de A en B
call GetPtrY ; Obtiene la coordenada Y (TTLLLSSSS)
; de la posición actual
cp b ; Lo compara con B. B = valor original de A = Límite vertical
ret
Compilamos, cargamos en el emulador y comprobamos que no se ha roto nada.
Detección de colisiones
Ahora vamos a implementar la detección de colisiones, en el archivo Game.asm.
Empezamos por la rutina que evalúa si hay colisión en el eje X. Esta rutina recibe en C la columna donde se produce la colisión, y activa el flag Z si se ha producido.
CheckCrossX:
ld a, (ballPos)
and $1f
cp c
ret
Cargamos la posición de la bola en A, LD A, (ballPos), nos quedamos con la columna, AND $1F, y comparamos el valor resultante con la columna de colisión, CP C.
El siguiente paso es implementar la rutina que evalúa si hay colisión en el eje Y. Esta rutina recibe en HL la posición de la pala, y activa el flag Z si hay colisión.
CheckCrossY:
call GetPtrY
inc a
ld c, a
Obtenemos la coordenada Y de la pala, CALL GetPtrY. Como el primer scanline de la pala es blanco, no lo tenemos en cuenta para las colisiones, así que pasamos al siguiente, INC A, y cargamos el valor en C, LD C, A.
ld hl, (ballPos)
call GetPtrY
ld b, a
Cargamos la posición de la bola en HL, LD HL, (ballPos), obtenemos la coordenada Y, CALL GetPtrY, y cargamos el valor en B, LD B, A.
add a, $04
sub c
ret c
En A tenemos la coordenada Y de la bola. Apuntamos A al penúltimo scanline de la bola, el último que no es blanco, ADD A, $04, le restamos la coordenada Y de la pala, SUB C, y si hay acarreo salimos, RET C, ya que la bola pasa por encima de la pala.
Si no hemos salido, tenemos que comprobar si la bola pasa por debajo de la pala.
ld a, c
add a, $16
ld c, a
ld a, b
inc a
sub c
ret nc
xor a
ret
Cargamos la coordenada Y de la pala en A, LD A, C, le sumamos $16 (22) para posicionarnos en el penúltimo scanline, el último que no está a 0, ADD A, $16, y cargamos el valor en C, LD C, A.
Cargamos la coordenada Y de la bola en A, LD A, B, la apuntamos al scanline 1, el primero que no está a 0, INC A, y le restamos la coordenada Y de la pala, SUB C.
Si tras la resta no hay acarreo salimos, RET NC, pues o bien la bola pasa por debajo, o colisiona en el último scanline de la pala, que está en la misma coordenada Y que el primer scanline de la bola, y al restar se activa el flag Z.
Si hay acarreo, la bola colisiona con el resto de la pala, por lo que activamos el flag Z, XOR A, y salimos, RET.
El siguiente paso es implementar la rutina principal a la que vamos a llamar para comprobar si hay colisión, en cuyo caso vamos a realizar las acciones necesarias.
CheckBallCross:
ld a, (ballSetting)
and $40
jr nz, checkBallCross_left
Cargamos la configuración de la bola en A, LD A, (ballSetting), y nos quedamos con el bit 6, AND $40, que especifica si la bola va hacia la derecha o hacia la izquierda. Si el bit 6 está a 1, la bola va hacia la izquierda y saltamos a comprobar si se produce colisión con la pala del jugador 1, JR NZ, checkBallCross_left.
Si no se ha producido el salto, la bola va hacia la derecha y comprobamos si hay colisión con la pala del jugador 2.
checkBallCross_right:
ld c, CROSS_RIGHT
call CheckCrossX
ret nz
ld hl, (paddle2pos)
call CheckCrossY
ret nz
Cargamos la columna de colisión en C, LD C, CROSS_RIGHT, evaluamos si se produce colisión en el eje X, CALL CheckCrossX. Si no se produce la colisión salimos de la rutina, RET NZ.
Si se ha producido colisión en el eje X, cargamos la posición de la pala 2 en HL, LD HL, (paddle2pos), y evaluamos si se produce colisión en el eje Y, CALL CheckCrossY. Si no se produce colisión, salimos de la rutina, RET NZ.
Si no hemos salido de la rutina, se ha producido colisión.
ld a, (ballSetting)
or $40
ld (ballSetting), a
ld a, $ff
ld (ballRotation), a
ret
Cargamos la configuración de la bola en A, LD A, (ballSetting), ponemos el bit 6 a 1 para cambiar la dirección de la bola hacia la izquierda, OR $40, y cargamos el valor en memoria, LD (ballSetting), A.
Cargamos -1 en A, LD A, $FF, cambiamos la rotación de la bola, LD (ballRotation), A, y salimos de la rutina, RET.
La comprobación de si hay colisión con la pala del jugador 1 es similar a lo visto anteriormente, por lo que solo vamos a poner el código y a marcar las diferencias, sin entrar en detalle.
checkBallCross_left: ; ¡CAMBIO!
ld c, CROSS_LEFT ; ¡CAMBIO!
call CheckCrossX
ret nz
ld hl, (paddle1pos) ; ¡CAMBIO!
call CheckCrossY
ret nz
ld a, (ballSetting)
and $bf ; ¡CAMBIO!
ld (ballSetting), a
ld a, $01 ; ¡CAMBIO!
ld (ballRotation), a
ret
El aspecto final de las rutinas de comprobación de colisiones entre las palas y la bola es el siguiente.
; -----------------------------------------------------------------------------
; Evalúa si hay colisión entre la bola y las palas.
; Altera el valor de los registros AF, C y HL.
; -----------------------------------------------------------------------------
CheckBallCross:
ld a, (ballSetting) ; Carga la dirección/velocidad de la bola en A
and $40 ; Se queda con el bit 6 (izquierda/derecha)
jr nz, checkBallCross_left ; Si no está a 0 va hacia la izquierda y salta
checkBallCross_right:
ld c, CROSS_RIGHT ; Carga la columna de colisión en C
call CheckCrossX ; Evalúa si se produce colisión en el eje X
ret nz ; Si no se produce, fin de la rutina
ld hl, (paddle2pos) ; Carga la posición de la pala 2 en HL
call CheckCrossY ; Evalúa si se produce colisión en el eje Y
ret nz ; Si no se produce colisión, fin de la rutina
; Si llega aquí hay colisión
ld a, (ballSetting) ; Carga la dirección/velocidad de la bola en A
or $40 ; Cambia la dirección, la pone hacia la izquierda
ld (ballSetting), a ; Carga el valor en memoria
ld a, $ff ; Cambia la rotación de la bola
ld (ballRotation), a ; La carga en memoria
ret ; Fin de la rutina
checkBallCross_left:
; La bola va hacia la izquierda
ld c, CROSS_LEFT ; Carga la columna de colisión en C
call CheckCrossX ; Evalúa si se produce colisión en el eje X
ret nz ; Si no se produce, fin de la rutina
ld hl, (paddle1pos) ; Carga la posición de la pala 1 en HL
call CheckCrossY ; Evalúa si se produce colisión en el eje Y
ret nz ; Si no se produce colisión, fin de la rutina
; Si llega aquí hay colisión
ld a, (ballSetting) ; Carga la dirección/velocidad de la bola en A
and $bf ; Cambia la dirección, la pone hacia la derecha
ld (ballSetting), a ; Carga el valor en memoria
ld a, $01 ; Cambia la rotación de la bola
ld (ballRotation), a ; La carga en memoria
ret ; Fin de la rutina
; -----------------------------------------------------------------------------
; Evalúa si la bola colisiona en el eje X con la pala.
; Entrada: C = Columna dónde se produce la colisión.
; Salida: Z = Colisiona.
; NZ = No colisiona.
; Altera el valor de los registros AF.
; -----------------------------------------------------------------------------
CheckCrossX:
ld a, (ballPos) ; Carga la línea y columna donde está la bola
and $1f ; Se queda con la columna
cp c ; Lo compara con la columna de colisión
ret
; -----------------------------------------------------------------------------
; Evalúa si la bola colisiona en el eje Y con la pala.
; Entrada: HL = Posición de la pala
; Salida: Z = Colisiona.
; NZ = No colisiona.
; Altera el valor de los registros AF, BC y HL.
; -----------------------------------------------------------------------------
CheckCrossY:
call GetPtrY ; Obtiene la posición vertical de la pala (TTLLLSSS)
; La posición devuelta apunta al primer scanline de la pala que está a 0,
; apunta al siguiente
inc a
ld c, a ; Carga el valor en C
ld hl, (ballPos) ; Carga en HL la posición de la bola
call GetPtrY ; Obtiene la posición vertical de la bola (TTLLLSSS)
ld b, a ; Carga el valor en B
; Comprueba si la bola pasa por encima de la pala
; La bola está compuesta de 1 scanline a 0, 4 a $3c y otro a 0
; La posición apunta al 1er scanline, y se comprueba la colisión con el 5º
add a, $04 ; Apunta la posición de la bola al 5º scanline
sub c ; Resta a la posición de la bola, la posición de la pala
ret c ; Si hay acarreo sale porque la bola pasa por encima
; Comprueba si la bola pasa por debajo de la pala
ld a, c ; Carga la posición vertical de la pala en A
add a, $16 ; Le suma 22 para apuntar al penúltimo scanline,
; último que no es 0
ld c, a ; Lo vuelve a cargar en C
ld a, b ; Carga la posición vertical de la bola
inc a ; Le suma 1 para apuntar el scanline 1, primero que no es 0
sub c ; Resta a la posición de la bola, la posición de la pala
ret nc ; Si no hay acarreo la bola pasa por debajo de la pala
; o colisiona en el último scanline.
; En este último caso se activa el flag Z
; Hay colisión
xor a ; Activa el flag Z
ret
Ahora ya solo nos queda ver si lo que hemos implementado hace lo que pretendemos.
Abrimos el archivo Main.asm y justo debajo de la etiqueta loop_continue, añadimos la siguiente línea.
call CheckBallCross
Compilamos y cargamos en el emulador para ver los resultados. Si todo ha ido bien, la bola choca contra las palas; la detección de colisiones está funcionando.
El aspecto final del archivo Main.asm es el siguiente.
; Detección de colisiones
org $8000
; -----------------------------------------------------------------------------
; Entrada al programa
; -----------------------------------------------------------------------------
Main:
ld a, $00 ; A = 0
out ($fe), a ; Pone el borde en negro
call Cls ; Limpia la pantalla
call PrintLine ; Imprime la línea central
call PrintBorder ; Imprime el borde del campo
Loop:
ld a, (countLoopBall) ; Carga el contador de vueltas de la bola
inc a ; Lo incrementa
ld (countLoopBall), a ; Lo carga en memoria
cp $06 ; Comprueba si ha llegado a 6
jr nz, loop_paddle ; Si no ha llegado a 6 salta
call MoveBall ; Mueve la bola
ld a, ZERO ; Pone el contador a 0
ld (countLoopBall), a ; Lo carga en memoria
loop_paddle:
ld a, (countLoopPaddle) ; Carga el contador de vueltas de las palas
inc a ; Lo incrementa
ld (countLoopPaddle), a ; Lo carga en memoria
cp $02 ; Comprueba si ha llegado a 2
jr nz, loop_continue ; Si no ha llegado a 2 salta
call ScanKeys ; Escanea las teclas pulsadas
call MovePaddle ; Mueva las palas
ld a, ZERO ; Pone el contador a 0
ld (countLoopPaddle), a ; Lo carga en memoria
loop_continue:
call CheckBallCross ; Evalúa si hay colisión entre la bola y las palas
call PrintBall ; Pinta la bola
call ReprintLine ; Repinta la línea
ld hl, (paddle1pos); Carga en HL la posición de la pala 1
call PrintPaddle ; Pinta la pala 1
ld hl, (paddle2pos); Carga en HL la posición de la pala 2
call PrintPaddle ; Pinta la pala 2
jr Loop ; Bucle infinito
include "Game.asm"
include "Controls.asm"
include "Sprite.asm"
include "Video.asm"
countLoopBall: db $00 ; Contador de vueltas de la bola
countLoopPaddle: db $00 ; Contador de vueltas de las palas
end $8000
Podéis descargar todo el código que hemos generado.
Enlaces de interés
- Notepad++
- Visual Studio Code
- Sublime Text
- ZEsarUX
- PASMO
- Git
- Curso de ensamblador Z80 de Compiler Software
- Referencia Z80
- Ensamblador Z80 en Telegram
- Tutorial completo en formato PDF
- Poyecto en itch.io.
- Archivo .dsk con los juegos de los tutoriales
- Personalización y depuración con ZEsarUX
Vídeo
Si lo prefieres, puedes ver el vídeo que grabamos de esta sesión.
Ensamblador para ZX Spectrum PONG por Juan Antonio Rubio García.
Comentarios al código por Spirax.
Correcciones al texto original realizadas por Joaquín Ferrero.
Esta obra está bajo licencia de Creative Commons Reconocimiento-NoComercial-CompartitIgual 4.0 Internacional License.
Este tutorial ha sido publicado con anterioridad en AUA y se han grabado vídeos que están publicados a través de Retro Parla.
No olvides visitar las webs amigas.
También puedes visitar el resto de tutoriales:
Y recuerda, si lo usas no te limites a copiarlo, intenta entenderlo y adaptarlo a tus necesidades.