0x05 Ensamblador ZX Spectrum Pong – Movemos la bola por la pantalla
Llegamos a una nueva entrega de Ensamblador ZX Spectrum Pong, y en esta ocasión vamos a mover la bola por toda la pantalla, finalizando la primera parte del tutorial.
Tabla de contenidos
Ensamblador ZX Spectrum Pong – Movemos la bola por la pantalla
Creamos la carpeta Paso05, dentro de la misma creamos los archivos Main.asm y Game.asm, y copiamos los archivos Sprite.asm y Video.asm que tenemos en la carpeta Paso04.
Empezamos editando Sprite.asm para añadir dos nuevas constantes que vamos a necesitar para mover la bola por la pantalla.
MARGIN_LEFT: EQU $00
MARGIN_RIGHT: EQU $1e
Igual que tenemos los límites superior e inferior, necesitamos los límites derecho e izquierdo para que la bola se mantenga dentro de los mismos.
El siguiente paso es implementar la lógica del movimiento de la bola, lo que haremos en Game.asm.
MoveBall:
ld a, (ballSetting)
and $80
jr nz, moveBall_down
Primero cargamos en A la configuración actual de la bola, LD A, (ballSetting), y nos quedamos con el bit 7, AND $80, que indica si la bola se desplaza hacia arriba o hacia abajo. Si el bit no está a 0, la bola se desplaza hacia abajo y salta, JR NZ, moveBall_down.
Si el bit está a 0, la bola se desplaza hacia arriba.
moveBall_up:
ld hl, (ballPos)
ld a, BALL_TOP
call CheckTop
jr z, moveBall_upChg
call PreviousScan
ld (ballPos), hl
jr moveBall_x
Cargamos la posición actual de la bola en HL, LD HL, (ballPos), el límite vertical en A, LD A, BALL_TOP, y comprobamos si se ha alcanzado dicho límite, CALL CheckTop. Si se activa el flag Z, se ha alcanzado el límite y saltamos para cambiar la dirección vertical de la bola, JR Z, moveBall_upChg.
Si la bola no ha llegado al límite vertical, calculamos la nueva posición, CALL PreviousScan, la cargamos en memoria, LD (ballPos), HL, y saltamos a comprobar el desplazamiento horizontal, JR moveBall_x.
En el caso de haber alcanzado el límite superior, hay que cambiar la dirección vertical de la bola.
moveBall_upChg:
ld a, (ballSetting)
or $80
ld (ballSetting), a
call NextScan
ld (ballPos), hl
jr moveBall_x
Primero cargamos la configuración de la bola en A, LD A, (ballSetting), luego activamos el bit 7, OR $80, para indicar que ahora la bola debe ir hacia abajo, y cargamos el valor en memoria, LD (ballSetting), A. Calculamos la nueva posición vertical de la bola, CALL NextScan, cargamos el valor en memoria, LD (ballPos), HL, y saltamos a comprobar el desplazamiento horizontal, JR moveBall_x.
Para activar el bit 7 hemos hecho un OR con $80 (10000000). Es conveniente recordar el resultado de la operación OR, dependiendo del valor de los bits.
Bit 1 | Bit 2 | Resultado |
---|---|---|
0 | 0 | 0 |
1 | 0 | 1 |
0 | 1 | 1 |
1 | 1 | 1 |
Según se ve en la tabla, al aplicar OR $80, pone el bit 7 a 1 y el resto los deja como estaban.
Si al iniciar la rutina la bola iba hacia abajo, hay que hacer algo parecido a lo visto anteriormente.
moveBall_down:
ld hl, (ballPos)
ld a, BALL_BOTTOM
call CheckBottom
jr z, moveBall_downChg
call NextScan
ld (ballPos), hl
jr moveBall_x
Primero cargamos la posición de la bola en HL, LD HL, (ballPos), el límite inferior en A, LD A, BALL_BOTTOM, y comprobamos si se ha alcanzado, CALL CheckBottom, en cuyo caso saltamos para cambiar la dirección de la bola, JR Z, moveBall_downChg.
Si no se ha alcanzado el límite inferior, calculamos la nueva posición de la bola, CALL NextScan, la cargamos en memoria, LD (ballPos), HL, y saltamos a comprobar el desplazamiento horizontal, JR moveBall_x.
En el caso de haber alcanzado el límite inferior, hay que cambiar la dirección vertical de la bola.
moveBall_downChg:
ld a, (ballSetting)
and $7f
ld (ballSetting), a
call PreviousScan
ld (ballPos), hl
Primero cargamos la configuración de la bola en A, LD A, (ballSetting), luego desactivamos el bit 7, AND $7F, para indicar que ahora la bola debe ir hacia arriba, y cargamos el valor en memoria, LD (ballSetting), A. Calculamos la nueva posición vertical de la bola, CALL PreviousScan, y cargamos el valor en memoria, LD (ballPos), HL.
Para desactivar el bit 7 hemos hecho un AND con $7F (01111111). Es conveniente recordar el resultado de la operación AND, dependiendo del valor de los bits.
Bit 1 | Bit 2 | Resultado |
---|---|---|
0 | 0 | 0 |
1 | 0 | 0 |
0 | 1 | 0 |
1 | 1 | 1 |
Según se ve en la tabla, al aplicar AND $7F, pone el bit 7 a 0 y el resto los deja como estaban.
Empezamos a calcular el desplazamiento horizontal.
moveBall_x:
ld a, (ballSetting)
and $40
jr nz, moveBall_left
Cargamos la configuración de la bola en A, LD A, (ballSetting), comprobamos el estado del bit 6, AND $40, y si no está a 0, la bola va hacia la izquierda y salta, JR NZ, moveBall_left.
Si el bit 6 está a 0, la bola va hacia la derecha.
moveBall_right:
ld a, (ballRotation)
cp $08
jr z, moveBall_rightLast
inc a
ld (ballRotation), a
jr moveBall_end
Cargamos la rotación en A, LD A, (ballRotation), y comprobamos si está en la última, CP $08, en cuyo caso saltamos, JR Z, moveBall_rightLast.
Si no está en la última rotación, incrementamos la misma, INC A, la cargamos en memoria, LD (ballRotation), A, y saltamos al final de la rutina, JR moveBall_end.
Si, por el contrario, ha llegado a la última rotación y no está en el límite derecho, desplazamos la bola a la siguiente columna.
moveBall_rightLast:
ld a, (ballPos)
and $1f
cp MARGIN_RIGHT
jr z, moveBall_rightChg
ld hl, ballPos
inc (hl)
ld a, $01
ld (ballRotation), a
jr moveBall_end
Cargamos la línea y columna en A, LD A, (ballPos), nos quedamos con la columna, AND $1F, y evaluamos si ha llegado al límite derecho, CP MARGIN_RIGHT, en cuyo caso saltamos para cambiar la dirección de la bola, JR Z, moveBall_rightChg.
Si no se ha llegado al límite derecho, desplazamos la bola a la siguiente columna. Cargamos la dirección donde se encuentra la posición de la bola en HL, LD HL, ballPos, e incrementamos la columna, INC (HL).
Ponemos la rotación de la bola a 1, LD A, $01, lo cargamos en memoria, LD (ballRotation), A, y saltamos al fin de la rutina, JR moveBall_end.
Como se puede ver, para cargar la columna en A, la instrucción usada ha sido LD A, (ballPos), y para incrementar la columna LD HL, ballPos y INC (HL).
Teniendo en cuenta que las posiciones de memoria de la VideRAM se codifican 010TTSS LLLCCCCC, ¿no estaríamos cargando y alterando el scanline? No, y ello se debe a que el Z80 es un micro de tipo Little Endian.
Un micro Little Endian, cuando carga valores de 16 bits en memoria, carga en la primera posición de memoria el byte menos significativo, y en la siguiente el más significativo, de tal manera que si en la posición de memoria $C000 se carga el valor $4000, en la posición $C000 se carga $00 y en la $C001 se carga $40. Es por eso que cuando se carga en A el valor desde (ballPos), lo que carga es el byte menos significativo que es donde están la línea y columna. De igual modo al incrementar (HL), incrementa la columna.
Si la carga se hace sobre un registro de 16 bits, carga el byte menos significativo en la parte baja del registro, y el más significativo en la parte alta. Es por eso que al cargar ballPos en HL, carga en H el byte más significativo de la dirección de memoria y en L el menos significativo.
Seguimos con la rutina…
Si ha llegado al límite derecho, hay que cambiar la dirección de la bola.
moveBall_rightChg:
ld a, (ballSetting)
or $40
ld (ballSetting), a
ld a, $ff
ld (ballRotation), a
jr moveBall_end
Cargamos la configuración de la bola en A, LD A, (ballSetting), activamos el bit 6 para cambiar la dirección hacia la izquierda, OR $40, y cargamos el valor en memoria, LD (ballSetting), A.
Ponemos la rotación de la bola a -1, LD A, $FF, la cargamos en memoria, LD (ballRotation), A, y saltamos al fin de la rutina, JR moveBall_end.
Si la bola va hacia la izquierda, hay que hacer algo parecido a lo visto anteriormente.
moveBall_left:
ld a, (ballRotation)
cp $f8
jr z, moveBall_leftLast
dec a
ld (ballRotation), a
jr moveBall_end
Cargamos la rotación en A, LD A, (ballRotation), comprobamos si está en la última, CP $F8, y de ser así saltamos, JR Z, moveBall_leftLast.
Si no está en la última rotación la decrementamos, DEC A, cargamos el valor en memoria, LD (ballRotation), A, y saltamos al fin de la rutina, JR moveBall_end.
Si ha llegado a la última rotación y no ha alcanzado el límite izquierdo, desplazamos la bola a la columna anterior.
moveBall_leftLast:
ld a, (ballPos)
and $1f
cp MARGIN_LEFT
jr z, moveBall_leftChg
ld hl, ballPos
dec (hl)
ld a, $ff
ld (ballRotation), a
jr moveBall_end
Cargamos la línea y columna en A, LD A, (ballPos), nos quedamos con la columna, AND $1F, y comprobamos si ha llegado al límite izquierdo, CP MARGIN_LEFT, en cuyo caso saltamos, JR Z, moveBall_leftChg.
Si no ha llegado al límite izquierdo, cargamos la dirección donde está la posición de la bola en HL, LD HL, ballPos, y decrementamos la columna, DEC (HL).
Ponemos la rotación de la bola a -1, LD A, $FF, cargamos el valor en memoria, LD (ballRotation), A, y saltamos al fin de la rutina.
Terminamos la rutina con el cambio de dirección, si se ha alcanzado el límite izquierdo.
moveBall_leftChg:
ld a, $01
ld (ballRotation), a
ld a, (ballSetting)
and $bf
ld (ballSetting), a
moveBall_end:
ret
Ponemos la rotación de la bola a 1, LD A, $01, y la cargamos en memoria, LD (ballRotation), A. Cargamos la configuración de la bola en A, LD A, (ballSetting), desactivamos el bit 6 para que la dirección sea hacia la derecha, AND $BF, y cargamos el valor en memoria, LD (ballSetting), A.
Podemos ahorrar 2 ciclos de reloj y 5 bytes haciendo una pequeña modificación. Lo dejamos en vuestras manos y veremos la forma de hacerlo al final del tutorial.
El aspecto final de la rutina es el siguiente.
; -----------------------------------------------------------------------------
; Calcula la posición, rotación y dirección de la bola para pintarla.
; Altera el valor de los registros AF y HL.
; -----------------------------------------------------------------------------
MoveBall:
ld a, (ballSetting) ; Carga en A la dirección y velocidad de la bola
and $80 ; Comprueba la dirección vertical
jr nz, moveBall_down ; Si el bit 7 está a uno, va hacia abajo
moveBall_up:
; La bola va hacia arriba
ld hl, (ballPos) ; Carga la posición de la bola en HL
ld a, BALL_TOP ; Carga en A el margen superior
call CheckTop ; Evalúa si se ha alcanzado el margen superior
jr z, moveBall_upChg ; Si se ha alcanzado salta
call PreviousScan ; Obtiene el scanline anterior a la posición de la bola
ld (ballPos), hl ; Carga en memoria la nueva posición de la bola
jr moveBall_x ; Salta
moveBall_upChg:
; La bola va hacia arriba, pero ha llegado al tope y cambia de dirección
ld a, (ballSetting) ; Carga en A la dirección y velocidad de la bola
or $80 ; Pone la dirección vertical hacia abajo
ld (ballSetting), a ; Carga en memoria la nueva dirección de la bola
call NextScan ; Obtiene el scanline siguiente a la posición de la bola
ld (ballPos), hl ; Carga en memoria la nueva posición de la bola
jr moveBall_x ; Salta
moveBall_down:
; La bola va hacia abajo
ld hl, (ballPos) ; Carga la posición de la bola en HL
ld a, BALL_BOTTOM ; Carga en A el margen superior
call CheckBottom ; Evalúa si se ha alcanzado el margen superior
jr z, moveBall_downChg ; Si se ha alcanzado salta
call NextScan ; Obtiene el scanline siguiente a la posición de la bola
ld (ballPos), hl ; Carga en memoria la nueva posición de la bola
jr moveBall_x ; Salta
moveBall_downChg:
; La bola va hacia abajo, pero ha llegado al tope y cambia de dirección
ld a, (ballSetting) ; Carga en A la dirección y velocidad de la bola
and $7f ; Pone la dirección vertical hacia arriba
ld (ballSetting), a ; Carga en memoria la nueva dirección de la bola
call PreviousScan ; Obtiene el scanline anterior a la posición de la bola
ld (ballPos), hl ; Carga en memoria la nueva posición de la bola
moveBall_x:
ld a, (ballSetting) ; Carga en A la dirección y velocidad de la bola
and $40 ; Comprueba la dirección horizontal
jr nz, moveBall_left ; Si el bit 6 está a uno, va hacia la izquierda
moveBall_right:
; La bola va hacia la derecha
ld a, (ballRotation) ; Carga la rotación actual de la bola
cp $08 ; Comprueba si ya está en la última rotación
jr z, moveBall_rightLast ; Si está en la última rotación salta
inc a ; Incrementa la rotación
ld (ballRotation), a ; La carga en memoria
jr moveBall_end ; Fin de la rutina
moveBall_rightLast:
; Está en la última rotación
; Si no ha llegado al límite derecho pone la rotación a 1
; y pone la bola en la siguiente columna
ld a, (ballPos) ; Carga la línea y columna de la bola en A
and $1f ; Se queda solo con la columna
cp MARGIN_RIGHT ; Lo comprara con el límite derecho
jr z, moveBall_rightChg ; Si lo ha alcanzado salta
ld hl, ballPos ; Carga la dirección de la posición de la bola en HL
inc (hl) ; Incrementa la columna
ld a, $01 ; Pone la rotación a 1
ld (ballRotation), a ; Carga el valor en memoria
jr moveBall_end ; Fin de la rutina
moveBall_rightChg:
; Ha llegado al límite derecho
; Pone la rotación a -1 y cambia la dirección horizontal de la bola
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
ld a, $ff ; Carga -1 en A
ld (ballRotation), a ; Lo carga en memoria Rotación = -1
jr moveBall_end ; Fin de la rutina
moveBall_left:
; La bola va hacia la izquierda
ld a, (ballRotation) ; Carga la rotación actual de la bola
cp $f8 ; Comprueba si ya está en la última rotación
jr z, moveBall_leftLast ; Si está en la última rotación salta
dec a ; Decrementa la rotación
ld (ballRotation), a ; La carga en memoria
jr moveBall_end ; Fin de la rutina
moveBall_leftLast:
; Esta en la última rotación
; Si no ha llegado al límite izquierdo pone la rotación a -1
; y pone la bola en la columna anterior
ld a, (ballPos) ; Carga la línea y columna en A
and $1f ; Se queda solo con la columna
cp MARGIN_LEFT ; Lo comprara con el límite izquierdo
jr z, moveBall_leftChg ; Si lo ha alcanzado salta
ld hl, ballPos ; Carga la dirección de la posición de la bola en HL
dec (hl) ; Pasa a la columna anterior
ld a, $ff ; Pone la rotación a -1
ld (ballRotation), a ; Carga el valor en memoria
jr moveBall_end ; Fin de la rutina
moveBall_leftChg:
; Ha llegado al límite izquierdo
; Pone la rotación a 1 y cambia la dirección
ld a, $01 ; Carga la posición de la bola en HL
ld (ballRotation), a ; Carga el valor 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
moveBall_end:
ret
Ha llegado el momento de probar todo lo implementado, vamos a editar el archivo Main.asm. En este caso la implementación es muy sencilla.
org $8000
ld a, $02
out ($fe), a
call PrintBall
Indicamos la dirección dónde cargar el programa, ponemos el borde en rojo y pintamos la bola en la posición inicial.
Loop:
call MoveBall
call PrintBall
halt
jr Loop
Implementamos un bucle infinito en el que movemos la bola, la pintamos, esperamos al refresco de la pantalla y volvemos a realizar estas tres operaciones indefinidamente.
Include "Game.asm"
Include "Sprite.asm"
Include "Video.asm"
end $8000
Por último, incluimos los archivos necesarios e indicamos a PASMO dónde tiene que llamar al cargar el programa.
El aspecto final de Main.asm es el siguiente.
; Mueve la bola por la pantalla trazando diagonales
org $8000
ld a, $02 ; A = 2
out ($fe), a ; Pone el borde en rojo
call PrintBall ; Imprime la bola
Loop:
call MoveBall ; Mueve la bola
call PrintBall ; Pinta la bola
halt ; Espera al refresco de pantalla
jr Loop ; Bucle infinito
include "Game.asm"
include "Sprite.asm"
include "Video.asm"
end $8000
Llega el gran momento… compilamos y vemos el resultado en el emulador.

Podéis descargar todo el código que hemos generado.
Enlaces de interés
- Notepad++
- Visual Studio Code
- Sublime Text
- ZEsarUX
- PASMO
- Git
- Curso de ensamblador Z80 de Compiler Software
- Referencia Z80
- Ensamblador Z80 en Telegram
- Tutorial completo en formato PDF
- Poyecto en itch.io.
- Archivo .dsk con los juegos de los tutoriales
- Personalización y depuración con ZEsarUX
Vídeo
Si lo prefieres, puedes ver el vídeo que grabamos de esta sesión.
Ensamblador para ZX Spectrum PONG por Juan Antonio Rubio García.
Comentarios al código por Spirax.
Correcciones al texto original realizadas por Joaquín Ferrero.
Esta obra está bajo licencia de Creative Commons Reconocimiento-NoComercial-CompartitIgual 4.0 Internacional License.
Este tutorial ha sido publicado con anterioridad en AUA y se han grabado vídeos que están publicados a través de Retro Parla.
No olvides visitar las webs amigas.
También puedes visitar el resto de tutoriales:
Y recuerda, si lo usas no te limites a copiarlo, intenta entenderlo y adaptarlo a tus necesidades.