0x09 Ensamblador ZX Spectrum Marciano – Comienza la partida
En este capítulo de Ensamblador ZX Spectrum Marciano, vamos a implementar el inicio y el fin de la partida.
Al igual que en capítulos anteriores, creamos la carpeta Paso09 y copiamos desde la carpeta Paso08 los archivos Cargador.tap, Const.asm, Ctrl.asm, Game.asm, Graph.asm, Int.asm, Main.asm, make o make.bat, Print.asm y Var.asm.
Antes de empezar con el objetivo de este capítulo, vamos a revisar la rutina PrintString para ver dos variaciones.
Tabla de contenidos
Rutina PrintString
Las variaciones que vamos a ver de la rutina PrintString las vamos a implementar en un nuevo archivo que vamos a llamar TestPrint.asm, luego decidiremos que rutina va a ser la definitiva.
Abrimos el archivo TestPrint.asm y añadimos el siguiente código.
org $5dad
TestPrint:
ld hl, string
ld b, stringEOF - string
call PrintString
ld hl, stringNull
call PrintStringNull
ld hl, stringFF
call PrintStringFF
ret
; -----------------------------------------------------------------------------
; Pinta cadenas.
;
; Entrada: HL = primera posición de memoria de la cadena
; B = longitud de la cadena.
;
; Altera el valor de los registros AF y HL
; -----------------------------------------------------------------------------
PrintString:
ld a, (hl) ; Carga en A el carácter a pintar
rst $10 ; Pinta el carácter
inc hl ; Apunta HL al siguiente carácter
djnz PrintString ; Hasta que B valga 0
ret
; -----------------------------------------------------------------------------
; Pinta cadenas.
;
; Entrada: HL = primera posición de memoria de la cadena
;
; Altera el valor de los registros AF y HL
; -----------------------------------------------------------------------------
PrintStringNull:
ld a, (hl) ; Carga en A el carácter a pintar
or a ; Comprueba si es 0
ret z ; De ser así, sale
rst $10 ; Pinta el carácter
inc hl ; Apunta HL al siguiente carácter
jr PrintStringNull ; Bucle hasta que termine de pintar la cadena
; -----------------------------------------------------------------------------
; Pinta cadenas.
;
; Entrada: HL = primera posición de memoria de la cadena
;
; Altera el valor de los registros AF y HL
; -----------------------------------------------------------------------------
PrintStringFF:
ld a, (hl) ; Carga en A el carácter a pintar
cp $ff ; Comprueba si es $FF
ret z ; De ser así sale
rst $10 ; Pinta el carácter
inc hl ; Apunta HL al siguiente carácter
jr PrintStringFF ; Bucle hasta que termine de pintar la cadena
string:
db $10, $05, $11, $03, $16, $05, $0a, "Hola Ensamblador"
stringEOF:
db $00
stringNull:
db $10, $07, $11, $01, $16, $07, $0a, "Hola Ensamblador", $00
stringFF:
db $10, $02, $11, $07, $16, $09, $0a, "Hola Ensamblador", $ff
end TestPrint
En este código podemos ver tres rutinas PrintString:
- PrintString: la rutina tal cual la tenemos ahora.
- PrintStringNull: rutina que pinta cadenas y usa como fin de cadena el carácter nulo ($00).
- PrintStringFF: rutina que pinta cadenas y usa como fin de cadena el carácter $FF.
La primera de estás rutinas ya la conocemos, por lo que vamos a explicar la rutina PrintStringNull, ya que PrintStringFF solo se diferencia en una línea a ésta.
PrintStringNull:
ld a, (hl)
or a
ret z
rst $10
inc hl
jr PrintStringNull
PrintStringNull y PrintStringFF, reciben en HL la primera posición de la cadena (al igual que PrintString), pero no necesitan conocer la longitud de la misma.
Cargamos en A el carácter al que apunta HL, LD A, (HL), comprobamos si es cero, OR A, y salimos si es así, RET Z. La línea que cambia en PrintStringFF con respecto a PrintStrinNull es OR A, que la cambiamos por CP $FF, ya que $FF es el carácter que se usa en este caso como fin de cadena. Debemos recordar que el resultado de OR A solo es cero si A vale cero.
Si el carácter cargado en A no es el de fin de cadena, pintamos el carácter, RST $10, apuntamos HL al siguiente carácter, INC HL, y seguimos en bucle hasta que pinte toda la cadena, JR PrintStringNull.
El uso de una u otra rutina tiene sus pros y sus contras. La primera comparativa la vamos a realizar sobre bytes y ciclos de reloj.
Bytes | Ciclos | |
PrintString | 6 | 47/42 |
PrintStringNull | 7 | 51/45 |
PrintStringFF | 8 | 54/48 |
Si vemos esta tabla, la rutina más optima es la primera ya que ocupa menos bytes y es más rápida. La realidad es que sí es más rápida, pero no ocupa menos bytes, ya que cada vez que la llamemos hay que añadir dos bytes de cargar en B la longitud de la cadena. Si usamos mucho esta rutina, rápidamente vemos que el ahorro de bytes no es tal.
La opción lógica es la segunda rutina, que es más rápida y ocupa menos bytes que la tercera, pero como vamos a ver más adelante, tiene sus desventajas.
TestPrint es un programa individual, para compilarlo tenemos que invocar PASMO desde la línea de comandos:
pasmo ––name TestPrint ––tapbas TestPrint.asm TestPrint.tap
Antes de continuar, vamos a compilar el programa TestPrint, lo cargamos en el emulador y vemos los resultados.
Como podemos ver, todo ha ido bien. Tenemos tres cadenas, y hemos pintado cada una con una rutina distinta.
Vamos a ver ahora los inconvenientes de la segunda rutina, para lo cual vamos a modificar la segunda cadena, que ahora es:
stringNull:
db $10, $07, $11, $01, $16, $07, $0a, "Hola Ensamblador", $00
Y vamos a modificar el segundo byte, $07, por $00.
stringNull:
db $10, $00, $11, $01, $16, $07, $0a, "Hola Ensamblador", $00
Compilamos, cargamos en el emulador y vemos los resultados.
Aquí hay algo que no funciona, pero ¿qué? Revisemos la definición de las cadenas.
string:
db $10, $05, $11, $03, $16, $05, $0a, "Hola Ensamblador"
stringEOF:
db $00
stringNull:
db $10, $00, $11, $01, $16, $07, $0a, "Hola Ensamblador", $00
stringFF:
db $10, $02, $11, $07, $16, $09, $0a, "Hola Ensamblador", $ff
La segunda cadena, stringNull, la terminamos con $00 porque la rutina pinta hasta que se encuentra este valor. Todas las cadenas empiezan con $10, que es el código de INK, por lo que el siguiente byte debe ser un código de color, del $00 al $07.
Cuando se pasa la cadena stringNull a la rutina PrintStringNull, lee el primer carácter, $10 (INK), lee el siguiente carácter, $00, y sale.
Lo siguiente que hace el programa es cargar en HL la cadena stringFF y llamar a la rutina PrintStringFF. Esta rutina lee el primer carácter, $10 (INK), y lo pinta, pero como lo anterior también ha sido un INK, lo que espera ahora es un código de color, y lo que le pasamos es $10 (16), un color que no es válido, de ahí el mensaje K Invalid Colour, 40:1.
Vamos a volver a modificar el segundo byte de la cadena stringNull, poniéndolo a $05, y vamos a modificar el segundo byte de la cadena stringFF, que ahora vale $02 y lo ponemos a $00.
Compilamos, cargamos en el emulador y vemos los resultados.
Como podemos ver, vuelve a funcionar y pinta la tercera cadena en color negro, $00, por lo que en esta ocasión nos decantamos por la rutina PrintStringFF.
Copiamos el código de la rutina, abrimos el archivo Print.asm, localizamos la rutina PrintString y sustituimos el código de dicha rutina por el código que acabamos de copiar. El aspecto final de la rutina deber ser el siguiente:
; -----------------------------------------------------------------------------
; Pinta cadenas terminadas en $FF.
;
; Entrada: HL = primera posición de memoria de la cadena
;
; Altera el valor de los registros AF y HL
; -----------------------------------------------------------------------------
PrintString:
ld a, (hl) ; Carga en A el carácter a pintar
cp $ff ; Comprueba si es $FF
ret z ; De ser así sale
rst $10 ; Pinta el carácter
inc hl ; Apunta HL al siguiente carácter
jr PrintString ; Bucle hasta que termine de pintar la cadena
Ahora tenemos que modificar la definición de las cadenas y las llamadas a PrintString.
Seguimos en el archivo Print.asm, localizamos la etiqueta PrintFramey borramos la segunda y la quinta línea.
ld hl, frameTopGraph ; Carga en HL la dirección de la parte superior
; ld b, frameBottomGraph - frameTopGraph ; Carga en B la longitud
call PrintString ; Pinta la cadena
ld hl, frameBottomGraph ; Carga en HL la dirección de la parte inferior
; ld b, frameEnd - frameBottomGraph ; Carga en B la longitud
call PrintString ; Pinta la cadena
Localizamos la etiqueta PrintInfoGame y borramos la cuarta línea.
ld a, $01 ; Carga 1 en A
call OPENCHAN ; Activa el canal 1, línea de comando
ld hl, infoGame ; Carga la dirección de la cadena de títulos en HL
;ld b, infoGame_end - infoGame ; Carga la longitud en B
Abrimos el archivo Var.asm, localizamos la etiqueta infoGame y después de enemigos añadimos $FF. Borramos la etiqueta infoGame_end pues ya no tiene ninguna utilidad.
infoGame:
db $10, $03, $16, $00, $00
db 'Vidas Puntos Nivel Enemigos', $ff
Localizamos la etiqueta frameTopGraph y añadimos al final de la definición de los bytes $FF. En la siguiente etiqueta, frameBottomGraph, hacemos lo mismo y borramos la etiqueta frameEnd, que ya no tiene ninguna utilidad.
frameTopGraph:
db $16, $00, $00, $10, $01
db $96, $97, $97, $97, $97, $97, $97, $97, $97, $97, $97, $97, $97, $97, $97, $97, $97, $97, $97, $97, $97, $97, $97, $97, $97, $97, $97, $97, $97, $97, $97, $98, $ff
frameBottomGraph:
db $16, $14, $00
db $9b, $9c, $9c, $9c, $9c, $9c, $9c, $9c, $9c, $9c, $9c, $9c, $9c, $9c, $9c, $9c, $9c, $9c, $9c, $9c, $9c, $9c, $9c, $9c, $9c, $9c, $9c, $9c, $9c, $9c, $9c, $9d, $ff
Compilamos (ahora ya lo volvemos a hacer ejecutando make o make.bat), cargamos en el emulador y comprobamos que todo sigue funcionando. También vemos que el programa ocupa ahora tres bytes menos; con cada línea que hemos quitado en la que cargábamos la longitud de la cadena en B, hemos reducido dos bytes (seis en total), pero al poner $ff al final de las cadenas hemos vuelto a añadir tres bytes.
Inicio y fin de la partida
Vamos a implementar una pantalla de inicio a modo de menú y dos finales, uno para cuando nos matan sin haber conseguido finalizar el juego, y otro para cuando logramos finalizar el juego.
Inicio de la partida
En la pantalla de inicio, vamos a mostrar un texto a modo de presentación, las teclas de control y la selección de los distintos tipos de control.
Abrimos el archivo Var.asm y al inicio del mismo incluimos la definición de la pantalla de inicio.
title:
db $10, $02, $16, $00, $08, "BATALLA ESPACIAL", $0d, $0d, $0d, $ff
firstScreen:
db $10, $06, "Las naves alienigenas atacan la", $0d
db "Tierra, el futuro depende de ti.", $0d, $0d
db "Destruye todos los enemigos que", $0d
db "puedas, y protege el planeta.", $0d, $0d, $0d
db $10, $03, "Z - Izquierda", $16, $0a, $15,"X - Derecha"
db $16, $0c, $0b, "V - Disparo", $0d, $0d
db $10, $04, "1 - Teclado 3 - Sinclair 1", $0d, $0d
db "2 - Kempston 4 - Sinclair 2", $0d, $0d, $0d
db $10, $05, "Apunta, dispara, esquiva a las", $0d
db "naves enemigas, vence y libera", $0d
db "al planeta de la amenza."
db $ff
Lo primero que hacemos es poner la tinta en rojo, $10, $02, luego posicionamos el cursor en la línea 0, columna 8, $16, $00, $08, pintamos el nombre del juego, BATALLA ESPACIAL, y añadimos tres retornos de carro, $0d, $0d, $0d. Seguimos definiendo el resto de líneas hasta que acabamos con el delimitador de cadena, $FF, que es valor que espera la rutina PrintString para saber hasta donde tiene que pintar.
Abrimos ahora el archivo Print.asm y al final del mismo vamos a implementar la rutina que pinta la pantalla de inicio y que, más adelante, guardará la elección de controles que hayamos hecho.
PrintFirstScreen:
call CLS
ld hl, title
call PrintString
ld hl, firstScreen
call PrintString
Limpiamos la pantalla, CALL CLS, cargamos en HL la dirección de memoria en la que empieza la definición del título, LD HL, title, lo pintamos, CALL PrintString, cargamos en HL la dirección de memoria en la que empieza la definición de la pantalla, LD HL, firtScreen, y llamamos a la rutina que pinta las cadenas, CALL PrintString.
Dado que más adelante vamos a permitir elegir entre cuatro tipo de controles, lo vamos a ir preparando.
printFirstScreen_op:
ld a, $f7
in a, ($fe)
bit $00, a
jr nz, printFirstScreen_op
call FadeScreen
ret
Cargamos en A la semifila 1-5, LD A, $F7, leemos el teclado, IN A, ($FE), comprobamos si se ha pulsado el uno (Teclado), BIT $00, A, y de no ser así sigue en bucle hasta que se pulse el uno, JR NZ, printFirstScreen_op. Realizamos el efecto de fundido de la pantalla, CALL FadeScreen, y salimos, RET.
El aspecto final de la rutina es el siguiente:
; -----------------------------------------------------------------------------
; Pantalla de presentación y selección de controles.
;
; Altera el valor de los registros AF y HL.
; -----------------------------------------------------------------------------
PrintFirstScreen:
call CLS ; Limpia la pantalla
ld hl, title ; Carga en HL la definición del título
call PrintString ; Pinta el título
ld hl, firstScreen ; Carga en HL la definición de la pantalla
call PrintString ; Pinta la pantalla
printFirstScreen_op:
ld a, $f7 ; Carga en A la semifila 1-5
in a, ($fe) ; Lee el teclado
bit $00, a ; Comprueba si se ha pulsado el 1
jr nz, printFirstScreen_op ; Si no se ha pulsado, sigue hasta que se pulse
call FadeScreen ; Fundido de pantalla
ret
Es hora de comprobar si lo que acabamos de implementar funciona. Abrimos el archivo Main.asm, localizamos la etiqueta Main y dentro de la misma la llamada a pintar el marco, CALL PrintFirstScreen. Justo por encima de esta llamada vamos a incluir la llamada a la rutina que pinta la pantalla de inicio y que más adelante servirá para seleccionar el tipo de controles.
call PrintFirstScreen
Compilamos, cargamos en el emulador y vemos los resultados.
Como podemos comprobar, ahora sale la pantalla de inicio, y no salimos de ella hasta que pulsamos el uno. Todavía quedan cosas por hacer, pero de momento lo dejamos así y seguimos con el fin de partida.
Fin de la partida
El fin de partida se puede dar de dos modos distintos: se nos acaban las cinco vidas de las que vamos a disponer y perdemos, o superamos el nivel treinta y ganamos.
En base a lo expuesto en el párrafo anterior, vamos a definir dos pantallas de fin distintas. Volvemos al archivo Var.asm y tras la definición de firstScreen, añadimos las definición de las dos pantallas de fin.
gameOverScreen:
db $10, $06, "Has perdido todas tus naves, no", $0d
db "has podido salvar la Tierra.", $0d, $0d
db "El planeta ha sido invadido por", $0d
db "los aliengenas.", $0d, $0d
db "Puedes volver a intentarlo, de", $0d
db "ti depende salvar la Tierra.", $ff
winScreen:
db $10, $06, "Enhorabuena, has destruido a los"
db "alienigenas, salvaste la Tierra.", $0d, $0d
db "Los habitantes del planeta te", $0d
db "estaran eternamente agradecidos.", $ff
pressEnter:
db $10, $04, $16, $10, $03, "Pulsa Enter para continuar", $ff
Igual que hicimos con la pantalla de inicio, vamos a implementar las rutinas que impriman las pantallas de fin, y que esperen la pulsación de la tecla Enter para continuar. Vamos al archivo Print.asm, y nos situamos al final del mismo.
PrintEndScreen:
push af
call FadeScreen
ld hl, title
call PrintString
pop af
or a
jr nz, printEndScreen_Win
Preservamos el valor de A, PUSH AF, hacemos el fundido de pantalla, CALL FadeScreen, apuntamos HL al inicio de la cadena del título, LD HL, title, y la pintamos, CALL PrintString. Recuperamos el valor de AF, POP AF, evaluamos si A vale cero, OR A, y saltamos si no es así, JR NZ, printEndScreen_Win.
printEndScreen_GameOver:
ld hl, gameOverScreen
call PrintString
jr printEndScreen_WaitKey
Si el valor de A es cero, apuntamos HL al inicio de la definición de la pantalla de fin de partida si hemos perdido, LD HL, gameOverScreen, la pintamos, CALL PrintString, y saltamos para esperar la pulsación de la tecla Enter, JR printEndScreen_WaitKey.
printEndScreen_Win:
ld hl, winScreen
call PrintString
Si el valor de A es distinto de cero, apuntamos HL al inicio de la definición de la pantalla de fin de partida si hemos ganado, LD HL, winScreen, y la pintamos, CALL PrintString.
Preparamos el resto para esperar a que el jugador presione la tecla Enter.
printEndScreen_WaitKey:
ld hl, pressEnter
call PrintString
call PrintInfoGame
call PrintInfoValue
Apuntamos HL al inicio de la cadena que pide que se pulse la tecla Enter, LD HL, pressEnter, la pintamos, CALL PrintString, pintamos los títulos de la información de la partida, CALL PrintInfoGame, y pintamos la información de la partida para mostrar al jugador el nivel al que ha llegado y los puntos que ha obtenido, CALL PrintInfoValue.
printEndScreen_WaitKeyLoop:
ld a, $bf
in a, ($fe)
rra
jr c, printEndScreen_WaitKeyLoop
call FadeScreen
ret
Cargamos en A la semifila Enter-H, LD A, $BF, leemos el teclado, IN A, ($FE), rotamos el registro A hacia la derecha, RRA, y seguimos en bucle hasta que el flag de acarreo no esté activo, JR C, printEndScreen_WaitKeyLoop. Una vez que el Enter se ha pulsado, hacemos el fundido de pantalla, CALL FadeScreen, y salimos, RET.
La forma en la que evaluamos si se ha pulsado el uno es la siguiente: cuando leemos del teclado la semifila Enter-H, el bit cero indica si el Enter se ha pulsado o no, a valor uno si no se ha pulsado y a cero si sí se ha pulsado. Al rotar el registro A hacia la derecha, el valor del bit cero se pone en el acarreo, de tal forma que si se activa, es que no se ha pulsado el Enter y si se desactiva, sí se ha pulsado.
El aspecto final de la rutina es el siguiente:
; -----------------------------------------------------------------------------
; Pantalla de fin de partida.
;
; Entrada: A -> Tipo de fin, 0 = Game Over, !0 = Win.
;
; Altera el valor de los registros AF y HL.
; -----------------------------------------------------------------------------
PrintEndScreen:
push af ; Preserva el valor de AF
call FadeScreen ; Fundido de pantalla
ld hl, title ; Apunta HL al título
call PrintString ; Pinta el título
pop af ; Recupera el valor de AF
or a ; Evalúa si A vale 0
jr nz, printEndScreen_Win ; Si no vale 0, salta
printEndScreen_GameOver:
ld hl, gameOverScreen ; Apunta HL a la pantalla de Game Over
call PrintString ; La pinta
jr printEndScreen_WaitKey ; Salta a esperar pulsación de Enter
printEndScreen_Win:
ld hl, winScreen ; Apunta HL a la pantalla de Win
call PrintString ; La pinta
printEndScreen_WaitKey:
ld hl, pressEnter ; Apunta HL a la cadena 'Pulse Enter'
call PrintString ; La pinta
call PrintInfoGame ; Pinta los títulos de información de la partida
call PrintInfoValue ; Pinta los datos de la partida
printEndScreen_WaitKeyLoop:
ld a, $bf ; Carga a semifila Enter-H en A
in a, ($fe) ; Lee el teclado
rra ; Rota A a la derecha para ver estado del Enter
jr c, printEndScreen_WaitKeyLoop ; Si hay acarreo no se ha pulsado, bucle
call FadeScreen ; Fundido de pantalla
ret
Ahora tenemos que probar si nuestras pantallas de fin de partida se muestran bien, vamos al archivo Main.asm, localizamos la línea CALL PrintFirstScreen que hemos añadido antes, y justo por encima de ella vamos a añadir las siguientes líneas:
xor a
call PrintEndScreen
ld a, $01
call PrintEndScreen
Ponemos A a cero, XOR A, pintamos la pantalla de fin de partida, CALL PrintEndScreen, ponemos A a uno, LD A, $01, y pintamos la pantalla de fin de partida.
Compilamos, cargamos en el emulador y vemos el resultado.
La primera llamada que hacemos a la rutina que pinta la pantalla de fin, la hacemos con A valiendo cero, de ahí que pinte la pantalla correspondiente a cuando hemos perdido todas nuestras vidas.
Si presionamos la tecla Enter, ponemos A a uno y volvemos a llamar a la rutina, esta vez se pinta la pantalla correspondiente a cuando hemos superado los treinta niveles.
Ahora si pulsamos Enter deberíamos ver la pantalla de inicio.
Ahora tenemos que encajar todo esto para que cada cosa esté en su lugar. Lo primero que vamos ha hacer es eliminar las últimas cuatro líneas que hemos utilizado para probar la rutina PrintEndScreen y las vamos a sustituir por las siguientes para inicializar los datos de la partida:
Main_start:
xor a
ld hl, enemiesCounter
ld (hl), $20
inc hl
ld (hl), a ; $1d
inc hl
ld (hl), $01 ; $29
inc hl
ld (hl), $05
inc hl
ld (hl), a
inc hl
ld (hl), a
Ponemos A a cero, XOR A. Apuntamos HL al contador de enemigos, LD HL, enemiesCounter, y lo ponemos a veinte en BCD, LD (HL), $20. Apuntamos HL al contador de niveles, INC HL, y lo ponemos a cero, LD (HL), A.
Apuntamos HL al contador de niveles BCD, INC HL, y lo ponemos a cero, LD (HL), A. Apuntamos HL al contador de vidas, INC HL, y lo ponemos a cinco, LD (HL), $05. Apuntamos HL al primer byte del marcador de puntos en BCD, INC HL, y lo ponemos a cero, LD (HL), A.
Apuntamos HL al segundo byte, INC HL, y lo ponemos a cero, LD (HL), A. Por último, llamamos al cambio de nivel para que reinicie los enemigos y cargue el nivel uno.
En las líneas que cargamos el nivel hemos comentado los valores $1D y $29. Más adelante pondremos estos valores para probar el fin del juego superando tan solo el último nivel.
Buscamos las líneas en las que cargamos el vector de interrupciones, desde DI hasta EI, las cortamos y las pegamos justo encima de la etiqueta MainStart.
Localizamos la etiqueta Main_loop y justo al final, encima de JR Main_loop añadimos la comprobación de si seguimos teniendo vidas.
ld a, (livesCounter)
or a
jr z, GameOver
Cargamos en A en número de vidas, LD A, (livesCounter), comprobamos si son cero, OR A, y saltamos si es así, JR Z, GameOver.
Localizamos la etiqueta Main_restart, y justo debajo de ella añadimos la comprobación de si hemos superado el último nivel.
ld a, (levelCounter)
cp $1e
jr z, Win
Cargamos en A el contador de nivel, LD A, (levelCounter), evaluamos si estamos en el último, CP $1E, y saltamos si es así, JR Z, Win.
Localizamos la línea CALL ChangeLevel, que está casi al final de Main_restart, la cortamos y la pegamos debajo de CALL FadeScreen, siendo de esta manera la quinta línea de Main_restart.
Vamos al final de Main_restart, y justo debajo vamos a implementar el fin de partida.
GameOver:
xor a
call PrintEndScreen
jp Main_start
Ponemos A a cero, XOR A, pintamos la pantalla de fin, CALL PrintEndScreen, y volvemos al inicio, JP MainStart.
Win:
ld a, $01
call PrintEndScreen
jp Main_start
Ponemos A a uno, LD A, $01, pintamos la pantalla de fin, CALL PrintEndScreen, y volvemos al inicio, JP MainStart.
Como podemos ver, en esta ocasión hemos usado JP en lugar de JR, debido a que si en Win ponemos JR Main_start nos da un error de salto fuera de rango.
Vamos a realizar un nuevo cambio, en esta ocasión vamos a hacer que al cambiar de nivel la nave se pinte en la posición inicial. Vamos al archivo Game.asm, localizamos la etiqueta changeLevel_end y antes del RET añadimos lo siguiente:
ld hl, shipPos ; Apunta HL a la posición de la nave
ld (hl), SHIP_INI ; Carga la posición inicial
Apuntamos HL a la posición de la nave, LD HL, shipPos, y cargamos la posición inicial, LD (HL), SHIP_INI. Dado que el Z80 es Little Endian, HL apunta a la coordenada X de la nave y al cargar SHIP_INI en (HL), carga el segundo byte definido en SHIP_INI en la coordenada X de la nave, LD (HL), $11.
Y llegamos a la hora de la verdad, compilamos cargamos en el emulador y si toda va bien, al perder las cinco vidas acaba la partida, Game Over.
Volvemos a Main.asm y las líneas:
ld (hl), a ; $1d
inc hl
ld (hl), a ; $29
Las dejamos como:
ld (hl), $1d
inc hl
ld (hl), $29
Compilamos, cargamos y al iniciar la partida lo hacemos en el nivel treinta. Lo superamos y fin de partida, Win.
Dado que hemos modificado varias cosas en Main.asm, el aspecto que debe tener ahora es el siguiente:
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 $c0
or $05
ld (BORDCR), a
di
ld a, $28
ld i, a
im 2
ei
Main_start:
xor a
ld hl, enemiesCounter
ld (hl), $20
inc hl
ld (hl), a ; $1d
inc hl
ld (hl), a ; $29
inc hl
ld (hl), $05
inc hl
ld (hl), a
inc hl
ld (hl), a
call ChangeLevel
call PrintFirstScreen
call PrintFrame
call PrintInfoGame
call PrintShip
call PrintInfoValue
call LoadUdgsEnemies
call PrintEnemies
Main_loop:
call CheckCtrl
call MoveFire
push de
call CheckCrashFire
pop de
ld a, (enemiesCounter)
cp $00
jr z, Main_restart
call MoveShip
call MoveEnemies
call CheckCrashShip
ld a, (livesCounter)
or a
jr z, GameOver
jr Main_loop
Main_restart:
ld a, (levelCounter)
cp $1e
jr z, Win
call FadeScreen
call ChangeLevel
call PrintFrame
call PrintInfoGame
call PrintShip
call PrintInfoValue
jr Main_loop
GameOver:
xor a
call PrintEndScreen
jp Main_start
Win:
ld a, $01
call PrintEndScreen
jp Main_start
include "Const.asm"
include "Var.asm"
include "Graph.asm"
include "Print.asm"
include "Ctrl.asm"
include "Game.asm"
end Main
Ensamblador ZX Spectrum, conclusión
Llegados a este punto, ya podemos echar nuestras primeras partidas, aunque todavía nos quedan cosas por hacer.
En el próximo capítulo implementaremos el control con joystick y daremos las posibilidad de obtener vidas extras.
Puedes 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.