0x0B Ensamblador ZX Spectrum Marciano – Comportamiento de los enemigos

En este capítulo de Ensamblador ZX Spectrum Marciano, nos vamos a centrar en el comportamiento de los enemigos.

Aunque al inicio del tutorial comenté que el desarrollo estaba hecho y que lo único que iba a hacer es tutorizarlo, la realidad es que según he ido revisando el código, he ido cambiando cosas con respecto del original, y una de ellas es en lo relativo al comportamiento de los enemigos.

Creamos la carpeta Paso11 y copiamos desde la carpeta Paso10 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.

Cambios de dirección

Para dar la sensación de que el movimiento de los enemigos es algo menos previsible, vamos a hacer que cada cuatro segundos se cambie la dirección de los mismos. En el programa original usaba una rutina que generaba números pseudoaleatorios, cosa que voy a simplificar en esa ocasión.

Lo primero que vamos a hacer es abrir el archivo Main.asm, localizamos la etiqueta flags al inicio del mismo, y añadimos un comentario para el bit tres.

; -----------------------------------------------------------------------------
; Indicadores
;
; Bit 0 -> se debe mover la nave            0 = No, 1 = Sí
; Bit 1 -> el disparo está activo           0 = No, 1 = Sí
; Bit 2 -> se deben mover los enemigos      0 = No, 1 = Sí
; Bit 3 -> cambia dirección enemigos        0 = No, 1 = Sí
; -----------------------------------------------------------------------------
flags:
db $00

Cada cuatro segundos, activaremos el bit tres y se cambiará la dirección de los enemigos.

Para que el cambio de dirección de los enemigos no sea siempre el mismo, vamos a utilizar una etiqueta auxiliar; abrimos el archivo Var.asm, localizamos la etiqueta extraCounter y añadimos las líneas siguientes:

; -----------------------------------------------------------------------------
; Valores auxiliares
; -----------------------------------------------------------------------------
swEnemies:
db  $00
enemiesColor:
db  $06

La etiqueta que vamos a usar es swEnemies. Como podéis ver, he añadido otra etiqueta más, que vamos a usar para añadir un pequeño efecto de color a los enemigos.

Ahora vamos a implementar la rutina que va a realizar el cambo de dirección de los enemigos. Abrimos el archivo Game.asm, e implementamos al principio la rutina que cambia la dirección de los enemigos.

ChangeEnemies:
ld      hl, flags
bit     $03, (hl)
ret     z
res     $03, (hl)

ld      b, $14
ld      hl, enemiesConfig 
ld      a, (swEnemies)
ld      c, a

Cargamos en HL la dirección de los flags, LD HL, flags, comprobamos si el bit de cambio de dirección está activo, BIT $03, (HL), y salimos si no lo está, RET Z.

Desactivamos el bit si está activo, RES $03, (HL), cargamos en B el número total de enemigos, LD B, $14, cargamos en HL la dirección de la configuración de los enemigos, LD HL, enemiesConfig, cargamos en A el valor de la etiqueta auxiliar que usamos para el cambio de dirección de los enemigos, LD A, (swEnemies), y preservamos el valor cargándolo en C, LD C, A.

changeEnemies_loop:
bit     $07, (hl)
jr      z, changeEnemies_endLoop

inc     hl
ld      a, (hl)
and     $3f
or      c
ld      (hl), a

dec     hl
ld      a, c
add     a, $40
ld      c, a

Comprobamos si el enemigo está activo, BIT $07, (HL), y saltamos si no lo está.

La dirección del enemigo está en los bits seis y siete del segundo byte de la configuración, por lo que apuntamos HL a este segundo byte, INC HL, cagamos el valor en A, LD A, (HL), desechamos la dirección actual del enemigo, AND $3F, agregamos la nueva dirección, OR C, y la actualizamos en memoria, LD (HL), A.

Apuntamos HL de nuevo al primer byte de la configuración, DEC HL, cargamos la nueva dirección en A, LD A, C, sumamos uno a la nueva dirección ($40 = 0100 0000), ADD A, $40, y cargamos el valor en C, LD C, A.

changeEnemies_endLoop:
inc     hl
inc     hl
djnz    changeEnemies_loop

Apuntamos HL al primer byte del siguiente enemigo, INC HL, INC HL, y repetimos hasta que B valga cero y hayamos recorrido los veinte enemigos, DJNZ chengeEnemies_loop.

changeEnemies_end:
ld      a, c
ld      (swEnemies), a

ret

Cargamos la nueva dirección en A, LD A, C, actualizamos el valor en memoria para la próxima vez que haya que cambiar la dirección, LD (swEnemies), A, y salimos, RET.

El aspecto final de la rutina es el siguiente:

; -----------------------------------------------------------------------------
; Cambia la dirección de los enemigos.
;
; Altera el valor de los registros AF, BC y HL.
; -----------------------------------------------------------------------------
ChangeEnemies:
ld      hl, flags           ; Carga la dirección de memoria de flags en HL
bit     $03, (hl)           ; Comprueba si el bit 3 (cambio dirección) está activo
ret     z                   ; Si no es así, sale                
res     $03, (hl)           ; Desactiva el bit 3 de flags

ld      b, $14              ; Carga en B el número total de enemigos (20)
ld      hl, enemiesConfig   ; Cara en HL la dirección de la configuración 
                            ; de los enemigos
ld      a, (swEnemies)      ; Carga en A el auxiliar para cambiar la dirección
ld      c, a                ; Preserva el valor en C
changeEnemies_loop:
bit     $07, (hl)           ; Comprueba si el enemigo está activo
jr      z, changeEnemies_endLoop  ; Si no lo está, salta a final del bucle

inc     hl                  ; Apunta HL al segundo byte de la configuración
ld      a, (hl)             ; Carga el valor en HL
and     $3f                 ; Desecha la dirección
or      c                   ; Agrega la nueva dirección
ld      (hl), a             ; Actualiza la dirección en memoria

dec     hl                  ; Apunta HL al primer byte de la configuración
ld      a, c                ; Recupera la nueva dirección
add     a, $40              ; Le suma uno a la dirección ($40 = 0100 0000)
ld      c, a                ; Preserva la nueva dirección en C

changeEnemies_endLoop:
inc     hl                  ; Apunta HL al primer byte de la configuración
inc     hl                  ; del siguiente enemigo
djnz    changeEnemies_loop  ; Hasta que B sea cero (20 enemigos)

changeEnemies_end:
ld      a, c                ; Recupera la nueva dirección
ld      (swEnemies), a      ; La actualiza en memoria

ret

A esta nueva rutina hay que llamarla desde el bucle principal del programa. Volvemos al archivo Main.asm, localizamos la etiqueta Main_loop, localizamos la línea CALL MoveShip, y justo debajo de ella y antes de CALL MoveEnemies, añadimos la siguiente línea:

call    ChangeEnemies

Si queréis, podéis compilar, cargar en el emulador y ver que todo sigue funcionando igual, no hemos roto nada, pero tampoco se produce el cambio de dirección, debido a que en ningún momento estamos activando el bit tres de flags.

Abrimos el archivo Int.asm para implementar la activación de este bit cada cuatro segundos (en sistemas PAL), dando uso así a las interrupciones.

Lo primero que vamos a hacer es añadir una constante al inicio del archivo, justo por debajo de ORG $7E5C:

T1:     EQU $c8 

A T1 le asignamos un valor de $C8, doscientos en decimal, que resulta de multiplicar cincuenta, que son las interrupciones que tenemos por segundo en sistemas PAL, por cuatro segundos.

Al final del archivo vamos a añadir una etiqueta que vamos a usar para llevar el conteo de las interrupciones hasta que lleguen a doscientas, cuatro segundos.

countT1:    db $00

Y ahora vamos a modificar la rutina de la interrupción. Localizamos la etiqueta Isr_end, y justo por encima de ella implementamos la parte con la que se controlan los cuatros segundos de los que venimos hablando.

Isr_T1:
ld      a, (countT1)
inc     a
ld      (countT1), a
sub     T1
jr      nz, Isr_end
ld      (countT1), a
set     $03, (hl)

Cargamos el valor del contador en A, LD A, (countT1), incrementamos A, INC A, y actualizamos el valor del contador, LD (countT1), A.

Restamos el número de interrupciones que hay que alcanzar para activar el flag de cambio de dirección, SUB T1, y saltamos si no se ha alcanzado, JR NZ, Isr_end.

Si se han alcanzado los cuatros segundos (doscientas interrupciones), el resultado de la resta anterior es cero, y con ese valor actualizamos el contador, LD (countT1), A, y por último activamos el bit de cambio de dirección, SET $03, (HL).

Hay que cambiar otra línea. Tres líneas por encima de Isr_T1, encontramos la línea JR NZ, Isr_end. Esta línea hay que cambiarla dejándola de la siguiente manera:

jr      nz, Isr_T1

Ahora sí, compilamos, cargamos en el emulador y comprobamos que, cada cuatro segundos, los enemigos cambian de dirección. De igual manera vemos que lo de irse a la derecha y disparar ya no da tan buen resultado, en el primer nivel quizá sí, pero en los siguientes, no.

Ensamblador ZX Spectrum, marciano
Ensamblador ZX Spectrum, marciano

Ahora obligamos al jugador a moverse por la pantalla, pero la velocidad a la que se mueven los enemigos no permite ver bien hacia dónde van, por lo que deberíamos bajar dicha velocidad. Volvemos a Int.asm, localizamos la parte en la que se activa el bit para mover los enemigos:

ld      a, (countEnemy)
inc     a
ld      (countEnemy), a
sub     $02
jr      nz, Isr_T1
ld      (countEnemy), a
set     $02, (hl)

Y cambiamos SUB $02 por SUB $03.

Compilamos, cargamos en el emulador y comprobamos que ahora es más llevadero. Ajustad la velocidad como más os guste.

Todavía tenemos que trabajar más en el comportamiento de lo enemigos, pero ahora vamos a añadir un efecto de color.

Cambio de color

Como comenté anteriormente en este capítulo, vamos a añadir un efecto de color al movimiento de los enemigos, para ello hemos añadido la etiqueta enemiesColor en Var.asm.

El efecto va a consistir en cambiar el color de los enemigos desde uno (azul) a siete (blanco) cada vez que se muevan.

Vamos al archivo Print.asm y localizamos la etiqueta PrintEnemies, y justo debajo de ella vamos a añadir la implementación del efecto de color.

Lo primero es cambiar la primera línea de la rutia, LD A, $06.

ld      a, (enemiesColor)           ; Carga en A la tinta

De esta manera, el color en el que se pintan los enemigos lo tomamos de la nueva etiqueta.

La primera vez que pintamos los enemigos en un nivel, lo hacemos en amarillo. Vamos al archivo Game.asm, localizamos la etiqueta ChangeLevel, y al inicio de la misma añadimos estas dos líneas:

ld      a, $06                  ; Carga el color amarillo en A
ld      (enemiesColor), a       ; Actualiza el color en memoria

Si ahora compilamos y cargamos en el emulador, debemos seguir viendo como los enemigos se pintan en amarillo.

Seguimos en el archivo Game.asm y localizamos la etiqueta MoveEnemies, cuyo aspecto inicial es el siguiente:

MoveEnemies:
ld      hl, flags           ; Cargamos la dirección de memoria de flags en HL
bit     $02, (hl)           ; Comprueba si el bit 2 está activo
ret     z                   ; Si no es así, sale                
res     $02, (hl)           ; Desactiva el bit 2 de flags

ld      d, $14              ; Carga en D el número total de enemigos (20)
ld      hl, enemiesConfig   ; Carga en HL la dirección de la configuración 
                            ; de los enemigos
moveEnemies_loop:

La implementación del cambio de color la vamos a realizar justo después de la línea RES $02, (HL).

ld      a, (enemiesColor)
inc     a
cp      $08
jr      c, moveEnemies_cont
ld      a, $01

moveEnemies_cont:
ld      (enemiesColor), a

Cargamos en A el color de los enemigos, LD A, (enemiesColor), lo incrementamos, INC A, comprobamos si hemos llegado a ocho, CP $08, y saltamos si no lo hemos hecho, JR C, moveEnemies_cont. Si no hemos saltado, hemos llegado a ocho y ponemos el color en azul, LD A, $01. Por último, actualizamos el color en memoria, LD (enemiesColor), A.

El aspecto del inicio de la rutina queda de la siguiente manera:

MoveEnemies:
ld      hl, flags           ; Carga la dirección de memoria de flags en HL
bit     $02, (hl)           ; Comprueba si el bit 2 está activo
ret     z                   ; Si no es así, sale                
res     $02, (hl)           ; Desactiva el bit 2 de flags

ld      a, (enemiesColor)   ; Carga el color de los enemigos en A
inc     a                   ; Lo incrementa
cp      $08                 ; Comprueba si ha llegado a 8
jr      c, moveEnemies_cont ; Si no ha llegado, salta
ld      a, $01              ; Pone el color en azul

moveEnemies_cont:
ld      (enemiesColor), a   ; Actualiza el color en memoria
ld      d, $14              ; Carga en D el número total de enemigos (20)
ld      hl, enemiesConfig   ; Carga en HL la dirección de la configuración 
                            ; de los enemigos
moveEnemies_loop:

Compilamos y cargamos en el emulador. Ahora sí podemos ver como los enemigos cambian de color.

Ensamblador ZX Spectrum, marciano
Ensamblador ZX Spectrum, marciano

Hasta ahora hemos ido cambiando el comportamiento de los enemigos, primero para que situarnos en una parte de la pantalla no nos permita superar los treinta niveles sin más, y segundo para dar un poco más de vistosidad.

Ha llegado el momento de abarcar el cambio más importante, vamos a dotar a nuestros enemigos de disparo.

Disparos enemigos

El disparo de los enemigos se va a activar cuando estén encima de nosotros y va a haber un máximo de cinco disparos activos a un mismo tiempo.

El primer paso es declarar las constantes que vamos a necesitar, abrimos Const.asm y localizamos la etiqueta WHITE_GRAPH que actualmente es una directiva EQU con el valor $9e, código de carácter que se corresponde con el gráfico que hemos definido para el carácter en blanco, lo cual es innecesario ya que el carácter en blanco ya está definido, es el carácter $20 (32). Dejamos la línea de la siguiente manera:

WHITE_GRAPH:EQU $20

Ahora localizamos la etiqueta ENEMY_TOP_R y justo debajo vamos a añadir las constantes para el número total de enemigos, el código de carácter para el disparo del enemigo y el número de disparos que puede haber activos a un mismo tiempo.

ENEMIES:        EQU $14
ENEMY_GRA_F:    EQU $9e
FIRES:          EQU $05

Como podemos observar, el código de carácter $9e va a ser ahora el disparo de los enemigos.

Abrimos el archivo Var.asm, localizamos la etiqueta udgsCommon y nos vamos a la última línea:

db $00, $00, $00, $00, $00, $00, $00, $00	; $9e Blanco

Modificamos esta línea y la dejamos como sigue:

db $00, $3c, $2c, $2c, $2c, $2c, $18, $00	; $9e Disparo enemigo

Si hicisteis las prácticas del capítulo 0x01 Ensamblador ZX Spectrum Marciano – Gráficos, deberíais ser capaces que dibujar en papel o en las plantillas que se proporcionaron las representación del disparo enemigo, solo tenéis que hacer la conversión de hexadecimal a binario.

Justo encima de la etiqueta udgsCommon vamos a añadir etiquetas para la configuración de los disparos enemigos, y para llevar la cuenta de cuántos de ellos están activos.

; -----------------------------------------------------------------------------
; Configuración de los disparos de los enemigos
;
; 2 bytes por disparo.
; -----------------------------------------------------------------------------
; Byte 1                        | Byte 2
; -----------------------------------------------------------------------------
; Bit 0-4:  Posición Y          | Bit 0-4:  Posición X
; Bit 5:    Libre               | Bit 5:    Libre
; Bit 6:    Libre               | Bit 6:    Libre
; Bit 7:	Activo 1/0          | Bit 7:    Libre
; -----------------------------------------------------------------------------
enemiesFire:
defs FIRES * $02
enemiesFireCount:
db $00

Con DEFS reservamos tantos bytes como sea el resultado de multiplicar FIRES (número máximo de disparos enemigos a un mismo tiempo) por dos (dos bytes por disparo). Como podéis ver, la configuración de los disparos enemigos guardan cierta similitud con la configuración de los enemigos.

Abrimos el archivo Game.asm y vamos a empezar con la implementación necesaria para que nuestros enemigos disparen y nos pongan las cosas más difíciles.

Vamos a implementar una rutina que desactive todos los disparos enemigos, que llamaremos cada vez que iniciemos un nuevo nivel. Esta rutina la vamos a poner justo antes de la rutina Sleep.

ResetEnemiesFire:
ld      hl, enemiesFire
ld      de, enemiesFire + $01
ld      bc, FIRES * 02
ld      (hl), $00
ldir

ret

Apuntamos HL al primer byte de la configuración de los disparos enemigos, LD HL, enemiesFire, apuntamos DE al byte siguiente, LD DE, enemiesFire + $01, cargamos en BC el número de bytes que vamos a limpiar, LD BC, FIRES * 02, limpiamos el primer byte, LD (HL), $02, limpiamos el resto de bytes, LDIR, y salimos, RET.

En realidad también estamos limpiando (poniendo a cero) el contador de disparos activos, lo cual no nos va a suponer ningún problema. No obstante, si lo queréis evitar podéis añadir DEC BC antes de LDIR.

El aspecto de la rutina, una vez comentada, es el siguiente:

; -----------------------------------------------------------------------------
; Inicializa la configuración de los disparos enemigos
;
; Altera el valor de los registros BC, DE y HL.
; -----------------------------------------------------------------------------
ResetEnemiesFire:
ld      hl, enemiesFire         ; Apunta HL a la configuración de los disparos
ld      de, enemiesFire + $01   ; Apunta DE al byte siguiente
ld      bc, FIRES * 02          ; Carga en BC en número de bytes a limpiar
ld      (hl), $00               ; Limpia el primer byte
ldir                            ; Limpia el resto

ret

La configuración de los disparos enemigos es una suerte de lista. Necesitamos una rutina para que actualice dicha lista, vea cuantos disparos están activos, los ponga al inicio de la lista y actualice en memoria el número de disparos activos. Esta rutina la vamos a implementar justo delante de ResetEnemiesFire.

Es poco probable que queramos tener más de cinco disparos enemigos a la vez, es posible que incluso tengamos menos. Basados en esto, vamos a hacer una rutina que no es la más optima, pero que funciona.

La rutina que vamos a implementar, por cada elemento de la lista va a recorrer la lista en su totalidad, por lo que vamos a usar dos bucles anidados. Para finalizar, vamos a implementar un tercer bucle para actualizar el número de disparos activos.

RefreshEnemiesFire:
ld      b, FIRES
xor     a
refreshEnemiesFire_loopExt:
push    bc
ld      ix, enemiesFire
ld      b, FIRES

Cargamos en B el número máximo de disparos para que sea el contador del bucle exterior, LD B, FIRES, y ponemos A a cero, XOR A. Preservamos BC, PUSH BC, apuntamos IX a la configuración de los disparos de los enemigos, LD IX, enemiesFire, y volvemos a cargar en B el número máximo de disparos, en este caso como contador del bucle interior, LD B, FIRES.

En este caso, a la memoria vamos a acceder de manera indexada con el registro IX; en este capítulo de Porompompong se habla sobre los registros del Z80.

refreshEnemiesFire_loopInt:
bit     $07, (ix+$00)
jr      nz, refreshEnemiesFire_loopIntCont
ld      c, (ix+$02)
ld      (ix+$00), c
ld      c, (ix+$03)
ld      (ix+$01), c
ld      (ix+$02), a

Evaluamos si el disparo está activo, BIT $07, (IX+$00), y saltamos si lo está, JR NZ, refreshEnemiesFire_loopIntCont.

Si no está activo cargamos el primer byte del siguiente disparo en C, LD C, (IX+$02), y luego lo cargamos en el primer byte del disparo apuntado por IX, LD (IX+$00), C. Cargamos el segundo byte del siguiente disparo en C, LD C, (IX+03), y luego lo cargamos en el segundo byte del disparo apuntado por IX, LD (IX+$01), C.

Por último, ponemos el primer byte del segundo disparo a cero, LD (IX+$02), A.

refreshEnemiesFire_loopIntCont:
inc     ix
inc     ix
djnz    refreshEnemiesFire_loopInt

pop     bc
djnz    refreshEnemiesFire_loopExt

Apuntamos IX al primer byte del siguiente disparo, INC IX, INC IX, y seguimos en bucle hasta que B sea igual a cero, DJNZ refreshEnemiesFire_loopInt.

Recuperamos BC para obtener el contador del bucle exterior, PUSH BC, y seguimos en bucle hasta que B sea igual a cero, DJNZ refreshEnemiesFire_loopExt.

Llegados a este punto ya tenemos todos los disparos activos al inicio de la lista, solo queda contar cuantos disparos están activos. El conteo de los disparos activos lo vamos a llevar en A, recordad que ya lo pusimos a cero al inicio de la rutina, XOR A.

ld      b, FIRES
ld      hl, enemiesFire
refreshEnemiesFire_loopCount:
bit     $07, (hl)
jr      z, refreshEnemiesFire_end
inc     a
refreshEnemiesFire_loopCountCont:
inc     hl
inc     hl
djnz    refreshEnemiesFire_loopCount

refreshEnemiesFire_end:
ld      (enemiesFireCount), a

ret

Cargamos en B el número máximo de disparos, LD B, FIRES, y apuntamos HL a la configuración de los disparos, LD HL, enemiesFire.

Evaluamos si el disparo está activo, BIT $07, (HL), y si no lo está salta, JR Z, refreshEnemiesFire_end.

Si está activo, incrementamos A para sumar un disparo activo, INC A, apuntamos HL al primer byte del siguiente disparo, INC HL, INC HL, y seguimos en bucle hasta que B sea igual a cero, DJNZ refreshEnemiesFire_loopCount.

Por último, actualizamos el número de disparos activos en memoria, LD (enemiesFireCount), A, y salimos, RET.

El aspecto final de la rutina es el siguiente:

; -----------------------------------------------------------------------------
; Actualiza la configuración de los disparos enemigos
;
; Altera el valor de los registros AD, BC, HL e IX.
; -----------------------------------------------------------------------------
RefreshEnemiesFire:
ld      b, FIRES                    ; Carga en B el número máximo de disparos
xor     a                           ; Pone A a 0
refreshEnemiesFire_loopExt:
push    bc                          ; Preserva BC
ld      ix, enemiesFire             ; Apunta IX a la configuración de los disparos
ld      b, FIRES                    ; Carga en B el número máximo de disparos
refreshEnemiesFire_loopInt:
bit     $07, (ix+$00)               ; Evalúa si el disparo está activo
jr      nz, refreshEnemiesFire_loopIntCont  ; Si lo está, salta
ld      c, (ix+$02)                 ; Carga el byte 1 del siguiente disparo en C
ld      (ix+$00), c                 ; Lo carga en el byte 1 del disparo actual
ld      c, (ix+$03)                 ; Carga el byte 2 del siguiente disparo en C
ld      (ix+$01), c                 ; Lo carga en el byte 2 del disparo actual               
ld      (ix+$02), a                 ; Pone a cero el byte 1 del siguiente disparo
refreshEnemiesFire_loopIntCont:
inc     ix
inc     ix                          ; Apunta IX al byte 1 del siguiente disparo
djnz    refreshEnemiesFire_loopInt  ; Bucle hasta que B = 0

pop     bc                          ; Recupera BC para bucle exterior
djnz    refreshEnemiesFire_loopExt  ; Bucle hasta que B = 0

; Actualiza el número de disparos activos
ld      b, FIRES                    ; Carga en B el número máximo de disparos
ld      hl, enemiesFire             ; Apunta HL a la configuración de los disparos
refreshEnemiesFire_loopCount:
bit     $07, (hl)                   ; Evalúa si el disparo está activo
jr      z, refreshEnemiesFire_end   ; Si no lo está, salta
inc     a                           ; Incrementa A = contador de disparos
refreshEnemiesFire_loopCountCont:
inc     hl
inc     hl                          ; Apunta HL al byte 1 del siguiente disparo
djnz    refreshEnemiesFire_loopCount    ; Bucle hasta que B = 0

refreshEnemiesFire_end:
ld      (enemiesFireCount), a       ; Actualiza el contador de disparos en memoria

ret

Ahora vamos a implementar la rutina que va a ir activando los disparos. Los disparos se van a activar cuando el enemigo esté en la misma coordenada horizontal que la nave, siempre que no estén ya activos todos los disparos.

Seguimos en Game.asm, localizamos la etiqueta MoveEnemies, y justo encima de ella implementamos la rutina que activa los disparos enemigos.

EnableEnemiesFire:
ld      de, (shipPos)
ld      hl, enemiesConfig
ld      b, ENEMIES

Cargamos en DE la posición de la nave, LD DE, (shipPos), apuntamos HL a la configuración de los enemigos, LD HL, enemiesConfig, y cargamos en B el número máximo de enemigos, LD B, ENEMIES.

enableEnemiesFire_loop:
ld      a, (enemiesFireCount)
cp      FIRES
ret     nc

push    bc
ld      a, (hl)
ld      b, a
inc     hl
and     $80
jr      z, enableEnemiesFire_loopCont

ld      a, (hl)
and     $1f
cp      e
jr      nz, enableEnemiesFire_loopCont

Cargamos en A el número de disparos activos, LD A, (enemiesFireCount), lo comparamos con el número máximo de disparos, CP FIRES, y salimos si lo hemos alcanzado, RET NC. Recordad que CP resta al registro A el valor especificado, sin cambiar el valor de A pero si el registro F. El flag de acarreo se activa mientras A sea menor que el valor indicado en CP, por lo tanto el flag de acarreo se desactiva cuando A sea mayor o igual al valor indicado en CP.

Preservamos el valor de BC, PUSH BC, cargamos en A el primer byte de la configuración del enemigo, LD A, (HL), lo cargamos en B, LD B, A, apuntamos HL al segundo byte de la configuración, INC HL, comprobamos si el enemigo está activo, AND $80, y saltamos si no lo está, JR Z, enableEnemiesFire_loopCont.

En el caso de que el enemigo esté activo, cargamos en A el segundo byte de la configuración, LD A, (HL), nos quedamos con la coordenada X, AND $1F, los comparamos con la coordenada X de la nave, CP E, y saltamos si no son la misa, JR NZ, enableEnemiesFire_loopCont.

Si no hemos saltado, tenemos que activar el disparo.

ld      c, a
push    hl
push    bc
ld      hl, enemiesFire
ld      a, (enemiesFireCount)
add     a, a
ld      b, $00
ld      c, a
add     hl, bc
pop     bc
ld      (hl), b
inc     hl
ld      (hl), c
ld      hl, enemiesFireCount
inc     (hl)
pop     hl

Cargamos en C la coordenada X del enemigo, LD C, A, con esto ya tenemos lista la configuración del disparo. Preservamos HL, PUSH HL, preservamos BC, PUSH BC, apuntamos HL a la configuración de los disparos enemigos, LD HL, enemiesFire, cargamos en A el número de disparos activos, LD A, (enemiesFireCount), lo multiplicamos por dos, ADD A, A, ponemos B a cero, LD B, $00, cargamos en C el número de bytes que hay que desplazarse, LD C, A, y se lo sumamos a HL para que apunte a la posición de la lista dónde vamos a poner la configuración del disparo, ADD HL, BC.

Recuperamos la configuración del disparo, POP BC, cargamos el primer byte de la configuración del disparo en memoria, LD (HL), B, apuntamos HL al segundo byte de la lista, INC HL, cargamos el segundo byte de la configuración del disparo en memoria, LD (HL), C, apuntamos HL al contador de disparos enemigos, LD HL, enemiesFireCount, lo incrementamos, INC (HL), y recuperamos HL para que apunte al segundo byte de la configuración del enemigo, POP HL.

enableEnemiesFire_loopCont:
pop     bc
inc     hl
djnz    enableEnemiesFire_loop

ret

Recuperamos el valor de BC para recuperar el contador del bucle, POP BC, apuntamos HL al primer byte de la configuración del siguiente enemigo, INC HL, y seguimos en bucle hasta que recorramos todos los enemigos, B sea igual a cero, DJNZ enebleEnemiesFire_loop. Por último, salimos, RET.

El aspecto final de la rutina es el siguiente:

; -----------------------------------------------------------------------------
; Habilita el disparo del enemigo.
;
; Altera el valor de los registros AF, BC, DE y HL.
; -----------------------------------------------------------------------------
EnableEnemiesFire:
ld      de, (shipPos)           ; Carga en DE la posición de la nave
ld      hl, enemiesConfig       ; Apunta HL a la configuración de los enemigos
ld      b, ENEMIES              ; Carga en B el número total de enemigos

enableEnemiesFire_loop:
ld      a, (enemiesFireCount)   ; Carga en A el número de disparos activos
cp      FIRES                   ; Lo compara con el máximo de disparos
ret     nc                      ; Sale si se ha alcanzado

push    bc                      ; Preserva el valor de BC
ld      a, (hl)                 ; Carga en A el primer byte de la configuración
ld      b, a                    ; Lo carga en B
inc     hl                      ; Apunta HL al segundo byte de la configuración
and     $80                     ; Evalúa si el enemigo está activo
jr      z, enableEnemiesFire_loopCont   ; Si no lo está, salta

ld      a, (hl)                 ; Carga en A el segundo byte de la configuración
and     $1f                     ; Se queda con la coordenada X
cp      e                       ; La compara con la de la nave
jr      nz, enableEnemiesFire_loopCont  ; Si no son la misma, salta

; Activa el disparo
; La configuración del disparo es la del enemigo
ld      c, a                    ; Carga en C la coordenada X del enemigo
push    hl                      ; Preserva HL
push    bc                      ; Preserva BC, configuración del disparo
ld      hl, enemiesFire         ; Apunta HL a los disparos de los enemigos
ld      a, (enemiesFireCount)   ; Carga en A el contador de disparos
add     a, a                    ; Lo multiplica por dos, dos bytes por disparo
ld      b, $00
ld      c, a                    ; Carga el desplazamiento en BC
add     hl, bc                  ; Apunta HL al disparo que hay que activar
pop     bc                      ; Recupera BC, configuración del disparo
ld      (hl), b                 ; Carga en memoria el primer byte de la configuración
inc     hl                      ; Apunta HL al segundo byte de la configuración
ld      (hl), c                 ; Lo carga en memoria
ld      hl, enemiesFireCount    ; Apunta HL al contador de disparos enemigos
inc     (hl)                    ; Lo incrementa en memoria
pop     hl                      ; Recupera HL, segundo byte configuración enemigo

enableEnemiesFire_loopCont:
pop     bc                      ; Recupera el valor de BC
inc     hl                      ; Apunta HL al primer byte de configuración
                                ; del enemigo siguiente
djnz    enableEnemiesFire_loop  ; Hasta que se recorra todos los enemigos, B = 0

ret

Como hemos dicho anteriormente, los disparos se habilitan cuando los enemigos están en la misma coordenada horizontal que la nave, por ese motivo la llamada a EnableEnemies la vamos a hacer desde la rutina MoveEnemies.

En MoveEnemies vamos a cambiar dos cosas: primero localizamos la etiqueta moveEnemies_cont, y la segunda línea, que ahora presenta este aspecto:

ld      d, $14              ; Carga en D el número total de enemigos (20)

La dejamos así:

ld      d, ENEMIES          ; Carga en D el número total de enemigos

Antes hemos declarado la constante ENEMIES, y ahora hemos de sustituir la constante en todos aquellos lugares donde se carga el número de enemigos.

Ahora localizamos la etiqueta moveEnemies_end y tras la primera línea:

call    PrintEnemies        ; Pinta los enemigos

Añadimos la llamada a EnableEnemiesFire:

call    EnableEnemiesFire   ; Habilita los disparos de los enemigos

Ahora vamos a implementar una rutina para que mueva los disparos enemigos, igual que tenemos una rutina para mover los enemigos.

Seguimos en Game.asm, localizamos la etiqueta MoveFire y justo delante de ella implementamos la rutina que va a mover los disparos enemigos.

MoveEnemiesFire:
ld      a, $03
call    Ink
ld      hl, flags
bit     $04, (hl)
ret     z
res     $04, (hl)

Cargamos en A la tinta magenta, LD A, $03, cambiamos la tinta, CALL Ink, cargamos en HL los flags, LD HL, flags, y comprobamos si el bit cuatro está activo, BIT $04, (HL). Si el bit no está activo salimos, RET Z, si sí lo está lo desactivamos, RES $04, (HL).

Por lo que vemos, vamos a usar otro bit de nuestro byte de flags.

ld      d, FIRES
ld      hl, enemiesFire
moveEnemiesFire_loop:
ld      b, (hl)
inc     hl
ld      c, (hl)
dec     hl

Cargamos en D el número máximo de disparos, LD D, FIRES, apuntamos HL a la configuración de los disparos enemigos, LD HL, enemiesFire, cargamos el primer byte de la configuración en B, LD B, (HL), apuntamos HL al segundo byte, INC HL, lo cargamos en C, LD C, (HL), y volvemos a apuntar HL al primer byte, DEC HL.

bit     $07, b
jr      z, moveEnemiesFire_loopCont
res     $07, b
call    DeleteChar
ld      a, ENEMY_TOP_B + $01
cp      b
jr      z, moveEnemiesFire_loopCont
dec     b
call    At
ld      a, ENEMY_GRA_F
rst     $10
set     $07, b

Evaluamos si el disparo está activo, BIT $07, B, y saltamos si no lo está, JR Z, moveEnemiesFire_loopCont. Si el disparo está inactivo podríamos salir de la rutina, pero no lo hacemos para que la rutina siempre tarde lo mismo en ejecutarse, o al menos lo más parecido posible entre cada ejecución.

Si el disparo está activo, nos quedamos con la coordenada Y, RES $07, B, borramos el disparo de su posición actual, CALL DeleteChar, cargamos en A el tope vertical al que puede llegar el disparo por abajo, LD A, ENEMY_TOP_B + $01, lo comparamos con la coordenada Y, CP B, y saltamos si la hemos alcanzado, JR Z, moveEnemiesFire_loopCont.

Si no hemos alcanzado el tope, apuntamos la coordenada Y a la línea siguiente, DEC B, posicionamos el cursor, CALL At, cargamos en A el gráfico del disparo enemigo, LD A, ENEMY_GRA_F, lo pintamos, RST $10, y dejamos el disparo activado, SET $07, B.

moveEnemiesFire_loopCont:
ld      (hl), b
inc     hl
inc     hl

dec     d
jr      nz, moveEnemiesFire_loop

jp      RefreshEnemiesFire

Al llegar a este punto, hemos activado o desactivado el disparo y actualizado la coordenada Y según corresponda. Actualizamos el primer byte de la configuración del disparo en memoria, LD (HL), D, apuntamos HL al primer byte del disparo siguiente, INC HL, INC HL, decrementamos D que es donde tenemos el número de iteraciones del bucle, DEC D, y seguimos en bucle hasta que D valga cero, JR NZ, moveEnemiesFire_loop.

Finalmente, saltamos a refrescar la lista de disparos y salimos por allí, JP RefreshEnemiesFire.

El aspecto final de la rutina es el siguiente:

; -----------------------------------------------------------------------------
; Mueve el disparo del enemigo.
;
; Altera el valor de los registros AF, BC, DE y HL.
; -----------------------------------------------------------------------------
MoveEnemiesFire:
ld      a, $03          ; Cara la tinta 3 en A
call    Ink             ; Cambia la tinta
ld      hl, flags       ; Apunta HL a los flags
bit     $04, (hl)       ; Comprueba si está activo el flag mover disparo enemigo
ret     z               ; Si no lo está, sale
res     $04, (hl)       ; Desactiva el flag mover disparo enemigo

ld      d, FIRES        ; Carga en D en número máximo de disparos
ld      hl, enemiesFire ; Apunta HL a los disparos enemigos
moveEnemiesFire_loop:
ld      b, (hl)         ; Carga en B la coordenada Y del disparo
inc     hl              ; Apunta HL a la coordenada X
ld      c, (hl)         ; La carga en C
dec     hl              ; Apunta HL a la coordenada Y

bit     $07, b          ; Evalúa si el disparo está activo
jr      z, moveEnemiesFire_loopCont ; Salta si no lo está
res     $07, b          ; Se queda con la coordenada Y
call    DeleteChar      ; Borra el disparo de su posición actual
ld      a, ENEMY_TOP_B + $01    ; Carga en A el tope
cp      b               ; Lo compara con la coordenada Y
jr      z, moveEnemiesFire_loopCont ; Si es la misma, salta
dec     b               ; Apunta B a la línea siguiente
call    At              ; Posiciona el cursor
ld      a, ENEMY_GRA_F  ; Carga en A el gráfico del disparo
rst     $10             ; Lo pinta
set     $07, b          ; Deja el disparo activo

moveEnemiesFire_loopCont:
ld      (hl), b         ; Actualiza la coordenada Y del disparo
inc     hl
inc     hl              ; Apunta HL al primer byte de la configuración
                        ; del siguiente enemigo

dec     d
jr      nz, moveEnemiesFire_loop 

jp      RefreshEnemiesFire  ; Actualiza los disparos enemigos y sale

Ya tenemos todo casi listo para poder ver los disparos enemigos en pantalla.

Al inicio de la rutina MoveEnemies, recuperamos el valor de la etiqueta flags y evaluamos si el bit cuatro está activo.

Vamos al archivo Main.asm, y en los comentarios de flags, vamos a añadir el siguiente:

; Bit 4 -> mover disparo enemigo            0 = No, 1 = Sí

Seguimos en Main.asm y vamos a aprovechar para incluir las llamadas a algunas de las rutinas que hemos implementado.

Localizamos la etiqueta Main_start, y justo delante de CALL ChangeLevel vamos a incluir una llamada a la inicialización de los disparos enemigos:

call    ResetEnemiesFire

Localizamos la rutina Main_loop, y entre las líneas CALL MoveEnemies y CALL CheckCrashShip, añadimos la llamada a la rutina que mueve los disparos enemigos:

call    MoveEnemiesFire

Aprovechamos también para comentar la línea CALL CheckCrashShip, para que no nos maten los enemigos y poder ver mejor como quedan los disparos.

Por último, localizamos la rutina Main_restart, y casi al final, justo antes de CALL Sleep, añadimos otra llamada a la inicialización de lo disparos enemigos:

call    ResetEnemiesFire

Hemos acabado en Main.asm, pero todavía nos queda activar el bit cuatro de flags para que todo esto funcione.

Vamos al archivo Int.asm, y lo primero que tenemos que hacer es decidir a que velocidad se va a mover el disparo enemigo. Sin implementar nada nuevo tenemos dos velocidades a elegir: a la velocidad a la que se mueve la nave (en cada interrupción), o la velocidad a la que se mueven los enemigos (cada N interrupciones).

En mi caso he optado por la segunda opción. Localizamos la línea SET $02, (HL), y justo debajo añadimos:

set     $04, (hl)

Si queréis que se mueva a la velocidad a la que se mueve la nave, esta línea la tenéis que poner justo debajo de SET $00, (HL).

Hemos implementado una buena cantidad de líneas, y ya es hora de que probemos y veamos los resultados; compilamos y cargamos en el emulador.

Ensamblador ZX Spectrum, marciano
Ensamblador ZX Spectrum, marciano

Si todo va bien, ya pueden verse los disparos enemigos.

Dije anteriormente que cinco disparos enemigos simultáneos quizá fuera excesivo. Para apreciar mejor esto, en Game.asm localizamos la rutina MoveEnemies y, casi al final, comentamos la línea CALL PrintEnemies para poder ver mejor los disparos.

Compilamos, cargamos en el emulador y vemos los resultados.

Ensamblador ZX Spectrum, marciano
Ensamblador ZX Spectrum, marciano

Si a esto le sumamos los enemigos, quizá sea demasiado. Descomentamos la línea CALL PrintEnemies, y en Main.asm, en la rutina MainLoop, localizamos la línea CALL CheckCrashShip y quitamos el comentario.

Compilamos, cargamos en el emulador y verificamos que los enemigos nos vuelven a matar. Ya solo queda hacer que los disparos enemigos también nos maten.

Antes de implementar las colisiones entre la nave y los disparos enemigos, recordad que declaramos una constante con el número total de enemigos, ENEMIES, pero todavía tenemos partes del código en las que no la estamos usando.

Vamos a Print.asm, localizamos la rutina PrintEnemies, y modificamos la línea LD D, $14 dejándola así:

ld      d, ENEMIES

El resto de modificaciones las vamos a realizar en Game.asm.

Localizamos la rutina ChangeEnemies, localizamos la línea LD B, $14 y la modificamos dejándola así:

ld      b, ENEMIES

Localizamos las rutina CheckCrashFire, borramos las líneas:

ld      b, enemiesConfigEnd - enemiesConfigIni
                                              
sra     b

Y las sustituimos por:

ld      b, ENEMIES

Hacemos la misma modificación en la rutina CheckCrashShip.

Y ahora ya sí, implementamos las colisiones entre la nave y los disparos enemigos. Seguimos en la rutina CheckCrashShip, vamos al final y justo antes de RET, añadimos las nuevas colisiones.

checkCrashShipFire:
ld      de, (shipPos)
ld      a, (enemiesFireCount)
ld      b, a
ld      hl, enemiesFire

Cargamos en DE la posición de la nave, LD DE, (shipPos), cargamos en A el número de disparos enemigos activados, LD A, (enemiesFireCount), lo cargamos en B, LD B, A, y apuntamos HL a la configuración de los disparos, LD HL, enemiesFire.

checkCrashShipFire_loop:
ld      a, (hl)
inc     hl
res     $07, a
cp      d
jr      nz, checkCrashShipFire_loopCont
ld      a, (hl)
cp      e
jr      nz, checkCrashShipFire_loopCont

Cargamos el primer byte de la configuración del disparo en A, LD A, (HL), apuntamos HL al segundo byte, INC HL, nos quedamos con la coordenada Y, RES $07, A, la comparamos con la coordenada Y de la nave, CP D, y saltamos si no son la misma, JR NZ, checkCrashShipFire_loopCont.

Si la coordenada Y del disparo y la de la nave son la misma, cargamos en A la coordenada X del disparo, LD A, (HL), la comparamos con la coordenada X de la nave, CP E, y saltamos si no son la misma, JR NZ, checkCrashShipFire_loopCont.

dec     hl
res     $07, (hl)
ld      a, (livesCounter)
dec     a
daa
ld      (livesCounter), a
call    PrintInfoValue
call    PrintExplosion
jp      RefreshEnemiesFire

Si hay colisión entre el disparo y la nave, apuntamos HL al primer byte de la configuración del disparo, DEC HL, desactivamos el disparo, RES $07, (HL), cargamos en A el número de vidas, LD A, (livesCounter), quitamos una vida, DEC A, hacemos el ajuste decimal, DAA, y actualizamos en memoria, LD (livesCounter), A.

Por último, pintamos la información de la partida, CALL PrintInfoValue, pintamos la explosión, CALL PrintExploxion, y actualizamos la lista de disparos enemigos y salimos por allí, JP RefreshEnemiesFire.

checkCrashShipFire_loopCont:
inc     hl                
djnz    checkCrashShipFire_loop

Si no ha habido colisión, apuntamos HL al primer byte de la configuración del siguiente disparo, INC HL, y seguimos en bucle hasta que hayamos recorrido todos los disparos y el valor de B sea cero, DJNZ checkCrashShipFire_loop.

El aspecto final de la detección de colisiones entre la nave y los enemigos (naves y disparos enemigos) es el siguiente:

; -----------------------------------------------------------------------------
; Evalúa las colisiones de los enemigos y los disparos con la nave.
;
; Altera el valor de lo registros AF, BC, DE y HL.
; -----------------------------------------------------------------------------
CheckCrashShip:
ld      de, (shipPos)       ; Carga en DE la posición de nave
ld      hl, enemiesConfig   ; Apunta HL a la configuración de los enemigos
ld      b, ENEMIES          ; Carga en B el número de enemigos
checkCrashShip_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, checkCrashShip_endLoop   ; Si no lo está, salta

and     $1f                 ; Se queda con la coordenada Y del enemigo
cp      d                   ; Compara con la coordenada Y de la nave
jr      nz, checkCrashShip_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 de enemigo
cp      e                   ; Compara con la coordenada X de la nave
jr      nz, checkCrashShip_endLoop  ; Si no son iguales, salta

dec     hl                  ; Apunta HL a la coordenada Y del enemigo
res     $07, (hl)           ; Desactiva 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, (livesCounter)   ; Carga las vidas en A
dec     a                   ; Quita una 
daa                         ; Hace el ajuste decimal
ld      (livesCounter), a   ; Actualiza el valor en memoria
call    PrintInfoValue      ; Pinta la información de la partida

jp      PrintExplosion      ; Pinta la explosión y sale

checkCrashShip_endLoop:
inc     hl                  ; Apunta HL a la coordenada Y del siguiente enemigo
djnz    checkCrashShip_loop ; En bucle hasta que B = 0

checkCrashShipFire:
; Comprueba colisiones entre disparos enemigos y nave
ld      de, (shipPos)           ; Carga en DE la posición de la nave
ld      a, (enemiesFireCount)   
ld      b, a                    ; Carga el B el número de disparos activos
ld      hl, enemiesFire         ; Apunta HL a la configuración de los disparos
checkCrashShipFire_loop:
ld      a, (hl)                 ; Carga la coordenada Y del disparo en A
inc     hl                      ; Apunta HL a la coordenada X
res     $07, a                  ; Se queda con la coordenada Y
cp      d                       ; Compara si es la misma que la de la nave
jr      nz, checkCrashShipFire_loopCont ; Si no es la misma, salta
ld      a, (hl)                 ; Carga la coordenada X del disparo en A
cp      e                       ; Compara si es la misma que la de la nave
jr      nz, checkCrashShipFire_loopCont ; Si no es la misma, salta
; Si llega aquí, la nave ha colisionado con el disparo
dec     hl                      ; Apunta HL al primer byte del disparo
res     $07, (hl)               ; Desactiva el disparo
ld      a, (livesCounter)       ; Carga las vidas en A
dec     a                       ; Quita una 
daa                             ; Hace el ajuste decimal
ld      (livesCounter), a       ; Actualiza el valor en memoria
call    PrintInfoValue          ; Pinta la información de la partida
call    PrintExplosion          ; Pinta la explosión
jp      RefreshEnemiesFire      ; Actualiza los disparos enemigos y sale

checkCrashShipFire_loopCont:
inc     hl                      ; Apunta HL al siguiente disparo
djnz    checkCrashShipFire_loop ; Bucle hasta que B = 0

ret

Llegados a este punto, ya tenemos el disparo de las naves enemigas implementado. Compilamos, cargamos en el emulador y vemos los resultados.

Ajuste de la dificultad

Quizá ahora la dificultad haya subido demasiado, o quizá no. Sea como fuere, vamos a ver algunos pequeños cambios que podemos realizar para ajustar la dificultad.

Los cambios que os propongo es para que hagáis pruebas, pero no los dejéis de manera permanente pues más adelante vamos a añadir una opción en el menú para que el jugador pueda seleccionar entre distintos niveles de dificultad.

La primera manera de reducir la dificultad va a ser reduciendo la velocidad a la que se mueven los enemigos y sus disparos. Vamos al archivo Int.asm, localizamos la línea SUB $03 y sustituimos $03 por $04, $05, $06, etc. Recordad que cuanto mayor sea este número, menor es la velocidad a la que se mueven los enemigos. Haced pruebas y veréis que la velocidad se reduce.

Otra forma de reducir la dificultad es reducir el número de disparos simultáneos. Vamos al archivo Const.asm, localizamos la etiqueta FIRES y cambiamos su valor por $01, $02, etc. Compilamos y vemos que al haber menos disparos a un mismo tiempo, la dificultad también se reduce.

También podemos reducir la dificultad evitando que los enemigos colisionen con la nave, para lo cual localizamos la etiqueta moveEnemies_Y_down y dos lineas más abajo tenemos SUB ENEMY_TOP_B, modificamos está línea y la dejamos como sigue:

sub     ENEMY_TOP_B  + $01

Como podéis observar, los enemigos ya no colisionan con la nave, lo que reduce la dificultad. Si la reduce demasiado para vuestro gusto, probad a aumentar el número de disparos enemigos simultáneos.

Si los enemigos no colisionan con la nave, nos podríamos ahorra gran parte de la rutina CheckCrashShip, pero como vamos a cambiar estos comportamientos en base a la selección del jugador, la dejamos como está.

Otra forma que se me ocurre de disminuir la dificultad es al estilo Plaga Galáctica, el primer juego que cargué en mi Amstrad CPC 464. En Plaga Galáctica se dispone de tres vidas para superar cada nivel.

Para comenzar cada nivel con cinco vidas, vamos a Main.asm, localizamos la etiqueta Main_restart, y antes de CALL FadeScreen añadimos las líneas siguientes:

ld      hl, livesCounter
ld      (hl), $05

Con estas líneas, contaremos con cinco vidas al inicio de cada nivel.

Ensamblador ZX Spectrum, conclusión

En este capítulo hemos cambiado el comportamiento de los enemigos y los hemos dotado de disparo. También hemos visto distintas formas de ajustar la dificultad.

En el próximo capítulo vamos a implementar el sonido y la selección de dificultad.

El código generado lo tenéis disponible en este enlace.

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: