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.
Tabla de contenidos
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 = 1 | Byte = 10000001 |
RLA | RRA |
C = 1 Byte = 00000011 | C = 1 Byte = 11000000 |
C = 0 Byte = 00000111 | C = 0 Byte = 11100000 |
C = 0 Byte = 00001110 | C = 0 Byte = 01110000 |
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.
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.
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.
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ónico | Ciclos | Bytes | S Z H P N C |
ADC A, r | 4 | 1 | * * * V 0 * |
ADC A, N | 7 | 2 | * * * V 0 * |
ADC A, (HL) | 7 | 1 | * * * V 0 * |
ADC A, (IX+N) | 19 | 3 | * * * V 0 * |
ADC A, (IY+N) | 19 | 3 | * * * V 0 * |
ADC HL, BC | 15 | 2 | * * ? V 0 * |
ADC HL, DE | 15 | 2 | * * ? V 0 * |
ADC HL, HL | 15 | 2 | * * ? V 0 * |
ADC HL, SP | 15 | 2 | * * ? V 0 * |
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, 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
- Notepad++
- Visual Studio Code
- Sublime Text
- ZEsarUX
- PASMO
- Git
- Curso de ensamblador Z80 de Compiler Software
- Referencia Z80
- Ensamblador Z80 en Telegram
- Tutorial completo en formato PDF y EPUB
- Proyecto en itch.io
- Archivo .dsk con los juegos de los tutoriales
- Personalización y depuración con ZEsarUX
Ensamblador para ZX Spectrum Batalla espacial por Juan Antonio Rubio García.
Esta obra está bajo licencia de Creative Commons Reconocimiento-NoComercial-CompartirIgual 4.0 Internacional License.
También puedes visitar el resto de tutoriales:
Y recuerda, si lo usas no te limites a copiarlo, intenta entenderlo y adaptarlo a tus necesidades.