Ensamblador para ZX Spectrum – Pong: $06 Empezamos a mover la bola
Primer paso para mover la bola lateralmente, píxel a píxel.
Ensamblador para ZX Spectrum – Pong: Paso 4, 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, 4 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/\; 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/$0000111100 00000000 -8/$f8 db $1e, $00 ; +1/\ 00011110 00000000 -7/$f9 db $0f, $00 ; +2/\ 00001111 00000000 -6/$fa db $07, $80 ; +3/\ 00000111 10000000 -5/$fb db $03, $c0 ; +4/\ 00000011 11000000 -4/$fc db $01, $e0 ; +5/\ 00000001 11100000 -3/$fd db $00, $f0 ; +6/\ 00000000 11110000 -2/$fe db $00, $78 ; +7/\ 00000000 01111000 -1/$ff ballLeft: db $00, $3c ; +8/\ 00000000 00111100 +0/\; 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 dos 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:
00011110 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íneas a 0, 4 líneas 3c, 1 líneas a 0 ballRight: ; Derecha Sprite Izquierda db $3c, $00 ; +0/\; Limites de los objetos en pantalla BALL_BOTTOM: EQU $ba ; TTLLLSSS BALL_TOP: EQU $00 ; TTLLLSSS ; Sprite de la bola. 1 líneas a 0, 4 líneas 3c, 1 líneas 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 $f800111100 00000000 -8/$f8 db $1e, $00 ; +1/\ 00011110 00000000 -7/$f9 db $0f, $00 ; +2/\ 00001111 00000000 -6/$fa db $07, $80 ; +3/\ 00000111 10000000 -5/$fb db $03, $c0 ; +4/\ 00000011 11000000 -4/$fc db $01, $e0 ; +5/\ 00000001 11100000 -3/$fd db $00, $f0 ; +6/\ 00000000 11110000 -2/$fe db $00, $78 ; +7/\ 00000000 01111000 -1/$ff ballLeft: db $00, $3c ; +8/\ 00000000 00111100 +0/\; Limites de los objetos en pantalla BALL_BOTTOM: EQU $ba ; TTLLLSSS BALL_TOP: EQU $00 ; TTLLLSSS ; Sprite de la bola. 1 líneas a 0, 4 líneas 3c, 1 líneas 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; 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 el valor directamente en C, previo paso por HL, pero dependiendo del valor obtenemos 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 posició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. Volveremos 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 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 en 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 solo 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 alternativos 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:
AF <--> 'AF 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 del 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 como 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 señalan los cambios, para que se vean las diferencias:
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 8 veces hacia la derecha y luego 8 veces hacia las 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:
En el próximo capítulo de Ensamblador para ZX Spectrum, moveremos la bola por toda la pantalla.
Enlaces de interés
- Notepad++.
- Visual Studio Code.
- Sublime Text.
- ZEsarUX.
- PASMO.
- Git.
- Curso de ensamblador Z80 de Compiler Software.
- Z80 instruction set.
ahi ahi moviendo la bola !!