Ensamblador para ZX Spectrum – Pong: $07 Movemos la bola por la pantalla
Moviendo la bola por toda la pantalla.

Ensamblador para ZX Spectrum – Pong: Paso 5, 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 el archivo 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 verticales y horizontales, 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 salta para cambiar la dirección vertical de la bola, JR Z, moveBall_upChg.
Si la bola no ha llegado al límite vertical, calcula la nueva posición, CALL PreviousScan, la carga en memoria, LD (ballPos), HL, y salta 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 movelBall_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, se 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, movelBall_downChg.
Si no se ha alcanzado el límite inferior, calculamos las 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, movelBall_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 la 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 final 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 VideoRAM se codifican 010TTSSS 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 la 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 la 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 dónde 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 en la última entrega.
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.

En el próximo capítulo de Ensamblador para ZX Spectrum, pintaremos el campo, las palas, la bola y temporizaremos.
Enlaces de interés
- Notepad++.
- Visual Studio Code.
- Sublime Text.
- ZEsarUX.
- PASMO.
- Git.
- Curso de ensamblador Z80 de Compiler Software.
- Z80 instruction set.