0x08 Ensamblador ZX Spectrum Marciano – Transición entre niveles y marcador

En este capítulo de Ensamblador ZX Spectrum Marciano, vamos a implementar una transición entre niveles y el marcador.

Como siempre, creamos la carpeta Paso08 y copiamos desde la carpeta Paso07 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.

Transición de cambio de nivel

Lo primero que vamos a implementar es una rutina que cambie los atributos de color de la pantalla, asignando los que vengan en el registro A. Abrimos el archivo Graph.asm.

Cla:
ld      hl, $5800
ld      (hl), a
ld      de, $5801
ld      bc, $02ff
ldir

ret

Apuntamos HL a la primera dirección del área de atributos, LD HL, $5800, cargamos los nuevos atributos en esa dirección, LD (HL), A, apuntamos DE a la siguiente dirección, LD DE, $5801, cargamos en BC el número de posiciones totales del area de atributos menos una (la primera ya esta cambiada), LD BC, $02FF, cambiamos todo el área de atributos, LDIR, y salimos, RET.

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

; -----------------------------------------------------------------------------
; Cambia los atributos de color de la pantalla.
;
; Entrada:	A = Atributos de color (FBPPPIII).
;
; Altera el valor de los registros AF, BC, DE y HL.
; -----------------------------------------------------------------------------
Cla:
ld      hl, $5800   ; Apunta HL a la dirección de inicio de los atributos
ld      (hl), a     ; Carga los atributos
ld      de, $5801   ; Apunta DE a la segunda dirección de los atributos
ld      bc, $02ff   ; Carga en BC el número de posiciones a cambiar
ldir                ; Cambia los atributos de la pantalla

ret

Ahora vamos a implementar, también en Graph.asm, la rutina que vamos a usar como transición de un nivel a otro. Esta rutina es una variación de la rutina FadeScreen que podéis encontrar en el Curso de Ensamblador Z80 de Compiler Software.

Vamos a recorrer todo el área de vídeo para realizar un máximo de ocho desplazamientos en cada byte para limpiarlo.

FadeScreen:
ld      b, $08

fadeScreen_loop1:
ld      hl, $4000
ld      de, $1800

Cargamos en B el número de iteraciones del bucle exterior, LD B, $08, apuntamos HL al inicio del área de vídeo, LD HL, $4000, y cargamos en DE la longitud total del área de vídeo (la parte de los píxeles), LD DE, $1800.

fadeScreen_loop2:
ld      a, (hl)
or      a
jr      z, fadeScreen_cont

bit     $00, l
jr      z, fadeScreen_right

rla
jr      fadeScreen_cont

fadeScreen_right:
rra

Cargamos en A el byte al que apunta HL, LD A, (HL), comprobamos si está limpio (está a cero), OR A, y saltamos si lo está, JR Z, fadeScreen_cont.

Si no saltamos, comprobamos si la dirección a la que apunta HL es par o impar, BIT $00, L, y si lo es (el bit 0 vale cero) saltamos, JR Z, fadeScreen_right. Si la dirección de memoria es impar, no saltamos, rotamos A hacia la izquierda, RLA, y saltamos, JR fadeScreen_cont. Si la dirección de memoria es par, rotamos A hacia la derecha, RRA.

Antes de continuar vamos a detenernos en tres líneas, la primera de ellas es OR A. Llegados a este punto, queremos saber si el byte del área de vídeo al que punta HL está limpio (tiene todos los bits a cero) y lo podríamos haber hecho con CP $00, consumiendo dos bytes y siete ciclos de reloj. OR A solo da como resultado cero, si A vale cero, consumiendo un byte y cuatro ciclos de reloj; la afectación de los flags es muy parecida, y en el caso del flag de acarreo el resultado es el mismo. En lugar de OR A, podríamos poner AND A, el resultado es el mismo.

Las siguientes líneas a tener en cuenta son RLA y RRA. En ambos casos se rota el registro A, en el caso de RLA hacia de izquierda, y hacia la derecha en el caso de RRA.

  • RLA: rota el byte hacia la izquierda, el valor del bit 7 lo pone en el acarreo, y el valor que tiene el acarreo lo pasa al bit 0.
  • RRA: rota el byte hacia la derecha, el valor del bit 0 lo pone en el acarreo, y el valor que tiene el acarreo lo pasa al bit 7.
C = 1Byte = 10000001
RLARRA
C = 1 Byte = 00000011C = 1 Byte = 11000000
C = 0 Byte = 00000111C = 0 Byte = 11100000
C = 0 Byte = 00001110C = 0 Byte = 01110000
Esamblador ZX Spectrum, RLA y RRA

Según podemos ver en esta tabla, si en algún momento de la rutina FadeScreen, antes de hacer la rotación, el acarreo está a uno, se nos puede quedar algún píxel sin limpiar; pero esto no nos va a pasar pues otras de las cosas que hace OR A es poner el acarreo a cero, de igual manera pasa si usamos CP $00.

Llegamos a la parte final de la rutina.

fadeScreen_cont:
ld      (hl), a
inc     hl

dec     de
ld      a, d
or      e
jr      nz, fadeScreen_loop2

ld      a, b
dec     a
push    bc
call    Cla
pop     bc

djnz    fadeScreen_loop1

ret

Actualizamos la posición de vídeo a la que apunta HL con el valor rotado, LD (HL), A, y apuntamos HL a la siguiente posición del área de vídeo, INC HL.

Decrementamos DE, DEC DE, cargamos el valor de D en A, LD A, D, lo mezclamos con E, OR E, y seguimos en bucle hasta que DE sea cero, JR NZ, fadeScreen_loop2.

Cargamos el valor de B en A, LD A, B, decrementamos A para que el valor quede comprendido entre siete y cero, DEC A, preservamos el valor de BC, PUSH BC, cambiamos los colores de la pantalla, CALL Cla, recuperamos el valor de BC, POP BC, y seguimos en bucle hasta que B valga cero, DJNZ fadeScreen_loop1. Finalmente, salimos.

Vamos a parar nuevamente para explicar una parte del código más en profundidad.

dec de
ld a, d
or e
jr nz, fadeScreen_loop2

Hasta ahora hemos hecho bucles usando registros de 8 bits, como es en este caso el bucle externo; cargamos ocho en B, LD B, $08, y más adelante, con DJNZ, decrementamos B y si el resultado no es cero saltamos y seguimos en el bucle, gracias a que INC o DEC cuando se hace sobre un registro de 8 bits afecta al flag Z.

En el caso de los registro de 16 bits, los incrementos y los decrementos no afectan al flag Z, de esta manera si solo decrementamos el registro y luego comprobamos si se ha activado el flag Z, nos encontramos ante un bucle infinito. Para hacer un bucle utilizando un registro de 16 bits, tras decrementar el registro, cargamos una de sus partes en A y luego hacemos OR con la otra parte, y en el caso de que ambos valores valgan cero, tal y como hemos visto anteriormente en este mismo capítulo, el resultado es cero, se activa el flag Z y saldremos del bucle.

El resultado final de la rutina es el siguiente.

; -----------------------------------------------------------------------------
; Efecto de desvanecimento de la pantalla.
;
; Altera el valor de los registros AF, BC, DE y HL.
; -----------------------------------------------------------------------------
FadeScreen:
ld      b, $08      ; El bucle exterior se repite 8 veces, una por bit
 
fadeScreen_loop1:
ld      hl, $4000   ; Apunta HL al inicio del área de vídeo
ld      de, $1800   ; Carga en DE la longitud del área de vídeo

fadeScreen_loop2:
ld      a, (hl)     ; Carga en A el byte apuntado por HL
or      a           ; Comprueba si tiene algún píxel activo
jr      z, fadeScreen_cont  ; Si no hay ninguno activo, salta

bit     $00, l      ; Comprueba si la dirección apuntada por HL es par/impar
jr      z, fadeScreen_right	; Si es par, salta

rla                 ; Rota A un bit a la izquierda 
jr      fadeScreen_cont

fadeScreen_right:
rra                 ; Rota A un bite a la derecha

fadeScreen_cont:
ld      (hl), a     ; Actualiza la posición de vídeo apuntada por HL
inc     hl          ; Apunta HL a la siguiente posición

dec     de
ld      a, d
or      e
jr      nz, fadeScreen_loop2    ; Bucle hasta que BC = 0

ld      a, b        ; Carga B en A
dec     a           ; Decrementa A para que quede entre 0 y 7
push    bc          ; Preserva el valor de BC
call    Cla         ; Cambia los colores de la pantalla
pop     bc          ; Recupera el valor de BC

djnz    fadeScreen_loop1    ; Bucle hasta que B = 0

ret

Y llega el momento de probar lo implementado; abrimos Main.asm, localizamos la rutina Main_restart, y justo debajo, antes de CALL ChangeLevel, agregamos las siguientes líneas:

call    FadeScreen
call    PrintFrame
call    PrintInfoGame
call    PrintShip

Llamamos al efecto de fusión de la pantalla, CALL FadeScreen, pintamos el marco de la pantalla, CALL PrintFrame, pintamos la información de la partida, CALL PrintInfoGame, y pintamos la nave, CALL PrintShip.

Compilamos, cargamos en el emulador, matamos a todas la naves enemigas y vemos el efecto de fundido de la pantalla.

Ensamblador ZX Spectrum, transición y marcador
Ensamblador ZX Spectrum, transición y marcador

Marcador

En el marcador vamos a mostrar el número de vidas que tenemos, los puntos conseguidos, el nivel por el que vamos y los enemigos que quedan, por lo que vamos a necesitar alguna declaración más y vamos a introducir un nuevo concepto: los números BCD.

Números BCD

Un byte es capaz de contener números de 0 a 255, si trabajamos con números BCD este rango se reduce de 0 a 99. Los números BCD dividen el byte en dos nibbles (4 bits) y en cada uno de ellos almacena valores del 0 al 9, por lo que el valor hexadecimal 0x10, que en decimal es 16, trabajando con BCD sería 10, es decir, veríamos el número en notación hexadecimal como si fiera decimal, siendo esto muy útil, por ejemplo a la hora de pintarlos o para operar con números de más de 16 bits.

Para poder operar de esta manera con los números, tenemos la instrucción DAA (Decimal Adjust Accumulator), que funciona de la siguiente forma:

  • Comprueba los bits 0, 1, 2 y 3, si contienen un dígito no BCD, mayor que nueve, o el flag H está activo, suma o resta $06 (0000 0110b) al byte, dependiendo de la operación que se ha realizado.
  • Comprueba los bits 4, 5, 6, y 7, si contienen un dígito no BCD, mayor que nueve, o el flag C está activo, suma o resta $60 (0110 0000b) al byte, dependiendo de la operación que se ha realizado.

Después de cada instrucción aritmética, incremento o decremento hay que ejecutar DAA. Veamos un ejemplo.

ld a, $09 ; A = $09
inc a ; A = $0a
daa ; A = $10
dec a ; A = $0f
daa ; A = $09
add a, $03 ; A = $0c
daa ; A = $12
sub a, $03 ; A = $0f
daa ; A = $09

Número de enemigos y nivel

Ahora, vamos a abrir el archivo Var.asm y a localizar la etiqueta enemiesCounter, que como vemos define veinte en hexadecimal ($14), y lo vamos a cambiar por el valor en BCD, $20. Localizamos la etiqueta levelCounter, y vemos que define $01; en este caso no lo vamos a cambiar, pero vamos añadir un byte con el mismo valor, usado el primer byte para cargar los enemigos de cada nivel y hacer el cambio de nivel, y el segundo byte para pintar el número de nivel en el que estamos.

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

Estas dos etiquetas las usamos en partes de nuestro programa, pero no estamos teniendo en cuenta que ahora vamos a trabajar con números BCD, por lo que tenemos que localizar los lugares donde se usan y modificar su comportamiento.

La primera modificación la vamos a hacer en la rutina ChangeLevel, que está en el archivo Game.asm, añadiendo siete líneas. Las cuatro primeras líneas las vamos a añadir al inicio de la rutina.

ld      a, (levelCounter + 1)
inc     a
daa
ld      b, a

Cargamos el nivel actual en formado BCD en A, LD A, (levelCounter + 1), incrementamos el nivel, INC A, realizamos el ajuste decimal, DAA, y cargamos el valor en B, LD B, A.

Ahora, justo por encima de la etiqueta changeLevel_end añadimos la siguiente línea:

ld      b, a

Si pasamos por aquí, el siguiente nivel sería el treinta y uno y solo tenemos treinta, por lo que cargamos $01 en A, y ahora cargamos ese valor en el registro donde tenemos el nivel en formato BCD, LD B, A.

Seguimos tomando como referencia la etiqueta changeLevel_end, tras la cual actualizamos el nivel en memoria. Justo debajo de esta línea, LD (levelCounter), a, vamos a añadir las líneas que actualizan en memoria el nivel en formato BCD.

ld      a, b
ld      (levelCounter + 1), a  

Cargamos en A el nivel actual en BCD, LD A, B, y lo actualizamos en memoria, LD (levelCounter + 1), A.

Dos líneas más abajo, sustituimos LD A, $14 por LD A, $20, para tener el número total de enemigos en BCD.

El aspecto final de la rutina, una vez realizadas las modificaciones, es el siguiente.

; -----------------------------------------------------------------------------
; Cambia de nivel.
;
; Altera el valor de los registros AF, BC, DE y HL.
; -----------------------------------------------------------------------------
ChangeLevel:
ld      a, (levelCounter + 1)   ; Carga en A el nivel actual en BCD
inc     a                       ; Incrementa el nivel
daa                             ; Hace el ajuste decimal
ld      b, a                    ; Carga el valor en B
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
ld      b, a                    ; Cargamos el valor en B

changeLevel_end:
ld      (levelCounter), a       ; Actualiza el nivel en memoria    
ld      a, b                    ; Carga en A el nivel en BCD
ld      (levelCounter + 1), a   ; Lo actualiza en memoria
call    LoadUdgsEnemies         ; Carga los gráficos de los enemigos

ld      a, $20                  ; 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

Si ahora compiláramos, veríamos que al matar a todos los enemigos no se produciría el cambio de nivel, debido a que ahora el número de enemigos es $20 (32) y no hemos adaptado todas las rutinas para trabajar con BCD.

Seguimos en el archivo Game.asm, localizamos las etiqueta checkCrashFire_endLoop, justo por encima de esta etiqueta hay un RET, y justo por encima están las instrucciones que restan un enemigo al contador, LD HL, enemiesCounter y DEC (HL). Vamos a sustituir esas dos líneas por las siguientes:

ld      a, (enemiesCounter)
dec     a
daa
ld      (enemiesCounter), a

Cargamos en A el número de enemigos, LD A, (enemiesCounter), le restamos uno, DEC A, realizamos el ajuste decimal, DAA, y actualizamos el valor en memoria, LD (enemiesCounter), A.

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 del 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

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

Hay otra rutina en dónde restamos un enemigo, la rutina que evalúa las colisiones de la nave con el enemigo. Localizamos la etiqueta checkCrashShip_endLoop, justo encima encontramos JP PrintExplosion, y justo encima encontramos dos líneas iguales a las que hemos sustituido, y que tenemos que sustituir igual que hemos hecho antes.

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      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

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

Si ahora compilamos y cargamos en el emulador, todo vuelve a funcionar.

Pintando números BCD

Vamos a implementar una rutina que pinte los números BCD en pantalla, y como veréis es algo relativamente sencillo. Para calcular el código de carácter de cada uno de los dígitos, solo hay que sumarle el carácter cero.

Abrimos Print.asm y vamos a implementar la rutina que pinte los números BCD, recibiendo en HL la dirección de memoria donde está el número a pintar.

PrintBCD:
ld      a, (hl)
and     $f0
rra
rra
rra
rra
add     a, '0'
rst     $10

Cargamos el número a pintar en A, LD A, (HL), nos quedamos con las decenas, AND $F0, ponemos el valor en los bits del cero al tres, RRA RRA RRA RRA, le sumamos el código del carácter 0, ADD A, ‘0’, y pintamos las decenas, RST $10.

