0x07 Ensamblador ZX Spectrum Marciano – Colisiones y cambio de nivel

En este capítulo de Ensamblador ZX Spectrum Marciano, vamos a incluir las colisiones del disparo con los enemigos, de los enemigos con la nave, y los cambios de nivel. Lo primero es crear la carpeta Paso07, copiamos desde la carpeta Paso06 los archivos Cargador.tap, Const.asm, Ctrl.asm, Game.asm, Graph.asm, Int.asm, Main.asm, Print.asm, Var.asm y make, o make.bat si estáis trabajando en Windows.

Colisiones de los enemigos con el disparo

Lo primero que vamos a implementar son las colisiones entre los enemigos y el disparo. Como recordaremos, en el primer byte de la configuración de cada enemigo, el bit siete nos dice si está activo o no, pudiendo con esto decidir si se pinta o no.

La rutina que vamos a implementar va a comprobar si algún enemigo está en las mismas coordenadas que el disparo, y de ser así lo deshabilita.

Implementamos la rutina al inicio de archivo Game.asm.

CheckCrashFire:
ld      a, (flags)
and     $02
ret     z

Cargamos en A el valor de los flags, LD A, (flags), nos quedamos con el bit uno para comprobar si el disparo está activo, AND $02, y salimos si no lo está, RET Z.

ld      de, (firePos)
ld      hl, enemiesConfig
ld      b, enemiesConfigEnd - enemiesConfigIni
sra     b

Cargamos en DE las coordenadas del disparo, LD DE, (firePos), apuntamos HL a la configuración de los enemigos, LD HL, enemiesConfig, cargamos en B el número de bytes totales de la configuración de los enemigos, LD B, enemiesConfigEnd – enemiesConfigIni, y lo dividimos entre dos para calcular el número de enemigos, SRA B, ya que la configuración de cada enemigo ocupa dos bytes.

SRA desplaza todos los bits hacia la derecha, el valor del bit cero lo pone en el acarreo y mantiene el valor del bit siete, para conservar el signo. SRA hace una división entera entre dos y dado que el número de enemigos que tenemos es par, nos vale.

checkCrashFire_loop:
ld      a, (hl)
inc     hl
bit     $07, a
jr      z, checkCrashFire_endLoop

Cargamos en A el primer byte de la configuración del enemigo, LD A, (HL), apuntamos HL al segundo byte de la configuración, INC HL, evaluamos si el enemigo está activo, BIT $07, A, y saltamos si no lo está, JR Z, checkCrashFire_endLoop.

and     $1f
cp      d
jr      nz, checkCrashFire_endLoop

Si el enemigo está activo, nos quedamos con la coordenada Y, AND $1F, comparamos con la coordenada Y del disparo, CP D, y saltamos si no son la misma, JR NZ, checkCrashFire_endLoop.

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

Cargamos en A el segundo byte de la configuración del enemigo, LD A, (HL), nos quedamos con la coordenada X, AND $1F, comparamos con la coordenada X del disparo, CP E, y saltamos si no son la misma.

dec     hl
res     $07, (hl)
ld      b, d
ld      c, e
call    DeleteChar

ret

Si el disparo y el enemigo colisionan, apuntamos HL al primer byte de la configuración del enemigo, DEC HL, desactivamos el enemigo, RES $07, (HL), cargamos la coordenada Y del disparo en B, LD B, D, cargamos la coordenada X del disparo en C, LD C, E, borramos el disparo y/o enemigo, CALL DeleteChar, y salimos de la rutina, RET.

checkCrashFire_endLoop:
inc     hl
djnz    checkCrashFire_loop

ret

Si el disparo y el enemigo no colisionan, apuntamos HL al byte primer byte de la configuración del siguiente enemigo, INC HL, y repetimos el bucle mientras B sea mayor que cero, DJNZ checkCrashFire_loop. Una vez finalizado el bucle, salimos de la rutina, RET.

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

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

Ha llegado el momento de probar si las colisiones funcionan, abrimos el archivo Main.asm, localizamos la etiqueta Main_loop, y justo debajo de CALL MoveFire, preservamos el valor de DE (en D tenemos las pulsaciones de los controles), PUSH DE, añadimos la llamada a la rutina que acabamos de implementar, CALL CheckCrashFire, y recuperamos el valor de DE, POP DE, quedando de la siguiente manera:

Main_loop:
call    CheckCtrl
call    MoveFire
push    de
call    CheckCrashFire
pop     de
call    MoveShip
call    MoveEnemies
jr      Main_loop

Compilamos, cargamos en el emulador y probamos.

Ensamblador ZX Spectrum, colisiones
Ensamblador ZX Spectrum, colisiones

Tenemos dos problemas, uno de ellos heredado:

  • Si no movemos la nave, se borra y no se vuelve a pintar.
  • Una vez que ya no hay naves, no podemos hacer otra cosa que volver a cargar el juego.

El primer problema no lo vamos a abordar, si la nave se borra es porque ha colisionado con un enemigo, más adelante pintaremos una explosión.

Cambio de nivel

Para el cambio de nivel lo primero que tenemos que controlar es el número de enemigos que hay activos, al llegar a cero hay que cambiar de nivel. Lo segundo, es el número de niveles que tenemos, un total de treinta; por ahora al pasar al nivel treinta y uno, volveremos al uno, más adelante llegaremos al final del juego.

Abrimos el archivo Var.asm y vamos a añadir una variable para el número de enemigos activos y otra para el nivel actual, al inicio del archivo, después de los títulos de información de la partida.

; -----------------------------------------------------------------------------
; Información de la partida
; -----------------------------------------------------------------------------
enemiesCounter:
db  $14
levelCounter:
db  $01

Antes de implementar la rutina que hace el cambio de nivel, vamos a hacer unos cambios para usar levelCounter. Abrimos el archivo Graph.asm y localizamos la rutina LoadUdgsEnemies. Esta rutina recibe en A el nivel, pero ya no es necesario pues ese valor lo va a tomar de levelCounter. Añadimos la siguiente línea al inicio de la rutina:

ld      a, (levelCounter)

Cargamos en A el nivel actual, LD A, (levelCounter).

En los comentarios de la rutina, borramos la línea referente a la entrada en A del nivel, quedando tal y como sigue:

; -----------------------------------------------------------------------------
; Carga los gráficos definidos por el usuario relativos a los enemigos
;
; Altera el valor de los registros AF, BC, DE y HL
; -----------------------------------------------------------------------------
LoadUdgsEnemies:
ld      a, (levelCounter)       ; Carga en A el nivel
dec     a                       ; Decrementa A para que no sume un nivel de más
ld      h, $00
ld      l, a                    ; Carga el resultado en HL
add     hl, hl                  ; Multiplica por 2
add     hl, hl                  ; por 4
add     hl, hl                  ; por 8
add     hl, hl                  ; por 16
add     hl, hl                  ; por 32
ld      de, udgsEnemiesLevel1   ; Carga la dirección del enemigo 1 en DE
add     hl, de                  ; Lo suma a HL
ld      de, udgsExtension       ; Carga en DE la dirección de la extensión
ld      bc, $20                 ; Carga en BC el número de bytes a copiar, 32
ldir                            ; Copia los bytes del enemigo en los de extensión

ret

En el archivo Game.asm, buscamos la etiqueta checkCrahsFire_endLoop, justo por encima de ella hay un RET y justo por encima de este RET añadimos las siguientes líneas:

ld      hl, enemiesCounter      ; Apunta HL al contador de enemigos
dec     (hl)                    ; Resta un enemigo

En el archivo Main.asm, tres líneas por encima de Main_loop, justo antes de CALL LoadUdgsEnemies, borramos la línea LD A, $01, pues el nivel se toma ya de levelCounter.

Compilamos, cargamos en el emulador y comprobamos que todo sigue funcionando.

Ahora, en el archivo Game.asm, vamos a implementar el cambio de nivel, que consiste en cargar los gráficos de los enemigos del siguiente nivel, reiniciar la configuración de los enemigos, y actualizar los contadores que acabamos de añadir.

ChangeLevel:
ld      a, (levelCounter)
inc     a
cp      $1f
jr      c, changeLevel_end
ld      a, $01

Cargamos en A el nivel actual, LD A, (levelCounter), incrementamos A para pasar al siguiente nivel, INC A, y comprobamos si el siguiente nivel es el treinta y uno, CP $1F. Si no hemos llegado al nivel treinta y uno saltamos a la parte final de la rutina, JR C, changeLevel_end. Si hemos llegado al nivel treinta y uno, recordad que solo tenemos treinta niveles, no saltamos y ponemos A a $01.

changeLevel_end:
ld      (levelCounter), a
call    LoadUdgsEnemies

ld      a, $14
ld      (enemiesCounter), a

ld      hl, enemiesConfigIni
ld      de, enemiesConfig
ld      bc, enemiesConfigEnd - enemiesConfigIni
ldir

ret

Cargamos el siguiente nivel en memoria, LD (levelCounter), A, cargamos los gráficos del enemigo del siguiente nivel, CALL LoadUdgsEnemies, cargamos el número de enemigos totales en A, LD A, $14, y actualizamos el valor en memoria, LD (enemiesCounter), A.

Por último, reiniciamos la configuración de los enemigos. Apuntamos HL a la configuración inicial de los enemigos, LD HL, enemiesConfigIni, apuntamos DE a la configuración de los enemigos, LD DE, enemiesConfig, cargamos en BC el número de bytes de los que se compone la configuración de los enemigos, LD BC, enemiesConfigEnd – enemiesConfigIni, cargamos la configuración inicial de los enemigos en la configuración de los enemigos, LDIR, y salimos, RET.

El funcionamiento de LDIR ya se explicó en PorompomPong.

El aspecto final de la rutina es el siguiente:

; -----------------------------------------------------------------------------
; Cambia de nivel.
;
; Altera el valor de los registros AF, BC, DE y HL.
; -----------------------------------------------------------------------------
ChangeLevel:
ld      a, (levelCounter)       ; Carga el nivel actual en A
inc     a                       ; Carga en A el siguiente nivel
cp      $1f                     ; Compara si el nivel es el 31
jr      c, changeLevel_end      ; Si no es el 31, salta
ld      a, $01                  ; Si es el 31, lo pone a 1

changeLevel_end:
ld      (levelCounter), a       ; Actualiza el nivel en memoria    
call    LoadUdgsEnemies         ; Carga los gráficos de los enemigos

ld      a, $14                  ; Carga en A el número total de enemigos
ld      (enemiesCounter), a     ; Lo carga en memoria

ld      hl, enemiesConfigIni    ; Apunta HL a la configuración inicial
ld      de, enemiesConfig       ; Apunta DE a la configuración
ld      bc, enemiesConfigEnd - enemiesConfigIni ; Carga en BC la longitud
                                                ; de la configuración
ldir                            ; Carga la configuración inicial en la configuración

ret

Para finalizar, tenemos que usar lo implementado; lo vamos a hacer en Main.asm. Localizamos la rutina MainLoop, localizamos la quinta línea, POP DE, y justo debajo de ella añadimos lo siguiente:

ld      a, (enemiesCounter)
or      a
jr      z, Main_restart

Cargamos en A el número de enemigos activos, LD A, (enemiesCounter), comprobamos si hemos llegado a cero, OR A y saltamos si es así, JR Z, Main_restart.

Ahora vamos al final del archivo, y justo encima del primer include añadimos lo siguiente:

Main_restart:
call    ChangeLevel
jr      Main_loop

Cambiamos al siguiente nivel, CALL ChangeLevel, y volvemos al inicio del bucle, JR Main_loop.

Dado que Main.asm va creciendo, veamos cual es el aspecto que debe tener ahora:

org     $5dad

; -----------------------------------------------------------------------------
; 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í
; -----------------------------------------------------------------------------
flags:
db $00

Main:
ld      a, $02
call    OPENCHAN

ld      hl, udgsCommon
ld      (UDG), hl

ld      hl, ATTR_P
ld      (hl), $07
call    CLS

xor     a
out     ($fe), a
ld      a, (BORDCR)
and     $c7
or      $07
ld      (BORDCR), a

call    PrintFrame
call    PrintInfoGame
call    PrintShip

di
ld      a, $28
ld      i, a
im      2
ei

call    LoadUdgsEnemies
call    PrintEnemies

Main_loop:
call    CheckCtrl
call    MoveFire

push    de
call    CheckCrashFire
pop     de

ld      a, (enemiesCounter)
or      a
jr      z, Main_restart

call    MoveShip
call    MoveEnemies
jr      Main_loop

Main_restart:
call    ChangeLevel
jr      Main_loop

include "Const.asm"
include "Var.asm"
include "Graph.asm"
include "Print.asm"
include "Ctrl.asm"
include "Game.asm"

end     Main

Compilamos, cargamos en el emulador y, si todo ha ido bien, vemos como cambian los enemigos cuando los hemos destruido todos.

Ensamblador ZX Spectrum, colisiones
Ensamblador ZX Spectrum, colisiones

Colisiones de los enemigos con la nave

En esta primera aproximación, lo único que vamos a hacer es pintar una explosión cuando algún enemigo choque contra la nave, más adelante nos restará una vida.

Primero vamos a implementar la rutina que pinta la explosión, abrimos el archivo Print.asm.

PrintExplosion:
ld      a, $02
call    Ink

ld      bc, (shipPos)
ld      d, $04
ld      e, $92

Cargamos dos en A (dos = color rojo), LD A, $02, y cambiamos el color de la tinta, CALL INK. Cargamos en BC la posición de la nave, LD BC, (shipPos), cargamos en D el número de UDG totales que tiene la explosión, LD D, $04, y cargamos en E el primer UDG de la explosión, LD E, $92.

printExplosion_loop:
call    At
ld      a, e
rst     $10
halt
halt
halt
halt
inc     e
dec     d
jr      nz, printExplosion_loop

jp      PrintShip

Posicionamos el cursor en las coordenadas de la nave, CALL AT, cargamos en A el UDG, LD A, E, y lo pintamos, RST $10. Esperamos cuatro interrupciones, HALT, HALT, HALT, HALT, apuntamos E al siguiente UDG, INC E, decrementamos D, DEC D, y nos quedamos en el bucle hasta que D valga cero, JR NZ, printExplosion_loop.

Por último, volvemos a pintar la nave y salimos, JP PrintShip. Aprovechamos el RET de PrintShip para salir. Podríamos haber llamado a PrintShip y luego salido:

call PrintShip
ret

Pero con JP nos ahorramos un byte y diecisiete ciclos de reloj.

El aspecto final de la rutina es el siguiente:

; -----------------------------------------------------------------------------
; Pinta la explosión de la nave
;
; Altera los valores de los registros AF, BC y DE.
; -----------------------------------------------------------------------------
PrintExplosion:
ld      a, $02
call    Ink             ; Pone la tinta en rojo

ld      bc, (shipPos)   ; Carga en BC la posición de la nave
ld      d, $04          ; Carga en D el número de UDG totales de la explosión
ld      e, $92          ; Carga en E el primer UDG de la explosión
printExplosion_loop:
call    At              ; Posiciona el cursor
ld      a, e            ; Carga en A el UDG
rst     $10             ; Lo pinta
halt
halt
halt
halt                    ; Espera 4 interrupciones
inc     e               ; Apunta E al siguiente UDG
dec     d               ; Decrementa D
jr      nz, printExplosion_loop ; Bucle hasta que D = 0

jp      PrintShip       ; Pinta la nave y sale por allí

Ahora, en Game.asm, vamos a implementar las colisiones entre los enemigos y la nave que, como veréis, es bastante parecida a la rutina que implementa las colisiones de los enemigos con el disparo.

CheckCrashShip:
ld      de, (shipPos)
ld      hl, enemiesConfig
ld      b, enemiesConfigEnd - enemiesConfigIni
sra     b

Cargamos en HL la posición de la nave, LD DE, (shipPos), apuntamos HL a la configuración de los enemigos LD HL, enemiesConfig, cargamos en B el número de bytes de la configuración, LD B, enemiesConfigEnd – enemiesConfigIni, y lo dividimos entre dos para obtener el número de enemigos, SRA B.

checkCrashShip_loop:
ld      a, (hl)
inc     hl
bit     $07, a
jr      z, checkCrashShip_endLoop

Cargamos en A el primer byte de la configuración del enemigo, LD A, (HL), apuntamos HL al segundo byte de la configuración del enemigo, INC HL, comprobamos si el enemigo está activo, BIT $07, A, y saltamos si no es así, JR Z, checkCrashShip_endLoop.

and     $1f
cp      d 
jr      nz, checkCrashShip_endLoop

Nos quedamos con la coordenada Y del enemigo, AND $1F, comparamos con la coordenada Y de la nave, CP D, y saltamos si no son la misma, JR NZ, checkCrashShip_endLoop.

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

Cargamos el segundo byte de la configuración del enemigo en A, LD A, (HL), nos quedamos con la coordenada X, AND $1F, comparamos con la coordenada X de la nave, CP E, y saltamos si no son la misma, JR NZ, checkCrashShip_endLoop.

dec     hl
res     $07, (hl)

ld      hl, enemiesCounter
dec     (hl)

jp      PrintExplosion

Si pasamos por aquí, ha habido colisión. Apuntamos HL al primer byte de la configuración del enemigo, DEC HL, desactivamos el enemigo, RES $07,(HL), apuntamos HL al contador de enemigos, LD HL, enemiesCounter, y le restamos uno, DEC (HL). Finalmente, saltamos a pintar la explosión y salimos, JP PrintExplosion, usando la misma técnica que hemos visto en PrintExplosion.

checkCrashShip_endLoop:
inc     hl
djnz    checkCrashShip_loop

ret

Si no ha habido colisión, apuntamos HL al primer byte de la configuración del siguiente enemigo, INC HL, y permanecemos en el bucle hasta que B valga cero y hayamos recorrido todos los enemigos, DJNZ checkCrashShip_loop. Para finalizar, salimos, RET.

El aspecto final de la rutina es el siguiente:

; -----------------------------------------------------------------------------
; Evalúa las colisiones de los enemigos 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, enemiesConfigEnd - enemiesConfigIni  ; B = bytes totales configuración
sra     b                   ; B = B / 2 = 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      hl, enemiesCounter  ; Apunta HL al contador de enemigos
dec     (hl)                ; Resta un enemigo

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

ret

Ha llegado el momento de probar las colisiones entre la nave y los enemigos. Abrimos Main.asm, localizamos la rutina Main_loop y vemos que la última línea es JR Main_loop. Justo encima de esta línea vamos a añadir la llamada a la comprobación de las colisiones entre nave y enemigos:

call    CheckCrashShip

Compilamos, cargamos en el emulador y vemos los resultados.

Ensamblador ZX Spectrum, colisiones
Ensamblador ZX Spectrum, colisiones

Ensamblador ZX Spectrum, conclusión

Hemos implementado las colisiones entre el disparo y los enemigos, y entre los enemigos y la nave. También hemos implementado el cambio de nivel cuando hemos destruido todos los enemigos.

En el próximo capítulo vamos a implementar una transición entre niveles y el marcador.

Todo el código que hemos generado lo podéis descargar desde aquí.

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: