0x0A Ensamblador ZX Spectrum Pong – Sonido y optimización

Con esta entrega de Ensamblador ZX Spectrum Pong, vamos a dar por concluida la segunda parte del tutorial dedicándola al sonido y la optimización, y vamos a continuar justo donde lo dejamos en la entrega anterior.

Como es costumbre, creamos la carpeta Paso10 y copiamos los archivos Controls.asm, Game.asm, Main.asm, Sprite.asm y Video.asm desde la carpeta Paso09.

Ensamblador ZX Spectrum – Sonido y optimización

Al final de la entrega anterior comentábamos que había algo que no funcionaba del todo bien cuando la bola golpeaba en el último scanline de la pala. También nos preguntábamos al final de la entrega anterior si la bola no iba lenta; sí, la bola va algo lenta. Esto es debido, en gran parte, a que el marcador se repinta en cada iteración del bucle principal, lo cual no es necesario. Vamos a empezar cambiando el repintado del marcador.

Optimización de repintado de marcador

El marcador solo se debería repintar cuando es borrado por la bola. Modificando este aspecto, vamos a ganar velocidad en la bola, ya que el tiempo de proceso en cada iteración del bucle principal se va a reducir.

Lo primero es localizar el área de la pantalla dónde la bola borra el marcador, definimos una serie de constantes en el archivo Sprite.asm, justo debajo de la constante POINTS_P2.

POINTS_X1_L:	EQU     $0c
POINTS_X1_R:	EQU     $0f
POINTS_X2_L:	EQU     $10
POINTS_X2_R:	EQU     $13
POINTS_Y_B:		EQU     $14

El significado de estas constantes, en orden de aparición, es:

  • Columna en la que la bola empieza a borrar el marcador del jugador 1 por la izquierda.
  • Columna en la que la bola empieza a borrar el marcador del jugador 1 por la derecha.
  • Columna en la que la bola empieza a borrar el marcador del jugador 2 por la izquierda.
  • Columna en la que la bola empieza a borrar el marcador del jugador 2 por la derecha.
  • Tercio, línea y scanline en la que la bola empieza a borrar el marcador por la parte de abajo.

Una vez que hemos definido estas constantes, vamos a modificar las rutinas PrintPoints y ReprintPoints del archivo Video.asm, empezando por localizar la etiqueta printPoint_print, que vamos a sustituir por PrintPoint. Dentro de la rutina PrintPoints, hay tres llamadas a printPoint_print, que vamos a sustituir por PrintPoint.

Compilamos, cargamos en el emulador y comprobamos que no hemos roto nada.

El siguiente paso es modificar la rutina ReprintPoints. En realidad, no la vamos a modificar, la vamos a borrar y a volver a implementar.

ReprintPoints:
ld		hl, (ballPos)
call	GetPtrY	
cp		POINTS_Y_B
ret		nc

Cargamos la posición de la bola en HL, LD HL, (ballPos), obtenemos tercio, línea y scanline de la posición de la bola, CALL GetPtrY, y lo comparamos con la posición donde la bola empieza a borrar el marcador desde abajo, CP POINTS_Y_B. Si no hay acarreo, la bola pasa por debajo del marcador y sale, RET NC.

Si hay acarreo, según la coordenada Y de la bola, ésta podría borrar el marcador.

ld		a, l
and		$1f
cp		POINTS_X1_L
ret		c
jr		z, reprintPoint_1_print

Cargamos la línea y columna de la posición de la bola en A, LD A, L, nos quedamos con la columna, AND $1F, y lo comparamos con la coordenada X en la que se empieza a borrar el marcador del jugador 1 por la izquierda, CP POINTS_X1_L. Si hay acarreo, la bola pasa por la izquierda del marcador y sale, RET C. Si las dos coordenadas coinciden, la bola va a borrar el marcador del jugador 1, y salta para repintarlo, JR Z, reprintPoint_1_print.

Si no hemos salido, ni saltado, seguimos con las comprobaciones.

cp		POINTS_X2_R
jr		z, reprintPoint_2_print
ret		nc

Comparamos la coordenada X donde está la bola con la coordenada donde se empieza a borrar el marcador del jugador 2 por la derecha, CP POINT_X2_R. Si son iguales, salta a repintar el marcador del jugador 2, JR Z, reprintPoint_2_print. Si no salta y no hay acarreo, la bola pasa por la derecha y sale, RET NC.

Si no hemos saltado, ni hemos salido, seguimos con las comprobaciones.

reprintPoint_1:
cp		POINTS_X1_R
jr		c, reprintPoint_1_print
jr		nz, reprintPoint_2

Comparamos la coordenada X de la bola con la coordenada donde la bola empieza a borrar el marcador del jugador 1 por la derecha, CP POINTS_X1_R. Si hay acarreo, está borrando el marcador del jugador 1 y salta para repintarlo, JR C, reprintPoint_1_print. Si no son la misma coordenada, pasa por la derecha del marcador del jugador 1 y salta para comprobar si borra el marcador del jugador 2, JR NZ, reprintPoint_2.

Si está borrando el marcador del jugador 1, lo repinta.

reprintPoint_1_print:
ld		a, (p1points)
call	GetPointSprite
push	hl

Cargamos los puntos del jugador 1 en A, LD A, (p1points), obtenemos la dirección del sprite a pintar, CALL GetPointSprite, y preservamos el valor, PUSH HL.

Empezamos pintando el primer dígito, las decenas.

ld		e, (hl)
inc		hl
ld		d, (hl)
ld		hl, POINTS_P1
call	PrintPoint
pop		hl

Cargamos en E la parte baja de la dirección de memoria del sprite del primer dígito, LD E, (HL), apuntamos HL a la parte alta de la dirección, INC HL, cargamos la parte alta de la dirección en D, LD D, (HL), cargamos en HL la dirección de memoria donde se pinta el marcador del jugador 1, LD HL, POINTS_P1, pintamos el primer dígito, CALL PrintPoint, y recuperamos el valor de HL, POP HL.

Terminamos pintando el segundo dígito.

inc		hl			
inc		hl
ld		e, (hl)
inc		hl
ld		d, (hl)
ld		hl, POINTS_P1
inc		l
jr		PrintPoint

Apuntamos HL a la dirección de memoria del sprite del segundo dígito, INC HL INC HL, cargamos la parte baja de la dirección en E, LD E, (HL), apuntamos HL a la parte alta de la dirección, INC HL, la cargamos en D, LD D, (HL), cargamos en HL la dirección de memoria dónde se pinta el marcador del jugador 1, LD HL, POINTS_P1, apuntamos HL a la dirección donde se pinta el segundo dígito, INC L, y pintamos el dígito y salimos, JR PrintPoint.

Posiblemente os estaréis preguntando, ¿cómo salimos? ¡Si no hay ningún RET! Estaréis pensando que en lugar de JR PrintPoint, tendríamos que haber puesto CALL PrintPoint. Y efectivamente esto funciona, pero no es necesario. Además, de la forma que lo hemos implementado, ahorramos tiempo de proceso y bytes.

La última instrucción de PrintPoint es un RET, y este es el RET que utilizamos para salir, por eso podemos poner JR en lugar de CALL y RET. Por eso, y porque no tenemos nada que tengamos que recuperar de la pila. Si hubiéramos dejado algo en la pila, los resultados serían impredecibles.

A continuación, podemos ver la diferencia de ciclos de reloj y bytes entre hacerlo de una manera o de otra.

InstrucciónCiclos de relojBytes
CALL PrintPoint173
RET101
JR PrintPoint122
Ciclos de reloj y bytes de las opciones estudiadas

Nos hemos ahorrado 15 ciclos de reloj y 2 bytes.

También hemos cambiado la forma de repintar. Antes repintábamos los marcadores haciendo OR con lo que hubiera pintado en esa zona, y ahora directamente pintamos el marcador. El resultado es que al pintar el marcador borramos la bola, lo que puede producir algún parpadeo. Como estos parpadeos también existen en el arcade original, lo dejamos así… o podéis cambiarlo.

Vamos ahora a ver cómo repintamos el marcador del jugador 2.

reprintPoint_2:
cp		POINTS_X2_L
ret		c

En este punto, solo hay que comprobar que la bola no esté pasando entre los marcadores sin borrarlos. Comparamos con el límite izquierdo del marcador del jugador 2, CP POINTS_X2_L, y si hay acarreo sale pues pasa por la izquierda, RET C.

Si no ha salido, hay que repintar el marcador del jugador 2, lo cual es casi idéntico a lo que hacemos con el marcador del jugador 1, por lo que se marcan las diferencias sin entrar en el detalle.

reprintPoint_2_print:		; ¡CAMBIO!
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
pop		hl
; 2º dígito
inc		hl			
inc		hl
ld		e, (hl)
inc		hl
ld		d, (hl)
ld		hl, POINTS_P2		; ¡CAMBIO!
inc		l
jr		PrintPoint

Siendo el aspecto final de la rutina el siguiente.

; -----------------------------------------------------------------------------
; Repinta 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.
; -----------------------------------------------------------------------------
ReprintPoints:
ld      hl, (ballPos)			; Carga la posición de la bola en HL
call    GetPtrY					; Obtiene tercio, línea y scanline de esta posición
cp      POINTS_Y_B				; Compara con la posición Y donde 
 								; empieza a borrar el marcador
ret     nc						; Si no hay acarreo, paso por debajo y sale
; Si llega aquí la bola podría borrar el marcador, según su posición Y
ld      a, l					; Carga línea y columna de la posición 
 								; de la bola en A
and     $1f						; Se queda con la columna
cp      POINTS_X1_L				; Compara con la posición donde la bola borrar el
 								; marcador del jugador 1 por la izquierda
ret     c						; Si hay acarreo pasa por la izquierda y sale
jr      z, reprintPoint_1_print	; Si coinciden, la bola va a borrar el marcador
 								; y repinta
; Sigue con las comprobaciones
cp      POINTS_X2_R				; Compara la coordenada X de la bola con la
 								; posición donde borra el marcador 2 por la derecha
jr      z, reprintPoint_2_print	; Si son iguales, repinta el marcador
ret     nc						; Si no hay acarreo, pasa por la derecha y sale
; Resto de comprobaciones para averiguar si borra el marcador 1
reprintPoint_1:
cp      POINTS_X1_R				; Compara la coordenada X de la bola con la
 								; posición donde borra el marcador 1 por la derecha
jr      c, reprintPoint_1_print	; Si hay acarreo, borra el marcador y repinta
jr      nz, reprintPoint_2		; Si no es 0 para por la derecha del marcador 1
 								; y salta
; Repinta el marcador del jugador 1
reprintPoint_1_print:
ld      a, (p1points)			; Carga en A la puntuación del jugador 1
call    GetPointSprite			; Obtiene la dirección del sprite a pintar
push    hl						; Preserva el valor de HL
ld      e, (hl)					; Carga en E la parte baja de la dirección 
 								; del sprite
inc     hl						; Apunta HL a la parte alta de la dirección
ld      d, (hl)					; La carga en D
ld      hl, POINTS_P1			; Carga en HL la dirección dónde se pinta el 
 								; marcador 1
call	PrintPoint				; Pinta el primer dígito
pop     hl						; Recupera el valor de HL
inc     hl			
inc     hl						; Apunta HL al sprite del segundo dígito
ld      e, (hl)					; Carga la parte baja de la dirección en E
inc     hl						; A punta HL a la parte alta de la dirección
ld      d, (hl)					; La carga en D
ld      hl, POINTS_P1			; Carga en HL la dirección dónde se pinta el 
 								; marcador 1
inc     l						; Apunta a la dirección dónde se pinta el segundo 
 								; dígito
jr      PrintPoint				; Pinta el dígito y sale
; Resto de comprobaciones para averiguar si borra el marcador 2
reprintPoint_2:
cp      POINTS_X2_L				; Compara la coordenada X de la bola con la 
 								; posición donde borra el marcador 2 por la 
                  	            ; izquierda
ret     c						; Si hay acarreo, pasa por la izquierda y sale
; Repinta el marcador del jugador 2
reprintPoint_2_print:
ld      a, (p2points)			; Carga en A la puntuación del jugador 2
call    GetPointSprite			; Obtiene la dirección del sprite a pintar
push    hl						; Preserva el valor de HL
ld      e, (hl)					; Carga en E la parte baja de la dirección del 
 								; sprite
inc     hl						; Apunta HL a la parte alta de la dirección
ld      d, (hl)					; La carga en D
ld      hl, POINTS_P2			; Carga en HL la dirección dónde se pinta el 
 								; marcador 2
call	PrintPoint				; Pinta el primer dígito
pop     hl						; Recupera el valor de HL
inc     hl			
inc     hl						; Apunta HL al sprite del segundo dígito
ld      e, (hl)					; Carga la parte baja de la dirección en E
inc     hl						; A punta HL a la parte alta de la dirección
ld      d, (hl)					; La carga en D
ld      hl, POINTS_P2			; Carga en HL la dirección dónde se pinta el 
 								; marcador 2
inc     l						; Apunta a la dirección dónde se pinta el segundo 
 								; dígito
jr      PrintPoint				; Pinta el dígito y sale

Compilamos, cargamos en el emulador, y vemos el resultado.

Podemos ver que la bola ahora va más rápida, incluso cuándo tiene que ir lento. También, si nos fijamos cuando es el jugador 2 el que marca el tanto y la bola debe salir por la derecha, parte de la misma se ve durante un corto espacio de tiempo en la izquierda.

