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.

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 1Bit 2Resultado
000
101
011
111
Ensamblador ZX Spectrum, resultado de OR

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 1Bit 2Resultado
000
100
010
111
Ensamblador ZX Spectrum, resultado de AND

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.

Ensamblador ZX Spectrum Pong - Movemos la bola por la pantalla
Ensamblador ZX Spectrum, movemos la bola por la pantalla

Podéis descargar todo el código que hemos generado.

Enlaces de interés

Vídeo

Si lo prefieres, puedes ver el vídeo que grabamos de esta sesión.

Ensamblador ZX Spectrum, movemos la bola por la pantalla

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.

AUA

Aquí puedes ver más cosas que he desarrollado para .Net, y aquí las desarrolladas en ensamblador para Z80.

Y recuerda, si lo usas no te limites a copiarlo, intenta entenderlo y adaptarlo a tus necesidades.

Deja una respuesta

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

Este sitio usa Akismet para reducir el spam. Aprende cómo se procesan los datos de tus comentarios.

Este sitio web utiliza cookies para que usted tenga la mejor experiencia de usuario. Si continúa navegando está dando su consentimiento para la aceptación de las mencionadas cookies y la aceptación de nuestra política de cookies, pinche el enlace para mayor información.plugin cookies

ACEPTAR
Aviso de cookies
A %d blogueros les gusta esto: