0x08 Ensamblador ZX Spectrum Pong – Partida a dos jugadores y cambio de velocidad de la bola

En esta nueva entrega de Ensamblador ZX Spectrum Pong vamos a implementar la partida a dos jugadores, con marcador, y la posibilidad de cambiar la velocidad de la bola.

Ensamblador ZX Spectrum Pong – Partida a dos jugadores y cambio de velocidad de la bola

Creamos la carpeta Paso08 y copiamos los archivos Controls.asm, Game.asm, Main.asm, Sprite.asm y Video.asm desde la carpeta Paso07.

Vamos a empezar por el marcador, definiendo la posición donde vamos a pintar la puntuación, y definiendo también los sprites necesarios en el archivo Sprite.asm.

Marcadores

POINTS_P1:	EQU	$450d
POINTS_P2:	EQU	$4511

Cada dígito de los marcadores ocupa 8×16 píxeles, o lo que es lo mismo, un carácter de ancho por dos de alto (1 byte x 16 bytes/scanlines).

Blanco_sprite:
ds $10	; 16 espacios = 16 bytes a $00

Cero_sprite:
db $00, $7e, $7e, $66, $66, $66, $66, $66
db $66, $66, $66, $66, $66, $7e, $7e, $00

Uno_sprite:
db $00, $18, $18, $18, $18, $18, $18, $18
db $18, $18, $18, $18, $18, $18, $18, $00

Dos_sprite:
db $00, $7e, $7e, $06, $06, $06, $06, $7e
db $7e, $60, $60, $60, $60, $7e, $7e, $00

Tres_sprite:
db $00, $7e, $7e, $06, $06, $06, $06, $3e
db $3e, $06, $06, $06, $06, $7e, $7e, $00

Cuatro_sprite:
db $00, $66, $66, $66, $66, $66, $66, $7e
db $7e, $06, $06, $06, $06, $06, $06, $00

Cinco_sprite:
db $00, $7e, $7e, $60, $60, $60, $60, $7e
db $7e, $06, $06, $06, $06, $7e, $7e, $00

Seis_sprite:
db $00, $7e, $7e, $60, $60, $60, $60, $7e
db $7e, $66, $66, $66, $66, $7e, $7e, $00

Siete_sprite:
db $00, $7e, $7e, $06, $06, $06, $06, $06
db $06, $06, $06, $06, $06, $06, $06, $00

Ocho_sprite:
db $00, $7e, $7e, $66, $66, $66, $66, $7e
db $7e, $66, $66, $66, $66, $7e, $7e, $00

Nueve_sprite:
db $00, $7e, $7e, $66, $66, $66, $66, $7e
db $7e, $06, $06, $06, $06, $7e, $7e, $00

Una vez que hemos definido los sprites, definimos la composición de los números haciendo referencia a las etiquetas de los sprites.

Cero:
dw		Blanco_sprite, Cero_sprite

Uno:
dw		Blanco_sprite, Uno_sprite

Dos:
dw		Blanco_sprite, Dos_sprite

Tres:
dw		Blanco_sprite, Tres_sprite

Cuatro:
dw		Blanco_sprite, Cuatro_sprite

Cinco:
dw		Blanco_sprite, Cinco_sprite

Seis:
dw		Blanco_sprite, Seis_sprite

Siete:
dw		Blanco_sprite, Siete_sprite

Ocho:
dw		Blanco_sprite, Ocho_sprite

Nueve:
dw		Blanco_sprite, Nueve_sprite

Diez:
dw		Uno_sprite, Cero_sprite

Once:
dw		Uno_sprite, Uno_sprite

Doce:
dw		Uno_sprite, Dos_sprite

Trece:
dw		Uno_sprite, Tres_sprite

Catorce:
dw		Uno_sprite, Cuatro_sprite

Quince:
dw		Uno_sprite, Cinco_sprite

Ahora necesitamos definir el lugar donde vamos a guardar la puntuación de cada jugador. Abrimos el archivo Main.asm y añadimos las siguientes variables antes de END $8000.

p1points:	db	$00
p2points:	db	$00

Ya tenemos todo listo para empezar a implementar el marcador.

Lo primero que tenemos que saber es que sprite tenemos que pintar, dependiendo del marcador de cada jugador. Para saber qué sprite pintar, vamos a implementar una rutina que recibe en A la puntuación, y devuelve en HL la dirección del sprite a pintar.

Abrimos el archivo Video.asm e implementamos justo antes de la rutina NextScan.

GetPointSprite:
ld		hl, Cero
ld		bc, $04
inc		a

Cargamos en HL la dirección del sprite para el cero, LD HL, Cero. Como cada sprite está a 4 bytes del anterior, cargamos este desplazamiento en BC, LD BC, $04, e incrementamos A para que el bucle no empiece en 0, INC A, en el caso de que la puntuación sea 0.

Ahora hacemos un bucle para que HL apunte al sprite correcto.

