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.

JoystickSemifilaPuertoArribaAbajoIzquierdaDerechaDisparo
Sinclair 1$EF (0-6)$FE12430
Sinclair 2$F7 (1-5)$FE32014
Kempston
$1F32104
Ensamblador ZX Spectrum, joystick y vida extra

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.

Ensamblador ZX Spectrum, joystick y vida extra
Ensamblador ZX Spectrum, joystick y vida extra

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.

Ensamblador ZX Spectrum, joystick y vida extra
Ensamblador ZX Spectrum, joystick y vida extra

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

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.

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.

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: