0x08 Ensamblador ZX Spectrum Pong – Partida a dos jugadores y cambio de velocidad de la bola
En esta nueva entrega de Ensamblador ZX Spectrum Pong vamos a implementar la partida a dos jugadores, con marcador, y la posibilidad de cambiar la velocidad de la bola.
Tabla de contenidos
- Ensamblador ZX Spectrum Pong – Partida a dos jugadores y cambio de velocidad de la bola
- Marcadores
- Cambio de velocidad de la bola
- Puntuación
- Inicio y fin de la partida
- Enlaces de interés
- Vídeo
Ensamblador ZX Spectrum Pong – Partida a dos jugadores y cambio de velocidad de la bola
Creamos la carpeta Paso08 y copiamos los archivos Controls.asm, Game.asm, Main.asm, Sprite.asm y Video.asm desde la carpeta Paso07.
Vamos a empezar por el marcador, definiendo la posición donde vamos a pintar la puntuación, y definiendo también los sprites necesarios en el archivo Sprite.asm.
Marcadores
POINTS_P1: EQU $450d
POINTS_P2: EQU $4511
Cada dígito de los marcadores ocupa 8×16 píxeles, o lo que es lo mismo, un carácter de ancho por dos de alto (1 byte x 16 bytes/scanlines).
Blanco_sprite:
ds $10 ; 16 espacios = 16 bytes a $00
Cero_sprite:
db $00, $7e, $7e, $66, $66, $66, $66, $66
db $66, $66, $66, $66, $66, $7e, $7e, $00
Uno_sprite:
db $00, $18, $18, $18, $18, $18, $18, $18
db $18, $18, $18, $18, $18, $18, $18, $00
Dos_sprite:
db $00, $7e, $7e, $06, $06, $06, $06, $7e
db $7e, $60, $60, $60, $60, $7e, $7e, $00
Tres_sprite:
db $00, $7e, $7e, $06, $06, $06, $06, $3e
db $3e, $06, $06, $06, $06, $7e, $7e, $00
Cuatro_sprite:
db $00, $66, $66, $66, $66, $66, $66, $7e
db $7e, $06, $06, $06, $06, $06, $06, $00
Cinco_sprite:
db $00, $7e, $7e, $60, $60, $60, $60, $7e
db $7e, $06, $06, $06, $06, $7e, $7e, $00
Seis_sprite:
db $00, $7e, $7e, $60, $60, $60, $60, $7e
db $7e, $66, $66, $66, $66, $7e, $7e, $00
Siete_sprite:
db $00, $7e, $7e, $06, $06, $06, $06, $06
db $06, $06, $06, $06, $06, $06, $06, $00
Ocho_sprite:
db $00, $7e, $7e, $66, $66, $66, $66, $7e
db $7e, $66, $66, $66, $66, $7e, $7e, $00
Nueve_sprite:
db $00, $7e, $7e, $66, $66, $66, $66, $7e
db $7e, $06, $06, $06, $06, $7e, $7e, $00
Una vez que hemos definido los sprites, definimos la composición de los números haciendo referencia a las etiquetas de los sprites.
Cero:
dw Blanco_sprite, Cero_sprite
Uno:
dw Blanco_sprite, Uno_sprite
Dos:
dw Blanco_sprite, Dos_sprite
Tres:
dw Blanco_sprite, Tres_sprite
Cuatro:
dw Blanco_sprite, Cuatro_sprite
Cinco:
dw Blanco_sprite, Cinco_sprite
Seis:
dw Blanco_sprite, Seis_sprite
Siete:
dw Blanco_sprite, Siete_sprite
Ocho:
dw Blanco_sprite, Ocho_sprite
Nueve:
dw Blanco_sprite, Nueve_sprite
Diez:
dw Uno_sprite, Cero_sprite
Once:
dw Uno_sprite, Uno_sprite
Doce:
dw Uno_sprite, Dos_sprite
Trece:
dw Uno_sprite, Tres_sprite
Catorce:
dw Uno_sprite, Cuatro_sprite
Quince:
dw Uno_sprite, Cinco_sprite
Ahora necesitamos definir el lugar donde vamos a guardar la puntuación de cada jugador. Abrimos el archivo Main.asm y añadimos las siguientes variables antes de END $8000.
p1points: db $00
p2points: db $00
Ya tenemos todo listo para empezar a implementar el marcador.
Lo primero que tenemos que saber es que sprite tenemos que pintar, dependiendo del marcador de cada jugador. Para saber qué sprite pintar, vamos a implementar una rutina que recibe en A la puntuación, y devuelve en HL la dirección del sprite a pintar.
Abrimos el archivo Video.asm e implementamos justo antes de la rutina NextScan.
GetPointSprite:
ld hl, Cero
ld bc, $04
inc a
Cargamos en HL la dirección del sprite para el cero, LD HL, Cero. Como cada sprite está a 4 bytes del anterior, cargamos este desplazamiento en BC, LD BC, $04, e incrementamos A para que el bucle no empiece en 0, INC A, en el caso de que la puntuación sea 0.
Ahora hacemos un bucle para que HL apunte al sprite correcto.
getPointSprite_loop:
dec a
ret z
add hl, bc
jr getPointSprite_loop
Decrementamos A, DEC A, y si hemos llegado a 0, HL ya apunta al sprite correcto y salimos, RET Z. Si todavía no hemos llegado a 0, sumamos el desplazamiento a HL, ADD HL, BC, y volvemos a ejecutar el bucle, JR getPointSprite_loop.
El aspecto final de la rutina es el siguiente.
; -----------------------------------------------------------------------------
; Obtiene el sprite correspondiente a pintar en el marcador.
; Entrada: A = puntuación.
; Salida: HL = Dirección del sprite a pintar.
; Altera el valor de los registros AF, BC y HL.
; -----------------------------------------------------------------------------
GetPointSprite:
ld hl, Cero ; Carga en HL la dirección del sprite del 0
ld bc, $04 ; Cada sprite está del anterior a 4 bytes
inc a ; Incrementa A para que el inicio del bucle no sea 0
getPointSprite_loop:
dec a ; Decrementa A
ret z ; Si ha llegado a 0, fin de rutina
add hl, bc ; Suma 4 a la dirección del sprite; siguiente sprite
jr getPointSprite_loop ; Bucle hasta que A = 0
ret
Y ahora vamos a implementar la rutina que pinta los marcadores, al final del archivo Video.asm.
PrintPoints:
ld a, (p1points)
call GetPointSprite
Cargamos la puntuación del jugador 1 en A, LD A, (p1points), y obtenemos la dirección de memoria donde está la definición del sprite correspondiente a dicha puntuación, CALL GetPointSprite.
GetPointSprite nos devuelve en HL la dirección de memoria donde está definido el sprite. Si la puntuación es cero, HL nos traerá la dirección de memoria donde está definida la etiqueta Cero, cuya definición es la siguiente.
Cero:
dw Blanco_sprite, Cero_sprite
Como podemos ver, Cero está definido por otras dos direcciones de memoria: la primera es la dirección de memoria donde está definido el sprite blanco, usado para justificar a dos dígitos, y la segunda es la dirección de memoria donde está definido el sprite del cero.
Si las direcciones de memoria fueran las siguientes.
$9000 Blanco_sprite
$9020 Cero_sprite
$9040 Cero
La definición de la etiqueta Cero, una vez que se sustituyen las etiquetas Blanco_sprite y Cero_sprite por las direcciones de memoria donde están definidas, sería.
Cero:
dw $9000, $9020
El valor que tendría HL tras llamar a GetPointSprite con el marcador a 0 sería $9040, o lo que es lo mismo, la dirección de memoria donde se define la etiqueta Cero.
Como el Z80 es Little Endian, los valores de las direcciones de memoria desde $9040 en adelante serían:
$9040 | $00 |
$9041 | $90 |
$9042 | $20 |
$9043 | $90 |
O lo que es lo mismo, las direcciones de memoria donde están definidos los sprites para Blanco_sprite y para Cero_sprite.
Esta explicación es necesaria para entender el funcionamiento del resto de la rutina.
push hl
ld e, (hl)
inc hl
ld d, (hl)
ld hl, POINTS_P1
call printPoint_print
Vamos a pintar el primer dígito del marcador del jugador 1. Preservamos el valor de HL, que apunta al sprite del marcador que tenemos que pintar, PUSH HL, cargamos en E la parte baja de la dirección donde está el sprite del primer dígito, LD E, (HL), apuntamos HL a la parte alta de la dirección, INC HL, y la cargamos en D, LD D, (HL).
Cargamos en HL la dirección de memoria de pantalla donde se pinta el primer dígito del marcador del jugador 1, LD HL, POINTS_P1, y llamamos al pintado del dígito, CALL printPoint_print.
Ahora pintamos el segundo dígito del marcador del jugador 1.
pop hl
inc hl
inc hl
Recuperamos el valor de HL, POP HL, y lo apuntamos a la parte baja de la dirección donde está definido el sprite del segundo dígito, INC HL INC HL.
ld e, (hl)
inc hl
ld d, (hl)
Cargamos la parte baja de dicha dirección en E, LD E, (HL), apuntamos HL a la parte alta de la dirección, INC HL, y la cargamos en D, LD D, (HL).
ld hl, POINTS_P1
inc l
call printPoint_print
Por último, cargamos en HL la posición de memoria de la pantalla donde se pinta el marcador del jugador 1, LD HL, POINTS_P1. Como cada dígito ocupa 1 byte (columna) de ancho, situamos HL en la columna dónde se pinta el segundo dígito, INC L, y lo pintamos.
La forma de pintar el marcador del jugador 2 es casi igual a la del jugador 1, por lo que mostramos el código marcando los cambios y sin entrar en detalle.
ld a, (p2points) ; !CAMBIO!
call GetPointSprite
push hl
; 1er dígito
ld e, (hl)
inc hl
ld d, (hl)
ld hl, POINTS_P2 ; !CAMBIO!
call printPoint_print
pop hl
; 2º dígito
inc hl
inc hl
ld e, (hl)
inc hl
ld d, (hl)
ld hl, POINTS_P2 ; !CAMBIO!
inc l
Como se puede observar, los cambios son pocos. Se ha quitado la última línea al no ser necesario llamar a pintar el segundo dígito del jugador 2, ya que lo vamos a implementar a continuación del último INC L.
Recordemos que cada dígito ocupa 8×16 píxeles (una columna x 16 scanlines).
printPoint_print:
ld b, $10
push de
push hl
Cargamos en B el número de scanlines que vamos a pintar, LD B, $10, y preservamos el valor del registro DE, PUSH DE, y de HL, PUSH HL.
printPoint_printLoop:
ld a, (de)
ld (hl), a
inc de
call NextScan
djnz printPoint_printLoop
Cargamos en A el byte a pintar, LD A, (DE), y lo pintamos en pantalla, LD (HL), A. Apuntamos DE al siguiente byte a pintar, obtenemos la dirección del siguiente scanline, CALL NextScan, y repetimos la operación hasta que B sea 0 y hayamos pintado los 16 scanlines, DJNZ printPoint_printLoop.
Para finalizar, recuperamos los valores de HL y DE y salimos.
pop hl
pop de
ret
El aspecto final de la rutina de pintado del marcador es el siguiente.
; -----------------------------------------------------------------------------
; Pinta el marcador.
; Cada número consta de 1 byte de ancho por 16 de alto.
; Altera el valor de los registros AF, BC, DE y HL.
; -----------------------------------------------------------------------------
PrintPoints:
ld a, (p1points) ; Carga en A los puntos del jugador 1
call GetPointSprite; Obtiene el sprite a pintar en el marcador
push hl ; Preserva el valor de HL
; 1er dígito del jugador 1
ld e, (hl) ; Carga en E la parte baja de la dirección
; donde está el primer dígito
inc hl ; Apunta HL a la parte alta de la dirección
; donde está el primer dígito
ld d, (hl) ; y la carga en D
ld hl, POINTS_P1 ; Carga en HL la dirección de memoria donde se pintan
; los puntos del jugador 1
call printPoint_print ; Pinta el primer dígito del marcador del jugador 1
pop hl ; Recupera el valor de HL
; 2º dígito del jugador 1
inc hl
inc hl ; Apunta HL a la parte baja de la dirección
; donde está el segundo dígito
ld e, (hl) ; y la carga en E
inc hl ; Apunta HL a la parte alta de la dirección
; donde está el segundo dígito
ld d, (hl) ; y la carga en D
ld hl, POINTS_P1 ; Carga en HL la dirección de memoria donde se pintan
; los puntos del jugador 1
inc l ; Apunta HL a la dirección donde se pinta el segundo dígito
call printPoint_print ; Pinta el segundo dígito del marcador del jugador 1
ld a, (p2points) ; Carga en A los puntos del jugador 2
call GetPointSprite; Obtiene el sprite a pintar en el marcador
push hl ; Preserva el valor de HL
; 1er dígito del jugador 2
ld e, (hl) ; Carga en E la parte baja de la dirección
; donde está el primer dígito
inc hl ; Apunta HL a la parte alta de la dirección
; donde está el primer dígito
ld d, (hl) ; y la carga en D
ld hl, POINTS_P2 ; Carga en HL la dirección de memoria donde se pintan
; los puntos del jugador 2
call printPoint_print ; Pinta el primer dígito del marcador del jugador 2
pop hl ; Recupera el valor de HL
; 2º dígito del jugador 2
inc hl
inc hl ; Apunta HL a la parte baja de la dirección
; donde está el segundo dígito
ld e, (hl) ; y la carga en E
inc hl ; Apunta HL a la parte alta de la dirección
; donde está el segundo dígito
ld d, (hl) ; y la carga en D
ld hl, POINTS_P2 ; Carga en HL la dirección de memoria donde se pintan
; los puntos del jugador 2
inc l ; Apunta HL a la dirección donde se pinta el segundo dígito
; Pinta el segundo dígito del marcador del jugador 2
printPoint_print:
ld b, $10 ; Cada dígito son 1 byte por 16 (scanlines)
push de ; Preserva el valor de DE
push hl ; Preserva el valor de HL
printPoint_printLoop:
ld a, (de) ; Carga en A el byte a pintar
ld (hl), a ; Pinta el byte
inc de ; Apunta DE al siguiente byte
call NextScan ; Apunta HL al siguiente scanline
djnz printPoint_printLoop ; Hasta que B = 0
pop hl ; Recupera el valor de HL
pop de ; Recupera el valor de DE
ret
En esta rutina es sencillo ahorrar 12 ciclos de reloj y 2 bytes. Para ello hay que cambiar dos instrucciones de lugar, lo que nos permite quitar otras dos; lo veremos al final del tutorial.
Y ahora solo nos queda ver si lo que hemos implementado funciona. Abrimos Main.asm, y debajo de la llamada a PrintBorder, justo antes de Loop, añadimos la siguiente línea.
call PrintPoints
Compilamos y cargamos en el emulador para ver los resultados.
En principio todo va bien, pero según se va moviendo la bola vemos que volvemos a tener un problema, viejo conocido nuestro, y es que la bola borra el marcador a su paso, cosa que vamos a solucionar a continuación.
Para evitar que la bola borre el marcador, hacemos lo mismo que hicimos con la línea central, vamos a repintar el marcador. Implementamos la rutina al final del archivo Video.asm.
En realidad, la rutina de repintado del marcador es prácticamente igual que la de pintado, cambiando el nombre de las etiquetas y añadiendo una línea. Vamos a copiar toda la rutina de pintado de marcador y la vamos a pegar al final del archivo Video.asm. Cambiamos los nombres de las etiquetas y añadimos una línea.
A continuación, mostramos el aspecto final, marcando los cambios producidos con respecto a la rutina de pintado del marcador.
; -----------------------------------------------------------------------------
; Repinta el marcador.
; Cada número consta de 1 bytes de ancho por 16 de alto.
; Altera el valor de los registros AF, BC, DE y HL.
; -----------------------------------------------------------------------------
ReprintPoints:
ld a, (p1points) ; Carga en A los puntos del jugador 1
call GetPointSprite; Obtiene el sprite a pintar en el marcador
push hl ; Preserva el valor de HL
; 1er dígito del jugador 1
ld e, (hl) ; Carga en E la parte baja de la dirección
; donde está el primer dígito
inc hl ; Apunta HL a la parte alta de la dirección
; donde está el primer dígito
ld d, (hl) ; y la carga en D
ld hl, POINTS_P1 ; Carga en HL la dirección de memoria donde se pintan
; los puntos del jugador 1
call reprintPoint_print ; Pinta el primer dígito del marcador del jugador 1
pop hl ; Recupera el valor de HL
; 2º dígito del jugador 1
inc hl
inc hl ; Apunta HL a la parte baja de la dirección
; donde está el segundo dígito
ld e, (hl) ; y la carga en E
inc hl ; Apunta HL a la parte alta de la dirección
; donde está el segundo dígito
ld d, (hl) ; y la carga en D
ld hl, POINTS_P1 ; Carga en HL la dirección de memoria donde se pintan
; los puntos del jugador 1
inc l ; Apunta HL a la dirección donde se pinta el segundo dígito
call reprintPoint_print ; Pinta el segundo dígito del marcador del jugador 1
ld a, (p2points) ; Carga en A los puntos del jugador 2
call GetPointSprite; Obtiene el sprite a pintar en el marcador
push hl ; Preserva el valor de HL
; 1er dígito del jugador 2
ld e, (hl) ; Carga en E la parte baja de la dirección
; donde está el primer dígito
inc hl ; Apunta HL a la parte alta de la dirección
; donde está el primer dígito
ld d, (hl) ; y la carga en D
ld hl, POINTS_P2 ; Carga en HL la dirección de memoria donde se pintan
; los puntos del jugador 2
call reprintPoint_print ; Pinta el primer dígito del marcador del jugador 2
pop hl ; Recupera el valor de HL
; 2º dígito del jugador 2
inc hl
inc hl ; Apunta HL a la parte baja de la dirección
; donde está el segundo dígito
ld e, (hl) ; y la carga en E
inc hl ; Apunta HL a la parte alta de la dirección
; donde está el segundo dígito
ld d, (hl) ; y la carga en D
ld hl, POINTS_P2 ; Carga en HL la dirección de memoria donde se pintan
; los puntos del jugador 2
inc l ; Apunta HL a la dirección donde se pinta el segundo dígito
; Pinta el segundo dígito del marcador del jugador 2
reprintPoint_print:
ld b, $10 ; Cada dígito es de 1 byte por 16 (scanlines)
push de
push hl ; Preserva el valor de los registros DE y HL
reprintPoint_printLoop:
ld a, (de) ; Carga en A el byte a pintar
or (hl) ; Lo mezcla con lo que hay pintado en pantalla
ld (hl), a ; Pinta el byte
inc de ; Apunta DE al siguiente byte
call NextScan ; Apunta HL al siguiente scanline
djnz reprintPoint_printLoop ; Hasta que B = 0
pop hl
pop de ; Recupera el valor de los registros HL y DE
ret
Vamos a explicar la línea que hemos añadido.
ld a, (de)
or (hl)
ld (hl), a
Lo que hacemos con OR (HL) es agregar los píxeles que hay en pantalla a los píxeles del sprite del número. De esta manera repintamos el número sin borrar la bola.
Ahora queda ver si funciona. Abrimos el archivo Main.asm y añadimos la siguiente línea después de la llamada a ReprintLine.
call ReprintPoints
Compilamos y cargamos en el emulador para ver los resultados.
Efectivamente, hemos solucionado un problema, pero ha surgido otro. El marcador ya no se borra, pero la bola va muy lenta. Por suerte la solución es sencilla, ya que la velocidad de la bola es una de las cosas que controlamos nosotros.
Como recordaréis, la bola se mueve una de cada seis iteraciones del bucle principal, por lo que lo único que tenemos que hacer es reducir este intervalo en Main.asm, por ejemplo a dos.
ld (countLoopBall), a
cp $02 ; ¡CAMBIO!
jr nz, loop_paddle
Compilamos, cargamos en el emulador y comprobamos que la velocidad de la bola ha aumentado.
Cambio de velocidad de la bola
Como recodaremos, en la variable ballSetting definimos la velocidad de la bola en los bits 4 y 5, pudiendo ser 1 la más rápida y 3 la más lenta. Vamos a utilizar este aspecto para definir y modificar la velocidad de la bola.
Lo primero es modificar el valor inicial de esta variable.
ballSetting: db $20
De esta manera el valor inicial es:
- Dirección vertical hacia arriba.
- Dirección horizontal hacia la derecha.
- Velocidad de la bola 2.
Y ahora vamos a usar este valor para controlar el intervalo para mover la bola. Abrimos Main.asm, localizamos la etiqueta Loop, y añadimos justo debajo.
ld a, (ballSetting)
rrca
rrca
rrca
rrca
and $03
ld b, a
Cargamos la configuración de la bola en A, LD A, (ballSetting), pasamos el valor de los bits 4 y 5 a los bits 0 y 1, RRCA RRCA RRCA RRCA, nos quedamos con el valor de los bits 0 y 1 (velocidad de la bola), AND $03, y cargamos el valor en B, LD B, A.
Cuatro líneas más abajo, cambiamos la línea CP $02.
cp b
Compilamos y comprobamos que todo sigue funcionando igual. La única diferencia es que ahora la velocidad de la bola la tomamos desde la configuración de la misma, y podremos cambiarla.
Para cambiar la velocidad de la bola, vamos a usar las teclas del 1 al 3. Abrimos el archivo Controls.asm y empezamos a escribir tras la etiqueta ScanKeys.
scanKeys_speed:
ld a, $00
ld (countLoopBall), a
scanKeys_ctrl:
Si se ha pulsado alguna de las teclas de cambio de velocidad, hay que poner a 0 el contador de vueltas de bucle para pintar la bola, de lo contrario, si el contador está en 2 y ponemos la velocidad a 1, habrá que esperar 254 iteraciones hasta que la bola se vuelva a mover.
Ponemos A = 0, LD A, $00, y ponemos el contador de iteraciones para la bola a 0, LD (countLoopBall), A.
La etiqueta scanKeys_ctrl marca el punto donde empieza la rutina tal y como la tenemos ahora. La nueva implementación la vamos a hacer entre las etiquetas ScanKeys y scanKeys_speed.
ld a, $f7
in a, ($fe)
Cargamos la semifila 1-5 en A, LD A, $F7, y leemos del puerto del teclado, IN A, ($FE).
bit $00, a
jr nz, scanKeys_2
Comprobamos si se ha pulsado el 1, BIT $00, A, y en caso de no haberlo pulsado saltamos a comprobar si se ha pulsado el 2, JR NZ, scanKeys_2.
Si se ha pulsado el 1, cambiamos la velocidad de la bola.
ld a, (ballSetting)
and $cf
or $10
ld (ballSetting), a
jr scanKeys_speed
Cargamos la configuración de la bola en A, LD A, (ballSetting), ponemos los bits de la velocidad a 0, AND $CF, ponemos la velocidad a 1, OR $10, cargamos la configuración en memoria, LD (ballSetting), A, y saltamos a poner a 0 el contador de iteraciones para la bola, JR scanKeys_speed.
La comprobación para el 2 y el 3 es muy parecida a la comprobación del 1, por lo que vemos el código completo y marcamos las diferencias.
scanKeys_2:
bit $01, a ; ¡CAMBIO!
jr nz, scanKeys_3 ; ¡CAMBIO!
ld a, (ballSetting)
and $cf
or $20 ; ¡CAMBIO!
ld (ballSetting), a
jr scanKeys_speed
scanKeys_3:
bit $02, a ; ¡CAMBIO!
jr nz, scanKeys_ctrl ; ¡CAMBIO!
ld a, (ballSetting)
and $cf ; ¡BORRAR!
or $30 ; ¡CAMBIO!
ld (ballSetting), a
El aspecto final de la rutina, una vez modificada, queda de la siguiente manera.
; -----------------------------------------------------------------------------
; ScanKeys
; Escanea las teclas de control y devuelve las pulsadas.
; Salida: D = Teclas pulsadas.
; Bit 0 = A pulsada 0/1.
; Bit 1 = Z pulsada 0/1.
; Bit 2 = 0 pulsada 0/1.
; Bit 3 = O pulsada 0/1.
; Altera el valor de los registros AF y D.
; -----------------------------------------------------------------------------
ScanKeys:
ld a, $f7 ; Carga en A la semifila 1-5
in a, ($fe) ; Lee el estado de la semifila
bit $00, a ; Comprueba si se ha pulsado el 1
jr nz, scanKeys_2 ; Si no se ha pulsado salta
; Se ha pulsado; cambia la velocidad de la bola 1 (rápido)
ld a, (ballSetting) ; Carga la configuración de la bola en A
and $cf ; Pone los bits de velocidad a 0
or $10 ; Pone los bits de velocidad a 1
ld (ballSetting), a; Carga el valor en memoria
jr scanKeys_speed ; Salta para comprobar los controles
scanKeys_2:
bit $01, a ; Comprueba si se ha pulsado el 2
jr nz, scanKeys_3 ; Si no se ha pulsado salta
; Se ha pulsado; cambia la velocidad de la bola 2 (medio)
ld a, (ballSetting) ; Carga la configuración de la bola en A
and $cf ; Pone los bits de velocidad a 0
or $20 ; Pone los bits de velocidad a 2
ld (ballSetting), a; Carga el valor en memoria
jr scanKeys_speed ; Salta para comprobar los controles
scanKeys_3:
bit $02, a ; Comprueba si se ha pulsado el 3
jr nz, scanKeys_ctrl ; Si no se ha pulsado salta
; Se ha pulsado; cambia la velocidad de la bola 3 (lento)
ld a, (ballSetting) ; Carga la configuración de la bola en A
or $30 ; Pone los bits de velocidad a 3
ld (ballSetting), a; Carga el valor en memoria
scanKeys_speed:
ld a, $00 ; Pone A = 0
ld ( countLoopBall), a ; Pone el contador de iteraciones para la bola a 0
scanKeys_ctrl:
ld d, $00 ; Pone el registro D a 0.
; Resto de la rutina desde ScanKeys_A
Es el momento de compilar y cargar en el emulador para comprobar cómo se comporta esta modificación. Si todo ha ido bien, podemos cambiar la velocidad de la bola pulsando las teclas 1, 2 y 3.
Puntuación
Lo último que tenemos que hacer es contabilizar los puntos de cada jugador, para lo cual vamos a modificar la rutina MoveBall, en concreto moveBall_rightChg y moveBall_leftChg. Estas rutinas se encargan de cambiar la dirección de la bola cuando llega al límite izquierdo o derecho. Vamos a implementar lo necesario para que marque los puntos.
El código nuevo lo vamos a poner justo debajo de dichas etiquetas, empezando por moveBall_rightChg.
moveBall_rightChg:
ld hl, p1points
inc (hl)
call PrintPoints
Cargamos en HL la dirección de memoria donde se encuentra el marcador del jugador 1, LD HL, p1points, lo incrementamos, INC (HL), y pintamos el marcador, CALL PrintPoints. El resto de la rutina se queda como estaba.
Las modificaciones en la etiqueta moveBall_leftChg son prácticamente las mismas.
moveBall_leftChg:
ld hl, p2points ; ¡CAMBIO!
inc (hl)
call PrintPoints
Compilamos y cargamos en el emulador para ver los resultados.
Inicio y fin de la partida
Ya tenemos marcador, pero la partida continúa interminablemente y en el momento que pasamos de 15 puntos, empieza a pintar cosas sin sentido.
También podemos apreciar que cada vez va más lento. ¿Pero por qué? Pintamos el marcador en cada iteración, y para localizar el sprite del número a pintar hacemos un bucle, y no es lo mismo un bucle con 15 iteraciones como máximo, que un bucle con hasta 255 iteraciones, ¿a qué no? En la última entrega veremos la forma de implementar GetPointSprite de tal manera que siempre tarde lo mismo, y de paso nos ahorraremos 2 bytes y unos cuantos ciclos de reloj.
Lo que tenemos que hacer ahora es parar la partida cuando alguno de los dos jugadores llegue a 15 puntos; de igual manera vamos a implementar un modo de iniciar la partida, por ejemplo, pulsando el 5.
Al final del archivo Controls.asm vamos a implementar la rutina que espere a que se pulse el 5 para iniciar la partida.
WaitStart:
ld a, $f7
in a, ($fe)
bit $04, a
jr nz, WaitStart
ret
Cargamos en A la semifila 1-5, LD A, $F7, leemos el teclado, IN A, ($FE), evaluamos si se ha pulsado el 5, BIT $04, A, y repetimos la operación hasta que se pulse, JR NZ, WaitStart.
El aspecto final de la rutina es el siguiente.
; -----------------------------------------------------------------------------
; WaitStart.
; Espera que se pulse la tecla 5 para empezar la partida.
; Altera el valor de los registros AF.
; -----------------------------------------------------------------------------
WaitStart:
ld a, $f7 ; Carga en A la semifila 1-5
in a, ($fe) ; Lee el teclado
bit $04, a ; Evalúa si se ha pulsado el 5
jr nz, WaitStart ; Bucle hasta que se pulse el 5
ret
Volvemos a Main.asm y después de la llamada a PrintPoints, ponemos la siguiente línea.
call WaitStart
Si compilamos y cargamos en el emulador, hasta que no pulsemos el 5, no empezaremos la partida, pero con esto no es suficiente, ya que la partida no finaliza cuando uno de los jugadores llega a 15 puntos.
Seguimos en Main.asm, pero esta vez al final de la rutina loop_continue, justo antes de JR Loop. Es aquí donde vamos a implementar el control de la puntuación.
ld a, (p1points)
cp $0f
jr z, Main
Cargamos la puntuación del jugador 1 en A, LD A, (p1points), la comparamos con 15, CP $0F, y si es quince saltamos al inicio del programa, JR Z, Main.
Hacemos lo mismo con la puntuación del jugador 2.
ld a, (p2points) ; ¡CAMBIO!
cp $0f
jr z, Main
Compilamos, cargamos en el emulador y comprobamos que cuando uno de los dos jugadores llega a quince puntos, la partida finaliza.
¿Pero qué pasa si volvemos a pulsar el 5? Ya no hay forma de iniciar la partida. En ningún momento ponemos el marcador a 0. Si dejamos pulsado el 5, veremos como a cada iteración del bucle, vuelve al inicio y se para.
Para solucionar esto, volvemos al inicio del archivo Main.asm, y justo después de la llamada a WaitStart, vamos a poner los marcadores a 0.
ld a, ZERO
ld (p1points), a
ld (p2points), a
call PrintPoints
Ponemos A = 0, LD A, ZERO, ponemos la puntuación del jugador 1 a 0, LD (p1points), A, ponemos la puntuación del jugador 2 a 0, LD (p2points), A, y pintamos el marcador, CALL PrintPoints. De esta manera, cada vez que iniciamos partida, ponemos los marcadores a 0 y los pintamos.
Compilamos y cargamos en el emulador para ver los resultados. Esto empieza a tomar forma.
Todavía nos quedan ajustes por realizar. Vamos a hacer que cuando se marque un tanto, la bola salga por el lado contrario, es decir, como si sacara el jugador que ha marcado. Vamos a implementar una rutina para borrar la bola, otra para situarla en la parte derecha de la pantalla, y otra para situarla en la parte izquierda.
La rutina para borrar la bola la vamos a implementar en el archivo Video.asm, justo antes de la rutina Cls.
ClearBall:
ld hl, (ballPos)
ld a, l
and $1f
cp $10
jr c, clearBall_continue
inc l
Cargamos la posición de la bola en HL, LD HL, (ballPos), cargamos la fila y la columna en A, LD A, L, nos quedamos con la columna, AND $1F, y lo comparamos con el centro de la pantalla, CP $10.
Si hay acarreo, solo puede estar en el margen izquierdo. Saltamos a borrar la bola, JR C, clearBall_continue. Si no salta, está en el margen derecho, pero la bola en realidad está pintada una columna más a la derecha (la bola se pinta en dos bytes/columnas); apuntamos HL a la columna dónde está pintada la bola, INC L.
clearBall_continue:
ld b, $06
clearBall_loop:
ld (hl), ZERO
call NextScan
djnz clearBall_loop
ret
Cargamos en B el número de scanlines que vamos a borrar, LD B, $06, borramos la posición apuntada por HL, LD (HL), ZERO, apuntamos HL al siguiente scanline, CALL NextScan, repetimos la operación hasta que B valga 0, DJNZ clearBall_loop, y salimos, RET.
El aspecto final de la rutina es el siguiente.
; -----------------------------------------------------------------------------
; Borra la bola.
; Altera el valor de los registros AF, B y HL.
; -----------------------------------------------------------------------------
ClearBall:
ld hl, (ballPos) ; Carga la posición de la bola en HL
ld a, l ; Carga la fila y columna en A
and $1f ; Se queda con la columna
cp $10 ; Lo compara con el centro de la pantalla
jr c, clearBall_continue ; Si está a la izquierda salta
inc l ; Incrementa la columna
clearBall_continue:
ld b, $06 ; Bucle por 6 scanlines
clearBall_loop:
ld (hl), ZERO ; Borra el byte apuntado por HL
call NextScan ; Obtiene el scanline siguiente
djnz clearBall_loop; Hasta que B = 0
ret
Las otras dos rutinas las vamos a implementar al final del archivo Game.asm.
SetBallLeft:
ld hl, $4d60
ld (ballPos), hl
ld a, $01
ld (ballRotation), a
ld a, (ballSetting)
and $bf
ld (ballSetting), a
ret
Cargamos en HL la nueva posición de la bola, LD HL, $4D60, y lo cargamos en memoria, LD (ballPos), HL. Cargamos la rotación de la bola en A, LD A, $01, y lo cargamos en memoria, LD (ballRotation), A. Cargamos la configuración de la bola en A, LD A, (ballSetting), ponemos la dirección horizontal hacia la derecha, AND $BF, lo cargamos en memoria, LD (ballSetting), A, y salimos, RET.
La rutina para posicionar la bola a la derecha es prácticamente igual; marcamos las diferencias sin entrar en detalle.
SetBallRight: ; ¡CAMBIO!
ld hl, $4d7e ; ¡CAMBIO!
ld (ballPos), hl
ld a, $ff ; ¡CAMBIO!
ld (ballRotation), a
ld a, (ballSetting)
or $40 ; ¡CAMBIO!
ld (ballSetting), a
ret
El aspecto final de las dos rutinas es el siguiente.
; -----------------------------------------------------------------------------
; Posiciona la bola a la izquierda.
; Altera el valor de los registros AF y HL.
; -----------------------------------------------------------------------------
SetBallLeft:
ld hl, $4d60 ; Carga en HL la posición de la bola
ld (ballPos), hl ; Carga el valor en memoria
ld a, $01 ; Carga 1 en A
ld (ballRotation), a ; Lo carga en memoria, Rotación = 1
ld a, (ballSetting); Carga en A la dirección y velocidad de la bola
and $bf ; Pone la dirección horizontal hacia la derecha
ld (ballSetting), a; Carga la nueva dirección de la bola en memoria
ret
; -----------------------------------------------------------------------------
; Posiciona la bola a la derecha.
; Altera el valor de los registros AF y HL.
; -----------------------------------------------------------------------------
SetBallRight:
ld hl, $4d7e ; Carga en HL la posición de la bola
ld (ballPos), hl ; Carga el valor en memoria
ld a, $ff ; Carga -1 en A
ld (ballRotation), a ; Lo carga en memoria, Rotación = -1
ld a, (ballSetting); Carga en A la dirección y velocidad de la bola
or $40 ; Pone la dirección horizontal hacia la izquierda
ld (ballSetting), a; Carga la nueva dirección de la bola en memoria
ret
Para acabar con este paso, solo nos queda utilizar estas rutinas. Vamos a modificar las rutinas moveBall_rightChg y moveBall_leftChg del archivo Game.asm.
En la rutina moveBall_rightChg, borramos las líneas que hay entre CALL PrintPoints y JR moveBall_end, y las sustituimos por las siguientes.
call ClearBall
call SetBallLeft
El aspecto final de la rutina es el siguiente.
moveBall_rightChg:
; Ha llegado al límite derecho, ¡PUNTO!
ld hl, p1points ; Carga en HL la dirección de la puntuación del jugador 1
inc (hl) ; Lo incrementa
call PrintPoints ; Pinta el marcador
call ClearBall ; Borra la bola
call SetBallLeft ; Pone la bola a la izquierda
jr moveBall_end ; Fin de la rutina
En la rutina moveBall_leftChg, borramos las líneas que hay entre CALL PrintPoints y la etiqueta moveBall_end, y las sustituimos por las siguientes.
call ClearBall
call SetBallRight
El aspecto final de la rutina es el siguiente.
moveBall_leftChg:
; Ha llegado al límite izquierdo, ¡PUNTO!
ld hl, p2points ; Carga en HL la dirección de la puntuación del jugador 2
inc (hl) ; Lo incrementa
call PrintPoints ; Pinta el marcador
call ClearBall ; Borra la bola
call SetBallRight ; Pone la bola a la derecha
Compilamos, cargamos en el emulador, y ya podemos empezar a jugar nuestras primeras partidas a dos jugadores, aunque todavía quedan cosas por hacer.
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.
Pingback: 0x06 Ensamblador ZX Spectrum Marciano - Enemigos - Espamática