Curso Programación ZX SpectrumCursos

Ensamblador para ZX Spectrum – Pong: $0A Partida a dos jugadores y cambio de velocidad de la bola

Búscate a un amigo, que vamos a empezar a jugar.

5
(1)

Ensamblador para ZX Spectrum – Pong: Paso 8, partida a dos jugadores y cambio de velocidad de la bola.

En este paso vamos a implementar la partida a dos jugadores, con marcador, y la posibilidad de cambiar la 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.

Marcador

Vamos a empezar por el marcador, definiendo la posición dónde vamos a pintar la puntuación, y definiendo también los sprites necesarios en el archivo Sprite.asm.

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, 1 carácter de ancho por 2 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 que 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:

; – ---------------------------------------------------------------------------
; 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 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 dónde 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
Ensamblador para ZX Spectrum, valores en memoria de la etiqueta Cero

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 dónde 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, CALL printPoint_print.

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 (1 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, INC DE, 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 en la última entrega.

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:

Ensamblador para ZX Spectrum, dibujamos el marcador, pero se borra
Ensamblador para ZX Spectrum, dibujamos el marcador, pero se borra

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 la rutina de pintado del 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 de la rutina:

; – ---------------------------------------------------------------------------
; Repinta el marcador.
; Cada número consta de 4 bytes de ancho por 16 de alto = 64 bytes.
; 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:

Ensamblador para ZX Spectrum, repintamos el marcador
Ensamblador para ZX Spectrum, repintamos el marcador

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 1 de cada 6 iteraciones del bucle principal, por lo que lo único que tenemos que hacer es reducir este intervalo en Main.asm, por ejemplo a 2:

ld      (countLoopBall), a
cp      $02                     ; # Cambiamos el intervalo #
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 recordaremos, 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: arriba.
  • Dirección horizontal: derecha.
  • Velocidad: 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 algunas 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 las 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)
ld      d, a
bit     $00, d
jr      nz, scanKeys_2

Cargamos la semifila 1-5 en A, LD A, $F7, leemos el puerto del teclado, IN A, ($FE), y cargamos el resultado en D, LD D, A.

bit     $00, d
jr      nz, scanKeys_2

Comprobamos si se ha pulsado el 1, BIT $00, D, 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, d                  ; # Cambio #
jr      nz, scanKeys_3          ; # Cambio #
ld      a, (ballSetting)
and     $cf
or      $20                     ; # Cambio #
ld      (ballSetting), a
jr      scanKeys_speed
scanKeys_3:
bit     $02, d                  ; # Cambio #
jr      nz, scanKeys_speed      ; # Cambio #
ld      a, (ballSetting)
; and     $cf                   ; # Eliminar línea #
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.

Empezamos la partida

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 movelBall_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 movelBall_leftChg son prácticamente las mismas:

moveBall_leftChg:
ld      hl, p2points
inc     (hl)
call    PrintPoints

Compilamos y cargamos en el emulador para ver los resultados:

Ensamblador para ZX Spectrum, ya tenemos marcador funcional
Ensamblador para ZX Spectrum, ya tenemos marcador funcional

Ya tenemos marcador, pero la partida continúa interminablemente y cuando pasamos de 15 puntos, empieza a pintar cosas sin sentido.

Ensamblador para ZX Spectrum. ¿Pero qué números son esos?
Ensamblador para ZX Spectrum. ¿Pero qué números son esos?

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?

Fin de la partida

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:

; – ---------------------------------------------------------------------------
; 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 los mismo con la puntuación del jugador 2:

ld      a, (p2points)
cp      $0f
jr      z, Main

Compilamos, cargamos en el emulador y comprobamos que cuando uno de los dos jugadores llega a 15 puntos, la partida finaliza:

Ensamblador para ZX Spectrum, la partida se para al llegar a 15 puntos
Ensamblador para ZX Spectrum, la partida se para al llegar a 15 puntos

¿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_right, borramos las líneas que hay entre CALL PrintPoints y JR moveBall_end, y las sustituimos por:

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:

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.

En el próximo capítulo de Ensamblador para ZX Spectrum, implementaremos el cambio de dirección y velocidad de la bola al golpear la pala, entrando de esta manera en la recta final.

Enlaces de interés

Ficheros

¿De cuánta utilidad te ha parecido este contenido?

¡Haz clic en una estrella para puntuar!

Mostrar más

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

Publicaciones relacionadas

Botón volver arriba