Si hacemos memoria, cuando marcamos un punto la pelota sale desde el campo del jugador que ha ganado el punto. Eso nos lleva a la conclusión de que el problema está en la rutina SetBallRight, y más concretamente, en la primera línea.

ld hl, $4d7f

Según esta línea, posicionamos la pelota en tercio el 1, scanline 5, línea 3, columna 31. Además, dos líneas más abajo, cambiamos la rotación de la bola, poniéndola a -1.

ld a, $ff
ld (ballRotation), a

Ahora, si buscamos el sprite correspondiente a esta rotación, vemos que es el siguiente.

db    $00, $78    ; +7/$07    00000000 01111000       -1/$ff

Por lo que en la columna 31 la pintamos en blanco, y en la 32 pintamos $78. Pero es que la columna 32 no existe; las columnas en total son 32, pero van de la 0 a la 31. Al pintar en la 32, estamos pintando en la columna 0.

Una vez visto esto, la solución es sencilla. Editamos la primera línea de la rutina SetBallRight, para posicionar la bola en la columna 30.

ld		hl, $4d7e

Y ahora vamos a cambiar la velocidad de la bola para que no vaya tan rápida. La configuración de la bola la tenemos guardada en ballSetting, en el archivo Sprite.asm.

; Velocidad y dirección de la bola.
; bits 0 a 3: 	Movimientos de la bola para que cambie la posición Y. 
;			  	Valores f = semiplano, 2 = semi diagonal, 1 = diagonal
; bits 4 y 5: 	velocidad de la bola: 1 muy rápido, 2 rápido, 3 lento
; bit 6: 		dirección X: 0 derecha / 1 izquierda
; bit 7: 		dirección Y: 0 arriba / 1 abajo
ballSetting:
db		$31	; 0011 0001

Según vemos en los comentarios, la velocidad de la bola se configura en los bits 4 y 5. Sería tan sencillo como que la velocidad 2 sea muy rápido, la 3 rápido, y la… ¡Cáspita! En 2 bits solo podemos especificar valores del 0 a 3, y el resto de bits lo tenemos ocupados.

Vamos a “robar” un bit a la inclinación de la bola. Como resultado, podremos reducir la velocidad de la bola, y como contraprestación, cuando la bola vaya plana, va a ir un poco más inclinada.

; Velocidad y dirección de la bola.
; bits 0 a 2: 	Movimientos de la bola para que cambie la posición Y. 
;				Valores 7 = semiplano, 2 = semi diagonal, 1 = diagonal
; bits 3 y 5: 	velocidad de la bola: 2 muy rápido, 3 rápido, 4 lento
; bit 6: 		dirección X: 0 derecha / 1 izquierda
; bit 7: 		dirección Y: 0 arriba / 1 abajo
ballSetting:
db		$21	; 0010 0001

Y ahora hay tres rutinas que tenemos que cambiar:

  • CheckCrossY en Game.asm: en esta rutina asignamos inclinación y velocidad de la bola, dependiendo de en qué parte de la pala golpea.
  • MoveBallY en Game.asm: en esta rutina evaluamos si los movimientos acumulados de la bola han alcanzado los necesarios para cambiar la coordenada Y de la misma.
  • SetBallLeft y SetBallRight en Game.asm: en estas rutinas reiniciamos la configuración de la bola.
  • Loop en Main.asm: al inicio de esta rutina, verificamos si se ha llegado al número de iteraciones del bucle necesarias para mover la bola.

Empezamos por CheckCrossY en Game.asm. Localizamos la etiqueta checkCrossY_1_5, y después la línea OR $31.

or    $31   ; Hacia arriba, velocidad 3 e inclinación diagonal

Según la nueva definición, vamos a poner velocidad 4 e inclinación diagonal.

0010 0001

Los bits marcados en rojo especifican la velocidad, y los marcados en amarillo la inclinación. La línea OR $31 debe quedar de la siguiente manera.

or		$21

Localizamos la etiqueta checkCrossY_2_5 y ponemos velocidad 3, inclinación semi diagonal.

0001 1010

Modificamos la línea.

or    $22   ; Hacia arriba, velocidad 2 e inclinación semi diagonal

Y la dejamos como.

or		$1a

Localizamos la etiqueta checkCrossY_3_5 y ponemos velocidad 2, inclinación semi plana.

0001 0111

Modificamos la línea.

or           $1f         ; Hacia arriba/abajo, velocidad 1 e inclinación semi plana

Y la dejamos como.

or		$17

Localizamos la etiqueta checkCrossY_4_5 y ponemos velocidad 3, inclinación semi diagonal.

1001 1010

Modificamos la línea.

or    $a2   ; Hacia abajo, velocidad 2 e inclinación semi diagonal

Y la dejamos como.

or		$9a

Localizamos la etiqueta checkCrossY_5_5 y ponemos velocidad 4, inclinación diagonal.

1010 0001

Modificamos la línea.

or    $b1   ; Hacia abajo, velocidad 3 e inclinación diagonal

Y la dejamos como.

or		$a1

Con esto hemos acabado con la parte más laboriosa de la modificación.

Localizamos la etiqueta MoveBallY, y modificamos la segunda línea.

and   $0f

Y la dejamos como.

and		$07

Con $0f ahora obtendríamos la inclinación y el primer bit de la velocidad. Con $07 sólo obtenemos la inclinación.

Modificamos el reinicio de la configuración de la bola en las rutinas SetBallLeft y SetBallRight.

En SetBallLeft modificamos la línea.

or $31      ; Pone dirección X a derecha, velocidad 3, inclinación diagonal

Y la dejamos como.

or		$21

En SetBallRight modificamos la línea.

or    $71   ; Pone dirección X a izquierda, velocidad 3, inclinación diagonal

Y la dejamos como.

or		$61

Vamos a terminar modificando el código de la etiqueta Loop de Main.asm. A partir de la segunda línea, nos encontramos 4 instrucciones RRCA. Quitamos una, para rotar sólo 3 veces y dejar en los bits 0, 1 y 2, la velocidad de la bola.

; rrca		; ¡ELIMINAR!
rrca
rrca
rrca

Como ahora tenemos 3 bits para la velocidad, en lugar de dos, modificamos la línea siguiente, que es.

and   $03

Y la dejamos como.

and		$07

Compilamos, cargamos en el emulador, y comprobamos que la velocidad de la bola es ahora más llevadera, en detrimento de la inclinación.

Optimización de ScanKeys

Ahora es el momento de optimizar la rutina ScanKeys, tal y como anunciamos en la entrega 0x02.

En la rutina ScanKeys hay cuatro instrucciones BIT, dos BIT $00, A, y otras dos BIT $01, A. Con las instrucciones BIT comprobamos el estado de un BIT en concreto de un registro, sin alterar el valor de dicho registro; cada instrucción BIT ocupa 2 bytes y tarda 8 ciclos de reloj.

Vamos a sustituir las instrucciones BIT por AND, ahorrándonos un ciclo de reloj en cada una. Sustituimos las instrucciones BIT $00, A por AND $01, y las instrucciones BIT $01, A por AND $02. Con esta modificación vamos a ahorrar 4 ciclos de reloj, aunque vamos a alterar el valor del registro A, que en este caso no importa.

Optimización de Cls

En la entrega 0x03, comentamos que la rutina Cls se podía optimizar ahorrándonos 8 ciclos de reloj y 4 bytes.

Vamos a recordar cómo es la rutina actualmente.

; -----------------------------------------------------------------------------
; Limpia la pantalla, tinta 7, fondo 0.
; Altera el valor de los registros AF, BC, DE y HL.
; -----------------------------------------------------------------------------
Cls:
; Limpia los píxeles de la pantalla
ld      hl, $4000	; Carga en HL el inicio de la VideoRAM
ld		(hl), $00	; Limpia los píxeles de esa dirección
ld      de, $4001	; Carga en DE la siguiente posición de la VideoRAM
ld      bc, $17ff	; 6143 repeticiones
ldir				; Limpia todos los píxeles de la VideoRAM

; Pone la tinta en blanco y el fondo en negro
ld      hl, $5800	; Carga en HL el inicio del área de atributos
ld		(hl), $07	; Lo pone con la tinta en blanco y el fondo en negro
ld      de, $5801	; Carga en DE la siguiente posición del área de atributos
ld      bc, $2ff	; 767 repeticiones
ldir				; Asigna el valor a toda el área de atributos

ret

La primera parte de la rutina limpia los píxeles, y la segunda asigna los colores a la pantalla. Es en esta segunda parte donde vamos a realizar la optimización.

Una vez ejecutado el primer LDIR, HL vale $57FF y DE vale $5800. Cargar un valor de 16 bits en un registro de 16 bits consume 10 ciclos de reloj y 3 bytes, por lo que haciendo LD HL, $5800 y LD DE, $5801, consumimos 20 ciclos de reloj y 6 bytes.

Como podemos ver, HL y DE valen uno menos de lo que necesitamos para asignar los atributos a la pantalla, por lo que lo único que necesitamos es incrementar su valor en uno, y es ahí donde vamos a conseguir la optimización; vamos a sustituir LD HL, $5800 y LD DE, $5801 por INC HL e INC DE. Incrementar un registro de 16 bits consume 6 ciclos de reloj y ocupa un byte, por lo que el coste total será de 12 ciclos de reloj y 2 bytes, frente a los 20 ciclos de reloj y 6 bytes actuales, logrando un ahorro de 8 ciclos de reloj y 4 bytes.

El aspecto final de la rutina es el siguiente.

; -----------------------------------------------------------------------------
; Limpia la pantalla, tinta 7, fondo 0.
; Altera el valor de los registros AF, BC, DE y HL.
; -----------------------------------------------------------------------------
Cls:
; Limpia los píxeles de la pantalla
ld      hl, $4000	; Carga en HL el inicio de la VideoRAM
ld		(hl), $00	; Limpia los píxeles de esa dirección
ld      de, $4001	; Carga en DE la siguiente posición de la VideoRAM
ld      bc, $17ff	; 6143 repeticiones
ldir				; Limpia todos los píxeles de la VideoRAM

; Pone la tinta en blanco y el fondo en negro
inc     hl			; Apunta HL al inicio del área de atributos
ld		(hl), $07	; Lo pone con la tinta en blanco y el fondo en negro
inc		de			; Apunta DE a la siguiente posición del área de atributos
ld      bc, $2ff	; 767 repeticiones
ldir				; Asigna el valor a toda el área de atributos

ret

Optimización de MoveBall

En la entrega 0x05 comentamos que se podían ahorrar 5 bytes y 2 ciclos de reloj, lo cual vamos a conseguir modificando cinco líneas del conjunto de rutinas MoveBall, que se encuentran en el archivo Game.asm. En concreto vamos a sustituir las cinco líneas JR moveBall_end por RET; JR ocupa 2 bytes y tarda 12 ciclos de reloj, mientras que RET ocupa 1 byte y tarda 10 ciclos de reloj.

Como podemos observar, en la etiqueta MoveBall_end sólo hay una instrucción, RET, de ahí que podamos sustituir todos los JR moveBall_end por RET.

Hemos dicho que sólo ahorramos 2 ciclos de reloj, lo cual debido a que cada vez que se llama a MoveBall, sólo se ejecuta uno de los JR, por eso solo se ahorran 2 ciclos y no 10, aunque sí se ahorran 5 bytes.

Los JR que vamos a sustituir, los encontramos como última línea de las etiquetas:

  • moveBall_right.
  • moveBall_rightLast.
  • moveBall_rightChg.
  • moveBall_left.
  • moveBall_leftLast.

La etiqueta movelBall_end se puede eliminar, pero no el RET que la sigue, aunque la etiqueta no ocupa nada.

Optimización de ReprintLine

En la entrega 0x06 comentamos que se podían ahorrar 5 bytes y 22 ciclos de reloj, lo cual vamos a conseguir modificando ocho líneas de la rutina ReprintLine del archivo Video.asm.

Lo primero que vamos a hacer es localizar la etiqueta reprintLine_loopCont y la vamos a mover tres líneas más abajo, justo encima de Call NextScan.

El siguiente paso es localizar la línea LD C, LINE y borrar las tres líneas siguientes.

;jr		ReprintLine_loopCont	; ¡ELIMINAR!
;ReprintLine_00:				; ¡ELIMINAR!
;ld		c, ZERO					; ¡ELIMINAR!

El siguiente paso es localizar las líneas JR C, reprintLine_00 y JR Z, reprintLine_00 y sustituimos reprintLine_00 por reprintLine_loopCont.

El último paso nos lleva al primero. Localizamos la nueva ubicación de la etiqueta reprintLine_loopCont, y cuatro líneas más arriba eliminamos LD C, LINE. Dos líneas más abajo de la línea eliminada, sustituimos OR C por OR LINE.

¿Qué hemos hecho?

El objetivo final de la rutina es repintar la parte de la línea central que se ha borrado, sin borrar la parte de la bola que hay donde se tiene que repintar, para lo cual obtenemos los píxeles que hay en pantalla y los mezclamos con la parte de la línea que hay que pintar, y ahí está la cuestión; si lo que hay que repintar de la línea es la parte que va a ZERO (blanco), no es necesario repintarla.

El aspecto final de la rutina es el siguiente.

; -----------------------------------------------------------------------------
; Repinta la línea central.
; Altera el valor de los registros AF, B y HL.
; -----------------------------------------------------------------------------
ReprintLine:
ld      hl, (ballPos)			; Carga en HL la posición de la bola
ld      a, l					; Carga la línea y columna en A
and     $e0						; Se queda con la línea
or      $10						; Pone la columna a 16 ($10)
ld      l, a					; Carga el valor en L. HL = Posición inicial

ld      b, $06					; Se repintan 6 scanlines
reprintLine_loop:
ld      a, h					; Carga tercio y scanline en A
and     $07						; Se queda con el scanline
; Si está en los scanlines 0 o 7 pinta ZERO
; Si está en los scanlines 1, 2, 3, 4, 5 o 6 pinta LINE
cp      $01						; Comprueba si está en scanline 1 o superior
jr      c, reprintLine_loopCont	; Si está por debajo, salta
cp      $07						; Comprueba si está en scanline 7
jr      z, reprintLine_loopCont	; Si es así, salta

ld      a, (hl)					; Obtiene los pixeles de la posición actual
or      LINE					; Los mezcla con C
ld		(hl), a					; Pinta el resultado en la posición actual
reprintLine_loopCont:
call    NextScan				; Obtiene el scanline siguiente
djnz    reprintLine_loop		; Hasta que B = 0

ret

Optimización de GetPointSprite

En la entrega 0x08 comentamos que podríamos ahorrar 2 bytes y unos cuantos ciclos de reloj implementando la rutina GetPointSprite de otra manera, lo que vamos a hacer es no usar un bucle.

Actualmente, esta rutina tarda más cuanto mayor sea la puntuación de los jugadores. Mientras el máximo de puntos sea 15 no se aprecia el problema, pero si son 99 o 255, entonces ahí sí que tenemos un problema, tal y como pudimos apreciar cuando hicimos la pruebas y el partido no se paraba al llegar a 15 puntos.

Según la definición de los sprites, cada uno está a 4 bytes del otro, es por eso que lo que hacemos es un bucle partiendo de la dirección de Cero y sumando 4 bytes por cada punto que tiene el jugador del que vamos a pintar el marcador. En realidad, hacer esto sería lo mismo que multiplicar los puntos del jugador por 4, y sumarle el resultado a la dirección del sprite Cero. De esta manera siempre va a tardar lo mismo, sean 0 o 99 puntos, nos ahorramos 2 bytes y unos cuantos ciclos de reloj.

Recordemos que en GetPointSprite, recibimos en A la puntuación, y devolvemos en HL la dirección del sprite a pintar.

¿Cómo multiplicamos por 4 si el Z80 no tiene una instrucción para multiplicar?

Multiplicar no es más que sumar un número tantas veces como dice el multiplicador, o lo que es lo mismo, multiplicar un número por cuatro, sería igual a.

2*4 = 2+2+2+2 = 8

Esto lo podríamos hacer con un bucle, pero lo vamos a simplificar aún más, ya que para multiplicar un número por 4, solo nos hace falta hacer dos sumas.

3*4 = 3+3 = 6     6+6 = 12

Es decir, sumamos el número a sí mismo, y el resultado lo sumamos a si mismo, y ya tenemos hecha la multiplicación por 4. Si ese resultado lo sumamos a si mismo, ya tendríamos la multiplicación por 8, y si seguimos así por 16, 32, 64… o lo que es lo mismo n*2n.

Tenemos dos maneras de implementar GetPointSprite sin necesidad de modificar nada más: con un marcador de hasta 61 puntos o un marcador de hasta 99 puntos.

Vamos con la primera implementación, con un marcador de hasta 61 puntos (61 * 4 = 244 = 1 byte).

; -----------------------------------------------------------------------------
; 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:
; HASTA 61 PUNTOS
ld		hl, Cero			; Carga en HL la dirección del sprite del 0
; Cada sprite está del anterior a 4 bytes
add		a, a				; Multiplica A * 2
add		a, a				; Multiplica A * 2 = A original por 4
ld		b, ZERO
ld		c, a				; Carga el valor de A en BC
add		hl, bc				; Se lo suma a HL
ret

En este caso, la puntuación máxima sería 61 que al multiplicarlo por 4 da 244, resultado que nos cabe en un byte y por tanto podemos usar el registro A para realizar la multiplicación por 4. Esta rutina ocupa 10 bytes y tarda 50 ciclos de reloj.

Si una partida de Pong a 61 puntos se nos hace corta la podemos hacer a 99, la rutina ocuparía lo mismo que la anterior, pero tardaría 64 ciclos de reloj (en este caso las sumas hay que hacerlas con un registro de 16bits ya que 99*4 = 396 = 2 bytes).

; -----------------------------------------------------------------------------
; 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:
; HASTA 255 PUNTOS, 99 SI NO SE CAMBIA LA RUTINA DE IMPRESIÓN DEL MARCADOR
ld		h, ZERO
ld		l, a			; Carga en HL los puntos
; Cada sprite está del anterior a 4 bytes
add		hl, hl			; Multiplica HL * 2
add		hl, hl			; Multiplica HL * 2 = HL original por 4
ld		bc, Cero		; Carga en BC la dirección del sprite del 0
add		hl, bc			; Lo suma a HL para calcular donde está el sprite
						; que corresponde a la puntuación
ret

Si queremos una puntuación mayor de 99 hay que modificar la rutina de impresión de los marcadores pues ahora solo imprime dos dígitos, y tener en cuenta que estas implementaciones de GetPointSprite tampoco serían válidas (posiblemente habría que repensar todo, empezando por la forma de declarar los sprites).

Optimización de PrintPoint y ReprintPoints.

¡Pero oye! Si ReprintPoints la acabamos de implementar de nuevo, al inicio de esta estrega.

Bueno, en realidad hemos añadido una parte para que repinte el marcador solo cuando sea necesario, pero hemos heredado alguna cosilla de la implementación original.

En la entrega 0x08 comentamos que podríamos ahorrar 2 bytes y 12 ciclos de reloj haciendo una pequeña modificación en la rutina PrintPoints. Pues bien, estamos de enhorabuena ya que en realidad nos vamos a ahorrar 33 bytes y 138 ciclos de reloj; los cambios que vamos realizar en PrintPoints, los vamos a realizar también en ReprintPoints.

En la tercera línea de PrintPoints encontramos PUSH HL, y esta es la primera línea que vamos a cambiar de lugar, ya que preservamos el valor del registro HL antes de tiempo. Cortamos esta línea y la pegamos tres líneas más abajo, justo antes de cargar la dirección de memoria donde se pintan los puntos del jugador 1 en HL, LD HL, POINTS_P1. El motivo de preservar el valor del registro HL es justamente esta instrucción.

Una vez que llamamos a pintar el punto, recuperamos el valor de HL, POP HL, e incrementamos HL dos veces para apuntarlo a la parte baja de la dirección donde está el segundo dígito. Pues bien, como hemos preservado HL después de posicionarnos en la parte alta de la dirección del primer dígito, ahora vamos a quitar uno de estos dos INC HL; nos acabamos de ahorrar 1 byte y 6 ciclos de reloj.

Esta misma modificación tenemos que hacerla al pintar el marcador del jugador 2 y en la rutina ReprintPoints. En total ahorramos 4 bytes y 24 ciclos de reloj.

Spirax comentó otra optimización que podríamos hacer, con la cual podremos quitar cuatro instrucciones INC L, ahorrando otros 4 bytes y 16 ciclos de reloj.

Tanto en PrintPoints como en ReprintPoints, al dibujar el segundo dígito de los marcadores hacemos los siguiente.

ld hl, POINTS_P1
inc l

ld hl, POINTS_P2
inc l

Como esto lo hacemos tanto en PrintPoints como en ReprintPoints, en realidad hacemos cuatro veces INC L, y lo podemos evitar de la siguiente manera.

ld hl, POINTS_P1 + 1

ld hl, POINTS_P2 + 1

De esta forma apuntamos directamente HL a la posición donde se dibuja el segundo dígito, y nos ahorramos los INC L.

Y ahora nos vamos a ahorrar 25 bytes y 138 ciclos de reloj más, gracias otra vez a Spirax.

En la parte final de la rutina ReprintPoints encontramos la etiqueta reprintPoint2_print, y justo por encima de esta etiqueta la instrucción RET C. Bien, vamos a borrar la etiqueta reprintPoint2_print y todo lo que tiene por debajo hasta el final de la rutina. Después de RET C vamos a incluir JR printPoint2_print.

En una implementación anterior, PrintPoints y ReprintPoints pintaban de distinta manera, pues ReprintPoints hacía OR con los píxeles de la pantalla, pero éste ya no es el caso, por lo que vamos a utilizar el código que pinta el marcador del jugador 2 para repintarlo, y nos vamos a ahorrar 25 bytes y 138 ciclos de reloj.

La etiqueta printPoint2_print no existe, por lo que vamos a incluirla. Buscamos la etiqueta PrintPoints, como vemos primero pinta el marcador del jugador 1, y una vez que ha finaliza pinta el marcador del jugador 2, que empieza justo debajo de la segunda llamada a PrintPoint. Pues es ahí, justo debajo del segundo CALL PrintPoint, donde vamos a añadir la etiqueta printPoint_2_print.

¡Muchas gracias Spirax!

El aspecto final de las rutinas 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

; 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
push	hl						; Preserva el valor de HL
ld		hl, POINTS_P1			; Carga en HL la dirección de memoria donde se pintan
								; los puntos del jugador 1					
call	PrintPoint				; 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						; 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
; Spirax
ld		hl, POINTS_P1 + 1		; Carga en HL la dirección de memoria donde se pinta
								; el segundo dígito de los puntos del jugador 1
call	PrintPoint				; Pinta el segundo dígito del marcador del jugador 1

printPoint_2_print:	
; 1er dígito del jugador 2
ld		a, (p2points)			; Carga en A los puntos del jugador 2
call	GetPointSprite			; Obtiene el sprite a pintar en el marcador
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
push	hl						; Preserva el valor de HL
ld		hl, POINTS_P2			; Carga en HL la dirección de memoria donde se pintan
								; los puntos del jugador 2
call	PrintPoint				; 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						; 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
; Spirax
ld		hl, POINTS_P2 + 1		; Carga en HL la dirección de memoria donde se pinta
								; el segundo dígito de los puntos del jugador 2
                                
PrintPoint:
ld		b, $10					; Cada dígito son 1 byte por 16 (scanlines)
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

ret

; -----------------------------------------------------------------------------
; Repinta 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.
; -----------------------------------------------------------------------------
ReprintPoints:
ld		hl, (ballPos)			; Carga en HL la posición de la bola
call	GetPtrY					; Obtiene tercio, línea y scanline de la posición de la bola
cp		POINTS_Y_B				; Lo compara con el límite inferior del marcador
ret		nc						; Si no hay acarreo, pasa por debajo y sale
ld		a, l					; Carga en A la línea y columna de la posición de la bola
and		$1f						; Se queda con la columna
cp		POINTS_X1_L				; Lo compara con el límite izquierdo del marcador 1
ret		c						; Si hay acarreo, pasa por la izquierda y sale
jr		z, reprintPoint_1_print ; Si es 0, está justo en el margen izquierdo 
 								; y salta para pintar

cp		POINTS_X2_R				; Lo compara con el límite derecho de marcador 2
jr		z, printPoint_2_print	; Si es 0, está justo en el margen derecho 
 								; y salta para pintar
ret		nc						; Si no hay acarreo, pasa por la derecha y sale

reprintPoint_1:
cp		POINTS_X1_R				; Lo compara con el límite derecho de marcador 1
jr		c, reprintPoint_1_print	; Si hay acarreo, pasa por el marcador 1 
 								; y salta para pintar
jr		nz, reprintPoint_2		; Si no es cero, pasa por la derecha 
 								; y salta para comprobar paso por marcador 2
                                
reprintPoint_1_print:
ld		a, (p1points)			; Carga en A los puntos del jugador 1
call	GetPointSprite			; Obtiene el sprite a pintar en el marcador

; 1er dígito
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
push	hl						; Preserva el valor de HL
ld		hl, POINTS_P1			; Carga en HL la dirección de memoria donde se pintan
								; los puntos del jugador 1
call	PrintPoint				; Pinta el primer dígito del marcador del jugador 1
pop		hl						; Recupera el valor de HL

; 2º dígito
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 + 1		; Carga en HL la dirección de memoria donde se pinta
								; el segundo dígito de los puntos del jugador 1
jr		PrintPoint				; Pinta el segundo dígito del marcado del jugador 2

reprintPoint_2:					
cp		POINTS_X2_L				; Lo compara con el límite derecho de marcador 2
ret		c						; Si hay acarreo, pasa por la izquierda y sale
; Spirax
jr		printPoint_2_print		; Pinta el marcador del jugador

Compilamos, cargamos en el emulador y comprobamos que todo sigue funcionando.

Bug del golpeo de la bola en la parte baja de la pala

Es el momento de arreglar un bug que arrastramos desde que implementamos el cambio de velocidad e inclinación de la bola en base a en que parte de la pala golpea. Cuando la bola golpea en el último scanline de la pala, no cambia inclinación, ni velocidad, ni dirección vertical. ¿A qué se debe?

El motivo está en la forma en la que implementamos la detección de colisiones. Antes de evaluar en que parte de la pala golpea, evaluamos si golpea en la pala, y aquí está el error; cuando golpea en el último scanline de la pala sale de la rutina, con el flag Z activado indicando que hay colisión, pero sin evaluar en qué parte de la pala golpea.

Abrimos el archivo Video.asm y localizamos la etiqueta CheckCrossY. Quince líneas más abajo nos encontramos con esto.

ret		nc		; Si no hay acarreo la bola pasa por debajo 
				; de la pala o colisiona en el último scanline.
				; En este último caso ya se ha activado el flag Z

Si leemos atentamente los comentarios, salimos de la rutina si no hay acarreo (flag Z desactivado = no hay colisión). El problema es qué si no hay acarreo, el resultado puede ser mayor o igual a 0. Es decir, si el resultado es 0, salimos de la rutina con el flag Z activado (hay colisión) sin evaluar en que parte de la pala ha golpeado.

Para solucionar este aspecto vamos a hacer una doble comprobación y añadir una nueva etiqueta a la que saltar.

El código actual de la parte que vamos a tocar es el siguiente.

ret		nc		; Si no hay acarreo la bola pasa por debajo 
				; de la pala o colisiona en el último scanline.
				; En este último caso ya se ha activado el flag Z

; Dependiendo de donde sea la colisión, se asigna grado de inclinación
; y velocidad a la bola
ld		a, c	; Carga la posición del penúltimo scanline de la pala en A

Vamos a añadir una línea antes de RET NC y una etiqueta antes de LD A, C, dejando el código de la siguiente manera.

jr		z, checkCrossY_eval	; Si es cero, choca en el último scanline
ret		nc					; Si no hay acarreo la bola pasa por debajo y sale.

; Dependiendo de donde sea la colisión, se asigna grado de inclinación
; y velocidad a la bola
checkCrossY_eval:
ld		a, c				; Carga la posición del penúltimo scanline de la pala en A

Incluso ese JR Z, checkCrossY_eval lo podríamos cambiar por JR Z, checkCrossY_5_5, pues sabemos que ha golpeado en la parte inferior de la pala (probad de las dos maneras).

Compilamos, cargamos en el emulador y comprobamos que hemos arreglado el bug.

Sonido

Y abordamos el penúltimo paso; vamos a implementar efectos de sonido cuando la bola golpea con los laterales, las palas, o cuando se marque algún punto.

Añadimos el archivo Sound.asm, y añadimos las contantes y rutinas necesarias para nuestros efectos de sonido, que van a ser los sonidos que se van a reproducir cuando la bola rebota contra los distintos elementos.

Vamos a definir tres sonidos distintos:

  • Cuando se marca un punto.
  • Cuando la bola choca con una pala.
  • Cuando la bola choca con el borde.

Para cada sonido tenemos que definir la nota y la frecuencia. La frecuencia es el tiempo que va a durar la nota, y la vamos a identificar con el sufijo FQ.

; Punto
C_3: 	EQU $0D07
C_3_FQ:	EQU $0082 / $10

; Pala
C_4: 	EQU $066E
C_4_FQ: EQU $0105 / $10

; Rebote
C_5: 	EQU $0326
C_5_FQ: EQU $020B / $10

Todos los sonidos que vamos a usar son DO, aunque en distintas escalas; a mayor escala, el sonido es más agudo. Las frecuencias especificadas son las que hacen que la nota dure un segundo, es por eso que las dividimos por 16. Si las multiplicáramos por 2, la nota duraría 2 segundos. A cada nota, en cada escala, le corresponde una frecuencia propia.

La siguiente constante que vamos a ver, es la dirección de memoria donde está alojada la rutina BEEPER de la ROM.

BEEPER:	EQU $03B5

Esta rutina recibe en HL la nota y en DE la duración, y altera el valor de los registros AF, BC, DE, HL e IX, además de otro aspecto que veremos más adelante. Debido a que la rutina BEEPER de la ROM altera tantos registros, es recomendable no llamarla directamente; vamos a implementar una rutina que lo haga.

La rutina que vamos a implementar, recibe en A el tipo de sonido a emitir, 1 = punto, 2 = pala, 3 = borde, y no altera el valor de ningún registro.

PlaySound:
push	de
push	hl

Preservamos el valor de los registros DE, PUSH DE, y HL, PUSH HL.

cp		$01
jr		z, playSound_point

Comprobamos si el sonido a reproducir es de tipo 1 (punto), CP $01, y de ser así saltamos, JR Z, playSound_point.

cp		$02
jr		z, playSound_paddle

Si el sonido no es de tipo 1, comprobamos si es de tipo 2 (pala), CP $02, y de ser así saltamos, JR Z, playSound_paddle.

Si el sonido no es de tipo 1, ni de tipo 2, es de tipo 3 (borde).

ld		hl, C_5
ld		de, C_5_FQ
jr		beep

Cargamos en HL la nota, LD HL, C_5, cargamos en DE la frecuencia (duración), LD DE, C_5_FQ, y saltamos a reproducir el sonido, JR beep.

Si el sonido es de tipo 1 o 2, hacemos lo mismo, pero con los valores de cada sonido.

playSound_point:
ld		hl, C_3
ld		de, C_3_FQ
jr		beep

playSound_paddle:
ld		hl, C_4
ld		de, C_4_FQ

Nos ahorramos el último JR, ya que justo después viene la rutina que reproduce el sonido.

beep:
push	af
push	bc
push	ix
call	BEEPER
pop		ix
pop		bc
pop		af

pop		hl
pop		de

ret

Preservamos los valores de AF, PUSH AF, de BC, PUSH BC, y de IX, PUSH IX. Llamamos a la rutina de la ROM, CALL BEEPER, y recuperamos los valores de IX, POP IX, de BC, POP BC, de AF, POP AF, de HL, POP HL, y de DE, POP DE. Los valores de HL y DE los preservamos al principio de la rutina PlaySound. Por último, salimos, RET.

El aspecto final del archivo Sound.asm, es el siguiente.

; -----------------------------------------------------------------------------
; Sound
; Fichero con los sonidos
; -----------------------------------------------------------------------------
; Punto
C_3: 	EQU $0D07
C_3_FQ: EQU $0082 / $10

; Pala
C_4: 	EQU $066E
C_4_FQ: EQU $0105 / $10

; Rebote
C_5: 	EQU $0326
C_5_FQ: EQU $020B / $10

; -----------------------------------------------------------------------------
; Rutina beeper de la ROM.
;
; Entrada:	HL	=	Nota.
;			DE	=	Duración.
;
; Altera el valor de los registros AF, BC, DE, HL e IX.
; -----------------------------------------------------------------------------
BEEPER:	EQU $03B5

; -----------------------------------------------------------------------------
; Reproduce el sonido de los rebotes.
; Entrada:	A	=	Tipo de rebote.	1. Punto
;									2. Pala
;									3. Borde
; -----------------------------------------------------------------------------
PlaySound:
; Preserva el valor de los registros
push    de
push    hl

cp      $01					; Evalúa si se emite el sonido de Punto
jr      z, playSound_point	; Si es así salta

cp      $02					; Evalúa si se emite el sonido de Pala
jr      z, playSound_paddle	; Si es así salta

; Se emite el sonido de Borde
ld      hl, C_5				; Carga en HL la nota
ld      de, C_5_FQ			; Carga en DE la duración (frecuencia)
jr      beep				; Salta a emitir el sonido

; Se emite el sonido de Punto
playSound_point:
ld      hl, C_3				; Carga en HL la nota
ld      de, C_3_FQ			; Carga en DE la duración (frecuencia)
jr      beep				; Salta a emitir el sonido

; Se emite el sonido de Pala
playSound_paddle:
ld      hl, C_4				; Carga en HL la nota
ld      de, C_4_FQ			; Carga en DE la duración (frecuencia)

; Hace sonar la nota
beep:
; Preserva el valor de los registros ya que la rutina BEEPER de la ROM los altera
push    af
push    bc
push    ix

call    BEEPER				; Llama a la rutina BEEPER de la ROM

; Recupera el valor de los registros
pop     ix
pop     bc
pop     af

pop     hl
pop     de

ret

Para acabar, tenemos que llamar a nuestra nueva rutina para emitir los sonidos de los rebotes de la bola. Abrimos el archivo Game.asm y localizamos la etiqueta checkBallCross_right. Vamos a añadir dos líneas entre la línea RET NZ, y la línea LD A, (ballSetting).

ld		a, $02
call	PlaySound

Carga el tipo de sonido en A, LD A, $02, y emite el sonido, CALL PlaySound.

Localizamos la etiqueta checkBallCross_left. Vamos a añadir las mismas dos líneas de antes entre la línea RET NZ, y la línea LD A, (ballSetting).

ld		a, $02
call	PlaySound

Localizamos la etiqueta moveBall_upChg. Justo debajo de la misma, añadimos dos líneas casi iguales a las anteriores.

ld		a, $03
call	PlaySound

Localizamos la etiqueta moveBall_downChg. Justo debajo de la misma, añadimos las dos líneas anteriores.

ld		a, $03
call	PlaySound

Localizamos la etiqueta moveBall_rightChg, y justo debajo añadimos.

ld		a, $01
call	PlaySound

Cinco líneas más abajo localizamos CALL SetBallLeft. Debajo añadimos.

ld		a, $03
call	PlaySound

Localizamos la etiqueta moveBall_leftChg, y justo debajo añadimos.

ld		a, $01
call	PlaySound

Cinco líneas más abajo localizamos CALL SetBallRight. Debajo añadimos.

ld		a, $03
call	PlaySound

Por último, abrimos el archivo Main.asm, localizamos la rutina Loop y justo encima añadimos las siguientes líneas.

ld		a, $03
call	PlaySound

Nos vamos al final del fichero, y en la parte de los “includes”, incluimos el archivo Sound.asm.

include	"Sound.asm"

Si todo ha ido bien, hemos llegado al final. Compilamos, cargamos en el emulador y…

Ensamblador ZX Spectrum - Borde en blanco
Ensamblador ZX Spectrum, borde en blanco

¿Qué le pasa al borde? ¿Por qué es blanco? Bueno, ya advertimos que la rutina BEEPER de la ROM altera muchas cosas, y una de ellas es el color del borde, aunque tiene fácil solución.

Por suerte, tenemos una variable de sistema donde podemos guardar el color del borde. En esta variable se guardan también los atributos de la pantalla inferior. El fondo de dicha pantalla es el color del borde.

Abrimos el archivo Video.asm y al inicio del mismo declaramos una constante con la dirección de memoria de dicha variable del sistema.

BORDCR:	EQU $5c48

Localizamos la rutina Cls, y antes de la línea LD HL, $5800, añadimos.

ld		a, $07	; Fondo negro, tinta blanca

Modificamos la línea LD (HL), $07 dejándola así.

ld		(hl), a

Por último, antes de RET, añadimos.

ld		(BORDCR), a

Compilamos, cargamos en el emulador, y ahora sí. ¿Hemos terminado nuestro PorompomPong?

Ensamblador ZX Spectrum - Borde en negro
Ensamblador ZX Spectrum, borde en negro

Compatibilidad con 16K

Todavía nos falta una última cosa por hacer. ¿Es compatible nuestro programa con el modelo 16K? Pues todavía no, pero como no trabajamos con interrupciones, es muy sencillo hacerlo compatible.

Vamos a abrir el archivo Main.asm, vamos a localizar las directivas ORG y END, y vamos a sustituir $8000 por $5dad en el caso de ORG. En el caso de END, vamos a sustituir $8000 por Main, que es la etiqueta de entrada al programa. Si ahora compilamos y cargamos en el emulador con el modelo 16K, nuestro programa es compatible.

Si nos fijamos bien, podemos observar que se ha perdido algo de velocidad. Esta pérdida es debida a que los segundos 16 KiB del ZX Spectrum, que es donde ahora cargamos el programa, es lo que se llama memoria contenida, y está compartida con la ULA; cuando la ULA trabaja, todo se para.

Vamos a volver a cambiar la velocidad a la que va la bola.

Abrimos el archivo Sprite.asm, localizamos la etiqueta ballSetting, comentamos la línea db $21 y escribimos justo debajo.

; or	$21
or		$19

Ahora la bola se inicia a velocidad 3, que va a ser la más lenta.

Abrimos el archivo Game.asm, localizamos la etiqueta SetBallLeft, comentamos la línea 7, y escribimos justo debajo.

; or	$21
or		$19

Ahora, cuando reiniciamos la bola para que salga por la izquierda de la pantalla, se inicia a velocidad 3.

Localizamos la etiqueta SetBallRight, comentamos la línea 7, y escribimos justo debajo.

; db	$61
db		$59

Ahora, cuando reiniciamos la bola para que salga por la derecha de la pantalla, se inicia a velocidad 3.

Localizamos la etiqueta checkCrossY_1_5, comentamos la línea 7, y escribimos justo debajo.

; or	$21
or		$19

Ahora la velocidad de la bola es 3 en lugar de 4.

Localizamos la etiqueta checkCrossY_2_5, comentamos la línea 7, y escribimos justo debajo.

; or	$1a
or		$12

Ahora la velocidad de la bola es 2 en lugar de 3.

Localizamos la etiqueta checkCrossY_3_5, comentamos la línea 7, y escribimos justo debajo.

; or	$17
or		$0f

Ahora la velocidad de la bola es 1 en lugar de 2.

Localizamos la etiqueta checkCrossY_4_5, comentamos la línea 7, y escribimos justo debajo.

; or	$9a
or		$92

Ahora la velocidad de la bola es 2 en lugar de 3.

Localizamos la etiqueta checkCrossY_5_5, comentamos la línea 3, y escribimos justo debajo.

; or	$a1
or		$99

Ahora la velocidad de la bola es 3 en lugar de 4.

Compilamos, cargamos en el emulador y probamos. ¿Hemos terminado?

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, sonido y optimizació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.

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 «0x0A Ensamblador ZX Spectrum Pong – Sonido y optimización»

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: