0x0A Ensamblador ZX Spectrum Marciano – Joystick y vida extra
En este capítulo de Ensamblador ZX Spectrum Marciano, vamos a implementar los controles con joystick y a conseguir una vida extra cada quinientos puntos. Creamos la carpeta Paso10 y copiamos desde la carpeta Paso09 los archivos Cargador.tap, Const.asm, Ctrl.asm, Game.asm, Graph.asm, Int.asm, Main.asm, make o make.bat, Print.asm y Var.asm.
Retardo
Antes de nada vamos a implementar un retardo entre nivel y nivel, para que nos dé tiempo a prepararnos.
Abrimos el archivo Game.asm y al final del mismo vamos a implementar la rutina que va a producir aproximadamente medio segundo de retardo. La ULA produce cincuenta interrupciones por segundo en sistemas PAL, sesenta en NTSC, y vamos a implementar un bucle que espera veinticinco interrupciones.
; ----------------------------------------------------------------------------- ; Espera veinticinco interrupciones. ; ----------------------------------------------------------------------------- Sleep: ld b, $19 ; Carga veinticinco en B sleep_Loop: halt ; Espera a una interrupción djnz sleep_Loop ; Hasta que B valga 0 ret
No explicamos el código pues con los comentarios, y los conocimientos que tenemos hasta ahora, es suficiente para entenderlo.
Para ver el funcionamiento de esta rutina, abrimos el archivo Main.asm, localizamos la etiqueta Main_start, y al final, justo después de CALL PrintEnemies, añadimos la llamada a la rutina de retardo.
call Sleep
Localizamos la etiqueta Main_restart, y al final, justo antes de JR Main_loop, añadimos las siguientes líneas:
call PrintEnemies call Sleep
Ahora compilamos, cargamos en el emulador y vemos que desde que se pitan los enemigos, hasta que se mueven, hay un retardo.
Joystick
Dado que vamos a implementar los controles usando el joystick, además de las teclas, tenemos otras tres posibilidades distintas de control, y en algún sitio tenemos que guardar el tipo de controles que ha seleccionado el jugador. Abrimos el archivo Var.asm, localizamos la etiqueta enemiesCounter y justo por encima de ella agregamos una nueva etiqueta:
controls: db $00
Aquí vamos a guardar la selección de controles que ha hecho el jugador.
Ahora abrimos el archivo Print.asm y localizamos la etiqueta printFirstScreen_op. Vamos a borrar las líneas BIT $00, A y JR NZ, printFirstScreen_op, pues las vamos a sustituir por la nueva implementación. El resto de líneas las dejamos y justo encima de CALL FadeScreen, vamos a añadir la líneas siguientes:
printFirstScreen_end: ld a, b ld (controls), a
Hemos añadido una nueva etiqueta, printFirstScreen_end, y según podemos ver, en B tenemos los controles que se han seleccionado, lo cargamos en A, LD A, B, y de ahí lo cargamos en memoria, LD (controls), A.
Ahora vamos a implementar el resto de la rutina en el lugar donde estaban las líneas que hemos borrado, justo debajo de la lectura del teclado, IN A, ($FE).
ld b, $01 rra jr nc, printFirstScreen_end inc b rra jr nc, printFirstScreen_end inc b rra jr nc, printFirstScreen_end inc b rra jr c, printFirstScreen_op
Conviene que recordemos que al leer el teclado, el estado de las teclas vienen en los bits del cero a cuatro, correspondiendo el bit cero con la tecla más alejada del centro del teclado y el cuatro con la más cercana. También conviene recordar que el bit viene a cero si la tecla se ha pulsado, y a uno si no se ha pulsado.
Ponemos B a uno, la opción de teclas, LD B, $01, rotamos A a la derecha, poniendo el valor del bit cero (tecla 1) en el flag de acarreo, RRA, y si el flag de acarreo se ha desactivado, el bit estaba a cero, la tecla se ha pulsado y saltamos porque han seleccionado teclado, JR NC, printFirstScreen_end.
Si el flag de acarreo está activo, incrementamos B para que contenga el valor dos (Kempston), volvemos a rotar poniendo el valor del bit cero (tecla 2 tras la rotación anterior) en el flag de acarreo, RRA, e igual que antes, salta si se ha desactivado el acarreo, JR NC, printFirstScreen_end.
Si no se ha pulsado la tecla 2, rotamos y comprobamos las teclas 3 y 4, con especial atención al último JR, en este caso JR C, printFirstScreen_op. Si no se ha pulsado tampoco la tecla 4, el acarreo está activo y salta para volver a leer en teclado y estar en bucle hasta que se pulse alguna tecla del 1 al 4.
El aspecto final de la rutina es el siguiente:
; ----------------------------------------------------------------------------- ; Pantalla de presentación y selección de controles. ; ; Altera el valor de los registros AF, B y HL. ; ----------------------------------------------------------------------------- PrintFirstScreen: call CLS ; Limpia la pantalla ld hl, title ; Carga en HL la definición del título call PrintString ; Pinta el título ld hl, firstScreen ; Carga en HL la definición de la pantalla call PrintString ; Pinta la pantalla printFirstScreen_op: ld a, $f7 ; Carga en A la semifila 1-5 in a, ($fe) ; Lee el teclado ld b, $01 ; Carga 1 en B, opción teclas rra ; Rota A a la derecha para saber si ha pulsado 1 jr nc, printFirstScreen_end ; Si no hay acarreo, se ha pulsado y salta inc b ; Incrementa B, opción Kempston rra ; Rota A a la derecha para saber si ha pulsado 2 jr nc, printFirstScreen_end ; Si no hay acarreo, se ha pulsado y salta inc b ; Incrementa B, opción Sinclar 1 rra ; Rota A a la derecha para saber si ha pulsado 3 jr nc, printFirstScreen_end ; Si no hay acarreo, se ha pulsado y salta inc b ; Incrementa B, opción Sinclar 2 rra ; Rota A a la derecha para saber si ha pulsado 4 jr c, printFirstScreen_op ; Si hay acarreo, no se ha pulsado, bucle printFirstScreen_end: ld a, b ; Carga en A la opción seleccionada ld (controls), a ; Lo carga en memoria call FadeScreen ; Fundido de pantalla ret
Y ahora hay que utilizar los controles que se hayan seleccionado, y lo primero que debemos saber es la manera de leer el joystick.
En el caso de los joystick Sinclair, cada uno de ellos está mapeado con una semifila del teclado, en el caso del Kempston, no. Otra diferencia es que en el caso de los joystick Sinclair, las direcciones pulsadas vienen a cero, mientras que en los Kempston vienen a uno.
A continuación, vemos una tabla en la que se detalla la manera de leer las pulsaciones del joystick, y en que bit tenemos cada dirección.
Joystick | Semifila | Puerto | Arriba | Abajo | Izquierda | Derecha | Disparo |
Sinclair 1 | $EF (0-6) | $FE | 1 | 2 | 4 | 3 | 0 |
Sinclair 2 | $F7 (1-5) | $FE | 3 | 2 | 0 | 1 | 4 |
Kempston | $1F | 3 | 2 | 1 | 0 | 4 |
Con estos datos, ya podemos abrir el archivo Ctrl.asm y modificar rutina CheckCtrl para que tenga en cuenta los cuatro tipos de controles disponibles.
La primera línea de esta rutina es LD D, $00 y justo debajo de ella vamos a implementar la gestión de los controles. Borramos desde justo debajo de LD D, $00 hasta justo encima de RET, quedando de la siguiente manera:
CheckCtrl: ld d, $00 ; Pone D a 0 ret
Al final de la rutina comprobábamos si se habían pulsado a la vez izquierda y derecha, checkCtrl_testLR,y de ser así omitíamos ambas pulsaciones. Vamos a prescindir de esta comprobación ya que si se pulsan las dos, la nave se moverá hacia la derecha (ver MoveShip en Game.asm) y quitando esta parte de la rutina ahorramos diez bytes y treinta y ocho o cuarenta y cuatro ciclos de reloj.
Y ahora empezamos a implementar justo después de LD D, $00.
ld a, (controls) dec a jr z, checkCtrl_Keys dec a jr z, checkCtrl_Kempston dec a jr z, checkCtrl_Sinclair1
Cargamos en A los controles seleccionados por el jugador, que puede ser un valor que va del uno al cuatro, LD A, (controls), y decrementamos A, DEC A. Si A valía uno, tras el decremento vale cero y saltamos, JR Z, checkCtrl_Keys. Si no se ha seleccionado el teclado, decrementamos de nuevo A y comprobamos si se ha seleccionado Kempston, y si no es así hacemos los mismo para comprobar si se ha seleccionado Sinclair 1. Si no se ha seleccionado ninguna de las opciones anteriores, se ha seleccionado Sinclair 2.
Anteriormente, para comprobar si se ha pulsado una tecla usábamos la instrucción BIT n, r, que ocupa dos bytes y tarda ocho ciclos de reloj. Dado que con una sola lectura al puerto obtenemos el estado de todas las direcciones, en esta ocasión vamos a utilizar rotaciones del registro A, que ocupan un byte y tardan cuatro ciclos de reloj. En concreto, vamos a utilizar un máximo de cinco rotaciones, ocupando cinco bytes y tardando veinte ciclos de reloj. La opción sería usar tres instrucciones BIT que ocupan seis bytes y veintiocho ciclos de reloj, por lo que con las rotaciones ahorramos bytes y ciclos de reloj.
checkCtrl_Sinclair2: ld a, $f7 in a, ($fe) checkCtrl_Sinclair2_left: rra jr c, checkCtrl_Sinclair2_right set $00, d checkCtrl_Sinclair2_right: rra jr c, checkCtrl_Sinclair2_fire set $01, d checkCtrl_Sinclair2_fire: and $04 ret nz set $02, d ret
Cargamos en A la semifila 1-5, LD A, $F7, y leemos el teclado, IN A, ($FE). Rotamos A hacia la derecha para comprobar si se ha pulsado la dirección izquierda, RRA, y en caso de no haberse pulsado se activa el flag de acarreo y salta, JR C, chechCtrl_Sinclair2_right. Si sí se ha pulsado, activamos el bit cero de D, SET $00, D.
Rotamos A hacia la derecha para comprobar si se ha pulsado la dirección derecha, RRA, y en caso de no haberse pulsado se activa el flag de acarreo y salta, JR C, chechCtrl_Sinclair2_fire. Si sí se ha pulsado, activamos el bit uno de D, SET $01, D.
Ahora, el disparo lo tenemos en el bit dos, comprobamos si está pulsado, AND $04, y salta si no lo está, RET NZ. Si sí se ha pulsado, activamos el bit dos de D, SET $02, D y salimos, RET.
Ahora vamos a gestionar la selección Kempston.
checkCtrl_Kempston: in a, ($1f) checkCtrl_Kempston_right: rra jr nc, checkCtrl_Kempston_left set $01, d checkCtrl_Kempston_left: rra jr nc, checkCtrl_Kempston_fire set $00, d checkCtrl_Kempston_fire: and $04 ret z set $02, d ret
Leemos el puerto treinta y uno, IN A, ($1F). Rotamos A hacia la derecha para comprobar si se ha pulsado la dirección derecha, RRA, y en caso de no haberse pulsado se desactiva el flag de acarreo y salta, JR NC, chechCtrl_Kempston_left. Si sí se ha pulsado, activamos el bit uno de D, SET $01, D.
Rotamos A hacia la derecha para comprobar si se ha pulsado la dirección izquierda, RRA, y en caso de no haberse pulsado se desactiva el flag de acarreo y salta, JR NC, chechCtrl_Kempston_fire. Si sí se ha pulsado, activamos el bit uno de D, SET $00, D.
Ahora, el disparo lo tenemos en el bit dos, comprobamos si está pulsado, AND $04, y salta si no lo está, RET Z. Si sí se ha pulsado, activamos el bit dos de D, SET $02, D y salimos, RET.
La gestión de la selección Sinclair 1 y teclado es igual a la de Sinclair 2, cambiando el orden de comprobación de las direcciones, por lo que vamos a ver el aspecto final de la rutina.
; ----------------------------------------------------------------------------- ; Evalúa si se ha pulsado alguna de la teclas de dirección. ; Las teclas de dirección son: ; Z -> Izquierda ; X -> Derecha ; V -> Disparo ; ; Kempston, Sinclair 1 y Sinclair 2 ; ; Retorna: D -> Teclas pulsadas. ; Bit 0 -> Izquierda ; Bit 1 -> Derecha ; Bit 2 -> Disparo ; ; Altera el valor de los registros A y D ; ----------------------------------------------------------------------------- CheckCtrl: ld d, $00 ; Pone D a 0 ld a, (controls) ; Carga en A la selección de controles dec a ; Decrementa A jr z, checkCtrl_Keys ; Si es 0 salta a control teclado dec a ; Decrementa A jr z, checkCtrl_Kempston ; Si es 0 salta a control Kempston dec a ; Decrementa A jr z, checkCtrl_Sinclair1 ; Si es 0 salta a control Sinclair 1 ; Control Sinclair 2 checkCtrl_Sinclair2: ld a, $f7 ; Carga la semifila 1-5 en A in a, ($fe) ; Lee el teclado checkCtrl_Sinclair2_left: rra ; Rota A para comprobar izquierda jr c, checkCtrl_Sinclair2_right ; Si hay acarreo, no pulsado, salta set $00, d ; Si no hay acarreo, activa bit izquierda checkCtrl_Sinclair2_right: rra ; Rota A para comprobar derecha jr c, checkCtrl_Sinclair2_fire ; Si hay acarreo, no pulsado, salta set $01, d ; Si no hay acarreo, activa bit derecha checkCtrl_Sinclair2_fire: and $04 ; Comprueba si el disparo está activo ret nz ; Si no es cero, no pulsado, sale set $02, d ; Si es cero, activa bit disparo ret ; Sale ; Control Kempston checkCtrl_Kempston: in a, ($1f) ; Lee el puerto 31 checkCtrl_Kempston_right: rra ; Rota A para comprobar derecha jr nc, checkCtrl_Kempston_left ; Si no hay acarreo, no pulsado, salta set $01, d ; Si hay acarreo, activa bit derecha checkCtrl_Kempston_left: rra ; Rota A para comprobar izquierda jr nc, checkCtrl_Kempston_fire ; Si no hay acarreo, no pulsado, salta set $00, d ; Si hay acarreo, activa bit izquierda checkCtrl_Kempston_fire: and $04 ; Comprueba si el disparo está activo ret z ; Si es cero, no pulsado, sale set $02, d ; Si no es cero, activa bit disparo ret ; Sale ; Control Sinclair 1 checkCtrl_Sinclair1: ld a, $ef ; Carga la semifila 0-6 en A in a, ($fe) ; Lee el teclado checkCtrl_Sinclair1_fire: rra ; Rota A para comprobar disparo jr c, checkCtrl_Sinclair1_right ; Si hay acarreo, no pulsado, salta set $02, d ; Si no hay acarreo, activa bit disparo checkCtrl_Sinclair1_right: rra rra rra ; Rota A para comprobar derecha jr c, checkCtrl_Sinclair1_left ; Si hay acarreo, no pulsado, salta set $01, d ; Si no hay acarreo, activa bit derecha checkCtrl_Sinclair1_left: rra ; Rota A para comprobar izquierda ret c ; Si hay acarreo, no pulsado, sale set $00, d ; Si no hay acarreo, activa bit izquierda ret ; Sale checkCtrl_Keys: ld a, $fe ; Carga la semifila Cs-V en A in a, ($fe) ; Lee el teclado checkCtrl_Key_left: rra rra ; Rota A para comprobar izquierda jr c, checkCtrl_right ; Si hay acarreo, no pulsado, salta set $00, d ; Si no hay acarreo, activa bit izquierda checkCtrl_right: rra ; Rota A para comprobar derecha jr c, checkCtrl_fire ; Si hay acarreo, no pulsado, salta set $01, d ; Si no hay acarreo, activa bit derecha checkCtrl_fire: and $02 ; Comprueba si el disparo está activo ret nz ; Si no es cero, no pulsado, sale set $02, d ; Si es cero, activa bit disparo ret ; Sale
Compilamos, cargamos en el emulador y probamos los distintos controles.
Vida extra
Vamos a implementar que cada quinientos putos conseguidos el jugador obtenga una vida extra.
Vamos al archivo Var.asm, localizamos la etiqueta pointsCounter, y justo debajo de ella vamos a añadir una nueva etiqueta:
extraCounter: dw $0000
En extraCounter vamos a controlar el acumulado de puntos, hasta que llegue a quinientos, para dar una vida extra.
El siguiente paso es inicializar el valor de extraCounter con cada inicio de partida. Vamos al archivo Main.asm, localizamos la etiqueta Main_start, y nos fijamos en las primeras líneas:
xor a
ld hl, enemiesCounter
ld (hl), $20
inc hl
ld (hl), a ; $1d
inc hl
ld (hl), a ; $29
inc hl
ld (hl), $05
inc hl
ld (hl), a
inc hl
ld (hl), a
En esta parte inicializamos los valores de la partida, y como vemos ocupa diecisiete bytes y tarda noventa y dos ciclos de reloj. Cada pareja de instrucciones INC HL y LD (HL), A, ocupa dos bytes y tarda trece ciclos de reloj. Dado que tendríamos que añadir dos parejas más para inicializar los dos nuevos bytes que hemos añadido con la etiqueta extraCounter, añadiríamos cuatro bytes y veintiséis ciclos de reloj, resultando en un total de veintiún bytes y ciento dieciocho ciclos de reloj, además de que el código crece de manera repetitiva.
En lugar de inicializar los valores como hacemos ahora, vamos a usar la instrucción LDIR, que copia el valor de la posición memoria a la que apunta el registro HL en la posición de memoria a la que apunta el registro DE. Tras la copia, incrementa HL, incrementa DE y decrementa BC. Repite estas operaciones hasta que BC sea cero.
Borramos el código que usamos para inicializar los valores y lo sustituimos por el siguiente:
ld hl, enemiesCounter ld de, enemiesCounter + 1 ld (hl), $00 ld bc, $08 ldir ld a, $05 ld (livesCounter), a
Apuntamos HL a la posición de memoria dónde se encuentra el contador de enemigos, LD HL, enemiesCounter, y apuntamos DE a la posición siguiente.
Ponemos la posición de memoria a la que apunta HL a cero, LD (HL), $00, cargamos en BC el número de posiciones que vamos a poner a cero, además de la primera, LD BC, $08, y ponemos a cero el resto de posiciones de memoria, LDIR.
No todos los valores se inician a cero, las vidas se inician a cinco, por lo que cargamos cinco en A, LD A, $05, y lo subimos a memoria, LD (livesCounter), A.
De la manera en la que acabamos de implementar la inicialización, el código ocupa dieciocho bytes y tarda ochenta y un ciclos de reloj, por lo que hemos ganado bytes y tiempo de proceso. De igual manera, el código queda más legible, y en el caso de que necesitemos añadir algún byte más que haya que inicializar, solo tendremos que cambiar el valor que cargamos en BC.
El aspecto final del inicio de Main_start es el siguiente:
Main_start: ld hl, enemiesCounter ld de, enemiesCounter + 1 ld (hl), $00 ld bc, $08 ldir ld a, $05 ld (livesCounter), a call ChangeLevel
Ya solo queda la parte final, acumular puntos en extraCounter, dar una vida extra y poner el contador a cero al llegar a quinientos puntos.
Vamos al archivo Game.asm, localizamos la etiqueta ChecCrashFire y vamos al final de la misma y vemos que el aspecto es el siguiente:
ld (pointsCounter + 1), a ; Actualiza el valor en memoria
call PrintInfoValue ; Pinta la información de la partida
ret ; Sale de la rutina
checkCrashFire_endLoop:
inc hl ; Apunta HL a la coordenada Y del siguiente enemigo
djnz checkCrashFire_loop ; Bucle mientras B > 0
ret
La parte en la que vamos a acumular los puntos para lograr la vida extra va entre las líneas LD (pointsCounter + 1), A y CALL PrintInfoValue, así que procedemos:
ld hl, (extraCounter) ld bc, $0005 add hl, bc ld (extraCounter), hl ld bc, $01f4 sbc hl, bc jr nz, checkCrashFire_cont ld (extraCounter), hl ld a, (livesCounter) inc a daa ld (livesCounter), a checkCrashFire_cont:
Si se llega a esta parte de la rutina es porque se ha alcanzado a un enemigo y hemos sumado cinco puntos.
Cargamos el contador de puntos para vida extra en HL, LD HL, (extraCounter), cargamos los cinco puntos que vamos a sumar en BC, LD BC, $0005, se lo sumamos a HL, ADD HL, BC, y lo actualizamos en memoria, LD (extraCounter), HL.
Cargamos en BC quinientos, LD BC, $01F4, se lo restamos a HL, SBC HL, BC, y si el resultado no es cero saltamos pues no se ha llegado a quinientos puntos, JR NZ, checkCrashFire_cont. Si el valor de HL tras la resta fuera cero, sí habríamos llegado a quinientos puntos.
SBC es la resta con acarreo, la única resta que nos permite realizar el Z80 al operar con registros de 16 bits. En este caso concreto, es muy importante que el flag de acarreo este desactivado, cosa que sabemos que es así pues antes de la resta hemos sumado cinco a HL y, en nuestro caso, el valor de HL nunca va a superar quinientos.
Si no hemos saltado es porque el valor de HL había llegado a quinientos, ahora es cero. Actualizamos el contador en memoria poniéndolo a cero, LD (extraCounter), HL, cargamos el contador de vidas en A, LD A, (livesCounter), incrementamos A para añadir una vida, INC A, hacemos el ajuste decimal, DAA, y actualizamos el valor en memoria, LD (livesCounter), A. Por último, antes de la línea CALL PrintInfoValue, añadimos la etiqueta a la que salta si no hemos llegado a quinientos puntos, checkCrashFire_cont.
El aspecto final de la rutina es el siguiente:
; ----------------------------------------------------------------------------- ; Evalúa las colisiones del disparo con los enemigos. ; ; Altera el valor de lo registros AF, BC, DE y HL. ; ----------------------------------------------------------------------------- CheckCrashFire: ld a, (flags) ; Carga los flags en A and $02 ; Evalúa si el disparo está activo ret z ; Si no está activo, sale ld de, (firePos) ; Carga en DE la posición del disparo ld hl, enemiesConfig ; Apunta HL a la definición del primer enemigo ld b, enemiesConfigEnd - enemiesConfigIni ; Carga en B el número de bytes ; de la configuración de los enemigos sra b ; Lo divide entre dos, B = número de enemigos checkCrashFire_loop: ld a, (hl) ; Carga en A la coordenada Y del enemigo inc hl ; Apunta HL a la coordenada X del enemigo bit $07, a ; Evalúa si el enemigo está activo jr z, checkCrashFire_endLoop ; Si no está activo, salta and $1f ; Se queda con la coordenada Y de enemigo cp d ; Lo compara con la coordenada Y del disparo jr nz, checkCrashFire_endLoop ; Si no son iguales salta ld a, (hl) ; Carga en A la coordenada X del enemigo and $1f ; Se queda con la coordenada X cp e ; Lo compara con la coordenada X del disparo jr nz, checkCrashFire_endLoop ; Si no son iguales, salta dec hl ; Apunta HL a la coordenada Y del enemigo res $07, (hl) ; Desactiva el enemigo ld b, d ; Carga la coordenada Y del disparo en B ld c, e ; Carga la coordenada X del disparo en C call DeleteChar ; Borra el disparo y/o el enemigo ld a, (enemiesCounter) ; Carga en A el número de enemigos dec a ; Resta uno daa ; Hace el ajuste decimal ld (enemiesCounter), a ; Actualiza el valor en memoria ld a, (pointsCounter) ; Carga en A las unidades y decenas add a, $05 ; Suma 5 daa ; Hace el ajuste decimal ld (pointsCounter), a ; Actualiza el valor en memoria ld a, (pointsCounter + 1) ; Carga en A las centenas y unidades de millar adc a, $00 ; Suma 1 con acarreo daa ; Hace el ajuste decimal ld (pointsCounter + 1), a ; Actualiza el valor en memoria ld hl, (extraCounter) ; Carga en HL el contador de vida extra ld bc, $0005 ; Carga 5 en BC add hl, bc ; Se lo suma a HL ld (extraCounter), hl ; Lo carga en memoria ld bc, $01f4 ; Carga 500 en BC sbc hl, bc ; Se lo resta a HL jr nz, checkCrashFire_cont ; Si el resultado no es 0, salta ld (extraCounter), hl ; Si es 0, pone a cero el contador de vida extra ld a, (livesCounter) ; Carga en A el contador de vidas inc a ; Suma una vida daa ; Hace el ajuste decimal ld (livesCounter), a ; Actualiza en memoria checkCrashFire_cont: call PrintInfoValue ; Pinta la información de la partida ret ; Sale de la rutina checkCrashFire_endLoop: inc hl ; Apunta HL a la coordenada Y del siguiente enemigo djnz checkCrashFire_loop ; Bucle mientras B > 0 ret
Compilamos, cargamos en el emulador y vemos los resultados; cada quinientos puntos conseguimos una vida extra.
Para poder verificar que funciona, podemos iniciar la partida con el extraCounter a $1EF (495), de tal manera que al alcanzar un enemigo conseguiremos una vida.
También podéis hacer algo de lo que quizá ya os hayáis percatado, al iniciar el nivel, con el disparo pulsado, desplazaos hacia la derecha y quedaos allí, iréis pasando casi todos los niveles sin que os maten. Este es un aspecto que tenemos que cambiar, de lo contrario se pueden pasar los treinta niveles usando esta técnica.

Cambio del disparo
Lo primero que vamos a hacer es cambiar el disparo, a ver si así solucionamos algo. Seguimos en el archivo Game.asm, localizamos la etiqueta MoveFire, y tras la primera linea, LD HL, flags, añadimos las siguientes:
bit $00, (hl) ret z
Comprobamos si el bit cero está activo, BIT $00, (HL), y salimos si no lo está.
El bit cero de flags es el que indica si debemos mover la nave, de manera que ahora el disparo se mueve con la misma cadencia que la nave.
Compilamos, cargamos en el emulador y vemos los resultados.

Como podréis comprobar vosotros mismos, la técnica de desplazarse haca la derecha ya no resulta, pero a mi me gusta más que el disparo de la sensación de que es continuo, y además habría que pulir algo más el movimiento ya que cuando quedan pocos enemigos, se queda parado.
Vamos a comentar las dos líneas que hemos añadido pues la solución va a estar en modificar el comportamiento de los enemigos.
Ensamblador ZX Spectrum, conclusión
En este capítulo hemos implementado un retardo para el cambio entre niveles, el control por joystick y la consecución de vidas extra. También hemos visto un truco para pasar todos los niveles sin el más mínimo esfuerzo, y hemos probado a cambiar el comportamiento del disparo para evitar esto, pero no nos ha convencido.
En el próximo capítulo vamos a centrarnos en modificar el comportamiento de los enemigos.
Podéis descargar desde aquí 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
- Ensamblador ZX Spectrum Pong
- Referencia Z80
- Ensamblador Z80 en Telegram
- Tutorial completo en formato PDF y EPUB
- Proyecto en itch.io
Ensamblador para ZX Spectrum Batalla espacial por Juan Antonio Rubio García.
Esta obra está bajo licencia de Creative Commons Reconocimiento-NoComercial-CompartirIgual 4.0 Internacional License.
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.