Curso Programación ZX SpectrumCursos

Ensamblador para ZX Spectrum – Pong: $06 Empezamos a mover la bola

Primer paso para mover la bola lateralmente, píxel a píxel.

4.6
(5)

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/$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/$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 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:

Ensamblador para ZX Spectrum, empezamos a mover la bola
Ensamblador para ZX Spectrum, empezamos a mover la bola

En el próximo capítulo de Ensamblador para ZX Spectrum, moveremos la bola por toda la pantalla.

Enlaces de interés

Ficheros

¿De cuánta utilidad te ha parecido este contenido?

¡Haz clic en una estrella para puntuar!

Mostrar más

Un comentario

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

Publicaciones relacionadas

Botón volver arriba