0x0B Ensamblador ZX Spectrum Pong – Optimización parte 2
Esta entrega de Ensamblador ZX Spectrum Pong surge a raíz de una serie de comentarios realizados por Spirax en el grupo de Ensamblador Z80 de Telegram, una vez terminadas las sesiones que se realizaron en colaboración con Retro Parla, y cuyos vídeos he ido añadiendo al final de los artículos. Estamos ante una entrega completamente nueva con respecto de la publicación original.
Tabla de contenidos
- Ensamblador ZX Spectrum Pong – Optimización parte 2
- Optimización de PlaySound
- Optimización de ReprintPoints
- Enlaces de interés
- Vídeo
Ensamblador ZX Spectrum Pong – Optimización parte 2
Vamos a realizar dos nuevas optimizaciones, que van a permitir reducir el número de bytes y ciclos de reloj de nuestro Pong.
Como viene siendo costumbre, creamos una carpeta que se llame Paso11 y copiamos todos los ficheros .asm desde la carpeta Paso10, y nos ponemos manos a la obra.
Optimización de PlaySound
La primera optimización la vamos a realizar en la rutina de sonido, siguiendo los comentarios de Spirax, en concreto en la manera en la que evaluamos el sonido que tenemos que emitir.
Abrimos el archivo Sound.asm y localizamos la etiqueta PlaySound, cuyas primeras líneas son.
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 el resultado es 0, el valor de A era 1 y emite el
; sonido del punto
cp $02 ; Evalúa si se emite el sonido de Pala
jr z, playSound_paddle ; Si el resultado es 0, el valor de A era 2 y emite el
; sonido de choque con la pala
En esta rutina utilizamos CP $01 y CP $02 para comprobar que sonido hay que emitir. Cada instrucción CP ocupa 2 bytes y tarda 7 ciclos de reloj; vamos a sustituir estas instrucciones por DEC A, que ocupa 1 byte y tarda 4 ciclos de reloj, por lo que nos vamos a ahorrar 2 bytes y 6 ciclos de reloj. Al contrario de CP, DEC si altera el valor del registro A, pero dado que lo que tenemos en A es el tipo de sonido a emitir, no nos afecta.
Veamos como queda el inicio de la rutina.
PlaySound:
; Preserva el valor de los registros
push de
push hl
; Spirax
dec a ; Evalúa si se emite el sonido de Punto
jr z, playSound_point ; Si el resultado es 0, el valor de A era 1 y emite el
; sonido del punto
; Spirax
dec a ; Evalúa si se emite el sonido de Pala
jr z, playSound_paddle ; Si el resultado es 0, el valor de A era 2 y emite el
; sonido de choque con la pala
Primero preserva el valor de DE, PUSH DE, luego el de HL, PUSH HL, y a continuación decrementa A, DEC A. Si A era 1, el resultado de la operación es 0 y salta a emitir el sonido, JR Z, PlaySound_point.
Si A no era uno, seguimos con las comprobaciones; decrementa A, DEC A, y si el resultado de la operación es 0 salta a emitir el sonido, JR Z, PlaySound_paddle. Si salta a reproducir el sonido es porque inicialmente A valía 2, con el primer decremento vale 1 y con este segundo decremento vale 0.
Si no ha saltado, la rutina sigue tal y como estaba y emite el sonido del punto.
Este es el momento de compilar, cargar en el emulador y comprobar que todo sigue funcionando.
Optimización de ReprintPoints
Esta rutina la implementamos de nuevo en la entrega anterior, la optimizamos y ahora volvemos a optimizarla.
Con esta optimización vamos a ahorrar 20 bytes y 107 ciclos de reloj. Para lograr este ahorro vamos a aplicar el mismo método que aplicamos, en la entrega anterior, siguiendo los comentarios de Spirax; vamos a seguir por el camino que nos marcó.
Como recordaréis, pusimos una etiqueta para que el pintado del marcador del jugador 2 se pudiera llamar de manera independiente; vamos a hacer los mismo con el marcador del jugador 1. Con esta modificación, los marcadores van a tardar algo más en pintarse (solo se pintan al inicio de la partida y al marcar un punto), pero vamos a simplificar la rutina ReprintPoints, ahorrando bytes y ciclos de reloj, y eliminando código redundante.
Vamos a empezar modificando la rutina PrintPoints para que se pueda llamar de manera independiente al pintado de los marcadores de ambos jugadores.
Abrimos el archivo Video.asm y localizamos la etiqueta PrintPoints; justo debajo de ella agregamos otra etiqueta; es la que vamos a llamar para pintar el marcador del jugador 1.
printPoint_1_print:
Entre las etiquetas PrintPoints y printPoint_1_print vamos a añadir las llamadas a pintar el marcador de cada jugador.
call printPoint_1_print ; Pinta el marcador del jugador 1
jr printPoint_2_print ; Pinta el marcador del jugador 2
Lo primero que hacemos es llamar a pintar el marcador del jugador 1, CALL printPoint_1_print, y luego saltar a pintar el marcador del jugador 2, JR printPoint_1_print.
Ya solo queda un cambio en PrintPoints, hay que añadir RET justo antes de la etiqueta printPoint_2_print, para que CALL printPoint_1_print salga correctamente; recordad que el resto de saltos salen por el RET de PrintPoint.
Añadimos RET antes de printPoint_2_print.
ret
printPoint_2_print:
Con esto hemos acabado con las modificaciones necesarias en PrintPoints, y como vemos no hemos ahorrado nada, al contrario, hemos añadido código, añadiendo bytes y ciclos de reloj.
Vamos ahora con el ahorro, para ello localizamos la etiqueta reprintPoint_1_print y la borramos. También borramos las líneas que la siguen hasta llegar a la etiqueta reprintPoint_2, está ultima etiqueta no la borramos.
Localizamos la etiqueta ReprintPoints y nueve líneas más abajo encontramos la instrucción JR Z, reprintPoint_1_print. Dado que esta etiqueta ya no existe, hay que cambiar esta línea y dejarla como sigue.
jr z, printPoint_1_print
Ahora Localizamos la etiqueta reprintPoint_1 y vamos a terminar con las modificaciones. El código actual de esta etiqueta, una vez borrada toda la parte de reprintPoint_1_print es el siguiente.
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
Tenemos que cambiar la doble comprobación; dado que la etiqueta reprintPoint_2 esta ahora justo debajo de la línea JR NZ, reprintPoint_2, ese salto ya no es necesario, pero si que es necesario comprobar si es cero, en cuyo caso hay que pintar el marcador del jugador 1, JR Z, printPoint_1_print, y cambiar el salto de JR C, reprintPoint_1_print por JR C, printPoint_1_print, por lo tanto, el código quedaría así.
reprintPoint_1:
cp POINTS_X1_R ; Lo compara con el límite derecho de marcador 1
jr z, printPoint_1_print
jr c, printPoint_1_print ; Si es 0 o hay acarreo, pasa por el marcador 1
; y salta para pintar
El aspecto final de las rutinas PrintPoints y ReprintPoints 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:
call printPoint_1_print ; Pinta el marcador del jugador 1
jr printPoint_2_print ; Pinta el marcador del jugador 2
printPoint_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 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
ret
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
; Pinta el segundo dígito del marcador 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, printPoint_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 z, printPoint_1_print
jr c, printPoint_1_print ; Si es 0 o hay acarreo, pasa por el marcador 1
; y salta para pintar
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 2
Si comparamos la implementación de ReprintPoints con la implementación que hicimos de esta rutina en el capítulo anterior, podemos observar que la rutina se ha simplificado significativamente, quedando prácticamente reducida a las comprobaciones que incluimos para que el marcador solo se repintase cuando es necesario.
Ahora ya solo queda compilar, cargar en el emulador y comprobar que todo sigue funcionando.
Hemos terminado. ¿O queréis añadir una pantalla de carga?
Podéis descargar todo el código que hemos generado.
Enlaces de interés
- Notepad++
- Visual Studio Code
- Sublime Text
- ZEsarUX
- PASMO
- Git
- Curso de ensamblador Z80 de Compiler Software
- Referencia Z80
- Ensamblador Z80 en Telegram
- Tutorial completo en formato PDF
- Poyecto en itch.io.
- Archivo .dsk con los juegos de los tutoriales
- Personalización y depuración con ZEsarUX
Vídeo
Si lo prefieres, puedes ver el vídeo que grabamos de esta sesió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.
También puedes visitar el resto de tutoriales:
Y recuerda, si lo usas no te limites a copiarlo, intenta entenderlo y adaptarlo a tus necesidades.