0x03 Ensamblador ZX Spectrum Marciano – Área de juego
En este capítulo de Ensamblador ZX Spectrum Marciano, vamos a pintar el área de juego.
Creamos la carpeta Paso03 y copiamos los archivos Const.asm, Graph.asm, Main.asm y Var.asm desde la carpeta Paso02.
Antes de comenzar a pintar el área de juego es necesario saber que la pantalla del ZX Spectrum se divide en dos zonas, la parte superior con veintidós líneas (de la cero a la veintiuna), y la parte inferior (la línea de comandos).
Si cargáis el programa resultante del capítulo anterior, una vez que se ejecuta, si pulsamos la tecla ENTER se muestra el listado del cargador. Si ejecutáis la línea 40 (RUN 40), se deberían volver a pintar nuestros gráficos, pero no es así. En realidad si se pintan, pero se pintan en la línea de comandos; si estáis atentos veréis como se pintan y luego desaparecen.
Tras ejecutar nuestro programa, la parte de la pantalla activa es la línea de comandos, así que necesitamos un mecanismo para activar la parte de la pantalla en dónde queramos pintar.
Tabla de contenidos
- Cambiando la pantalla activa
- Pintamos cadenas de texto
- Pintamos la pantalla de juego
- Limpiamos y coloreamos la pantalla
- Pintamos la información de la partida
- Ensamblador ZX Spectrum, conclusión
- Enlaces de interés
Cambiando la pantalla activa
La parte superior de la pantalla es la dos, y la inferior es la uno. En la ROM hay una rutina que activa uno u otro canal, dependiendo del valor que haya en el registro A.
Abrimos el archivo Const.asm y añadimos la líneas siguientes:
; ----------------------------------------------------------------------------
; Rutina de la ROM que abre el canal de la pantalla.
;
; Entrada: A -> Canal 1 = línea de comandos
; 2 = pantalla superior
; ----------------------------------------------------------------------------
OPENCHAN: EQU $1601
A continuación, abrimos el archivo Main.asm y justo debajo de la etiqueta Main añadimos las líneas siguientes:
ld a, $02
call OPENCHAN
Cargamos en A el canal que queremos activar, LD A, $02, y luego llamamos a la rutina de la ROM para activarlo, CALL OPENCHAN.
Compilamos, cargamos en el emulador, pulsamos cualquier tecla, ejecutamos la línea 40 (RUN 40) y ahora sí se vuelven a pintar nuestros gráficos en el lugar correcto.
Pintamos cadenas de texto
Al trabajar con UDG, podríamos decir que pintamos caracteres, y como tal vamos a pintar la pantalla de juego.
Lo primero que vamos a implementar es una rutina que pinta cadenas de caracteres, indicando la dirección de memoria de la cadena y la longitud de la misma.
Creamos un nuevo archivo, Print.asm, e implementamos la rutina PrintString, que recibe en HL la dirección de la cadena y en B la longitud de la misma. Esta rutina altera el valor de los registros AF, B y HL.
PrintString:
ld a, (hl)
rst $10
inc hl
djnz PrintString
ret
Carga en A el carácter a pintar, LD A, (HL), pinta el carácter, RST $10, apunta HL al siguiente carácter, INC HL, y repite la operación hasta que B valga 0, DJNZ PrintString. Finalmente, sale, RET.
El aspecto final de la rutina, una vez comentada, es el siguiente:
; -----------------------------------------------------------------------------
; Pinta cadenas.
;
; Entrada: HL = primera posición de memoria de la cadena
; B = longitud de la cadena.
; Altera el valor de los registros AF, B 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
El siguiente paso es probar, así que vamos a editar el archivo Main.asm. Lo primero, para que no se nos olvide, es incluir el archivo Print.asm antes END Main:
include "Print.asm"
Justo antes de los includes, vamos a definir una cadena con dos etiquetas, la de la propia cadena y una segunda etiqueta para marcar el final de la misma.
Cadena:
db 'Hola Mundo'
Cadena_Fin:
db $
Justo antes del RET que nos saca al Basic, vamos a añadir la llamada a la nueva rutina.
ld hl, Cadena
ld b, Cadena_Fin - Cadena
call PrintString
Compilamos, cargamos en el emulador y vemos los resultados.
Como se puede apreciar, se ha pintado la cadena Hola Mundo a continuación de nuestros gráficos.
Vamos a añadir unos caracteres antes de la cadena, y la vamos a dejar como sigue:
db $16, $0a, $0a, 'Hola Mundo'
Volvemos compilar, cargamos en el emulador y vemos el resultado.
Como se puede observar, la cadena Hola Mundo aparece más centrada, lo que hemos conseguido con los caracteres que hemos añadido por delante de la cadena, en concreto $16 que es el carácter de control de AT (instrucción Basic para posicionar el cursor), y las coordenadas Y y X.
A continuación se muestra una lista de los caracteres de control que podemos usar al pintar las cadenas de esta manera, y los parámetros que hay que enviar.
Carácter | Código | Parámetros | Valores |
DELETE | $0c | ||
ENTER | $0d | ||
INK | $10 | Color | De $00 a $07 |
PAPER | $11 | Color | De $00 a $07 |
FLASH | $12 | No/Sí | De $00 a $01 |
BRIGHT | $13 | No/Sí | De $00 a $01 |
INVERSE | $14 | No/Sí | De $00 a $01 |
OVER | $15 | No/Sí | De $00 a $01 |
AT | $16 | Coordenadas Y y X | Y = de $00 a $15 X = de $00 a $1f |
TAB | $17 | Número de tabulaciones |
Es muy importante que todos los códigos de control vayan seguidos de sus parámetros, para evitar resultados no deseados. Así mismo, si se imprime una cadena después de TAB, hay que añadir un espacio en blanco como primer carácter de la cadena.
Como ejercicio, probad distintas combinaciones con los caracteres de control, probad a darle color, parpadeo, etc.
Pintamos la pantalla de juego
La pantalla de juego esta bordeada por un marco, pero antes de pintar nada vamos a limpiar el archivo Main.asm, quitando todo lo que nos sobra; borramos desde las dos líneas anteriores a la etiqueta Loop, hasta la línea anterior de la instrucción RET, también borramos la definición de Cadena y Cadena_Fin.
El aspecto de Main.asm debe ser el siguiente:
org $5dad
Main:
ld a, $02
call OPENCHAN
ld hl, udgsCommon
ld (UDG), hl
ret
include "Const.asm"
include "Var.asm"
include "Graph.asm"
include "Print.asm"
end Main
Ahora vamos a definir en Var.asm las cadenas necesarias para pintar el marco. La vamos a incluir antes de udgsCommon.
; ----------------------------------------------------------------------------
; Marco de la pantalla
; ----------------------------------------------------------------------------
frameTopGraph:
db $16, $00, $00, $10, $01
db $96, $97, $97, $97, $97, $97, $97, $97, $97, $97, $97, $97, $97, $97, $97
db $97, $97, $97, $97, $97, $97, $97, $97, $97, $97, $97, $97, $97, $97, $97
db $97, $98
frameBottomGraph:
db $16, $15, $00
db $9b, $9c, $9c, $9c, $9c, $9c, $9c, $9c, $9c, $9c, $9c, $9c, $9c, $9c, $9c
db $9c, $9c, $9c, $9c, $9c, $9c, $9c, $9c, $9c, $9c, $9c, $9c, $9c, $9c, $9c
db $9c, $9d
frameEnd:
En la primera línea DB definimos la posición de la parte de arriba, $16, $00, $00 y la tinta, $10, $01.
En la siguiente línea definimos la parte superior del marco, primero la esquina superior izquierda, $96, luego treinta posiciones de horizontal superior, $97, y por último la esquina superior derecha, $98; todos estos números los pusimos en los comentarios de la definición de los gráficos.
En la siguiente línea definimos la posición de la parte de abajo, $16, $15, $00.
En la siguiente línea definimos la parte inferior del marco, primero la esquina inferior izquierda, $9b, luego treinta posiciones de la horizontal inferior, $9c, y por último la esquina inferior derecha, $9d.
Vamos a ver como se pinta todo esto. Volvemos a Main.asm y justo encima del RET, incluimos las líneas siguientes:
ld hl, frameTopGraph
ld b, frameEnd - frameTopGraph
call PrintString
Compilamos, cargamos en el emulador y vemos los resultados.
Hemos pintado la parte superior del marco y la inferior, aunque el marco no está completo ya que faltan los laterales.
Vamos a implementar una rutina que imprima el marco, y lo vamos a hacer en Print.asm.
PrintFrame:
ld hl, frameTopGraph
ld b, frameBottomGraph - frameTopGraph
call PrintString
Cargamos en HL la dirección de memoria de la parte superior del marco, LD HL, frameTopGraph, cargamos en B la longitud, restando a la dirección de inicio de la parte inferior la dirección de inicio de la parte superior, LD B, frameBottomGraph – frameTopGraph, y llamamos a la rutina que pinta las cadenas, CALL PrintString.
ld hl, frameBottomGraph
ld b, frameEnd - frameBottomGraph
call PrintString
Cargamos en HL la dirección de memoria de la parte inferior del marco, LD HL, frameBottomGraph, cargamos en B la longitud, restando a la dirección de memoria donde acaba la cadena la dirección de inicio de la parte inferior, LD B, frameEnd – frameBottomGraph, y llamamos a la rutina que pinta las cadenas, CALL PrintString.
Ya solo queda implementar un bucle para pintar los laterales.
ld b, $01
printFrame_loop:
ld a, $16
rst $10
ld a, b
rst $10
ld a, $00
rst $10
ld a, $99
rst $10
Cargamos en B la línea en la que empezamos a pintar los laterales, LD B, $01, cargamos en A el carácter de control de AT, LD A, $16, y lo “pintamos”, RST $10, cargamos en A la coordenada Y, LD A, B, y la “pintamos”, RST $10, cargamos en A la columna cero, LD A, $00, y la “pintamos”, RST $10, y por último, cargamos en A el carácter del lateral izquierdo, LD A, $99, y lo pintamos, RST $10.
Hacemos lo mismo con el lateral derecho, debido a que el código es prácticamente el mismo, solo marcamos las dos líneas que cambian.
ld a, $16
rst $10
ld a, b
rst $10
ld a, $1f ; ¡CAMBIO!
rst $10
ld a, $9a ; ¡CAMBIO!
rst $10
Y llegamos a la parte final de la rutina.
inc b
ld a, b
cp $15
jr nz, printFrame_loop
ret
Apuntamos B a la siguiente línea, INC B, cargamos el valor en A, LD A, B, y comprobamos si B apunta a la línea veintiuno (línea donde se encuentra la parte inferior del marco), CP $15, y de no ser así repite el bucle hasta que llegue a la línea veintiuno, JR NZ, printFrame_loop. Una vez que B apunta a la línea veintiuno, salimos, RET.
El aspecto final de la rutina es el siguiente:
; ----------------------------------------------------------------------------
; Pinta el marco de la pantalla.
;
; Altera el valor de los registros HL, B y AF.
; ----------------------------------------------------------------------------
PrintFrame:
ld hl, frameTopGraph ; Carga en HL la dirección de la parte superior
ld b, frameBottomGraph - frameTopGraph ; Carga en B la longutid
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 longitudd
call PrintString ; Pinta la cadena
ld b, $01 ; Apunta B a la línea 1
printFrame_loop:
ld a, $16 ; Carga en A el carácter de control de AT
rst $10 ; Lo "pinta"
ld a, b ; Carga en A la línea
rst $10 ; La "pinta"
ld a, $00 ; Carga en A la columna
rst $10 ; La "pinta"
ld a, $99 ; Carga en A el carácter lateral izquierdo
rst $10 ; Lo pinta
ld a, $16 ; Carga en A el carácter de control de AT
rst $10 ; Lo "pinta"
ld a, b ; Carga en A la línea
rst $10 ; La "pinta"
ld a, $1f ; Carga en A la columna
rst $10 ; La "pinta"
ld a, $9a ; Carga en A el carácter lateral derecho
rst $10 ; Lo pinta
inc b ; Apunta B a la línea siguiente
ld a, b ; Carga el valor de B en A
cp $15 ; Comprueba si está en la línea veintiuno
jr nz, printFrame_loop ; Si no es así, sigue con el bucle
ret
Ha llegado el momento de probar si se pinta todo el marco. Volvemos al archivo Main.asm y sustituimos estas lineas:
ld hl, frameTopGraph
ld b, frameEnd - frameTopGraph
call PrintString
Por esta otra:
call PrintFrame
Compilamos, cargamos en el emulador y vemos el resultado.
Como vemos, ya hemos pintado el marco de la pantalla, pero quedan cosas por hacer; no hemos borrado la pantalla y se ven cosas que no deberían estar.
Limpiamos y coloreamos la pantalla
Muchos de los listados Basic que se pueden ver, en algún momento tienen una línea parecida a esta:
BORDER 0: INK 7: PAPER 0: CLS
Con esta línea se pone el borde de la pantalla en negro, la tinta en blanco y el fondo negro, y por último se limpia la pantalla aplicando los atributos de tinta y fondo.
Vamos a usar la ROM del ZX Spectrum para asignar tinta, fondo, limpiar la pantalla y vamos a implementar el cambio de color del borde.
Empezamos con la parte en la que limpiamos la pantalla, para lo cual abrimos el archivo Const.asm y añadimos las líneas siguientes:
; Variable de sistema donde están los atributos de color permanentes
ATTR_P: EQU $5c8d
; ----------------------------------------------------------------------------
; Rutina de la ROM que limpia la pantalla usando el valor que hay en ATTR_P.
; ----------------------------------------------------------------------------
CLS: EQU $0daf
En ATTR_P se guardan los atributos permanentes de color en formato FBPPPIII, dónde F = FLASH (0/1), B = BRIGHT (0/1), PPP = PAPER (de 0 a 7) e III = INK (de 0 a 7). Por otro lado, CLS limpia la pantalla aplicando los atributos que hay en ATTR_P.
Volvemos a Main.asm y justo antes de la llamada a PrintFrame, añadimos las líneas siguientes:
ld hl, ATTR_P
ld (hl), $07
call CLS
Cargamos la dirección de memoria de los atributos permanentes en HL, LD HL, ATTR_P, y ponemos FLASH a 0, BRIGHT a 0, PAPER a 0 e INK a 7, LD (HL), $07. Por último, llamamos a la rutina que limpia la pantalla, CALL CLS.
Compilamos, cargamos en el emulador y vemos los resultados.
Ahora vamos a cambiar el color al borde. Ya vimos en PorompomPong que la rutina BEEPER de la ROM cambia el color del borde, por lo que es necesario guardar los atributos en una variable de sistema; los atributos tienen el mismo formato que el visto para ATTR_P.
Volvemos al archivo Const.asm y añadimos la constante para la variable de sistema donde se guardan los atributos del borde.
; Variable de sistema donde se guarda el borde. También usada por BEEPER.
; También se guardan aquí los atributos de la línea de comandos.
BORDCR: EQU $5c48
Y ahora volvemos a Main.asm y ponemos el borde en negro, justo debajo de CALL CLS.
xor a
out ($fe), a
ld a, (BORDCR)
and $c7
or $07
ld (BORDCR), a
Ponemos A a cero, XOR A, y el borde en negro, OUT ($FE), A. A continuación, cargamos el valor de BORDCR en A, LD A, (BORDCR), desechamos el color del borde y así lo ponemos en negro = 0, AND $C7, ponemos la tinta en blanco, OR $07, y cargamos el valor en BORDCR, LD (BORDCR), A.
La instrucción OR $07 solo es necesaria mientras volvamos al Basic, recordemos que en BORDCR están los atributos de color de la línea de comandos y si no cambiamos la tinta a blanca, se queda como originalmente está, en negro, igual que el color que le hemos dado al fondo.
Compilamos, cargamos en el emulador y vemos los resultados.
Ya solo nos queda pintar el área de información de la partida, cosa que haremos en la línea de comandos.
Pintamos la información de la partida
Lo primero es definir la línea de título del área de información de la partida, lo que vamos a hacer al inicio de Var.asm.
; ----------------------------------------------------------------------------
; Título de información de la partida
; ----------------------------------------------------------------------------
infoGame:
db $10, $03, $16, $00, $00
db 'Vidas Puntos Nivel Enemigos'
infoGame_end:
En la primera línea ponemos la tinta en magenta y posicionamos el cursor en las coordenadas 0,0. En la línea siguiente definimos los títulos de la información.
Vamos implementar en Print.asm la rutina que pinta los títulos de la información.
PrintInfoGame:
ld a, $01
call OPENCHAN
Como los títulos de la información se pintan en la línea de comandos, lo primero que hay que hacer es activar el canal uno. Cargamos uno en A, LD A, $01, y llamamos al cambio de canal, CALL OPENCHAN.
ld hl, infoGame
ld b, infoGame_end - infoGame
call PrintString
Cargamos en HL la dirección del mensaje de información, LD HL, infoGame, cargamos en B la longitud del mensaje, LD B, infoGame_end – infoGame, y llamamos a pintar la cadena, CALL PrintString.
ld a, $02
call OPENCHAN
ret
Por último, volvemos a activar el canal dos (la pantalla superior), y salimos.
El aspecto final de la rutina es el siguiente:
; ----------------------------------------------------------------------------
; Pinta los títulos de información de la partida.
; Altera el valor de los registros A, C y HL.
; ----------------------------------------------------------------------------
PrintInfoGame:
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
call PrintString ; Pinta la cadena de títulos
ld a, $02 ; Carga 2 en A
call OPENCHAN ; Activa el canal 2, pantalla superior
ret
Ahora volvemos a Main.asm y justo después de CALL PrintFrame, añadimos la llamada para pintar los títulos de información de partida.
call PrintInfoGame
Si ahora mismo compiláramos, probad si queréis, da la sensación de que lo último que hemos implementado no funciona, no pinta los títulos de información. En realidad si lo hace, pero al volver al Basic, el mensaje 0 OK, 40:1 lo borra.
Para evitar esto, vamos a quedarnos en un bucle infinito; localizamos la instrucción RET que nos devuelve a Basic y la sustituimos por las líneas siguientes:
Main_loop:
jr Main_loop
El código final de Main.asm es el siguiente:
org $5dad
Main:
ld a, $02
call OPENCHAN
ld hl, udgsCommon
ld (UDG), hl
ld hl, ATTR_P
ld (hl), $07
call CLS
xor a
out ($fe), a
ld a, (BORDCR)
and $c7
or $07
ld (BORDCR), a
call PrintFrame
call PrintInfoGame
Main_loop:
jr Main_loop
include "Const.asm"
include "Var.asm"
include "Graph.asm"
include "Print.asm"
end Main
Ahora sí, compilamos, cargamos en el emulador y vemos el resultado.
No sé que os parece a vosotros, pero a mí, que los títulos estén tan pegados al marco, no me gusta. Dado que en la línea de comandos solo disponemos de dos líneas sin que haga scroll, y debajo de la línea de títulos hay que pintar los datos, solo nos queda una opción, quitarle una línea al área de juego.
Vamos al archivo Var.asm, localizamos la etiqueta frameBottomGraph, y justo en la línea de abajo vemos el DB que posiciona el cursor; vamos a modificar esta línea para que la coordenada Y sea veinte en lugar de veintiuno.
db $16, $14, $00
Ahora tenemos que ir a Print.asm y localizar la etiqueta printFrame_loop. Este bucle se ejecuta hasta que B vale veintiuno; tenemos que modificar esta condición para que se ejecute hasta que valga veinte. Dos líneas por encima del RET de este método tenemos CP $15, esta es la línea que tenemos que modificar dejándola de la siguiente manera:
Volvemos a compilar, cargamos en el emulador y vemos los resultados.
Ensamblador ZX Spectrum, conclusión
Llegados a este punto, ya tenemos nuestro área de juego.
En el próximo capítulo incluiremos la nave y la moveremos.
Puedes descargar desde aquí todo el código que hemos generado.
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.