0x04 Ensamblador ZX Spectrum Pong – Empezamos a mover la bola
En esta nueva entrega de Ensamblador ZX Spectrum Pong vamos a empezar a mover la bola, en este caso solo de izquierda a derecha, pero nos va a servir para lo que abordaremos más adelante.
Tabla de contenidos
Ensamblador ZX Spectrum – Empezamos a mover la bola
Creamos la carpeta Paso04, dentro de la misma creamos el archivo Main.asm y copiamos los archivos Sprite.asm y Video.asm que tenemos en la carpeta Paso03.
Empezamos editando el archivo Sprite.asm para definir los datos necesarios relativos a la bola.
BALL_BOTTOM: EQU $ba
BALL_TOP: EQU $00
Como hicimos con las palas, definimos los límites inferior y superior para la bola, en formato TTLLLSSS.
ballPos: dw $4870
ballSetting: db $00
ballRotation: db $f8
Al igual que con las palas, vamos a usar una variable donde vamos a tener la posición de la bola en cada momento, ballPos.
En ballSetting vamos a guardar en los bits 0 a 3 la velocidad X, en los bits 4 y 5 la velocidad Y, en el bit 6 la dirección X (0 derecha / 1 izquierda) y en el bit 7 la dirección Y (0 arriba / 1 abajo).
Por último, en ballRotation vamos a guardar la rotación de la bola, indicando con los valores positivos la rotación hacia la derecha y con los negativos hacia la izquierda. La rotación es necesaria debido a la forma en la que vamos a realizar el movimiento horizontal.
La bola va a constar de un scanline en blanco, cuatro scanlines con la parte visible y otro scanline en blanco. Los scanlines en blanco hacen que la bola no deje rastro al moverse.
Vamos a definir 2 bytes para pintar la bola, y a definir cada movimiento píxel a píxel.
; Sprite de la bola. 1 línea a 0, 4 líneas visibles, 1 línea a 0
ballRight: ; Derecha Sprite Izquierda
db $3c, $00 ; +0/$00 00111100 00000000 -8/$f8
db $1e, $00 ; +1/$01 00011110 00000000 -7/$f9
db $0f, $00 ; +2/$02 00001111 00000000 -6/$fa
db $07, $80 ; +3/$03 00000111 10000000 -5/$fb
db $03, $c0 ; +4/$04 00000011 11000000 -4/$fc
db $01, $e0 ; +5/$05 00000001 11100000 -3/$fd
db $00, $f0 ; +6/$06 00000000 11110000 -2/$fe
db $00, $78 ; +7/$07 00000000 01111000 -1/$ff
ballLeft:
db $00, $3c ; +8/$08 00000000 00111100 +0/$00
Cada línea define la parte visible de la bola, dependiendo de como estén los píxeles. Definimos 2 bytes por cada posición. En el comentario vemos la rotación cuando la bola va hacia la derecha, los bits que vamos a pintar, y la rotación cuando la bola va hacia la izquierda.
La bola inicialmente se pinta tal y como muestra el primer sprite.
00111100 00000000
Si se mueve un píxel a la derecha, no cambiamos la posición de la bola, cambiamos la rotación y pintamos el segundo sprite.
000111100 00000000
Al llegar a la última rotación, es cuando cambiamos la posición de la bola, más concretamente la columna. El aspecto final del código es:
; Limites de los objetos en pantalla
BALL_BOTTOM: EQU $ba ; TTLLLSSS
BALL_TOP: EQU $00 ; TTLLLSSS
; Sprite de la bola. 1 línea a 0, 4 líneas 3c, 1 línea a 0
ballRight: ; Derecha Sprite Izquierda
db $3c, $00 ; +0/$00 00111100 00000000 -8/$f8
db $1e, $00 ; +1/$01 00011110 00000000 -7/$f9
db $0f, $00 ; +2/$02 00001111 00000000 -6/$fa
db $07, $80 ; +3/$03 00000111 10000000 -5/$fb
db $03, $c0 ; +4/$04 00000011 11000000 -4/$fc
db $01, $e0 ; +5/$05 00000001 11100000 -3/$fd
db $00, $f0 ; +6/$06 00000000 11110000 -2/$fe
db $00, $78 ; +7/$07 00000000 01111000 -1/$ff
ballLeft:
db $00, $3c ; +8/$08 00000000 00111100 +0/$00
; Posición de la bola
ballPos: dw $4870 ; 010T TSSS LLLC CCCC
; Velocidad y dirección de la bola.
; bits 0 a 3: velocidad X: 1 a 4
; bits 4 a 5: velocidad Y: 0 a 3
; bit 6: dirección X: 0 derecha / 1 izquierda
; bit 7: dirección Y: 0 arriba / 1 abajo
ballSetting: db $00
; Rotación de la bola
; Valores positivos derecha, negativos izquierda
ballRotation: db $f8
Ahora vamos a implementar, en el archivo Video.asm, la rutina que pinta la bola, que vamos a poner después de la rutina PreviousScan.
PrintBall:
ld b, $00
ld a, (ballRotation)
ld c, a
cp $00
ld a, $00
jp p, printBall_right
Lo primero es averiguar hacia dónde va la bola, izquierda o derecha. Una vez averiguado, al sprite base de la bola hay que sumarle o restarle la rotación, para obtener el sprite correcto. La dirección del sprite base la vamos a guardar en HL y restaremos o sumaremos la rotación que tendremos en BC, por eso lo primero es poner B a 0, LD B, $00.
El siguiente paso es cargar la rotación de la bola en A, LD A, (ballRotation), y de ahí cargarlo en C, LD C, A. Podríamos cargar directamente en C, previo paso por HL, pero dependiendo del valor obtendremos si va a derecha o izquierda. Para obtener este valor, comparamos el valor con 0, y como las comparaciones siempre se hacen contra el registro A, de ahí que sea necesario cargar la rotación en este registro.
Comparamos el valor de A con 0, CP A, $00, y si el resultado es positivo la bola se mueve hacia la derecha y salta, JP P, printBall_right. Antes de eso hemos cargado 0 en A para los siguientes cálculos, LD A, $00.
Continuamos implementando el movimiento hacia la izquierda.
printBall_left:
ld hl, ballLeft
sub c
add a, a
ld c, a
sbc hl, bc
jr printBall_continue
Si la bola se mueve hacia la izquierda, lo primero es cargar en HL la dirección del sprite base izquierda, LD HL, ballLeft.
En este punto A vale 0, por lo que se le resta la rotación que tenemos en C, de esta forma conseguimos el valor a restar para situarnos en el sprite correcto.
Ejemplo: C = $FF, A = $00 A – C = $01
Debido a que cada sprite ocupa 2 bytes, hay que duplicar el valor que se va a restar a HL, ADD A, A, y posteriormente cargarlo en C, LD C, A.
Ahora ya podemos calcular la dirección de memoria donde se encuentra el sprite a imprimir, SBC HL, BC, y saltar a imprimir la bola, JR printBall_continue.
Implementamos ahora el movimiento hacia la derecha.
printBall_right:
ld hl, ballRight
add a, c
add a, a
ld c, a
add hl, bc
Si la bola se mueve hacia la derecha, la rutina es ligeramente distinta a la anterior. Volvemos a cargar en HL la dirección del sprite base, LD HL, ballRight, en este caso hacia la derecha, sumamos la rotación en A, ADD A, C, multiplicamos por dos, ADD A, A, y cargamos el resultado en C, LD C, A, para luego sumárselo a HL, ADD HL, BC, y así obtenemos la dirección del sprite a imprimir.
Y ahora imprimimos la bola.
printBall_continue:
ex de, hl
ld hl, (ballPos)
Como la rutina NextScan recibe en HL la dirección actual y devuelve, también en HL, la nueva dirección, lo primero es cargar el valor de HL en DE, EX DE, HL. Con EX intercambiamos el valor de los registros y ahorramos 4 ciclos de reloj y un byte con respecto de hacerlo con LD (LD D, H y LD E, L).
Después cargamos la posición de la bola en HL, LD HL, (ballPos).
ld (hl), ZERO
inc l
ld (hl), ZERO
dec l
call NextScan
Pintamos a 0 el primer byte del primer scanline, LD (HL), ZERO, pasamos al siguiente byte incrementando la columna, INC L, pintamos el segundo byte, LD (HL), ZERO, volvemos a dejar la columna como estaba, DEC L, y calculamos la dirección del siguiente scanline, CALL NextScan.
El siguiente paso es pintar los 4 scanlines que realmente se ven de la bola.
ld b, $04
printBall_loop:
ld a, (de)
ld (hl), a
inc de
inc l
ld a, (de)
ld (hl), a
dec de
dec l
call NextScan
djnz printBall_loop
Carga en B el número de scanlines que vamos a pintar, LD B, $04, carga el primer byte del sprite en A, LD A, (DE), y lo pinta en pantalla, LD (HL), A.
Apunta DE al siguiente byte del sprite, INC DE, apunta HL a la siguiente columna, INC L, carga el sprite en A, LD A, (DE), y lo pinta en pantalla, LD (HL), A.
Vuelve a apuntar DE al primer byte del sprite, DEC DE, vuelve a apuntar HL a la columna anterior, DEC L, y calcula la dirección del scanline siguiente, CALL NextScan.
Repite estas operaciones hasta que B valga 0, DJNZ printBall_loop.
ld (hl), ZERO
inc l
ld (hl), ZERO
ret
Pinta el último scanline de la bola en blanco, primero el primer byte, LD (HL), ZERO, y tras apuntar HL a la siguiente columna, INC L, el segundo, LD (HL), ZERO.
El código final de la rutina queda de la siguiente manera.
; -----------------------------------------------------------------------------
; Pinta la bola.
; Altera el valor de los registros AF, BC, DE y HL.
; -----------------------------------------------------------------------------
PrintBall:
ld b, $00 ; Pone B a 0
ld a, (ballRotation) ; Obtiene la rotación de la bola, para averiguar qué pintar
ld c, a ; Carga el valor en C
cp $00 ; Compara el valor de la rotación con 0 para ver
; si rota a derecha o izquierda
ld a, $00 ; Pone A = 0
jp p, printBall_right ; Si es positivo salta, rota a derecha
printBall_left:
; La rotación de la bola es a izquierda
ld hl, ballLeft ; Carga la dirección donde están los bytes de la bola
sub c ; Resta de A el valor de C, rotación de la bola
add a, a ; Suma A + A. Cada definición de la bola son dos bytes
ld c, a ; Carga el valor en C
sbc hl, bc ; Resta a HL (dirección de los bytes de la bola)
; el desplazamiento para posicionarse en los correctos
jr printBall_continue
printBall_right:
; La rotación de la bola es a derecha
ld hl, ballRight ; Carga la dirección donde están los bytes de la bola
add a, c ; Suma en A el valor de C, rotación de la bola
add a, a ; Suma A + A. Cada definición de la bola son dos bytes
ld c, a ; Carga el valor en C
add hl, bc ; Suma a HL (dirección de los bytes de la bola)
; el desplazamiento para posicionarse en los correctos
printBall_continue:
; Se carga en DE la dirección dónde está la definición de la bola
ex de, hl
ld hl, (ballPos) ; Carga en HL la posición de la bola
; Pinta la primera línea en blanco
ld (hl), ZERO ; Mueve blanco a la posición de pantalla
inc l ; Pasa a la siguiente columna
ld (hl), ZERO ; Mueve blanco a la posición de pantalla
dec l ; Vuelve a la columna anterior
call NextScan ; Pasa al siguiente scanline
ld b, $04 ; Pinta la definición de la bola en las siguientes 4 líneas
printBall_loop:
ld a, (de) ; Carga en A la definición de la bola
ld (hl), a ; Carga la definición de la bola a la posición de pantalla
inc de ; Pasa al siguiente byte de la definición de la bola
inc l ; Pasa a la siguiente columna
ld a, (de) ; Carga en A la definición de la bola
ld (hl), a ; Carga la definición de la bola a la posición de pantalla
dec de ; Vuelve al primer byte de la definición de la bola
dec l ; Vuelve a la columna anterior
call NextScan ; Pasa al siguiente scanline
djnz printBall_loop; Hasta que B = 0
; Pinta la última línea en blanco
ld (hl), ZERO ; Mueve blanco a la posición de pantalla
inc l ; Pasa a la siguiente columna
ld (hl), ZERO ; Mueve blanco a la posición de pantalla
ret
Y ahora ya sólo queda ver si todo lo que hemos implementado funciona, para lo cual vamos a editar el archivo Main.asm.
org $8000
ld a, $02
out ($fe), a
ld a, $00
ld (ballRotation), a
Indicamos la dirección donde cargar el programa, ORG $8000, ponemos A = 2, LD A, $02, para poner el borde en rojo, OUT ($FE), A, y luego ponemos A = 0, LD A, $00, para inicializar la rotación de la bola, LD (ballRotation), A.
Vamos a implementar un bucle infinito para que la bola se mueva indefinidamente.
Loop:
call PrintBall
Lo primero es imprimir la bola, CALL PrintBall, en la posición inicial.
loop_cont:
ld b, $08
loopRight:
exx
ld a, (ballRotation)
inc a
ld (ballRotation), a
call PrintBall
exx
halt
djnz loopRight
En esta primera parte vamos a desplazar, rotar, la bola 8 píxeles hacia la derecha, LD B, $08, haciendo un intercambio de valores con los registros para preservar el valor de B, EXX.
EXX intercambia el valor de los registros de propósito común, con el de los registros alternativos.
BC <= => ‘BC
DE <= => ‘DE
HL <= => ‘HL
Hemos optado en este caso por EXX porque tarda 4 ciclos de reloj y ocupa 1 byte, mientras que PUSH BC tarda 11 ciclos de reloj, y el valor de los registros, exceptuando el de B, no es crítico para ninguna operación que debamos realizar en el bucle, y de paso vemos esta instrucción.
Cargamos en A la rotación actual de la bola, LD A, (ballRotation), incrementamos la rotación, INC A, y cargamos el valor resultante en memoria, LD (ballRotation), A.
Pintamos la bola, CALL PrintBall, volvemos a intercambiar el valor de los registros, EXX, para recuperar el valor de B y hacemos una pausa para poder ver cómo se mueve la bola, HALT.
Repetimos hasta que B valga 0, DJNZ loopRight.
Volvemos a poner a 0 la rotación de la bola, pero esta vez sin pintarla, para empezar a rotar los píxeles hacia la izquierda (ver definición del sprite de la bola).
ld a, $00
ld (ballRotation), a
Ahora vamos a desplazar, rotar, la bola 8 píxeles hacia la izquierda. Solo cambian una instrucción y una etiqueta respecto al desplazamiento hacia la derecha, por lo que no se explica la rutina, simplemente se marca la instrucción que cambia, para que se vea la diferencia.
ld b, $08
loopLeft: ; ¡CAMBIO!
exx
ld a, (ballRotation)
dec a ; ¡CAMBIO!
ld (ballRotation), a
call PrintBall
exx
halt
djnz loopLeft ; ¡CAMBIO!
Para terminar, volvemos a poner la rotación a 0, cargamos el valor en memoria y volvemos a repetir el bucle.
ld a, $00
ld (ballRotation), a
jr loop_cont
Sin olvidarnos de incluir los ficheros Sprite.asm y Video.asm, e indicarle a PASMO dónde tiene que llamar al cargar el programa.
include "Sprite.asm"
include "Video.asm"
end $8000
En realidad, la bola no se mueve, muy al contrario, lo que hacemos es pintarla siempre en las mismas dos columnas, desplazando los píxeles ocho veces hacia la derecha y luego ocho veces hacia la izquierda, para volver a empezar una y otra vez.
El aspecto final del archivo Main.asm es el siguiente.
; Mueve la bola de izquierda a derecha entre dos columnas.
org $8000
ld a, $02 ; A = 2
out ($fe), a ; Pone el borde en rojo
ld a, $00 ; A = 0
ld (ballRotation), a ; Pone la rotación de la bola a 0
Loop:
call PrintBall ; Imprime la bola
loop_cont:
ld b, $08 ; Mueve la bola 8 píxeles a la derecha
loopRight:
exx ; Intercambia el valor de los registros para preservar B
ld a, (ballRotation) ; Recupera la rotación de la bola
inc a ; Incrementa la rotación
ld (ballRotation), a ; Guarda el valor de la rotación
call PrintBall ; Imprime la bola
exx ; Intercambia el valor de los registros para recuperar B
halt ; Se sincroniza con el refresco de la pantalla
djnz loopRight ; Hasta que B = 0
ld a, $00 ; A = 0
ld (ballRotation), a ; Pone la rotación de la bola a 0
ld b, $08 ; Mueve la bola 8 píxeles a la derecha
loopLeft:
exx ; Intercambia el valor de los registros para preservar B
ld a, (ballRotation) ; Recupera la rotación de la bola
dec a ; Decrementa la rotación
ld (ballRotation), a ; Guarda el valor de la rotación
call PrintBall ; Imprime la bola
exx ; Intercambia el valor de los registros para recuperar B
halt ; Se sincroniza con el refresco de la pantalla
djnz loopLeft ; Hasta que B = 0
ld a, $00 ; A = 0
ld (ballRotation), a ; Pone la rotación de la bola a 0
jr loop_cont ; Bucle infinito
include "Sprite.asm"
include "Video.asm"
end $8000
Ya solo queda compilar y ver los resultados 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.