¡Hola a todos!, hay problemas con la web que espero poder arreglar lo antes posible, ¡mil perdones!

Ver índice de webs/blogs

Espamatica : 0x03 Ensamblador ZX Spectrum Pong – Palas y línea central Leer


Llegados a esta nueva entrega de Ensamblador ZX Spectrum Pong, ya hemos adquirido los conocimientos suficientes para empezar con el desarrollo de nuestro PorompomPong, hemos implementado una buena parte de la base del programa.

Ensamblador ZX Spectrum Pong – Palas y línea central

En este paso vamos a:

  • Cambiar el color del borde.
  • Asignar 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 de 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 dónde 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 y luego escribimos este valor en el puerto $FE (256), OUT ($FE), A. Este puerto ya lo conocemos, pues es el puerto desde dónde leemos el estado del teclado.

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

ret
end		$8000

Compilamos con PASMO y vemos el resultado final.

Ensamblador ZX Spectrum Pong - Borde
Ensamblador ZX Spectrum, color del borde en rojo

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 $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 éste 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 a7)Tinta (0 a 7)
Ensamblador ZX Spectrum, definición de los atributos 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 ZX Spectrum Pong - Cls
Ensamblador ZX Spectrum, Cls

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 número 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 ZX Spectrum Pong - Cls completo
Ensamblador ZX Spectrum, Cls completo

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 al final del tutorial. 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

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 contrario, 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 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 pinte 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 ya sólo 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 ZX Spectrum Pong - Línea central
Ensamblador ZX Spectrum, 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.

Las palas van a ser elementos móviles, por lo que además de su «sprite», necesitamos saber en qué posición se encuentran y cuáles 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 las dos primeras constantes, que son los límites hasta donde podemos mover las palas, vamos a especificar la 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.

Por otro lado, paddle1pos y paddle2pos no son constantantes, pues estos valores van a cambiar respondiendo a las pulsaciones de las teclas de control.

La posición inicio de las palas es:

Pala 1Pala 2
Tercio11
Línea33
Scanline00
Columna130
Ensamblador ZX Spectrum, posición inicial de las palas

Una vez definido esto, vamos al archivo Video.asm e implementamos las 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 pintar, y la otra alternativa sería duplicar la rutina y que cada una pintara 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 si 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 cuando cambiamos de línea (en realidad si 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 fina 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 ZX Spectrum Pong - Palas
Ensamblador ZX Spectrum, 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. Estas 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 que 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 qué 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 el valor 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 ZX Spectrum, resultados de CP B

Dependiendo de estos flags, y si se está evaluando el límite inferior o el superior, sabremos si 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 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 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		2, 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		3, 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 ZX Spectrum Pong - Palas en movimiento
Ensamblador ZX Spectrum, palas en movimiento

Podéis descargar todo el código que hemos generado.

Enlaces de interés

Vídeo

Si lo prefieres, puedes ver el vídeo que grabamos de esta sesión.

Ensamblador ZX Spectrum, palas y línea central

Ensamblador para ZX Spectrum PONG por Juan Antonio Rubio García.
Esta obra está bajo licencia de Creative Commons Reconocimiento-NoComercial-CompartitIgual 4.0 Internacional License.
Correcciones al texto original realizadas por Joaquín Ferrero.
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.

AUA

Aquí puedes ver más cosas que he desarrollado para .Net, y aquí la desarrolladas en ensamblador para Z80.

Y recuerda, si lo usas no te limites a copiarlo, intenta entenderlo y adaptarlo a tus necesidades.