Ensamblador para ZX Spectrum – Pong: $08 Campo, palas, bola y temporización
PorompomPong empieza a tomar forma.

Ensamblador para ZX Spectrum – Pong: Paso 6, campo, palas, bola y temporización
Creamos la carpeta Paso06 y copiamos desde la carpeta Paso05 los archivos Game.asm, Sprite.asm y Video.asm, y desde la carpeta Paso03 el archivo Controls.asm. También creamos el archivo Main.asm.
Empezamos editando el archivo Main.asm, indicando la posición de carga, poniendo el borde en negro, limpiando la pantalla, pintando la línea central y haciendo un bucle infinito para no volver al Basic.
También vamos a incluir el resto de ficheros e indicarle a PASMO dónde llamar al cargar el programa:
org $8000 Main: ld a, $00 out ($fe), a call Cls call PrintLine Loop: jr Loop include "Controls.asm" include "Game.asm" include "Sprite.asm" include "Video.asm" end $8000
Compilamos y vemos el resultado en el emulador.

Pintamos el campo
El siguiente paso es pintar el borde del campo.
Incluimos al inicio del fichero Sprite.asm una nueva constante:
FILL: EQU $ff
La rutina para pintar el borde la implementamos en el archivo Video.asm, antes de la rutina PrintLine:
PrintBorder: ld hl, $4100 ld de, $56e0 ld b, $20
Cargamos en HL la dirección del tercio 0, línea 0, scanline 1, LD HL, $4100, cargamos en DE la dirección del tercio 2, línea 7, scanline 6, LD DE, $56E0, y en B las 32 columnas en las que pintar el borde, LD B, $20.
Implementamos el bucle para pintar el borde:
printBorder_loop: ld a, FILL ld (hl), a ld (de), a inc l inc e djnz printBorder_loop ret
Cargamos en A el sprite del borde, LD A, FILL, lo pintamos en la dirección a la que apunta HL, LD (HL), A, y hacemos los mismo con la dirección a la que apunta DE, LD (DE), A.
Apuntamos HL a la siguiente columna, INC L, y hacemos lo mismo con DE, INC E. Repetimos hasta que B valga 0, DJNZ printBorder_loop, tras lo cual salimos de la rutina, RET.
El aspecto final de la rutina PrintBorder es el siguiente:
; – --------------------------------------------------------------------------- ; Pinta el borde del campo. ; Altera el valor de los registros AD, B, DE y HL. ; – --------------------------------------------------------------------------- PrintBorder: ld hl, $4100 ; Carga en HL la dirección del tercio 0, línea 0 y scanline 1 ld de, $56e0 ; Carga en DE la dirección del tercio 2, línea 7 y scanline 6 ld b, $20 ; Carga en B las 32 columnas en las que pintar printBorder_loop: ld a, FILL ; Carga en A el byte a pintar ld (hl), a ; Pinta en la dirección apuntada por HL ld (de), a ; Pinta en la dirección apuntada por DE inc l ; Apunta HL a la siguiente columna inc e ; Apunta DE a la siguiente columna djnz printBorder_loop ; Bucle hasta que B llegue a 0 ret
Para probar esta rutina, volvemos al archivo Main.asm y tras la llamada a PrintLine ponemos la llamada a la nueva rutina.
call PrintBorder
Compilamos y vemos los resultados en el emulador.

Ya tenemos dibujado el campo donde se va a desarrollar la acción.
Pintamos la bola
Vamos a introducir la bola en nuestro campo. Como la vamos a estar moviendo y pitando constantemente, vamos a introducir las llamadas a mover y pintar la bola dentro del bucle, entre Loop y JR Loop:
call MoveBall call PrintBall
Compilamos y vemos los resultados en el emulador:

Al ver el resultado, observamos dos problemas: la bola borra la línea central y el borde, y se mueve a una velocidad endiablada.
Lo primero que vamos a abordar es la velocidad a la que se mueve la bola.
En el paso anterior poníamos un HALT para esperar el refresco de la pantalla, pero esto hace que vaya demasiado lenta.
Para reducir la velocidad de la bola, vamos a hacer que no se mueva cada vez que pase por el bucle; se va a mover una de cada N veces.
Seguimos en el archivo Main.asm y, antes de END $8000, declaramos la variable donde vamos a llevar la cuenta de las veces que se ha pasado por el bucle:
countLoopBall: db $00
Y ahora vamos a implementar la parte en la que comprobamos si ha pasado las veces suficientes para que movamos la bola, justo después de la etiqueta Loop:
ld a, (countLoopBall) inc a ld (countLoopBall), a cp $0f jr nz, loop_continue call MoveBall
Cargamos el contador donde guardamos las veces que se ha pasado por el bucle sin mover la bola en A, LD A, (countLoopBall), lo incrementamos, INC A, y lo guardamos en memoria, LD (countLoopBall), A.
Comparamos si el contador ha llegado al número de veces necesarias para mover la bola, CP $0F, y si no ha llegado salta, JR NZ, loop_continue.
Si ya hemos llegado al número de veces necesarias de pasadas por el bucle para mover la bola, la movemos, CALL MoveBall.
La etiqueta loop_continue es nueva y la vamos a poner justo encima de la llamada a PrintBall:
loop_continue: call PrintBall
Tenemos que hacer una última cosa. Si el contador ha llegado al número de veces necesario para mover la bola, después de moverla hay que volver a poner el contador a cero, de lo contrario habría que esperar otras 255 veces, en lugar de las que hemos puesto.
Añadimos las siguientes líneas después de la llamada a MoveBall y antes de la etiqueta loop_continue:
ld a, ZERO ld (countLoopBall), a
La implementación de Main.asm quedaría así:
org $8000 Main: ld a, $00 out ($fe), a ; Pone el borde en negro call Cls ; Limpia la pantalla call PrintLine ; Pinta la línea central call PrintBorder ; Pinta el borde Loop: ld a, (countLoopBall) ; Carga en A el contador de la bola inc a ; Incrementa el contador ld (countLoopBall), a ; Carga el valor en memoria cp $0f ; Comprueba si el contador ha llegado a 15 jr nz, loop_continue ; Si no ha llegado, salta call MoveBall ; Mueve la posición de la bola ld a, ZERO ; Pone A = 0 ld (countLoopBall), a ; Carga el valor en memoria loop_continue: call PrintBall ; Pinta la bola jr Loop ; Bucle infinito include "Game.asm" include "Controls.asm" include "Sprite.asm" include "Video.asm" countLoopBall: db $00 ; Contador para controlar cuando se mueve la bola end $8000
Compilamos y vemos el resultado en el emulador. Esta vez si vemos como se mueve la bola a una velocidad más aceptable.
No hemos definido una constante para la comprobación con el contador de la bola ya que, en un futuro, la velocidad será variable.
Ahora vamos a abordar el problema de las partes que va borrando la bola a su paso, y vamos a empezar por la línea central.
En una primera aproximación, vamos a repintar la parte de la línea que coincide en la coordenada Y con la bola, sin importar si la bola está pasando por encima o no. Parece innecesario, pero nos va a ayudar a temporizar.
Abrimos el archivo Video.asm e implementamos después de la rutina PrintLine:
ReprintLine: ld hl, (ballPos) ld a, l and $e0 or $10 ld l, a
Cargamos la posición de la bola en HL, LD HL, (ballPos), cargamos la línea y columna en A, LD A, L, nos quedamos con el valor de la línea, AND $E0, ponemos la columna a 16, que es donde está la línea vertical, OR $10, y cargamos el valor en L, LD L, A.
Vamos a repintar 6 scanlines, que son los mismos que tiene la bola:
ld b, $06 reprintLine_loop: ld a, h
Carga en B el número de scanlines que se repintan, LD B, $06, y carga tercio y scanline en A, LD A, H.
Para pintar la línea, en los scanlines 0 y 7 pintábamos en blanco, y en el resto la parte visible de la línea:
and $07 cp $01 jr c, reprintLine_00 cp $07 jr z, reprintLine_00
Nos quedamos con la parte del scanline, AND $07, y comprobamos si es 1, CP $01. Si el scanline es menor que 1 saltamos, JR C, reprintLine_00, en el caso contrario comprobamos si es 7, CP $07. Si el scanline es 7 saltamos, JR Z, reprintLine_00.
Si no hemos saltado, el scanline está entre 1 y 6:
ld c, LINE jr reprintLine_loopCont
Cargamos el sprite de la línea en C, LD C, LINE, y saltamos, JR reprintLine_loopCont.
Si anteriormente saltamos, el scanline es 0 o 7:
reprintLine_00: ld c, ZERO
Cargamos el sprite blanco en C, LC C, ZERO, y pintamos lo que corresponda:
reprintLine_loopCont: ld a, (hl) or c ld (hl), a call NextScan djnz reprintLine_loop ret
Cargamos el valor de la dirección de memoria del byte que vamos a repintar en A, LD A, (HL), le añadimos los píxeles del repintado de la línea, OR C, y lo pintamos en pantalla, LD (HL), A. Calculamos la dirección de memoria del scanline siguiente, CALL NextScan, y repetimos la operación hasta que B valga 0, DJNZ reprintLine_loop. Por último, salimos, RET.
El aspecto final de la rutina es el siguiente:
; – --------------------------------------------------------------------------- ; Repinta la línea central. ; Altera el valor de los registros AF, BC y HL. ; – --------------------------------------------------------------------------- ReprintLine: ld hl, (ballPos) ; Carga en HL la posición de la bola ld a, l ; Carga la línea y columna en A and $e0 ; Se queda con la línea or $10 ; Pone la columna a 16 ($10) ld l, a ; Carga el valor en L. HL = Posición inicial repintar ld b, $06 ; Se repintan 6 scanlines reprintLine_loop: ld a, h ; Carga tercio y scanline en A and $07 ; Se queda con el scanline ; Si está en los scanlines 0 o 7 pinta ZERO ; Si está en los scanlines 1, 2, 3, 4, 5 o 6 pinta LINE cp $01 ; Comprueba si está en scanline 1 o superior jr c, reprintLine_00 ; Si está por debajo, pinta $00 cp $07 ; Comprueba si está en scanline 7 jr z, reprintLine_00 ; Si es así, pinta ZERO ld c, LINE ; Esta en scanline 1 a 6, pinta LINE jr reprintLine_loopCont ; Salta reprintLine_00: ld c, ZERO ; Está en scanline 0 o 7, pinta ZERO reprintLine_loopCont: ld a, (hl) ; Obtiene los pixeles de la posición actual or c ; Los mezcla con C ld (hl), a ; Pinta el resultado en la posición actual call NextScan ; Obtiene el scanline siguiente djnz reprintLine_loop ; Hasta que B = 0 ret
Podemos ahorrar 5 bytes y 22 ciclos de reloj, modificando 8 líneas de la rutina. Lo dejamos en vuestras manos y veremos la forma de hacerlo en la última entrega.
Ensamblador para ZX Spectrum
Ya solo queda probar lo que hemos implementado, para lo cual abrimos el archivo Main.asm y después de la llamada a PrintBall incluimos la llamada a ReprintLine.
call ReprintLine
Compilamos y vemos los resultados en el emulador:

La línea central ya no se borra, pero podemos apreciar que la velocidad de la bola ha disminuido. Hay que tener en cuenta que ahora realizamos más operaciones que antes. Según avancemos iremos ajustando la velocidad de la bola.
Vamos a evitar que se borre el borde, para lo cual vamos a modificar los límites superior e inferior de la bola, en el fichero Sprite.asm:
BALL_BOTTOM: EQU $b8 BALL_TOP: EQU $02
Compilamos, cargamos en el emulador y comprobamos que ya no se borra el borde.
Incluímos las palas
Ahora vamos a empezar con las palas. Volvemos a Main.asm y añadimos las siguientes líneas entre CALL ReprintLine y JR Loop:
ld hl, (paddle1pos) call PrintPaddle ld hl, (paddle2pos) call PrintPaddle
Cargamos la posición de la pala 1 en HL, LD HL, (paddle1pos), y pintamos, CALL PrintPaddle. Hacemos los mismo con la pala 2.
Como se puede apreciar, las palas se pintan en todas las iteraciones del bucle, al igual que la bola y el repintado de línea.
Compilamos y vemos los resultados en el emulador:

Se dibujan las palas y la bola no las borra al pasar. También se aprecia que ahora la bola va mucho más lenta, debido a que hacemos más operaciones en cada iteración del bucle.
Para que la bola vuelva a ir más rápida, vamos a cambiar en Main.asm el valor que tenía que alcanzar el contador para que la bola se moviese:
ld (countLoopBall), a cp $06 ; # Cambiamos $0f por $06 # jr nz, loop_continue
Compilamos, cargamos en el emulador y comprobamos que la bola vuelve a ir más rápido.
Ahora vamos a implementar la rutina para mover las palas. Ya vimos como hacerlo en el paso 03. Editamos el archivo Game.asm y vamos al final del mismo.
La rutina que vamos a implementar, recibe en el registro D las pulsaciones de las teclas de control:
MovePaddle: bit $00, d jr z, movePaddle_1Down
Evaluamos si se ha pulsado la tecla arriba del jugador 1, BIT $00, D. Si no se ha pulsado saltamos a comprobar si se ha pulsado la tecla abajo, JR Z, movePaddle_1Down.
Si no salta, se ha pulsado la tecla abajo del jugador 1:
ld hl, (paddle1pos) ld a, PADDLE_TOP call CheckTop jr z, movePaddle_2Up
Cargamos la posición de la pala 1 en HL, LD HL, (paddle1pos), el límite superior para las palas en A, LD A, PADDLE_TOP, y comprobamos si se ha alcanzado, CALL CheckTop. Si se ha alcanzado el límite, saltamos a comprobar los controles del jugador 2, JR Z, movePaddle_2Up.
Si no se ha alcanzado el límite superior, movemos la pala 1:
call PreviousScan ld (paddle1pos), hl jr movePaddle_2Up
Calculamos la nueva posición para la pala 1, CALL PreviousScan, la cargamos en memoria, LD (paddle1pos), HL, y saltamos a comprobar los controles del jugador 2, JR movePaddle_2Up.
Si no se ha pulsado la tecla arriba del jugador 1, comprobamos si se ha pulsado la tecla abajo:
movePaddle_1Down: bit $01, d jr z, movePaddle_2Up
Evaluamos si se ha pulsado la tecla abajo del jugador 1, BIT $01, D. Si no se ha pulsado salta a comprobar los controles del jugador 2, JR Z, movePaddle_2Up.
Si no salta, se ha pulsado la tecla abajo del jugador 1:
ld hl, (paddle1pos) ld a, PADDLE_BOTTOM call CheckBottom jr z, movePaddle_2Up
Cargamos la posición de la pala 1 en HL, LD HL, (paddle1pos), el límite inferior para las palas en A, LD A, PADDLE_BOTTOM, y comprobamos si se ha alcanzado, CALL CheckBottom. Si se ha alcanzado el límite saltamos a comprobar los controles del jugador 2, JR Z, movePaddle_2Up.
Si no se ha alcanzado el límite inferior, mueve la pala 1:
call NextScan ld (paddle1pos), hl
Calculamos la nueva posición para la pala 1, CALL NextScan, y la cargamos en memoria, LD (paddle1pos), HL.
Hacemos las comprobaciones con los controles del jugador 2. Dada la semejanza, simplemente marcamos los cambios con respecto a la comprobación del jugador 1:
movePaddle_2Up: ; # Cambio # bit $02, d ; # Cambio # jr z, movePaddle_2Down ; # Cambio # ld hl, (paddle2pos) ; # Cambio # ld a, PADDLE_TOP call CheckTop jr z, movePaddle_End ; # Cambio # call PreviousScan ld (paddle2pos), hl ; # Cambio # jr movePaddle_End ; # Cambio # movePaddle_2Down: ; # Cambio # bit $03, d ; # Cambio # jr z, movePaddle_End ; # Cambio # ld hl, (paddle2pos) ; # Cambio # ld a, PADDLE_BOTTOM call CheckBottom jr z, movePaddle_End ; # Cambio # call NextScan ld (paddle2pos), hl ; # Cambio # movePaddle_End: ; # Cambio # ret ; # Cambio #
El aspecto final de la rutina es el siguiente:
; – --------------------------------------------------------------------------- ; Calcula la posición de las palas para moverlas. ; Entrada: D -> Pulsaciones de los controles ; Altera el valor de los registros AF y HL. ; – --------------------------------------------------------------------------- MovePaddle: bit $00, d ; Evalúa si se ha pulsado la A jr z, movePaddle_1Down ; 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, movePaddle_2Up ; 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 movePaddle_2Up ; Salta movePaddle_1Down: bit $01, d ; Evalúa si se ha pulsado la Z jr z, movePaddle_2Up ; 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, movePaddle_2Up ; 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 movePaddle_2Up: bit $02, d ; Evalúa si se ha pulsado el 0 jr z, movePaddle_2Down ; 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, movePaddle_End ; 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 movePaddle_End ; Salta movePaddle_2Down: bit $03, d ; Evalúa si se ha pulsado la O jr z, movePaddle_End ; 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, movePaddle_End ; 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 movePaddle_End: ret
Podemos ahorrar 2 bytes y 2 ciclos de reloj, de la misma forma que en la entrega anterior. Esta vez no daremos la solución en la última entrega, ya que es similar a la que se verá para la entrega anterior.
Ensamblador para ZX Spectrum
Para terminar, vamos a implementar en Main.asm las llamadas a esta rutina, dentro de nuestro bucle infinito, justo encima de la etiqueta loop_continue:
loop_paddle: call ScanKeys call MovePaddle
Primero comprobamos las teclas de control que se han pulsado, CALL ScanKeys, y luego movemos las palas, CALL MovePaddle.
También tenemos que cambiar la etiqueta a la que salta cuando la bola no se mueve, 4 líneas más arriba:
cp $06 jr nz, loop_paddle ; # Cambio # call MoveBall
Compilamos y probamos en el emulador:

Observamos dos problemas:
- Las palas borran el borde.
- Las palas se mueven muy rápido y son difíciles de controlar.
Para resolver el primer problema vamos a modificar las constantes que marcan los límites superior e inferior de las palas, que están en Sprite.asm:
PADDLE_BOTTOM: EQU $a6 PADDLE_TOP: EQU $02
Compilamos, cargamos en el emulador y comprobamos que ya no se borra el borde:

Para reducir la velocidad del movimiento de las palas, vamos a usar la misma técnica que usamos con la bola; no vamos a mover las palas a cada iteración del bucle.
Lo primero es declarar la variable que usaremos como contador, lo que hacemos antes de END $8000:
countLoopPaddle: db $00
Ahora, justo debajo de la etiqueta loop_paddle, implementamos la comprobación del contador:
ld a, (countLoopPaddle) inc a ld (countLoopPaddle), a cp $02 jr nz, loop_continue call MovePaddle
Cargamos el contador en A, LD A, (countLoopPaddle), lo incrementamos, INC A, y lo cargamos en memoria, LD (countLoopPaddle), A. Evaluamos si han pasado las veces que hemos definido para mover las palas, CP $02, y si no es así saltamos, JR NZ, loop_continue.
Si no salta, movemos las palas, CALL MovePaddle, y tal y como hicimos con la bola, hay que poner el contador a cero. Añadimos las líneas siguientes antes de la etiqueta loop_continue:
ld a, ZERO ld (countLoopPaddle), a
Cargamos 0 en A, LD A, ZERO, y lo cargamos en memoria, LD (countLoopPaddle), A, poniendo el contador a 0.
Compilamos y cargamos en el emulador. Ahora el control de las palas es menos rápido y más preciso.
El código final de Main.asm es el siguiente:
; Pintado de campo, movimiento de palas y bola y temporización org $8000 ; – --------------------------------------------------------------------------- ; Entrada al programa ; – --------------------------------------------------------------------------- Main: ld a, $00 ; A = 0 out ($fe), a ; Pone el borde en negro call Cls ; Limpia la pantalla call PrintLine ; Imprime la línea central call PrintBorder ; Imprime el borde del campo Loop: ld a, (countLoopBall) ; Carga el contador de vueltas de la bola inc a ; Lo incrementa ld (countLoopBall), a ; Lo carga en memoria cp $06 ; Comprueba si ha llegado a 6 jr nz, loop_paddle ; Si no ha llegado a 4 salta call MoveBall ; Mueve la bola ld a, ZERO ; Pone el contador a 0 ld (countLoopBall), a ; Lo carga en memoria loop_paddle: ld a, (countLoopPaddle) ; Carga el contador de vueltas de las palas inc a ; Lo incrementa ld (countLoopPaddle), a ; Lo carga en memoria cp $02 ; Comprueba si ha llegado a 2 jr nz, loop_continue ; Si no ha llegado a 2 salta call ScanKeys ; Escanea las teclas pulsadas call MovePaddle ; Mueva las palas ld a, ZERO ; Pone el contador a 0 ld (countLoopPaddle), a ; Lo carga en memoria loop_continue: call PrintBall ; Pinta la bola call ReprintLine ; Repinta la línea ld hl, (paddle1pos) ; Carga en HL la posición de la pala 1 call PrintPaddle ; Pinta la pala 1 ld hl, (paddle2pos) ; Carga en HL la posición de la pala 2 call PrintPaddle ; Pinta la pala 2 jr Loop ; Bucle infinito include "Game.asm" include "Controls.asm" include "Sprite.asm" include "Video.asm" countLoopBall: db $00 ; Contador de vueltas de la bola countLoopPaddle: db $00 ; Contador de vueltas de las palas end $8000
En el próximo capítulo de Ensamblador para ZX Spectrum, implementaremos la detección de colisiones.
Enlaces de interés
- Notepad++.
- Visual Studio Code.
- Sublime Text.
- ZEsarUX.
- PASMO.
- Git.
- Curso de ensamblador Z80 de Compiler Software.
- Z80 instruction set.
poco a poco va fomando forma !!!