getPointSprite_loop:
dec		a
ret		z
add		hl, bc
jr		getPointSprite_loop

Decrementamos A, DEC A, y si hemos llegado a 0, HL ya apunta al sprite correcto y salimos, RET Z. Si todavía no hemos llegado a 0, sumamos el desplazamiento a HL, ADD HL, BC, y volvemos a ejecutar el bucle, JR getPointSprite_loop.

El aspecto final de la rutina es el siguiente.

; -----------------------------------------------------------------------------
; Obtiene el sprite correspondiente a pintar en el marcador.
; Entrada:	A = puntuación.
; Salida:	HL = Dirección del sprite a pintar.
; Altera el valor de los registros AF, BC y HL.
; -----------------------------------------------------------------------------
GetPointSprite:
ld		hl, Cero			; Carga en HL la dirección del sprite del 0
ld		bc, $04				; Cada sprite está del anterior a 4 bytes
inc		a					; Incrementa A para que el inicio del bucle no sea 0
getPointSprite_loop:
dec		a					; Decrementa A
ret		z					; Si ha llegado a 0, fin de rutina
add		hl, bc				; Suma 4 a la dirección del sprite; siguiente sprite
jr		getPointSprite_loop	; Bucle hasta que A = 0	

ret

Y ahora vamos a implementar la rutina que pinta los marcadores, al final del archivo Video.asm.

PrintPoints:
ld		a, (p1points)
call	GetPointSprite

Cargamos la puntuación del jugador 1 en A, LD A, (p1points), y obtenemos la dirección de memoria donde está la definición del sprite correspondiente a dicha puntuación, CALL GetPointSprite.

GetPointSprite nos devuelve en HL la dirección de memoria donde está definido el sprite. Si la puntuación es cero, HL nos traerá la dirección de memoria donde está definida la etiqueta Cero, cuya definición es la siguiente.

Cero:
dw Blanco_sprite, Cero_sprite

Como podemos ver, Cero está definido por otras dos direcciones de memoria: la primera es la dirección de memoria donde está definido el sprite blanco, usado para justificar a dos dígitos, y la segunda es la dirección de memoria donde está definido el sprite del cero.

Si las direcciones de memoria fueran las siguientes.

$9000 Blanco_sprite
$9020 Cero_sprite
$9040 Cero

La definición de la etiqueta Cero, una vez que se sustituyen las etiquetas Blanco_sprite y Cero_sprite por las direcciones de memoria donde están definidas, sería.

Cero:
dw $9000, $9020

El valor que tendría HL tras llamar a GetPointSprite con el marcador a 0 sería $9040, o lo que es lo mismo, la dirección de memoria donde se define la etiqueta Cero.

Como el Z80 es Little Endian, los valores de las direcciones de memoria desde $9040 en adelante serían-

$9040$00
$9041$90
$9042$20
$9043$90

O lo que es lo mismo, las direcciones de memoria donde están definidos los sprites para Blanco_sprite y para Cero_sprite.

Esta explicación es necesaria para entender el funcionamiento del resto de la rutina.

push	hl
ld		e, (hl)
inc		hl
ld		d, (hl)
ld		hl, POINTS_P1
call	printPoint_print

Vamos a pintar el primer dígito del marcador del jugador 1. Preservamos el valor de HL, que apunta al sprite del marcador que tenemos que pintar, PUSH HL, cargamos en E la parte baja de la dirección donde está el sprite del primer dígito, LD E, (HL), apuntamos HL a la parte alta de la dirección, INC HL, y la cargamos en D, LD D, (HL).

Cargamos en HL la dirección de memoria de pantalla donde se pinta el primer dígito del marcador del jugador 1, LD HL, POINTS_P1, y llamamos al pintado del dígito, CALL printPoint_print.

Ahora pintamos el segundo dígito del marcador del jugador 1.

pop		hl
inc		hl			
inc		hl

Recuperamos el valor de HL, POP HL, y lo apuntamos a la parte baja de la dirección donde está definido el sprite del segundo dígito, INC HL INC HL.

ld		e, (hl)	
inc		hl 
ld		d, (hl)

Cargamos la parte baja de dicha dirección en E, LD E, (HL), apuntamos HL a la parte alta de la dirección, INC HL, y la cargamos en D, LD D, (HL).

ld		hl, POINTS_P1
inc		l
call	printPoint_print

Por último, cargamos en HL la posición de memoria de la pantalla donde se pinta el marcador del jugador 1, LD HL, POINTS_P1. Como cada dígito ocupa 1 byte (columna) de ancho, situamos HL en la columna dónde se pinta el segundo dígito, INC L, y lo pintamos.

La forma de pintar el marcador del jugador 2 es casi igual a la del jugador 1, por lo que mostramos el código marcando los cambios y sin entrar en detalle.

ld		a, (p2points)	; !CAMBIO!
call	GetPointSprite
push	hl
; 1er dígito
ld		e, (hl)
inc		hl
ld		d, (hl)
ld		hl, POINTS_P2	; !CAMBIO!
call	printPoint_print

pop		hl
; 2º dígito
inc		hl			
inc		hl
ld		e, (hl)
inc		hl
ld		d, (hl)
ld		hl, POINTS_P2	; !CAMBIO!
inc		l

Como se puede observar, los cambios son pocos. Se ha quitado la última línea al no ser necesario llamar a pintar el segundo dígito del jugador 2, ya que lo vamos a implementar a continuación del último INC L.

Recordemos que cada dígito ocupa 8×16 píxeles (una columna x 16 scanlines).

printPoint_print:
ld		b, $10
push	de
push	hl

Cargamos en B el número de scanlines que vamos a pintar, LD B, $10, y preservamos el valor del registro DE, PUSH DE, y de HL, PUSH HL.

printPoint_printLoop:
ld		a, (de)
ld		(hl), a
inc		de
call	NextScan
djnz	printPoint_printLoop

Cargamos en A el byte a pintar, LD A, (DE), y lo pintamos en pantalla, LD (HL), A. Apuntamos DE al siguiente byte a pintar, obtenemos la dirección del siguiente scanline, CALL NextScan, y repetimos la operación hasta que B sea 0 y hayamos pintado los 16 scanlines, DJNZ printPoint_printLoop.

Para finalizar, recuperamos los valores de HL y DE y salimos.

pop		hl
pop		de
ret

El aspecto final de la rutina de pintado del marcador es el siguiente.

; -----------------------------------------------------------------------------
; Pinta el marcador.
; Cada número consta de 1 byte de ancho por 16 de alto.
; Altera el valor de los registros AF, BC, DE y HL.
; -----------------------------------------------------------------------------
PrintPoints:
ld		a, (p1points)			; Carga en A los puntos del jugador 1
call	GetPointSprite			; Obtiene el sprite a pintar en el marcador
push	hl						; Preserva el valor de HL
; 1er dígito del jugador 1
ld		e, (hl)					; Carga en E la parte baja de la dirección 
 								; donde está el primer dígito
inc		hl						; Apunta HL a la parte alta de la dirección 
 								; donde está el primer dígito
ld		d, (hl)					; y la carga en D
ld		hl, POINTS_P1			; Carga en HL la dirección de memoria donde se pintan
								; los puntos del jugador 1
call	printPoint_print		; Pinta el primer dígito del marcador del jugador 1

pop		hl						; Recupera el valor de HL
; 2º dígito del jugador 1
inc		hl			
inc		hl						; Apunta HL a la parte baja de la dirección 
 								; donde está el segundo dígito
ld		e, (hl)					; y la carga en E
inc		hl						; Apunta HL a la parte alta de la dirección 
 								; donde está el segundo dígito
ld		d, (hl)					; y la carga en D
ld		hl, POINTS_P1			; Carga en HL la dirección de memoria donde se pintan
								; los puntos del jugador 1
inc		l						; Apunta HL a la dirección donde se pinta el segundo dígito
call	printPoint_print		; Pinta el segundo dígito del marcador del jugador 1

ld		a, (p2points)			; Carga en A los puntos del jugador 2
call	GetPointSprite			; Obtiene el sprite a pintar en el marcador
push	hl						; Preserva el valor de HL
; 1er dígito del jugador 2
ld		e, (hl)					; Carga en E la parte baja de la dirección
 								; donde está el primer dígito
inc		hl						; Apunta HL a la parte alta de la dirección
 								; donde está el primer dígito
ld		d, (hl)					; y la carga en D
ld		hl, POINTS_P2			; Carga en HL la dirección de memoria donde se pintan
								; los puntos del jugador 2
call	printPoint_print		; Pinta el primer dígito del marcador del jugador 2

pop		hl						; Recupera el valor de HL
; 2º dígito del jugador 2
inc		hl			
inc		hl						; Apunta HL a la parte baja de la dirección
 								; donde está el segundo dígito
ld		e, (hl)					; y la carga en E
inc		hl						; Apunta HL a la parte alta de la dirección
 								; donde está el segundo dígito
ld		d, (hl)					; y la carga en D
ld		hl, POINTS_P2			; Carga en HL la dirección de memoria donde se pintan
								; los puntos del jugador 2
inc		l						; Apunta HL a la dirección donde se pinta el segundo dígito
; Pinta el segundo dígito del marcador del jugador 2

printPoint_print:
ld		b, $10					; Cada dígito son 1 byte por 16 (scanlines)
push	de						; Preserva el valor de DE
push	hl						; Preserva el valor de HL
printPoint_printLoop:
ld		a, (de)					; Carga en A el byte a pintar
ld		(hl), a					; Pinta el byte
inc		de						; Apunta DE al siguiente byte
call	NextScan				; Apunta HL al siguiente scanline
djnz	printPoint_printLoop	; Hasta que B = 0

pop		hl						; Recupera el valor de HL
pop		de						; Recupera el valor de DE

ret

En esta rutina es sencillo ahorrar 12 ciclos de reloj y 2 bytes. Para ello hay que cambiar dos instrucciones de lugar, lo que nos permite quitar otras dos; lo veremos al final del tutorial.

Y ahora solo nos queda ver si lo que hemos implementado funciona. Abrimos Main.asm, y debajo de la llamada a PrintBorder, justo antes de Loop, añadimos la siguiente línea.

call	PrintPoints

Compilamos y cargamos en el emulador para ver los resultados.

Ensamblador ZX Spectrum - Bola borra marcador
Ensamblador ZX Spectrum, bola borra marcador

En principio todo va bien, pero según se va moviendo la bola vemos que volvemos a tener un problema, viejo conocido nuestro, y es que la bola borra el marcador a su paso, cosa que vamos a solucionar a continuación.

Para evitar que la bola borre el marcador, hacemos lo mismo que hicimos con la línea central, vamos a repintar el marcador. Implementamos la rutina al final del archivo Video.asm.

En realidad, la rutina de repintado del marcador es prácticamente igual que la de pintado, cambiando el nombre de las etiquetas y añadiendo una línea. Vamos a copiar toda la rutina de pintado de marcador y la vamos a pegar al final del archivo Video.asm. Cambiamos los nombres de las etiquetas y añadimos una línea.

A continuación, mostramos el aspecto final, marcando los cambios producidos con respecto a la rutina de pintado del marcador.

; -----------------------------------------------------------------------------
; Repinta el marcador.
; Cada número consta de 1 bytes de ancho por 16 de alto.
; Altera el valor de los registros AF, BC, DE y HL.
; -----------------------------------------------------------------------------
ReprintPoints:
ld		a, (p1points)			; Carga en A los puntos del jugador 1
call	GetPointSprite			; Obtiene el sprite a pintar en el marcador
push	hl						; Preserva el valor de HL
; 1er dígito del jugador 1
ld		e, (hl)					; Carga en E la parte baja de la dirección
 								; donde está el primer dígito
inc		hl						; Apunta HL a la parte alta de la dirección
 								; donde está el primer dígito
ld		d, (hl)					; y la carga en D
ld		hl, POINTS_P1			; Carga en HL la dirección de memoria donde se pintan
								; los puntos del jugador 1
call	reprintPoint_print		; Pinta el primer dígito del marcador del jugador 1

pop		hl						; Recupera el valor de HL
; 2º dígito del jugador 1
inc		hl			
inc		hl						; Apunta HL a la parte baja de la dirección
 								; donde está el segundo dígito
ld		e, (hl)					; y la carga en E
inc		hl						; Apunta HL a la parte alta de la dirección
 								; donde está el segundo dígito
ld		d, (hl)					; y la carga en D
ld		hl, POINTS_P1			; Carga en HL la dirección de memoria donde se pintan
								; los puntos del jugador 1
inc		l						; Apunta HL a la dirección donde se pinta el segundo dígito
call	reprintPoint_print		; Pinta el segundo dígito del marcador del jugador 1

ld		a, (p2points)			; Carga en A los puntos del jugador 2
call	GetPointSprite			; Obtiene el sprite a pintar en el marcador
push	hl						; Preserva el valor de HL
; 1er dígito del jugador 2
ld		e, (hl)					; Carga en E la parte baja de la dirección
 								; donde está el primer dígito
inc		hl						; Apunta HL a la parte alta de la dirección
 								; donde está el primer dígito
ld		d, (hl)					; y la carga en D
ld		hl, POINTS_P2			; Carga en HL la dirección de memoria donde se pintan
								; los puntos del jugador 2
call	reprintPoint_print		; Pinta el primer dígito del marcador del jugador 2

pop	hl							; Recupera el valor de HL
; 2º dígito del jugador 2
inc		hl			
inc		hl						; Apunta HL a la parte baja de la dirección
 								; donde está el segundo dígito
ld		e, (hl)					; y la carga en E
inc		hl						; Apunta HL a la parte alta de la dirección
 								; donde está el segundo dígito
ld		d, (hl)					; y la carga en D
ld		hl, POINTS_P2			; Carga en HL la dirección de memoria donde se pintan
								; los puntos del jugador 2
inc		l						; Apunta HL a la dirección donde se pinta el segundo dígito
; Pinta el segundo dígito del marcador del jugador 2

reprintPoint_print:
ld		b, $10					; Cada dígito es de 1 byte por 16 (scanlines)
push	de
push	hl						; Preserva el valor de los registros DE y HL
reprintPoint_printLoop:
ld		a, (de)					; Carga en A el byte a pintar
or		(hl)					; Lo mezcla con lo que hay pintado en pantalla
ld		(hl), a					; Pinta el byte
inc		de						; Apunta DE al siguiente byte
call	NextScan				; Apunta HL al siguiente scanline
djnz	reprintPoint_printLoop	; Hasta que B = 0

pop		hl
pop		de						; Recupera el valor de los registros HL y DE

ret

Vamos a explicar la línea que hemos añadido.

ld a, (de)
or (hl)
ld (hl), a

Lo que hacemos con OR (HL) es agregar los píxeles que hay en pantalla a los píxeles del sprite del número. De esta manera repintamos el número sin borrar la bola.

Ahora queda ver si funciona. Abrimos el archivo Main.asm y añadimos la siguiente línea después de la llamada a ReprintLine.

call 	ReprintPoints

Compilamos y cargamos en el emulador para ver los resultados.

Ensamblador ZX Spectrum - Bola no borra marcador
Ensamblador ZX Spectrum, bola no borra marcador

Efectivamente, hemos solucionado un problema, pero ha surgido otro. El marcador ya no se borra, pero la bola va muy lenta. Por suerte la solución es sencilla, ya que la velocidad de la bola es una de las cosas que controlamos nosotros.

Como recordaréis, la bola se mueve una de cada seis iteraciones del bucle principal, por lo que lo único que tenemos que hacer es reducir este intervalo en Main.asm, por ejemplo a dos.

ld		(countLoopBall), a
cp		$02					; ¡CAMBIO!
jr		nz, loop_paddle

Compilamos, cargamos en el emulador y comprobamos que la velocidad de la bola ha aumentado.

Cambio de velocidad de la bola

Como recodaremos, en la variable ballSetting definimos la velocidad de la bola en los bits 4 y 5, pudiendo ser 1 la más rápida y 3 la más lenta. Vamos a utilizar este aspecto para definir y modificar la velocidad de la bola.

Lo primero es modificar el valor inicial de esta variable.

ballSetting:	db	$20

De esta manera el valor inicial es:

  • Dirección vertical hacia arriba.
  • Dirección horizontal hacia la derecha.
  • Velocidad de la bola 2.

Y ahora vamos a usar este valor para controlar el intervalo para mover la bola. Abrimos Main.asm, localizamos la etiqueta Loop, y añadimos justo debajo.

ld		a, (ballSetting)
rrca
rrca
rrca
rrca
and		$03
ld		b, a

Cargamos la configuración de la bola en A, LD A, (ballSetting), pasamos el valor de los bits 4 y 5 a los bits 0 y 1, RRCA RRCA RRCA RRCA, nos quedamos con el valor de los bits 0 y 1 (velocidad de la bola), AND $03, y cargamos el valor en B, LD B, A.

Cuatro líneas más abajo, cambiamos la línea CP $02.

cp		b

Compilamos y comprobamos que todo sigue funcionando igual. La única diferencia es que ahora la velocidad de la bola la tomamos desde la configuración de la misma, y podremos cambiarla.

Para cambiar la velocidad de la bola, vamos a usar las teclas del 1 al 3. Abrimos el archivo Controls.asm y empezamos a escribir tras la etiqueta ScanKeys.

scanKeys_speed:
ld		a, $00
ld		(countLoopBall), a
scanKeys_ctrl:

Si se ha pulsado alguna de las teclas de cambio de velocidad, hay que poner a 0 el contador de vueltas de bucle para pintar la bola, de lo contrario, si el contador está en 2 y ponemos la velocidad a 1, habrá que esperar 254 iteraciones hasta que la bola se vuelva a mover.

Ponemos A = 0, LD A, $00, y ponemos el contador de iteraciones para la bola a 0, LD (countLoopBall), A.

La etiqueta scanKeys_ctrl marca el punto donde empieza la rutina tal y como la tenemos ahora. La nueva implementación la vamos a hacer entre las etiquetas ScanKeys y scanKeys_speed.

ld		a, $f7
in		a, ($fe)

Cargamos la semifila 1-5 en A, LD A, $F7, y leemos del puerto del teclado, IN A, ($FE).

bit		$00, a
jr		nz, scanKeys_2

Comprobamos si se ha pulsado el 1, BIT $00, A, y en caso de no haberlo pulsado saltamos a comprobar si se ha pulsado el 2, JR NZ, scanKeys_2.

Si se ha pulsado el 1, cambiamos la velocidad de la bola.

ld		a, (ballSetting)
and   	$cf
or    	$10
ld		(ballSetting), a
jr		scanKeys_speed

Cargamos la configuración de la bola en A, LD A, (ballSetting), ponemos los bits de la velocidad a 0, AND $CF, ponemos la velocidad a 1, OR $10, cargamos la configuración en memoria, LD (ballSetting), A, y saltamos a poner a 0 el contador de iteraciones para la bola, JR scanKeys_speed.

La comprobación para el 2 y el 3 es muy parecida a la comprobación del 1, por lo que vemos el código completo y marcamos las diferencias.

scanKeys_2:
bit		$01, a				; ¡CAMBIO!
jr		nz, scanKeys_3		; ¡CAMBIO!
ld		a, (ballSetting)
and		$cf
or		$20					; ¡CAMBIO!
ld		(ballSetting), a
jr		scanKeys_speed
scanKeys_3:
bit		$02, a				; ¡CAMBIO!
jr		nz, scanKeys_ctrl	; ¡CAMBIO!
ld		a, (ballSetting)
and		$cf					; ¡BORRAR!
or		$30					; ¡CAMBIO!
ld		(ballSetting), a

El aspecto final de la rutina, una vez modificada, queda de la siguiente manera.

; -----------------------------------------------------------------------------
; ScanKeys
; Escanea las teclas de control y devuelve las pulsadas.
; Salida:	D = Teclas pulsadas.
;			Bit 0 = A pulsada 0/1.
;			Bit 1 = Z pulsada 0/1.
;			Bit 2 = 0 pulsada 0/1.
;			Bit 3 = O pulsada 0/1.
; Altera el valor de los registros AF y D.
; -----------------------------------------------------------------------------
ScanKeys:
ld      a, $f7				; Carga en A la semifila 1-5
in      a, ($fe)			; Lee el estado de la semifila
bit     $00, a				; Comprueba si se ha pulsado el 1
jr		nz, scanKeys_2		; Si no se ha pulsado salta
; Se ha pulsado; cambia la velocidad de la bola 1 (rápido)
ld      a, (ballSetting)	; Carga la configuración de la bola en A
and     $cf					; Pone los bits de velocidad a 0
or      $10					; Pone los bits de velocidad a 1
ld		(ballSetting), a	; Carga el valor en memoria
jr		scanKeys_speed		; Salta para comprobar los controles
scanKeys_2:
bit     $01, a				; Comprueba si se ha pulsado el 2
jr		nz, scanKeys_3      ; Si no se ha pulsado salta
; Se ha pulsado; cambia la velocidad de la bola 2 (medio)
ld      a, (ballSetting)	; Carga la configuración de la bola en A
and     $cf					; Pone los bits de velocidad a 0
or      $20					; Pone los bits de velocidad a 2
ld		(ballSetting), a	; Carga el valor en memoria
jr		scanKeys_speed		; Salta para comprobar los controles
scanKeys_3:
bit     $02, a				; Comprueba si se ha pulsado el 3
jr		nz, scanKeys_ctrl	; Si no se ha pulsado salta
; Se ha pulsado; cambia la velocidad de la bola 3 (lento)
ld      a, (ballSetting)	; Carga la configuración de la bola en A
or      $30					; Pone los bits de velocidad a 3
ld		(ballSetting), a	; Carga el valor en memoria

scanKeys_speed:
ld		a, $00				; Pone A = 0
ld	(	countLoopBall), a	; Pone el contador de iteraciones para la bola a 0
scanKeys_ctrl:
ld		d, $00				; Pone el registro D a 0.

; Resto de la rutina desde ScanKeys_A

Es el momento de compilar y cargar en el emulador para comprobar cómo se comporta esta modificación. Si todo ha ido bien, podemos cambiar la velocidad de la bola pulsando las teclas 1, 2 y 3.

Puntuación

Lo último que tenemos que hacer es contabilizar los puntos de cada jugador, para lo cual vamos a modificar la rutina MoveBall, en concreto moveBall_rightChg y moveBall_leftChg. Estas rutinas se encargan de cambiar la dirección de la bola cuando llega al límite izquierdo o derecho. Vamos a implementar lo necesario para que marque los puntos.

El código nuevo lo vamos a poner justo debajo de dichas etiquetas, empezando por moveBall_rightChg.

moveBall_rightChg:
ld		hl, p1points
inc		(hl)
call	PrintPoints

Cargamos en HL la dirección de memoria donde se encuentra el marcador del jugador 1, LD HL, p1points, lo incrementamos, INC (HL), y pintamos el marcador, CALL PrintPoints. El resto de la rutina se queda como estaba.

Las modificaciones en la etiqueta moveBall_leftChg son prácticamente las mismas.

moveBall_leftChg:
ld		hl, p2points	; ¡CAMBIO!
inc		(hl)
call	PrintPoints

Compilamos y cargamos en el emulador para ver los resultados.

Ensamblador ZX Spectrum - Puntuación
Ensamblador ZX Spectrum, puntuación

Inicio y fin de la partida

Ya tenemos marcador, pero la partida continúa interminablemente y en el momento que pasamos de 15 puntos, empieza a pintar cosas sin sentido.

Ensamblador ZX Spectrum - Puntuación sin sentido
Ensamblador ZX Spectrum, puntuación sin sentido

También podemos apreciar que cada vez va más lento. ¿Pero por qué? Pintamos el marcador en cada iteración, y para localizar el sprite del número a pintar hacemos un bucle, y no es lo mismo un bucle con 15 iteraciones como máximo, que un bucle con hasta 255 iteraciones, ¿a qué no? En la última entrega veremos la forma de implementar GetPointSprite de tal manera que siempre tarde lo mismo, y de paso nos ahorraremos 2 bytes y unos cuantos ciclos de reloj.

Lo que tenemos que hacer ahora es parar la partida cuando alguno de los dos jugadores llegue a 15 puntos; de igual manera vamos a implementar un modo de iniciar la partida, por ejemplo, pulsando el 5.

Al final del archivo Controls.asm vamos a implementar la rutina que espere a que se pulse el 5 para iniciar la partida.

WaitStart:
ld		a, $f7
in		a, ($fe)
bit		$04, a
jr		nz, WaitStart
ret

Cargamos en A la semifila 1-5, LD A, $F7, leemos el teclado, IN A, ($FE), evaluamos si se ha pulsado el 5, BIT $04, A, y repetimos la operación hasta que se pulse, JR NZ, WaitStart.

El aspecto final de la rutina es el siguiente.

; -----------------------------------------------------------------------------
; WaitStart.
; Espera que se pulse la tecla 5 para empezar la partida.
; Altera el valor de los registros AF.
; -----------------------------------------------------------------------------
WaitStart:
ld		a, $f7 			; Carga en A la semifila 1-5
in		a, ($fe)		; Lee el teclado
bit		$04, a			; Evalúa si se ha pulsado el 5
jr		nz, WaitStart	; Bucle hasta que se pulse el 5

ret

Volvemos a Main.asm y después de la llamada a PrintPoints, ponemos la siguiente línea.

call	WaitStart

Si compilamos y cargamos en el emulador, hasta que no pulsemos el 5, no empezaremos la partida, pero con esto no es suficiente, ya que la partida no finaliza cuando uno de los jugadores llega a 15 puntos.

Seguimos en Main.asm, pero esta vez al final de la rutina loop_continue, justo antes de JR Loop. Es aquí donde vamos a implementar el control de la puntuación.

ld		a, (p1points)
cp		$0f
jr		z, Main

Cargamos la puntuación del jugador 1 en A, LD A, (p1points), la comparamos con 15, CP $0F, y si es quince saltamos al inicio del programa, JR Z, Main.

Hacemos lo mismo con la puntuación del jugador 2.

ld		a, (p2points)	; ¡CAMBIO!
cp		$0f
jr		z, Main

Compilamos, cargamos en el emulador y comprobamos que cuando uno de los dos jugadores llega a quince puntos, la partida finaliza.

Ensamblador ZX Spectrum - Y el ganador es...
Ensamblador ZX Spectrum, y el ganador es…

¿Pero qué pasa si volvemos a pulsar el 5? Ya no hay forma de iniciar la partida. En ningún momento ponemos el marcador a 0. Si dejamos pulsado el 5, veremos como a cada iteración del bucle, vuelve al inicio y se para.

Para solucionar esto, volvemos al inicio del archivo Main.asm, y justo después de la llamada a WaitStart, vamos a poner los marcadores a 0.

ld		a, ZERO
ld		(p1points), a
ld		(p2points), a
call	PrintPoints

Ponemos A = 0, LD A, ZERO, ponemos la puntuación del jugador 1 a 0, LD (p1points), A, ponemos la puntuación del jugador 2 a 0, LD (p2points), A, y pintamos el marcador, CALL PrintPoints. De esta manera, cada vez que iniciamos partida, ponemos los marcadores a 0 y los pintamos.

Compilamos y cargamos en el emulador para ver los resultados. Esto empieza a tomar forma.

Todavía nos quedan ajustes por realizar. Vamos a hacer que cuando se marque un tanto, la bola salga por el lado contrario, es decir, como si sacara el jugador que ha marcado. Vamos a implementar una rutina para borrar la bola, otra para situarla en la parte derecha de la pantalla, y otra para situarla en la parte izquierda.

La rutina para borrar la bola la vamos a implementar en el archivo Video.asm, justo antes de la rutina Cls.

ClearBall:
ld		hl, (ballPos)
ld		a, l
and		$1f
cp		$10
jr		c, clearBall_continue
inc		l

Cargamos la posición de la bola en HL, LD HL, (ballPos), cargamos la fila y la columna en A, LD A, L, nos quedamos con la columna, AND $1F, y lo comparamos con el centro de la pantalla, CP $10.

Si hay acarreo, solo puede estar en el margen izquierdo. Saltamos a borrar la bola, JR C, clearBall_continue. Si no salta, está en el margen derecho, pero la bola en realidad está pintada una columna más a la derecha (la bola se pinta en dos bytes/columnas); apuntamos HL a la columna dónde está pintada la bola, INC L.

clearBall_continue:
ld		b, $06
clearBall_loop:
ld		(hl), ZERO
call	NextScan

djnz	clearBall_loop

ret

Cargamos en B el número de scanlines que vamos a borrar, LD B, $06, borramos la posición apuntada por HL, LD (HL), ZERO, apuntamos HL al siguiente scanline, CALL NextScan, repetimos la operación hasta que B valga 0, DJNZ clearBall_loop, y salimos, RET.

El aspecto final de la rutina es el siguiente.

; -----------------------------------------------------------------------------
; Borra la bola.
; Altera el valor de los registros AF, B y HL.
; -----------------------------------------------------------------------------
ClearBall:
ld		hl, (ballPos)			; Carga la posición de la bola en HL
ld		a, l					; Carga la fila y columna en A
and		$1f						; Se queda con la columna
cp		$10						; Lo compara con el centro de la pantalla
jr		c, clearBall_continue	; Si está a la izquierda salta
inc		l						; Incrementa la columna
clearBall_continue:
ld		b, $06					; Bucle por 6 scanlines
clearBall_loop:				
ld		(hl), ZERO				; Borra el byte apuntado por HL
call	NextScan				; Obtiene el scanline siguiente
djnz	clearBall_loop			; Hasta que B = 0

ret

Las otras dos rutinas las vamos a implementar al final del archivo Game.asm.

SetBallLeft:
ld		hl, $4d60
ld		(ballPos), hl
ld		a, $01
ld		(ballRotation), a
ld		a, (ballSetting)
and		$bf
ld		(ballSetting), a
ret

Cargamos en HL la nueva posición de la bola, LD HL, $4D60, y lo cargamos en memoria, LD (ballPos), HL. Cargamos la rotación de la bola en A, LD A, $01, y lo cargamos en memoria, LD (ballRotation), A. Cargamos la configuración de la bola en A, LD A, (ballSetting), ponemos la dirección horizontal hacia la derecha, AND $BF, lo cargamos en memoria, LD (ballSetting), A, y salimos, RET.

La rutina para posicionar la bola a la derecha es prácticamente igual; marcamos las diferencias sin entrar en detalle.

SetBallRight:				; ¡CAMBIO!
ld   	hl, $4d7e			; ¡CAMBIO!
ld		(ballPos), hl
ld   	a, $ff				; ¡CAMBIO!
ld 		(ballRotation), a
ld   	a, (ballSetting)
or		$40					; ¡CAMBIO!
ld		(ballSetting), a
ret

El aspecto final de las dos rutinas es el siguiente.

; -----------------------------------------------------------------------------
; Posiciona la bola a la izquierda.
; Altera el valor de los registros AF y HL.
; -----------------------------------------------------------------------------
SetBallLeft:
ld		hl, $4d60			; Carga en HL la posición de la bola
ld		(ballPos), hl		; Carga el valor en memoria
ld		a, $01				; Carga 1 en A
ld		(ballRotation), a	; Lo carga 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

ret

; -----------------------------------------------------------------------------
; Posiciona la bola a la derecha.
; Altera el valor de los registros AF y HL.
; -----------------------------------------------------------------------------
SetBallRight:
ld		hl, $4d7e			; Carga en HL la posición de la bola
ld		(ballPos), hl		; Carga el valor en memoria
ld		a, $ff				; Carga -1 en A
ld		(ballRotation), a	; Lo carga en memoria, Rotación = -1
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

ret

Para acabar con este paso, solo nos queda utilizar estas rutinas. Vamos a modificar las rutinas moveBall_rightChg y moveBall_leftChg del archivo Game.asm.

En la rutina moveBall_rightChg, borramos las líneas que hay entre CALL PrintPoints y JR moveBall_end, y las sustituimos por las siguientes.

call	ClearBall
call	SetBallLeft

El aspecto final de la rutina es el siguiente.

moveBall_rightChg:
; Ha llegado al límite derecho, ¡PUNTO!
ld		hl, p1points	; Carga en HL la dirección de la puntuación del jugador 1
inc		(hl)			; Lo incrementa
call	PrintPoints		; Pinta el marcador
call	ClearBall		; Borra la bola
call	SetBallLeft		; Pone la bola a la izquierda
jr		moveBall_end	; Fin de la rutina

En la rutina moveBall_leftChg, borramos las líneas que hay entre CALL PrintPoints y la etiqueta moveBall_end, y las sustituimos por las siguientes.

call	ClearBall
call	SetBallRight

El aspecto final de la rutina es el siguiente.

moveBall_leftChg:
; Ha llegado al límite izquierdo, ¡PUNTO!
ld		hl, p2points	; Carga en HL la dirección de la puntuación del jugador 2
inc		(hl)			; Lo incrementa
call	PrintPoints		; Pinta el marcador
call	ClearBall		; Borra la bola
call	SetBallRight	; Pone la bola a la derecha

Compilamos, cargamos en el emulador, y ya podemos empezar a jugar nuestras primeras partidas a dos jugadores, aunque todavía quedan cosas por hacer.

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, partida a dos jugadores y cambio de velocidad de la bola

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.

Un comentario en «0x08 Ensamblador ZX Spectrum Pong – Partida a dos jugadores y cambio de velocidad de la bola»

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: