Ensamblador para ZX Spectrum – Pong: $0B cambio de dirección/velocidad de la bola al golpear la pala
Llegamos a la recta final, ya casi estamos acabando.

Ensamblador para ZX Spectrum – Pong: Paso 9, cambio de dirección/velocidad de la bola al golpear la pala
En este paso vamos a prescindir de parte de lo que hemos implementado en el paso anterior. La velocidad de la bola va a cambiar dependiendo de con qué parte de la pala colisione.
Creamos la carpeta Paso09 y copiamos los archivos Controls.asm, Game.asm, Main.asm, Sprite.asm y Video.asm desde la carpeta Paso08.
Lo primero que vamos a hacer es quitar la posibilidad de cambiar la velocidad de la bola con las teclas del 1 al 3.
Abrimos el archivo Controls.asm y en la rutina ScanKeys, borramos todas las líneas hasta la etiqueta scanKeys_ctrl, quedando el inicio de la rutina de la siguiente manera:
ScanKeys: ld d, $00 scanKeys_A:
Si compilamos y cargamos en el emulador, vemos que la velocidad de la bola no cambia.
Vamos a añadir nuevas constantes y variables en el archivo Sprite.asm, para poder controlar la inclinación de la bola. También vamos a cambiar los sprites de las palas; ambas van a dibujar cuatro píxeles, pero en ambos casos dibujaremos los más cercanos al centro de la pantalla.
Añadimos las constantes que indican la rotación a asignar a la bola cuando se produce la colisión con la pala:
CROSS_LEFT_ROT: EQU $ff CROSS_RIGHT_ROT: EQU $01
Añadimos la posición inicial de la bola, y el número acumulado de movimientos que debe llevar la bola para cambiar la posición Y. Este último dato lo vamos a usar para cambiar la inclinación de la bola:
BALLPOS_INI: EQU $4850 ballMovCount: db $00
Cambiamos la configuración inicial de la bola y la documentación (comentarios) de la misma:
; 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) y 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 la nueva configuración, la bola inicialmente se mueve hacia la derecha y hacia arriba, con una velocidad lenta, y en cada movimiento cambia la posición Y.
Añadimos distintos sprites para las palas y eliminamos el anterior:
;PADDLE: EQU $3c ; # Eliminar línea # PADDLE1: EQU $0f PADDLE2: EQU $f0
Por último, añadimos las posiciones iniciales de las palas:
PADDLE1POS_INI: EQU $4861 PADDLE2POS_INI: EQU $487e
Hemos añadido sprites distintos para cada pala y eliminado la constante que usábamos para pintar las palas; si compilamos, nos dará errores.
Vamos a solucionar esos errores modificando la rutina PrintPaddle de Video.asm.
La rutina PrintPaddle recibe en el registro HL la posición de la pala. En el registro C recibirá el sprite de la pala.
Modificamos la línea justo debajo de la etiqueta printPaddle_loop:
ld (hl), PADDLE
y la dejamos como sigue:
ld (hl), c
Compilamos, y aunque no da ningún error, al cargar en el emulador vemos que los resultados no son los deseados:

La pala que pinta no se corresponde con el sprite que hemos definido. Esto es debido a que no hemos cargado en C cual es el sprite que debe pintar.
Abrimos el archivo Main.asm, y buscamos la etiqueta loop_continue. A partir de la línea 5 es donde imprimimos las palas, cargando en HL la posición de la pala y llamando al pintado de la misma. Antes de llamar al pintado de la pala, debemos especificar que sprite debe pintar.
Este es el aspecto una vez hecha la modificación:
ld hl, (paddle1pos) ld c, PADDLE1 call PrintPaddle ld hl, (paddle2pos) ld c, PADDLE2 call PrintPaddle
Compilamos, abrimos en el emulador, y comprobamos que las palas se vuelven a pintar bien:

Aprovechando que estamos en Main.asm, vamos a cambiar un comportamiento del que quizás no os habéis percatado. Cuando se acaba un partido, y al iniciar otro, las palas siguen en la misma posición donde estaban al acabar el partido anterior, y la bola sale desde el campo del jugador que anotó el último punto.
Para modificar este comportamiento, vamos a añadir las siguientes líneas antes de la etiqueta Loop:
ld hl, BALLPOS_INI ld (ballPos), hl ld hl, PADDLE1POS_INI ld (paddle1pos), hl ld hl, PADDLE2POS_INI ld (paddle2pos), hl
Con estas líneas situamos la bola y las palas en sus posiciones iniciales.
Si compilamos, vemos que nos da un error:
ERROR on line 68 of file Main.asm ERROR: Relative jump out of range
Este error es debido a que, al ir añadiendo líneas, tenemos algún JR que está fuera de rango. JR solo puede saltar 127 bytes hacia adelante o 128 hacia atrás, y tenemos algún JR que salta a alguna dirección fuera de este rango.
En concreto, tenemos al final del archivo Main.asm, dos JR Main y un JR Loop. Sustituimos estos tres JR por JP, y solucionamos el error. JP ocupa un byte más que JR, por lo que nuestro programa acaba de crecer 3 bytes, pero hemos reducido 6 ciclos de reloj.
Compilamos, cargamos en el emulador y comprobamos que al acabar la partida e iniciar otra, tanto la bola como las palas, vuelven a su posición inicial.
Vamos a implementar el cambio de velocidad, inclinación y dirección de la bola al colisionar con las palas.
Abrimos el archivo Game.asm y buscamos la etiqueta checkBallCross_left. Tres líneas por encima encontramos:
ld a, $ff
Modificamos esta línea y la dejamos como sigue:
ld a, CROSS_LEFT_ROT
Buscamos la etiqueta CheckCrossX. Tres líneas por encima encontramos:
ld a, $01
Modificamos esta línea y la dejamos como sigue:
ld a, CROSS_RIGHT_ROT
Hemos cambiado los valores por constantes, para si en un futuro hay que cambiar los valores, tenerlos mejor localizados.
El siguiente paso es cambiar la configuración de la bola, dependiendo de en qué parte de la pala colisiona.
Vamos a dividir la pala en 5 partes. Dependiendo de donde colisione la bola, el comportamiento será:
Zona de golpeo | Dirección verical | Inclinación | Velocidad |
1/5 | Arriba | Diagonal | 3 (lento) |
2/5 | Arriba | Semi diagonal | 2 (normal) |
3/5 | No cambia | Semi plano | 1 (rápido) |
4/5 | Abajo | Semi diagonal | 2 (normal) |
5/5 | Abajo | Diagonal | 3 (lento) |
Localizamos la etiqueta CheckCrossY, y nos vamos a la penúltima línea, XOR A, e implementamos justo antes de ella:
ld a, c sub $15 ld c, a ld a, b add a, $04 ld b, a
Cuando llegamos a este punto, en C tenemos la posición del penúltimo scanline de la pala, y en B la posición de la bola. Ambas posiciones están en formato TTLLLLSSS.
Cargamos en A la posición del penúltimo scanline de la pala, LD A, C, nos posicionamos en el primero, SUB $15, y volvemos a cargar el valor en C, LD C, A.
Cargamos en A la posición de la bola, LD A, B, nos posicionamos en la parte baja de la bola, ADD A, $04, y volvemos a cargar el valor en B, LD B, A.
A partir de aquí implementamos el cambio de comportamiento, dependiendo de donde colisiona la bola:
checkCrossY_1_5: ld a, c add a, $04 cp b jr c, checkCrossY_2_5
Cargamos la posición vertical de la pala en A, LD A, C, nos posicionamos en el último scanline de la primera parte, ADD A, $04, y lo comparamos con la posición de la bola, CP B. Si hay acarreo, la bola está más abajo y salta a comprobar la siguiente parte, JR C, checkCrossY_2_5.
Si no hay acarreo, la bola ha colisionado en esta parte y tenemos que cambiar su configuración:
ld a, (ballSetting) and $40 or $31 jr checkCrossY_end
Cargamos la configuración de la bola en A, LD A, (ballSetting), nos quedamos con la dirección horizontal (ya viene calculada), AND $40, y ponemos dirección vertical hacia arriba, velocidad 3 e inclinación diagonal, OR $31. Saltamos al final de la rutina, JR checkCrossY_end.
Si la bola no ha colisionado con la primera parte de la pala, comprobamos si lo ha hecho con la segunda:
checkCrossY_2_5: ld a, c add a, $09 cp b jr c, checkCrossY_3_5
Cargamos la posición vertical de la pala en A, LD A, C, nos posicionamos en el último scanline de la segunda parte, ADD A, $09, y lo comparamos con la posición de la bola, CP B. Si hay acarreo, la bola está más abajo y salta a comprobar la siguiente parte, JR C, checkCrossY_3_5.
Si no hay acarreo, la bola ha colisionado en esta parte y tenemos que cambiar su configuración:
ld a, (ballSetting) and $40 or $22 jr checkCrossY_end
Cargamos la configuración de la bola en A, LD A, (ballSetting), nos quedamos con la dirección horizontal (ya viene calculada), AND $40, y ponemos dirección vertical hacia arriba, velocidad 2 e inclinación semi diagonal, OR $22. Saltamos al final de la rutina, JR checkCrossY_end.
Si la bola no ha colisionado con la segunda parte de la pala, comprobamos si lo ha hecho con la tercera:
checkCrossY_3_5: ld a, c add a, $0d cp b jr c, checkCrossY_4_5
Cargamos la posición vertical de la pala en A, LD A, C, nos posicionamos en el último scanline de la tercera parte, ADD A, $0D, y lo comparamos con la posición de la bola, CP B. Si hay acarreo, la bola está más abajo y salta a comprobar la siguiente parte, JR C, checkCrossY_4_5.
Si no hay acarreo, la bola ha colisionado en esta parte y tenemos que cambiar su configuración:
ld a, (ballSetting) and $c0 or $1f jr checkCrossY_end
Cargamos la configuración de la bola en A, LD A, (ballSetting), nos quedamos con la dirección horizontal y con la vertical (ya vienen calculadas), AND $C0, y ponemos velocidad 1 e inclinación semi plana, OR $1F. Saltamos al final de la rutina, JR checkCrossY_end.
Si la bola no ha colisionado con la tercera parte de la pala, comprobamos si lo ha hecho con la cuarta:
checkCrossY_4_5: ld a, c add a, $11 cp b jr c, checkCrossY_5_5
Cargamos la posición vertical de la pala en A, LD A, C, nos posicionamos en el último scanline de la cuarta parte, ADD A, $11, y lo comparamos con la posición de la bola, CP B. Si hay acarreo, la bola está más abajo y salta a comprobar la siguiente parte, JR C, checkCrossY_5_5.
Si no hay acarreo, la bola ha colisionado en esta parte y tenemos que cambiar su configuración:
ld a, (ballSetting) and $40 or $a2 jr checkCrossY_end
Cargamos la configuración de la bola en A, LD A, (ballSetting), nos quedamos con la dirección horizontal (ya viene calculada), AND $40, y ponemos dirección vertical hacia abajo, velocidad 2 e inclinación semi diagonal, OR $A2. Saltamos al final de la rutina, JR checkCrossY_end.
Si la bola no ha colisionado con la cuarta parte de la pala, lo ha hecho con la quinta:
checkCrossY_5_5: ld a, (ballSetting) and $40 or $b1
Cargamos la configuración de la bola en A, LD A, (ballSetting), nos quedamos con la dirección horizontal (ya viene calculada), AND $40, y ponemos dirección vertical hacia abajo, velocidad 3 e inclinación diagonal, OR $B1.
Por último, justo por encima de XOR A, vamos a añadir la etiqueta de fin de función a la que hemos estado haciendo referencia, y vamos a cargar la nueva configuración de la bola en memoria:
checkCrossY_end: ld (ballSetting), a
Después de XOR A, vamos a poner el contador de movimientos de la bola a 0:
ld (ballMovCount), a
El aspecto final de la rutina es el siguiente:
; – --------------------------------------------------------------------------- ; Evalúa si la bola colisiona en el eje Y con la pala. ; En el caso de colisionar, actualiza la configuración de la bola. ; Entrada: HL -> Posición de la pala ; Salida: Z -> Colisiona. ; NZ -> No colisiona. ; Altera el valor de los registros AF, BC y HL. ; – --------------------------------------------------------------------------- CheckCrossY: call GetPtrY ; Obtiene la posición vertical de la pala (TTLLLSSS) ; La posición devuelta apunta al primer scanline de la pala que está a 0 ; apunta al siguiente inc a ld c, a ; Carga el valor en C ld hl, (ballPos) ; Carga en HL la posición de la bola call GetPtrY ; Obtiene la posición vertical de la bola (TTLLLSSS) ld b, a ; Carga el valor en B ; Comprueba si la bola pasa por encima de la pala ; La bola está compuesta de 1 scanline a 0, 4 a $3c y otro a 0 ; La posición apunta al 1er scanline, y se comprueba la colisión con el 5º add a, $04 ; Apunta la posición de la bola al 5º scanline sub c ; Resta a la posición de la bola, la posición de la pala ret c ; Si hay acarreo sale porque la bola pasa por encima ; Comprueba si la bola pasa por debajo de la pala ld a, c ; Carga la posición vertical de la pala en A add a, $16 ; Le suma 22 para apuntar al penúltimo scanline, ; último que no es 0 ld c, a ; Lo vuelve a cargar en C ld a, b ; Carga la posición vertical de la bola inc a ; Le suma 1 para apuntar el scanline 1, primero que no es 0 sub c ; Resta a la posición de la bola, la posición de la pala ret nc ; Si no hay acarreo la bola pasa por debajo ; de la pala o colisiona en el último scanline. ; En este último caso se activa 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 sub $15 ; Lo vuelve a posicionar en el primero ld c, a ; Carga el valor en C ld a, b ; Carga en A la posición de la bola add a, $04 ; Se posiciona en la parte baja de la bola ld b, a ; Carga el valor en B checkCrossY_1_5: ld a, c ; Carga la posición vertical de la pala en A add a, $04 ; Se posiciona en el último scanline de 1/5 cp b ; Lo compara con la posición de la bola jr c, checkCrossY_2_5 ; La bola está más abajo, salta ld a, (ballSetting) ; Carga la configuración de la bola en A and $40 ; Se queda con la dirección horizontal or $31 ; Hacia arriba, velocidad 3 e inclinación diagonal jr checkCrossY_end ; Fin de la rutina checkCrossY_2_5: ld a, c ; Carga la posición vertical de la pala en A add a, $09 ; Se posiciona en el último byte de 2/5 cp b ; Lo compara con la posición de la bola jr c, checkCrossY_3_5 ; La bola está más abajo, salta ld a, (ballSetting) ; Carga la configuración de la bola en A and $40 ; Se queda con la dirección horizontal or $22 ; Hacia arriba, velocidad 2 e inclinación semi diagonal jr checkCrossY_end ; Fin de la rutina checkCrossY_3_5: ld a, c ; Carga la posición vertical de la pala en A add a, $0d ; Se posiciona en el último byte de 3/5 cp b ; Lo compara con la posición de la bola jr c, checkCrossY_4_5 ; La bola está más abajo, salta ld a, (ballSetting) ; Carga la configuración de la bola en A and $c0 ; Se queda con la dirección horizontal y vertical or $1f ; Hacia arriba/abajo, velocidad 1 e inclinación semi plano jr checkCrossY_end ; Fin de la rutina checkCrossY_4_5: ld a, c ; Carga la posición vertical de la pala en A add a, $11 ; Se posiciona en el último byte de 4/5 cp b ; Lo compara con la posición de la bola jr c, checkCrossY_5_5 ; La bola está más abajo, salta ld a, (ballSetting) ; Carga la configuración de la bola en A and $40 ; Se queda con la dirección horizontal y vertical or $a2 ; Hacia abajo, velocidad 2 e inclinación semi diagonal jr checkCrossY_end ; Fin de la rutina checkCrossY_5_5: ld a, (ballSetting) ; Carga la configuración de la bola en A and $40 ; Se queda con la dirección horizontal or $b1 ; Hacia abajo, velocidad 3 e inclinación diagonal ; Hay colisión checkCrossY_end: ld (ballSetting), a ; Carga en memoria la configuración actual de la bola xor a ; Activa el flag Z y pone A = 0 ld (ballMovCount), a ; Pone el contador de movimientos de la bola a 0 ret
Compilamos, cargamos en el emulador y vemos los resultados.
Vemos que la velocidad sí cambia dependiendo de donde colisiona la bola, pero no la inclinación. Además, al marcar un tanto, la velocidad no se reinicia, lo cual hace que sea muy difícil seguir jugando si la bola va a la velocidad máxima.
¿Por qué cambia la velocidad, pero no la inclinación?
Si hacemos memoria, en el paso anterior implementamos la posibilidad de cambiar la velocidad de la bola con las teclas del 1 al 3. De hecho, este paso lo iniciamos avisando de que íbamos a prescindir de esta implementación, pero de lo que no se ha prescindido es del cambio que hicimos en Main.asm para tener en cuenta la velocidad de la bola que marque la configuración; por eso la velocidad cambia.
Nos falta la implementación para tener en cuenta la inclinación, y para que cuando se marca un punto, velocidad e inclinación de la bola se reinicien.
Vamos a empezar con el cambio de inclinación. Seguimos en el archivo Game.asm, implementando la rutina que va a cambiar la posición Y de la bola. La vamos a implementar después del RET de la etiqueta moveBall_end:
MoveBallY: ld a, (ballSetting) and $0f ld d, a
Cargamos en A la configuración de la bola, LD A, (ballSetting), nos quedamos con la inclinación, AND $0F, y cargamos el valor en D, LD A, D.
ld a, (ballMovCount) inc a ld (ballMovCount), a cp d ret nz
Cargamos los movimiento de la bola en A, LD A, (ballMovCount), lo incrementamos en 1, INC A, cargamos el valor en memoria, LD (ballCount), A, y lo comparamos con D, que contiene el número de movimientos necesarios para cambiar la posición Y de la bola, CP D. Si no son iguales, no se ha llegado al valor necesario y salimos, RET NZ.
xor a ld (ballMovCount), a ret
Si hemos llegado al valor, ponemos A = 0 y activamos el flag Z, XOR A, ponemos a 0 los movimientos acumulados de la bola, LD (ballMovCount), A, y salimos, RET. Al activar el flag Z se indica, a quien llame, que se debe cambiar la posición Y de la bola.
El aspecto final de la rutina es el siguiente:
; – --------------------------------------------------------------------------- ; Cambia la posición Y de la bola ; Altera el valor de los registros AF y D. ; – --------------------------------------------------------------------------- MoveBallY: ld a, (ballSetting) ; Carga en A la configuración de la bola and $0f ; Se queda con la inclinación ld d, a ; Carga el valor en D ld a, (ballMovCount) ; Carga en A los movimientos acumulados de la bola inc a ; Incrementa A ld (ballMovCount), a ; Carga el valor en memoria cp d ; Lo compara con la inclinación ret nz ; Si no son iguales, sale. No se cambia la posición ; La posición debe cambiar xor a ; Pone A = 0 y activa el flag Z ld (ballMovCount), a ; Pone los movimientos acumulados de la bola a 0 ret
Localizamos la etiqueta moveBall_up, y entre las líneas JR Z, movelBall_upChg y CALL PreviousScan, añadimos las siguientes líneas:
call MoveBallY jr nz, moveBall_x
Evaluamos si se tiene que cambiar la posición Y de la bola, CALL MoveBallY, y de no ser así salta, JR NZ, moveBall_x.
Localizamos la etiqueta moveBall_down, y entre las líneas JR Z, moveBall_downChg y CALL NextScan, añadimos las siguientes líneas:
call MoveBallY jr nz, moveBall_x
Evaluamos si se tiene que cambiar la posición Y de la bola, CALL MoveBallY, y de no ser así salta, JR NZ, moveBall_x.
Compilamos, cargamos en el emulador, y comprobamos que ahora cambian la inclinación y la velocidad.
Por último, vamos a hacer que cuando se marque un punto, se reinicien la velocidad y la inclinación de la bola.
Localizamos la rutina SetBallLeft, eliminamos la línea AND $BF, y la sustituimos por las siguientes:
;and $bf ; # Eliminar línea # and $80 or $31
Se queda con la dirección Y, AND $80, y pone dirección horizontal hacia la derecha, velocidad 3 e inclinación diagonal, OR $31.
Antes de la instrucción RET, añadimos las siguientes líneas:
ld a, $00 ld (ballMovCount), a
Ponemos A = 0, LD A, $00, y ponemos los movimientos de la bola a 0, LD (ballMovCount), A.
Localizamos la rutina SetBallRight y eliminamos la línea OR $40 y la sustituimos por las siguientes:
;or $40 ; # Eliminar línea # and $80 or $71
Se queda con la dirección Y, AND $80, y pone dirección horizontal hacia la izquierda, velocidad 3 e inclinación diagonal, OR $11.
Antes de la instrucción RET, añadimos las siguientes líneas:
ld a, $00 ld (ballMovCount), a
Ponemos A = 0, LD A, $00, y ponemos los movimientos de la bola a 0, LD (ballMovCount), A.
El aspecto final de ambas 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 $80 ; Se queda con la dirección Y or $31 ; Pone dirección X a derecha, velocidad 3 ; e inclinación diagonal ld (ballSetting), a ; Carga la nueva dirección de la bola en memoria ld a, $00 ld (ballMovCount), a 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 and $80 ; Se queda con la dirección Y or $71 ; Pone dirección X a izquierda, velocidad 3 ; e inclinación diagonal ld (ballSetting), a ; Carga la nueva dirección de la bola en memoria ld a, $00 ld (ballMovCount), a ret
Compilamos, cargamos en el emulador y vemos los resultados, que deben ser los esperados, aunque la bola va algo lenta, ¿o no?
En el próximo capítulo de Ensamblador para ZX Spectrum, implementaremos los efectos de sonido, optimizaremos algunos aspectos de nuestro programa y lo haremos compatible con el modelo de 16K, llegando así a la línea de meta.
Enlaces de interés
- Notepad++.
- Visual Studio Code.
- Sublime Text.
- ZEsarUX.
- PASMO.
- Git.
- Curso de ensamblador Z80 de Compiler Software.
- Z80 instruction set.
Ficheros