ld      a, (hl)
and     $0f
add     a, '0'
rst     $10

ret

Cargamos el número a pintar en A, LD A, (HL), nos quedamos con las unidades, AND $0F, le sumamos el código del carácter 0, ADD A, ‘0’, y pintamos las unidades, RST $10. Finalmente, salimos, RET.

El aspecto final de la rutina es el siguiente:

; -----------------------------------------------------------------------------
; Pinta números en formato BCD
;
; Entrada:  HL -> Puntero al número a pintar
;
; Altera el valor de los registros AF.
; -----------------------------------------------------------------------------
PrintBCD:
ld      a, (hl)     ; Carga en A el número a pintar
and     $f0         ; Se queda con las decenas
rra
rra
rra
rra                 ; Lo pone en los bits 0 a 3
add     a, '0'      ; Le suma el carácter 0
rst     $10         ; Pinta el dígito

ld      a, (hl)     ; Carga el A el número a pintar
and     $0f         ; Se queda con las unidades
add     a, '0'      ; Le suma el carácter 0
rst     $10         ; Pinta el dígito

ret

Pintando el marcador

Vamos a implementar la rutina que pinta el marcador: vidas, puntos, nivel y enemigos.

Lo primero que vamos a hacer es definir las constantes de localización de cada uno de los elementos del marcador, para lo que abrimos el archivo Const.asm y añadimos las siguientes líneas:

COR_ENEMY:  EQU $1705   ; Coordenadas de la información de los enemigos
COR_LEVEL:  EQU $170d   ; Coordenadas de la información del nivel
COR_LIVE:   EQU $171e   ; Coordenadas de la información de las vidas
COR_POINT:  EQU $1717   ; Coordenadas de la información de los puntos

Los valores de la información de la partida los vamos a pintar en la línea de comandos, disponemos de dos líneas en este lugar. Recordad que para la rutina de la ROM que posiciona el cursor, la esquina superior izquierda es $1820, o lo que es lo mismo Y = 24, X = 32, por lo que los valores se van a pintar en la línea 23 y en las columnas 5, 13, 30 y 23. Si restamos a 24 y 32 lo valores de fila y columnas, el resultado son las coordenadas si la esquina superior derecha fuera $0000.

En Var.asm vamos a añadir las definiciones para llevar el control de las vidas y los puntos.

livesCounter:
db  $05
pointsCounter:
dw  $0000

Y ahora abrimos Print.asm para implementar la rutina que va a pintar los valores del marcador.

PrintInfoValue:
ld      a, $05
call    Ink

ld      a, $01
call    OPENCHAN

Cargamos en A la tinta cinco, LD A, $05, y llamamos al cambio de tinta, CALL Ink. Dado que los valores los vamos a pintar en la línea de comandos, cargamos en A el canal uno, LD A, $01, y abrimos el canal, CALL OPENCHAN.

ld      bc, COR_LIVE
call    At
ld      hl, livesCounter
call    PrintBCD

Cargamos en BC la posición donde pintamos las vidas, LD BC, COR_LIVE, posicionamos el cursor, CALL At, apuntamos HL al contador de vidas, LD HL, livesCounter, y pintamos las vidas, CALL PrintBCD.

ld      bc, COR_POINT
call    At
ld      hl, pointsCounter + 1
call    PrintBCD
ld      hl, pointsCounter
call    PrintBCD

Cargamos en BC la posición donde pintamos los puntos, LD BC, COR_POINT, posicionamos el cursor, CALL At, apuntamos HL a las unidades de millar y las centenas de los puntos, LD HL, pointsCounter + 1, y lo pintamos, CALL PrintBCD. Apuntamos HL a las decenas y las unidades de los puntos, LD HL, pointsCounter, y lo pintamos, CALL PrintBCD.

ld      bc, COR_LEVEL
call    At
ld      hl, levelCounter + 1
call    PrintBCD

Cargamos en BC la posición donde pintamos el nivel, LD BC, COR_LEVEL, posicionamos el cursor, CALL At, apuntamos HL al contador de niveles en formato BCD, LD HL, levelCounter + 1, y lo pintamos, CALL PrintBCD.

ld      bc, COR_ENEMY
call    At
ld      hl, enemiesCounter
call    PrintBCD

Cargamos en BC la posición donde pintamos el contador de enemigos, LD BC, COR_ENEMY, posicionamos el cursor, CALL At, apuntamos HL al contador de enemigos, LD HL, enemiesCounter, y lo pintamos, CALL PrintBCD.

ld      a, $02
call    OPENCHAN

ret

Antes de salir, activamos la pantalla superior. Cargamos el canal dos en A, LD A, $02, cambiamos el canal, CALL OPENCHAN, y salimos, RET.

El aspecto final de la rutina es el siguiente:

; -----------------------------------------------------------------------------
; Pinta los valores de la información de la partida.
;
; Altera el valor de los registros AF, BC y HL.
; -----------------------------------------------------------------------------
PrintInfoValue:
ld      a, $05                  ; Carga la tinta 5 en A
call    Ink                     ; Cambia la tinta

ld      a, $01                  ; Carga 1 en A
call    OPENCHAN                ; Activa el canal 1, línea de comando

ld      bc, COR_LIVE            ; Carga la posición de las vidas en BC
call    At                      ; Posiciona el cursor
ld      hl, livesCounter        ; Apunta HL al contador de vidas
call    PrintBCD                ; Lo pinta

ld      bc, COR_POINT           ; Carga en BC la posición de los puntos
call    At                      ; Posiciona el cusor
ld      hl, pointsCounter + 1   ; Apunta HL a unidades de millar y centenas
call    PrintBCD                ; Lo pinta
ld      hl, pointsCounter       ; Apunta HL a decenas y unidades
call    PrintBCD                ; Lo pinta

ld      bc, COR_LEVEL           ; Carga en BC la posición de los niveles
call    At                      ; Posiciona el cursor
ld      hl, levelCounter + 1    ; Apunta HL al contador de niveles en BCD
call    PrintBCD                ; Lo pinta

ld      bc, COR_ENEMY           ; Carga en BC la posición de los enemigos
call    At                      ; Posiciona el cursor
ld      hl, enemiesCounter      ; Apunta HL al contador de enemigos
call    PrintBCD                ; Lo pinta

ld      a, $02                  ; Carga 2 en A
call    OPENCHAN                ; Activa el canal 2, pantalla superior

ret

Es el momento de ver si lo que hemos implementado funciona, abrimos Main.asm, localizamos la etiqueta Main y la instrucción DI, justo encima añadimos la siguiente línea para llamar a pintar la información de la partida:

call    PrintInfoValue

Localizamos ahora la etiqueta Main_restart, y justo antes de la última línea, JR, Main_loop, añadimos la misma línea de antes. Compilamos, cargamos en el emulador y vemos los resultados.

Ensamblador ZX Spectrum, transición y marcador
Ensamblador ZX Spectrum, transición y marcador

Como podemos observar, solo se está actualizando el nivel, pero el resto de información de la partida no se actualiza. Además, no se está pintando con el color que hemos definido.

La parte del color es lo primero que vamos a solucionar. La variable de sistema donde cargamos los atributos de la pantalla en la rutina Ink, afecta a la pantalla superior, por lo que no se ve afectada la línea de comandos. Los atributos de la linea de comandos están en la misma variable de sistema donde están los atributos del borde (BORDCR), de manera que vamos a realizar dos modificaciones.

Abrimos el archivo Print.asm, localizamos la rutina PrintInfoValue, y borramos las dos primeras líneas, LD A, $05 y CALL Ink, ya que como hemos visto, no cambia los atributos de la línea de comandos.

Volvemos al archivo Main.asm, localizamos la etiqueta Main, y vamos a modificar la parte en la que se asigna el color al borde, cuyo aspecto actual es el siguiente:

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

Vamos a modificar las líneas cuatro y cinco, tras lo cual el aspecto queda de la siguiente manera:

xor     a
out     ($fe), a
ld      a, (BORDCR)
and     $c0
or      $05
ld      (BORDCR), a

Compilamos, cargamos en el emulador y comprobamos que ahora sí, los valores se pintan en el color elegido.

Ensamblador ZX Spectrum, transición y marcador
Ensamblador ZX Spectrum, transición y marcador

Y ahora vamos a actualizar el resto de valores del marcador. Cada vez que el disparo alcanza un enemigo, tenemos que restar un enemigo al contador y sumar cinco puntos. Por otro lado, cada vez que un enemigo alcanza nuestra nave, tenemos que restar una vida y restar un enemigo.

Abrimos el archivo Game.asm, localizamos la etiqueta checkCrahsFire_endLoop y observamos que en las líneas que hay por encima ya se resta un enemigo al contador.

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

ret

Nos falta añadir cinco puntos por haber acabado con un enemigo y pintar la información de la partida. Vamos a añadir las siguientes líneas entre LD (enemiesCounter), A y RET.

ld      a, (pointsCounter)
add     a, $05
daa
ld      (pointsCounter), a
ld      a, (pointsCounter + 1)
adc     a, $00
daa
ld      (pointsCounter + 1), a
call    PrintInfoValue

Cargamos la unidades y las decenas de los puntos en A, LD A, (pointsCounter), le añadimos cinco, ADD A, $05, hacemos el ajuste decimal, DAA, y cargamos el valor en memoria, LD (pointsCounter), A.

Sumar cinco a las unidades y el ajuste decimal puede provocar un acarreo, por ejemplo si el valor era noventa y cinco, por lo que tenemos que sumar uno a las centenas, en concreto el acarreo.

Cargamos en A las centenas y las unidades de millar, LD A, (pointsCounter + 1), sumamos cero con acarreo a A, ADC A, $00, hacemos el ajuste decimal, DAA, cargamos el valor en memoria, LD (PointsCounter + 1), A, y pintamos la información de la partida, CALL PrintInfoValue.

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 que 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 unidadaes 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 0 con acarreo
daa                             ; Hace el ajuste decimal
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

En este código hemos utilizado una instrucción de no habíamos visto hasta ahora, ADC (Add With Carry). Esta instrucción suma el valor indicado a A, más el valor del acarreo, de tal manera que al sumar cero a A, si el acarreo está a uno sumaríamos uno a A, lo que comúnmente conocemos como “me llevo una”.

La posibilidades de ADC son:

MnemócicoCiclosBytesS Z H P N C
ADC A, r41* * * V 0 *
ADC A, N72* * * V 0 *
ADC A, (HL)71* * * V 0 *
ADC A, (IX+N)193* * * V 0 *
ADC A, (IY+N)193* * * V 0 *
ADC HL, BC152* * ? V 0 *
ADC HL, DE152* * ? V 0 *
ADC HL, HL152* * ? V 0 *
ADC HL, SP152* * ? V 0 *
* Afecta al flag, V = overflow, 0 = pone el flag a 0, ? = valor desconocido

Ahora ya solo son queda restar una vida cuando la nave choca contra un enemigo. Seguimos en Game.asm, localizamos la etiqueta checkCrashShip_endLoop, justo por encima encontramos la línea JP PrintExplosion, y justo por encima vamos a añadir las líneas siguientes:

ld      a, (livesCounter)
dec     a
daa
ld      (livesCounter), a
call    PrintInfoValue

Cargamos en A las vidas, LD A, (livesCounter), quitamos una, DEC A, hacemos el ajuste decimal, DAA, cargamos el valor en memoria, LD (livesCounter), A, y pintamos la información de la partida, CALL PrintInfoValue. Como podemos ver, al ser el contador de vidas de un solo byte, la modificación ha sido menos que la que hemos realizado para el contador de puntos.

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      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

ret

Es hora de ver si lo que hemos implementado funciona. Compilamos, cargamos en el emulador y vemos los resultados.

Ensamblador ZX Spectrum, transición y marcador
Ensamblador ZX Spectrum, transición y marcador

Ensamblador ZX Spectrum, conclusión

Ya tenemos todo listo para poder empezar a jugar nuestras primeras partidas. Hemos implementado una transición entre niveles y el marcador.

En el siguiente capítulo implementaremos el menú de inicio y el fin de la partida.

Podéis descargar el código generado 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: