Curso Programación ZX SpectrumCursos

Ensamblador para ZX Spectrum – Pong: $05 Palas y línea central

Palas y línea central, primeros elementos de PorompomPong.

4.8
(6)

Ensamblador para ZX Spectrum – Pong: Paso 3, palas y línea central

Ya hemos adquirido los conocimientos suficientes para empezar con el desarrollo de nuestro Pong. Hemos implementado una buena parte de la base del programa.

En este paso vamos a:

  • Cambiar el color del borde.
  • Asignar los atributos de color a la pantalla.
  • Dibujar la línea central del campo.
  • Dibujar las palas de ambos jugadores.
  • Mover las palas hacia arriba y hacia abajo.

Como siempre, creamos una carpeta a la que vamos a llamar Paso03, y dentro de la misma creamos los archivos Main.asm y Sprite.asm.

Esta vez no empezamos desde cero, ya que hemos desarrollado en los pasos anteriores código, en los ficheros Controls.asm y Video.asm, que vamos a usar en este paso, por lo que copiamos los dos ficheros en el nuevo directorio.

Cambiar el color del borde

Es el primer paso que vamos a realizar. Aunque el color del borde final será igual al del resto de la pantalla, en los primeros pasos lo vamos a poner en rojo para visualizar los límites de la misma.

Vamos a editar el fichero Main.asm, y lo primero, como siempre, es indicar la dirección de memoria donde vamos a cargar el programa:

org     $8000

Lo siguiente es poner el borde en rojo:

ld      a, $02
out     ($fe), a

Con LD A, $02 cargamos el valor del color rojo en A. Luego escribimos este valor en el puerto $FE (256), OUT ($FE), A. Este puerto ya lo conocemos, pues es el puerto desde donde leemos el estado del teclado.

Por último, salimos del programa e indicamos a PASMO donde llamar cuando lo cargue.

ret
end     $8000

Compilamos con PASMO y vemos el resultado final:

Ensamblador para ZX Spectrum, cambiamos el color del borde
Ensamblador para ZX Spectrum, cambiamos el color del borde

El código de Main.asm queda así:

org     $8000
ld      a, $02      ; A = 2
out     ($fe), a    ; Pone el borde en rojo
ret
end     $8000

Asignar los atributos de color a la pantalla

En nuestro caso, los atributos son blanco para la tinta y negro para el fondo.

Vamos a implementar una rutina, Cls, que limpia la pantalla y pone el fondo en negro y la tinta en blanco.

Los atributos de la pantalla se encuentran a continuación del área donde se dibuja; empieza en la dirección $5800 y tiene una longitud de $300 (768) bytes, 32 columnas por 24 líneas. En el ZX Spectrum, los atributos de color van a nivel de carácter. Cada atributo afecta a un área de 8×8 píxeles, siendo este el motivo del famoso «Attribute Clash«.

Los atributos de un carácter están definidos en un byte:

Bit 7Bit 6Bit 5 – Bit 4 – Bit 3Bit 2 – Bit 1 – Bit – 0
Parpadeo (0/1)Brillo (0/1)Fondo (0 a 7)Tinta (0 a 7)
Ensamblador para ZX Spectrum, definición de un atributo de color

La rutina Cls consta de dos partes:

  • Limpia la pantalla.
  • Asigna el color de tinta y fondo.

Vamos a editar el archivo Video.asm y vamos a implementar la rutina:

Cls:
ld      hl, $4000
ld      (hl), $00
ld      de, $4001
ld      bc, $17ff
ldir
ret

Lo primero que hace nuestra rutina es apuntar HL al inicio de la VideoRAM, LD HL, $4000, y limpia ese byte de la pantalla, LD (HL), $00.

El siguiente paso es apuntar DE a la posición siguiente a HL, LD DE, $4001, y cargar en BC el número de bytes a limpiar, LD BC, $17FF, que es toda el área de la VideoRAM ($1800) menos uno, que es la posición donde apunta HL, y ya está limpia.

LDIR, LoadData, Increment and Repeat, carga el valor que hay en la posición de memoria a la que apunta HL, a la posición de memoria a la que apunta DE. Una vez realizado esto, incrementa HL y DE. Repite en bucle hasta que BC llegue a 0. Por último, salimos de la rutina.

Abrimos el archivo Main.asm y antes de RET añadimos la llamada a Cls:

call    Cls

Antes de END $8000, añadimos la línea para incluir el archivo Video.asm:

include "Video.asm"

Compilamos con PASMO y cargamos en el emulador:

Ensamblador para ZX Spectrum, limpiamos los píxeles de la pantalla
Ensamblador para ZX Spectrum, limpiamos los píxeles de la pantalla

Como se aprecia en la imagen, ya no sale la línea Bytes: PoromPong, lo cual demuestra que hemos limpiado la pantalla.

Para implementar la segunda parte de la rutina, la asignación de los atributos de color, vamos a escribir las siguientes líneas justo antes de la instrucción RET de la rutina Cls:

ld      hl, $5800
ld      (hl), $07
ld      de, $5801
ld      bc, $2ff
ldir

Lo primero que hace esta parte de la rutina es apuntar HL al inicio del área de atributos, LD HL, $5800, y pone esa zona sin parpadeo, sin brillo, con el fondo en negro y la tinta en blanco, LD (HL), $07.

$07 = 0000 0111 = 0 (parpadeo) 0 (brillo) 000 (fondo) 111 (tinta)

El siguiente paso es apuntar DE a la posición siguiente a HL, LD DE, $5801, y cargar en BC el numero de bytes a cargar, LD BC, $2FF, que es toda el área de atributos ($300) menos uno, que es la posición donde apunta HL, y ya tiene los atributos. Se ejecuta LDIR, y se asigna el color a toda la pantalla.

El código completo de la rutina es:

; – ---------------------------------------------------------------------------
; Limpia la pantalla, tinta 7, fondo 0.
; Altera el valor de los registros AF, BC, DE y HL.
; – ---------------------------------------------------------------------------
Cls:
; Limpia los píxeles de la pantalla
ld      hl, $4000   ; Carga en HL el inicio de la VideoRAM
ld      (hl), $00   ; Limpia los píxeles de esa dirección
ld      de, $4001   ; Carga en DE la siguiente posición de la VideoRAM
ld      bc, $17ff   ; 6143 repeticiones
ldir                ; Limpia todos los píxeles de la VideoRAM

; Pone la tinta en blanco y el fondo en negro
ld      hl, $5800   ; Carga en HL el inicio del área de atributos
ld      (hl), $07   ; Lo pone con la tinta en blanco y el fondo en negro
ld      de, $5801   ; Carga en DE la siguiente posición del área de atributos
ld      bc, $2ff    ; 767 repeticiones
ldir                ; Asigna el valor a toda el área de atributos

ret

Llegados a este punto, compilamos y vemos el resultado:

Ensamblador para ZX Spectrum, limpiamos la pantalla
Ensamblador para ZX Spectrum, limpiamos la pantalla

Como se puede observar, además de limpiar la pantalla, ha puesto el fondo en negro y la tinta en blanco, aunque al no haber pintado nada en la pantalla, no se ve si la tinta está realmente en blanco.

Para ver distintos efectos, cambiad los valores que cargáis en (HL).

Esta rutina se puede cambiar, haciéndonos ahorrar 8 ciclos de reloj y 4 bytes. Dejamos en vuestras manos averiguar la manera de hacerlo, y daremos la solución en la última entrega de curso. No os preocupéis, no es una rutina crítica, así que no va a afectar al desarrollo de nuestro videojuego.

Dibujar la línea central del campo

La línea central del campo está compuesta por un primer scanline en blanco, otros seis con el bit 7 a 1, y un último scanline en blanco:

00000000
10000000
10000000
10000000
10000000
10000000
10000000
00000000

En este caso solo vamos a definir la parte en blanco y la parte que pinta la línea. Abrimos el fichero Sprite.asm y añadimos las siguientes líneas:

ZERO:   EQU $00
LINE:   EQU $80

Con la directiva EQU, se definen valores constantes que no se compilan, al contario, lo que hace el compilador es sustituir todas las referencias que haya en el código a estas etiquetas, por el valor que se ha asignado a las mismas.

Ejemplo:    ld a, ZERO    ->    Compilador    ->    ld a, $00

Una vez que tenemos el «sprite» de la línea, vamos a implementar la rutina para pintarla. Volvemos al archivo Video.asm:

PrintLine:
ld      b, $18
ld      hl, $4010

Vamos a pintar el «sprite» de nuestra línea en las 24 líneas de la pantalla, LD B, $18, y vamos a empezar en el primer scanline, de la primera línea, del primer tercio, columna 16, LD HL, $4010.

printLine_loop:
ld      (hl), ZERO
inc     h
push    bc

Pintamos el primer scanline en blanco, LD (HL), ZERO, luego pasamos al siguiente scanline, INC H, y por último preservamos el valor de BC en la pila, ya que vamos a usar B para hacer un bucle que pinte la parte que se ve de la línea.

Para cambiar de scanline, directamente incrementamos H en lugar de llamar a NextScan. ¿Por qué? Sencillo. Dado que vamos a pintar los 8 scanlines de un mismo carácter, ni cambiamos de línea, ni de tercio, por lo que con aumentar el scanline es suficiente, y ahorramos tiempo de proceso y bytes.

Otra cosa que hacemos es subir un valor a la pila, concretamente el de BC. Es muy importante recordar que cada PUSH debe tener un POP, y además, si hay varios PUSH, tiene que haber el mismo número de POP, pero en orden inverso:

push af
push bc
pop  bc
pop  af

Ahora vamos a hacer el bucle que pinta la parte que se ve de la línea:

ld      b, $06
printLine_loop2:
ld      (hl), LINE
inc     h
djnz    printLine_loop2
pop     bc

Lo primero es indicar el número de iteraciones del nuevo bucle, LD B, $06, pintamos el scanline con la parte visible de la línea, LD (HL), LINE, pasamos al siguiente scanline, INC H, y repetimos hasta que B valga 0, DJNZ printLine_loop2. Cuando B valga 0, recuperamos el valor de BC de la pila para continuar con el bucle de las 24 líneas de la pantalla, POP BC.

Y llegamos así a la parte final de la rutina:

ld      (hl), ZERO
call    NextScan
djnz    printLine_loop
ret

Pintamos el último scanline del carácter, LD (HL), ZERO, recuperamos el siguiente scanline, CALL NextScan, y repetimos hasta que B valga 0 y se hayan pintado las 24 líneas de la pantalla, DJNZ printLine_loop. Esta vez sí llamamos a NextScan, ya que cambiamos de línea.

El aspecto final de la rutina es el siguiente:

; – ---------------------------------------------------------------------------
; Imprime la línea central.
; Altera el valor de los registros AF, B y HL.
; – ---------------------------------------------------------------------------
PrintLine:
ld      b, $18          ; Se imprime en las 24 líneas de pantalla
ld      hl, $4010       ; Se empieza en la línea 0, columna 16

printLine_loop:
ld      (hl), ZERO      ; En el primer scanline se imprime el byte en blanco
inc     h               ; Pasa al siguiente scanline

push    bc              ; Preserva el valor de BC para realizar el segundo bucle
ld      b, $06          ; Se imprime seis veces
printLine_loop2:
ld      (hl), LINE      ; Imprime el byte de la línea, $10, b00010000
inc     h               ; Pasa el siguiente scanline
djnz    printLine_loop2 ; Hasta que B = 0
pop     bc              ; Recupera el valor de BC
ld      (hl), ZERO      ; Imprime el último byte de la línea a 0
call    NextScan        ; Pasa al siguiente scanline
djnz    printLine_loop  ; Hasta que B = 0 = 24 líneas
ret

Y ahora solo queda probarlo, para lo cual abrimos el fichero Main.asm y añadimos tras la llamada a Cls, la llamada a PrintLine e incluimos el fichero Sprite.asm igual que hicimos con el fichero Video.asm:

call    PrintLine

include "Sprite.asm"

Compilamos y vemos el resultado en el emulador:

Ensamblador para ZX Spectrum, pintamos la línea central
Ensamblador para ZX Spectrum, pintamos la línea central

Ahora sí, se puede observar que habíamos puesto la tinta en blanco.

Dibujar las palas de ambos jugadores

En este paso vamos a dibujar las palas de ambos jugadores, que van a ocupar 1×3 caracteres, 1 byte (8 píxeles) y 24 scanlines.

Vamos a usar el mismo tipo de definición que usamos para definir la línea horizontal, y lo vamos a hacer en el archivo Sprite.asm:

PADDLE: EQU $3c

Esta sería la parte visible de la pala, 00111100, ya que vamos a pintar el primer scanline en blanco, 22 scanlines con esta definición y el último scanline en blanco.

La palas van a ser elementos móviles, por lo que además de su «sprite», necesitamos saber en qué posición se encuentran y cuales son los márgenes superior e inferior a los que las podemos mover.

Seguimos en el fichero Sprite.asm:

PADDLE_BOTTOM:  EQU $a8     ; TTLLLSSS
PADDLE_TOP:     EQU $00     ; TTLLLSSS
paddle1pos:     dw  $4861   ; 010T TSSS LLLC CCCC
paddle2pos:     dw  $487e   ; 010T TSSS LLLC CCCC

En la dos primeras constantes, que son los límites hasta donde podemos mover las palas, vamos a especificar las coordenada Y expresada en tercio, línea y scanline. Mientras que PADDLE_TOP sí apunta al límite superior de la pantalla (tercio 0, línea 0, scanline 0), PADDLE_BOTTOM no apunta al límite inferior de la pantalla (tercio 2, línea 7, scanline 7), por el contrario, apunta al tercio 2, línea 5, scanline 0, que es el resultado de restarle al límite inferior ($BF), 23 scanlines para que podamos pintar los 24 scanlines del sprite de la pala, sin invadir el área de atributos de la pantalla.

paddle1pos y paddle2pos no son constantes, pues estos valores van a cambiar respondiendo a las pulsaciones de la teclas de control.

La posición inicial de las palas es:

Pala 1Pala 2
Tercio11
Línea33
Scanline00
Columna130
Ensamblador para ZX Spectrum, posiciones iniciales de las palas

Una vez definido esto, vamos al archivo Video.asm e implementamos la rutina que dibuja las palas. Esta rutina tiene como parámetro de entrada la posición de la pala, que se recibe en HL. Es necesario porque tenemos dos palas que imprimir, y la otra alternativa sería duplicar la rutina y que cada una imprimiera una pala.

PrintPaddle:
ld      (hl), ZERO
call    NextScan

Lo primero que hace es pintar en blanco el primer scanline de la pala, LD (HL), ZERO, y luego obtiene el siguiente scanline.

Al contrario de lo que pasaba al pintar la línea central, en esta rutina sí son necesarias las llamadas a NextScan. Nuestro movimiento de la pala va a ser pixel a pixel, esto en vertical es scanline a scanline, lo que hace que no sepamos de antemano cuándo cambiamos de línea (en realidad sí podríamos saberlo).

Lo siguiente es pintar la parte visible de la pala:

ld      b, $16
printPaddle_loop:
ld      (hl), PADDLE
call    NextScan
djnz    printPaddle_loop

La parte visible de la pala la vamos a pintar en 22 scanlines, LD B, $16, cargando en la posición apuntada por HL el sprite de la pala, LD (HL), PADDLE, y obteniendo el siguiente scanline, CALL NextScan, hasta que B valga 0, DJNZ printPaddle_loop.

Por último, pinta en blanco el último scanline de la pala:

ld      (hl), ZERO
ret

Pintar en blanco el primer y el último scanline sirve para que, al mover la pala, se vaya auto borrando y no deje rastro.

El aspecto final de la rutina es el siguiente:

; – ---------------------------------------------------------------------------
; Imprime la pala.
; Entrada:  HL -> Posición de la pala
; Altera el valor de los registros B y HL.
; – ---------------------------------------------------------------------------
PrintPaddle:
ld      (hl), ZERO          ; Imprime el primer byte de la pala en blanco
call    NextScan            ; Pasa al siguiente scanline

ld      b, $16              ; Pinta el byte visible de la pala 22 veces
printPaddle_loop:
ld      (hl), PADDLE        ; Imprime el byte de la pala
call    NextScan            ; Pasa al siguiente scanline
djnz    printPaddle_loop    ; Hasta que B = 0

ld      (hl), ZERO          ; Imprime el último byte de la pala en blanco

ret

Por último, tenemos que probar si nuestra rutina funciona. Abrimos el archivo Main.asm y añadimos después de la llamada a PrintLine:

ld      hl, (paddle1pos)
call    PrintPaddle
ld      hl, (paddle2pos)
call    PrintPaddle

Cargamos en HL la posición de la pala 1, LD HL, (paddle1pos), y la pintamos, CALL PrintPaddle. Hacemos lo mismo con la pala 2.

Compilamos y vemos los resultados:

Ensamblador para ZX Spectrum, pintamos la palas
Ensamblador para ZX Spectrum, pintamos la palas

Mover las palas hacia arriba y hacia abajo

Abordamos la última parte del paso 3.

Anteriormente declaramos unas constantes con los límites inferior y superior. Ahora vamos a implementar las rutinas que comprueban si, una posición de memoria de la VideoRAM, ha llegado o está fuera de un límite especificado.

El conjunto de rutinas que vamos a implementar, recibe en el registro A el límite en formato TTLLLSSS, y la posición actual en HL, en formato 010TTSSS LLLCCCCC. Estás rutinas devuelven Z si se ha alcanzado el límite y NZ en el caso contrario:

CheckBottom:
call    checkVerticalLimit
ret     c

Lo primero que hace es llamar a la rutina checkVerticalLimit, CALL checkVerticalLimit, y en el caso de haya acarreo sale, RET C, con NZ. Si hay acarreo, la posición de memoria está por encima del límite inferior.

checkBottom_bottom:
xor     a
ret

Si llega hasta aquí es porque ha llegado al límite inferior, activa el flag Z, XOR A, y sale, RET.

Esta rutina no hace gran cosa, por lo que se puede suponer que el grueso de la lógica estará en checkVerticalLimit.

Vamos a implementar la rutina para el límite superior:

CheckTop:
call    checkVerticalLimit
jr      c, checkTop_top
ret     nz

Igual que en la rutina anterior, se llama a checkVerticalLimit. En este caso no se ha llegado al límite si no hay acarreo y el resultado de checkVerticalLimit no es 0, o lo que es lo mismo, es mayor de 0, de ahí la doble condición, JR C, checkTop_top y RET NZ.

checkTop_top:
xor     a
ret

Llega aquí si el resultado de checkVerticalLimit es <= 0 (hay acarreo o el resultado es 0), en cuyo caso activa el flag Z, XOR A, y sale, RET.

El grueso de la detección de los límites, inferior y superior, lo realiza la rutina checkVerticalLimit, que recibe en A el límite vertical (TTLLLSSS) y en HL la posición actual (010TTSSS LLLCCCCC), o posición con la que comparar.

Debido al distinto formato que tenemos en HL y en A, el primer paso es pasar el contenido que tiene HL, al mismo formato que tiene el contenido de A.

checkVerticalLimit:
ld      b, a
ld      a, h
and     $18
rlca
rlca
rlca
ld      c, a

Lo primero que hacemos es preservar el valor de A, LD B, A, y acto seguido cargamos el valor de H en A, LD A, H, y nos quedamos con el tercio, AND $18. Rotamos circularmente tres veces el registro A hacia la izquierda, RLCA, para poner el tercio en los bits 6 y 7, y cargamos el valor en C, LD C, A. Ahora C tiene el tercio de la posición que hemos recibido en HL.

ld      a, h
and     $07
or      c
ld      c, a

Volvemos a cargar el valor de H en A, LD A, H, pero esta vez nos quedamos con el scanline, AND $07. Ahora tenemos en A el scanline que viene en HL, y le añadimos el tercio que hemos guardado en C, OR C, y cargamos el resultado en C, LD C, A. Ahora C tiene el tercio y el scanline que hemos recibido en HL, pero con el mismo formato que hemos recibido en A (TT000SSS).

ld      a, l
and     $e0
rrca
rrca
or      c

Ahora vamos a poner el valor de la línea donde le corresponde, cargando el valor de L en A, LD A, L, quedándonos con los bits donde viene la línea, AND $E0, y rotando circularmente dos veces los bits resultantes para poner la línea en los bits 3, 4 y 5, RRCA. Por último, agregamos el tercio y el scanline que hemos guardado en C, OR C, de tal manera que en A tenemos ahora el tercio, la línea y el scanline que venían en HL, pero con el formato que necesitamos (TTLLLSSS).

cp      b
ret

El último paso es comparar lo que ahora tenemos en A con lo que tenemos en B, que es el valor original de A (límite vertical), CP B.

Esta última operación va a alterar, entre otros, los flags de acarreo y cero:

ResultadoZC
A = B1 – Z0 – NC
A < B0 – NZ1 – C
A > B0 – NZ0 – NC
Ensamblador para ZX Spectrum, flags Z y C según el resultado de la instrucción CP

Dependiendo de estos flags, y si se está evaluando el límite inferior o el superior, sabremos sí se ha llegado o traspasado dicho límite.

El código completo de este conjunto de rutinas es el siguiente:

; – ---------------------------------------------------------------------------
; Evalúa si se ha alcanzado el límite inferior.
; Entrada:  A -> Límite superior (TTLLLSSS).
;           HL -> Posición actual (010TTSSS LLLCCCCC).
; Salida:   Z = Se ha alcanzado.
;           NZ = No se ha alcanzado.
; Altera el valor de los registros AF y BC.
; – ---------------------------------------------------------------------------
CheckBottom:
call    checkVerticalLimit	; Compara la posición actual con el límite
; Si Z o NC, ha llegado al tope, se pone Z, de lo contrario NZ
ret     c
checkBottom_bottom:
xor     a                   ; Activa Z
ret

; – ---------------------------------------------------------------------------
; Evalúa si se ha alcanzado el límite superior.
; Entrada:  A -> Margen superior (TTLLLSSS).
;           HL -> Posición actual (010TTSSS LLLCCCCC).
; Salida:   Z = Se ha alcanzado.
;           NZ = No se ha alcanzado.
; Altera el valor de los registros AF y BC.
; – ---------------------------------------------------------------------------
CheckTop:
call    checkVerticalLimit  ; Compara la posición actual con el límite
; Si Z o C, ha llegado al tope, se pone Z, de lo contrario NZ
jr      c, checkTop_top     ; Ha llegado al límite superior y salta
ret     nz                  ; No ha llegado al límite superior y sale
checkTop_top:
xor     a                   ; Activa Z
ret

; – ---------------------------------------------------------------------------
; 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
ld      a, h    ; Carga en A el valor de H (010TTSSSS)
and     $18     ; Se queda con el tercio
rlca
rlca
rlca            ; Pone el valor del tercio en los bits 6 y 7
ld      c, a    ; Carga el valor en C
ld      a, h    ; Vuelve a cargar en A el valor de H (010TTSSSS)
and     $07     ; Se queda con el scanline
or      c       ; Añade el tercio
ld      c, a    ; Carga el valor en C
ld      a, l    ; Carga en A el valor de L (LLLCCCCC)
and     $e0     ; Se queda con la línea
rrca
rrca            ; Pone el valor de la línea en los bits 3, 4 y 5
or      c       ; Añade el tercio y el scanline. A = TTLLLSSS
cp      b       ; Lo compara con B. B = valor original de A = Límite vertical
ret

Usando estas rutinas, ya podemos implementar el movimiento de las palas y evitar que se salgan de la pantalla.

Editamos el fichero Main.asm e incluimos el fichero Controls.asm:

include "Controls.asm"

Vamos a implementar un bucle infinito en el que se evalúa si se ha pulsado alguna tecla de control, en cuyo caso movemos la pala que corresponda. El bucle lo vamos a implementar justo después de la llamada a PrintLine:

loop:
call    ScanKeys

Lo primero que hace el bucle es evaluar si se ha pulsado alguna de las teclas de control, CALL ScanKeys.

MovePaddle1Up:
bit     $00, d
jr      z, MovePaddle1Down
ld      hl, (paddle1pos)
ld      a, PADDLE_TOP
call    CheckTop
jr      z, MovePaddle2Up
call    PreviousScan
ld      (paddle1pos), hl
jr      MovePaddle2Up

Después de evaluar los controles, evalúa si se ha pulsado la tecla de control para mover la pala 1 hacia arriba, BIT $00, D, y si no es así salta a la siguiente comprobación, JR Z, MovePaddle1Down.

Para mover la pala hacia arriba tenemos que ver si al moverla se sale del límite superior, para lo cual necesitamos saber la posición actual de la pala, LD HL, (paddle1pos), obtener el límite superior, LD A, PADDLE_TOP, y verificar si se ha alcanzado, CALL CheckTop.

Si CheckTop activa el flag Z siginifica que hemos alcanzado el límite, por lo que saltamos a comprobar el movimiento de la pala 2, JR Z, MovePaddle2Up.

Si no se activa el flag Z, obtenemos la posición en la que se debe pintar la pala, CALL PreviousScan, y la cargamos en memoria, LD (paddle1pos), HL. Por último, saltamos a comprobar el movimiento de la pala 2, JR MovePaddle2Up.

Si no se ha pulsado la tecla de control arriba de la pala 1, se verifica si se ha pulsado la de abajo:

MovePaddle1Down:
bit     $01, d 
jr      z, MovePaddle2Up
ld      hl, (paddle1pos)
ld      a, PADDLE_BOTTOM
call    CheckBottom
jr      z, MovePaddle2Up
call    NextScan
ld      (paddle1pos), hl

Evalúa si se ha pulsado la tecla de control para mover la pala 1 hacia abajo, BIT $01, D, y si no es así salta a la siguiente comprobación, JR Z, MovePaddle2Up.

Para mover la pala hacia abajo tenemos que comprobar si, al moverla, se sale del límite inferior, para lo cual necesitamos saber la posición actual de la pala, LD HL, (paddle1pos), obtener el límite inferior, LD A, PADDLE_BOTTOM, y verificar si se ha alcanzado, CALL CheckBottom.

Si CheckBottom activa el flag Z significa que hemos alcanzado el límite, por lo que saltamos a comprobar el movimiento de la pala 2, JR Z, MovePaddle2Up.

Si no se activa el flag Z, obtenemos la posición en la que se debe pintar la pala, CALL NextScan, y la cargamos en memoria, LD (paddle1pos), HL. En esta ocasión no saltamos, ya que en la siguiente instrucción se empieza a comprobar el movimiento de la pala 2.

Debido a que la comprobación del movimiento de la pala 2 es muy parecido al de la pala 1, cambian las posiciones de memoria para obtener la posición de la pala 2 y las de salto, no vamos a entrar a explicarlo en detalle:

MovePaddle2Up:
bit     $02, d
jr      z, MovePaddle2Down
ld      hl, (paddle2pos)	
ld      a, PADDLE_TOP
call    CheckTop
jr      z, MovePaddleEnd	
call    PreviousScan
ld      (paddle2pos), hl
jr      MovePaddleEnd

MovePaddle2Down:
bit     $03, d 
jr      z, MovePaddleEnd
ld      hl, (paddle2pos)
ld      a, PADDLE_BOTTOM
call    CheckBottom
jr      z, MovePaddleEnd
call    NextScan
ld      (paddle2pos), hl

MovePaddleEnd:

La última línea, MovePaddleEnd, es una etiqueta que hemos usado para poder saltar a la zona donde se pintan las palas.

Por último, después de pintar las palas, vamos a sustituir RET por JR loop, para quedarnos en un bucle infinito.

El código final del archivo Main.asm queda como sigue:

; Dibuja las dos palas y la línea central.
; Mueve las palas arriba y abajo como respuesta a la pulsación de las teclas de control.
org     $8000
ld      a, $02              ; A = 2
out     ($fe), a            ; Pone el borde en rojo

call    Cls                 ; Limpia la pantalla
call    PrintLine           ; Imprime la línea central

loop:
call    ScanKeys            ; Escanea las teclas pulsadas

MovePaddle1Up:
bit     $00, d              ; Evalúa si se ha pulsado la A
jr      z, MovePaddle1Down  ; Si no se ha pulsado salta
ld      hl, (paddle1pos)    ; Carga en HL la posición de la pala 1
ld      a, PADDLE_TOP       ; Carga en A el margen superior
call    CheckTop            ; Evalúa si se ha alcanzado el margen superior
jr      z, MovePaddle2Up    ; Si se ha alcanzado, salta
call    PreviousScan        ; Obtiene el scanline anterior a la posición de la pala 1
ld      (paddle1pos), hl    ; Carga en memoria la nueva posición de la pala 1
jr      MovePaddle2Up       ; Salta

MovePaddle1Down:
bit     $01, d              ; Evalúa si se ha pulsado la Z               
jr      z, MovePaddle2Up    ; Si no se ha pulsado salta
ld      hl, (paddle1pos)    ; Carga en HL la posición de la pala 1
ld      a, PADDLE_BOTTOM    ; Carga en A el margen inferior
call    CheckBottom         ; Evalúa si se ha alcanzado el margen inferior
jr      z, MovePaddle2Up    ; Si se ha alcanzado, salta
call    NextScan            ; Obtiene el scanline siguiente a la posición de la pala 1
ld      (paddle1pos), hl    ; Carga en memoria la nueva posición de la pala 1

MovePaddle2Up:
bit     $02, d              ; Evalúa si se ha pulsado el 0
jr      z, MovePaddle2Down  ; Si no se ha pulsado salta
ld      hl, (paddle2pos)    ; Carga en HL la posición de la pala 2
ld      a, PADDLE_TOP       ; Carga en A el margen superior
call    CheckTop            ; Evalúa si se ha alcanzado el margen superior
jr      z, MovePaddleEnd    ; Si se ha alcanzado, salta
call    PreviousScan        ; Obtiene el scanline anterior a la posición de la pala 2
ld      (paddle2pos), hl    ; Carga en memoria la nueva posición de la pala 2
jr      MovePaddleEnd       ; Salta

MovePaddle2Down:
bit     $03, d              ; Evalúa si se ha pulsado la O
jr      z, MovePaddleEnd    ; Si no se ha pulsado salta
ld      hl, (paddle2pos)    ; Carga en HL la posición de la pala 2
ld      a, PADDLE_BOTTOM    ; Carga en A el margen inferior
call    CheckBottom         ; Evalúa si se ha alcanzado el margen inferior
jr      z, MovePaddleEnd    ; Si se ha alcanzado, salta
call    NextScan            ; Obtiene el scanline siguiente a la posición de la pala 2
ld      (paddle2pos), hl    ; Carga en memoria la nueva posición de la pala 2

MovePaddleEnd:
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 "Controls.asm"
include "Sprite.asm"
include "Video.asm"

end     $8000

Compilamos y vemos los resultados en el emulador:

Ensamblador para ZX Spectrum, movemos las palas
Ensamblador para ZX Spectrum, movemos las palas

En el próximo capítulo de Ensamblador para ZX Spectrum, empezaremos a mover la bola.

Enlaces de interés

Ficheros

¿Te ha Resultado útil este artículo?

Ayúdanos a mejorar y danos tu opinión:

Mostrar más

Un comentario

Deja una respuesta

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

Publicaciones relacionadas

Mira también
Cerrar
Botón volver arriba