Tutorial de ZX Basic + Fourspriter #16: El axioma del movimiento carácter a carácter

Imprimid esto bien grande y enmarcarlo con un marco dorado de greca y perifollo, gordo y pesado. Luego colocadlo en el sitio más visible del salón de vuestro palacete de verano:

Esto parece una chorrada pero entender esto es MUY importante. Si queremos mover al muñeco a la derecha cuando el jugador pulse P, antes de hacerlo tendremos que asegurarnos que no nos topemos con nada. Y eso implica mirar que no haya nada en ninguna de las posiciones nuevas que ocuparíamos si hiciésemos el movimiento. Si cualquiera de esas posiciones contuviese un trozo de tile no traspasable, NO PODREMOS MOVERNOS. He usado tanta negrilla en este párrafo que al final lo que destaca es lo que no está en negrilla. Yuju.

Girad el dibujo 90, 180 y 270 grados y tendréis el concepto básico del movimiento con colisión con el escenario. En el próximo capítulo usaremos los conceptos que hemos aprendido para leer el teclado y el diagramilla de ahí arriba para programar nuestro primer motor de movimiento.

Hasta entonces, hay que EMPAPARSE ese concepto. 100% empapado, como un campo de papas. Por cierto, el diagrama ha sido elaborado por Cutre Designers LTD (c) 2012, y nos ha costado un riñón de Emanuel, el mono fiel.

Anuncios

Tutorial de ZX Basic + Fourspriter #15: Leyendo el teclado.

Hace algún tiempo (mucho), ya hablé de esto en este mismo blog, pero ahora la explicación va a ser mucho más mejor. BASIC nos ofrece una función para leer el teclado: Inkey. El problema es que esta función, que simplemente devuelve una cadena con el carácter correspondiente a la tecla que se esté pulsando en el momento de llamarla, o la cadena vacía si no se está pulsando nada, no nos sirve para detectar pulsaciones simultaneas y, por tanto, no viene bien para usarla en un juego. Esto no quiere decir que un servidor y muchos de los que me leéis no la hayamos usado mil veces… Pero no es lo más adecuado.

Para poder detectar pulsaciones simultaneas, hay que mirar directamente el hardware del Spectrum (¡hostia!). El teclado se lee mirando el valor que hay en el puerto $FE (254). En el valor obtenido, leeremos un 0 si una tecla está pulsada y un 1 si no. Pero el Spectrum tiene 40 teclas (el de gomas tiene 40 teclas físicas, los demás usan barateces para simular combinaciones de teclas en las teclas extra que traen), ¿de dónde sacamos 40 bits?

La solución fue dividir el teclado en 8 “semifilas” de 5 teclas. El teclado del Spectrum es este:

 1   2   3   4   5  |  6   7   8   9   0
 Q   W   E   R   T  |  Y   U   I   O   P
 A   S   D   F   G  |  H   J   K   L   EN
 CS  Z   X   C   V  |  B   N   M   SS  SP

El orden de las filas es un poco raro: empieza abajo a la izquierda, va subiendo, pasa a la derecha, y termina abajo a la derecha, esto es:

  1. Empezamos abajo a la izquierda: la primera fila es de Caps Shift a V.
  2. La segunda fila es de A a G.
  3. La tercera fila es de Q a T.
  4. La cuarta, de 1 a 5.
  5. Ahora nos pasamos a la derecha: la quinta es de 6 a 0.
  6. La sexta, de Y a P.
  7. La séptima, de H a Enter.
  8. Y la octava y última, de B a Space.

Para leer una fila u otra, lo que se hace es leer del puerto XXFEh, con XX un número especial para cada fila. Este número, en binario, son todos 1 menos la posición correspondiente a la fila que sea, donde hay un cero:

  1. Para la primera fila (CS-V) tendremos que leer el puerto 11111110b FEh, o sea, FEFEh, 65278.
  2. Para la segunda fila (A-G) tendremos que leer el puerto 11111101b FEh, o sea, FDFEh, 65022.
  3. Para la tercera fila (Q-T) tendremos que leer el puerto 11111011b FEh, o sea, FBFEh, 64510.
  4. Para la cuarta fila (1-5) tendremos que leer el puerto 1111011b FEh, o sea, F7FEh, 63486.
  5. Para la quinta fila (6-0) tendremos que leer el puerto 11101111b FEh, o sea, EFFEh, 61438.
  6. Para la sexta fila (Y-P) tendremos que leer el puerto 11011111b FEh, o sea, DFFEh, 57342.
  7. Para la séptima fila (H-EN) tendremos que leer el puerto 10111111b FEh, o sea, BFFEh, 49150.
  8. Para la octava fila (B-SP) tendremos que leer el puerto 01111111b FEh, o sea, 7FFEh, 32766.

Recapitulando, para leer bits asociados a teclas pulsadas o no pulsadas, habrá que leer del puerto correspondiente a su semifila. Durante muchos años no tenía ni puta idea de dónde venían los números, sólo los miraba en una lista que tenía. La tabla de arriba es simplemente para que sepáis de donde salen los números. Con apuntar la lista, a efectos prácticos, es suficiente. Pero para aquellos de vosotros que disfrutéis con el saber, pues ya sabéis.

Entonces, si hacemos un a = In (57342), por ejemplo, estaremos obteniendo un byte en el que se representa el estado de las teclas Y, U, I, O y P. ¿Cómo? Pues muy sencillo: los cinco primeros bits corresponden a las cinco teclas, siempre contando desde fuera del teclado hacia dentro:

Bits:  7 6 5 4 3 2 1 0
Tecla: - - - Y U I O P

De este modo, si el bit 0, por ejemplo, está a “0”, es que “P” está pulsada. Si está a “1”, es que “P” no está pulsada.

Si hacemos un a = In (64510), estaremos obteniendo un byte en el que se representa el estado de las teclas Q, W, E, R y T. De la misma forma, y siempre contando desde fuera del teclado hacia dentro:

Bits:  7 6 5 4 3 2 1 0
Tecla: - - - T R E W Q

O sea, si el bit 0 está a “0”, es que “Q” está pulsada. Si el bit 2 está a “0”, es que “E” está pulsada.

Y así con todas las filas.

¿Y cómo leemos un bit en concreto del valor que leemos del puerto que sea? Por suerte, ZX Basic incluye ya operaciones a nivel de bit. En concreto, la que usaremos sea bAnd. (a bAnd b) hace un and de cada bit de A con cada bit de B y devuelve el resultado. Esto se puede usar con una potencia de 2 para ver si un bit en concreto está a 0:

If (a bAnd 2^X) = 0 Then...

Con “a” el valor leído con In del puerto correspondiente y X el número de bit que queremos mirar si está a 0 (y, por tanto, su tecla correspondiente está pulsada). Por tanto, si, por ejemplo, queremos ver si el usuario está pulsando “O”, tendríamos que hacer:

If (In (57342) bAnd 2) = 0 Then Print "O está pulsada"

Por partes: ¿en qué semifila esta la O? En la sexta. Por tanto, hay que leer el puerto 57342. ¿Qué bit ocupa la O dentro de la semifila? el 1. Calculamos 2^1 = 2.

Otro ejemplo. Queremos ver si el usuario está pulsando “4”. El tema sería así:

If (In (63486) bAnd 8) = 0 Then Print "4 está pulsada"

De nuevo, ¿en qué semifila está el 4? En la cuarta. Por tanto, hay que leer el puerto 63486. ¿Qué bit ocupa el 4 dentro de la semificla? El 3. Calculamos 2^3 = 8.

¿Un lío? Sí y no. Sí porque hay mucho jaleo de bits y de paranoias. No porque en realidad nuestro juego tendrá cuatro o cinco teclas y sólo tendremos que calcular los numeritos una vez.

Vamos a probar esto. En nuestro feoncio.bas, borramos la última prueba (la de pintar la colisión sobre el mapa), y escribimos este código en su lugar:

' Empezar

'' Esto es una prueba. Eliminar luego
Dim terminado as uByte
terminado = 0
While Not terminado
   '' Detectar "O": Sexta semifila, bit 1
   If (In (57342) bAnd 2) = 0 Then
      Print At 10, 0; "LEFT"
   Else
      Print At 10, 0; "     "
   End If
   '' Detectar "P": Sexta semifila, bit 0
   If (In (57342) bAnd 1) = 0 Then
      Print At 10, 8; "RIGHT"
   Else
      Print At 10, 8; "     "
   End If
   '' Detectar "Q": Tercera semifila, bit 0
   If (In (64510) bAnd 1) = 0 Then
      Print At 10, 16; "UP"
   Else
      Print At 10, 16; "  "
   End If
   '' Detectar "A": Segunda semifila, bit 0
   If (In (65022) bAnd 1) = 0 Then
      Print At 10, 24; "DOWN"
   Else
      Print At 10, 24; "    "
   End If
Wend
'' Fin de la prueba.

Compilad el proyecto y ejecutadlo en un emulador. Probad a pulsar las teclas O, P, Q y A y veréis como funciona. Disclaimer: veréis que no detecta algunas combinaciones. En especial, no detecta si pulsamos las cuatro teclas a la vez. Esto es un problema del hardware de nuestro PC, donde estamos ejecutando el emulador. En un Spectrum real, no existe este problema.

Soy consciente de que este capítulo tiene mucha chicha a bajo nivel y puede parecer complicado, pero en realidad es algo muy sencillo una vez que le pillas la lógica. Además, ese código de prueba nos servirá para empezar a programar nuestra rutina de movimiento. De todos modos, os pongo tarea: ¿Cómo sería el IF para detectar si estamos pulsando la tecla “M”? A ver si lo habéis pillao.

Como siempre, podéis descargaros el paquetico con todos los tiestos.

Tutorial de ZX Basic + Fourspriter #14: Colisiones con el escenario

Antes de seguir, hay que dejar muy claro el concepto de pantalla de juego y pantalla del ordenador. Toda la lógica del juego, todas las detecciones, el valor de las coordenadas, etcétera, se referirán siempre a la pantalla de juego. En nuestro ejemplo, esta pantalla de juego mide 12×12 tiles, o, lo que es lo mismo, 24×24 caracteres, cuyas coordenadas van de 0 a 23. La esquina superior izquierda de la pantalla de juego es 0,0.

Aparte tenemos la pantalla del ordenador, sobre la que irá impresa la pantalla de juego. La pantalla de juego se imprimirá en unas coordenadas (x, y) de la pantalla del ordenador. En nuestro ejemplo, la vamos a imprimir en (x, y) = (4, 0). El primer tile de la pantalla empezará a dibujarse en (4, 0), pero esta posición será (0,0) en la pantalla de juego.

¿Qué significa esto? Pues que nosotros nos olvidaremos de la pantalla del ordenador en todo momento excepto cuando tengamos que dibujar algo: por ejemplo, los sprites. Un sprite tendrá unas coordenadas (x, y) en la pantalla de juego, por ejemplo (x, y) = (10, 10). Sin embargo, en la pantalla del ordenador habrá que dibujarlo con respecto a la posición del area de juego: en nuestro ejemplo, el area de juego se imprime a partir de (4, 0), por lo que el sprite habrá que pintarlo en (10 + 4, 10 + 0) = (14, 10).

Por ello, lo primero que hay que hacer, para facilitarnos todo, es crear dos constantes en nuestro programa principal feoncio.bas:

'' constantes
Const MAPOFFSETX as uByte = 4
Const MAPOFFSETY as uByte = 0

Estas constantes contienen la posición de la pantalla de juego en la pantalla real. Las emplearemos como offsets siempre que queramos dibujar cualquier cosa. De este modo, para dibujar la pantalla actual sólo tendremos que llamar a pintaMapa (MAPOFFSETX, MAPOFFSETY, n), con n = número de la pantalla. Igualmente, cuando tengamos que mover un sprite, habrá que llamar a fsp21MoveSprite (n, MAPOFFSETX + x, MAPOFFSETY + y), donde n es el número del sprite, y (x, y) son sus coordenadas en la pantalla de juego.

Es muy importante tener muy claro este tema, y saber distinguir entre la pantalla de juego y la pantalla del ordenador.

Dicho esto, vamos a empezar a currarnos una serie de datos y unas rutinas que nos permitan averiguar si un caracter (x, y) de la pantalla de juego pertenece a un tile traspasable o a un tile obstáculo. Estas rutinas las utilizaremos continuamente antes de mover cada sprite para comprobar si no nos estamos topando con una parte del escenario por la que no podamos andar (una pared, vaya). Vamos abriendo engine.bas, pues todo esto pertenece al engine.

Lo primero que tenemos que hacer es definir el comportamiento de cada uno de los tiles de nuestro tileset. Para empezar, sólo definiremos dos comportamientos: TRASPASABLE y OBSTÁCULO. Les daremos los valores 0 y 4. ¿Por qué no 0 y 1? Pues porque así tenemos sitio para, en un futuro, hacer tiles traspasables pero que hagan otras cosas (como, por ejemplo, matarte), y todo lo que tenga un comportamiento < 4 será traspasable.

Definimos, pues, el array de comportamientos. Como tenemos 16 tiles, tendrá 16 elementos. Nos vamos al principio de engine.bas, justo debajo de las constantes, y creamos nuestro array fijándonos en el tileset.

' Comportamiento de los tiles
' 0 = traspasable, 4 = obstáculo
Dim comportamientoTiles (15) as uByte => {_
   0, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 0, 0, 4, 4, 4 _
}

Recuerda: 15 y no 16. BASIC. Bla, bla, bla. Y eso y tal.

Bueno. Ahora toca escribir código. Empecemos por una función que te diga qué número de tile hay en la posición (x, y) a nivel de tiles de una pantalla n del mapa:

' Devuelve el valor del tile en x, y de la pantalla n
' x, y = coordenadas de tile
Function getTileAt (x as uByte, y as uByte, n as uInteger) as uByte
   return mapa (n * MAPSCREENWIDTH * MAPSCREENHEIGHT + y * MAPSCREENWIDTH + x)
End Function

Esto es una función. Como una función devuelve un valor, habrá que definir su tipo (mira el as uByte al final de la primera linea). El valor devuelto es lo que va detrás del RETURN. En este caso, lo que hacemos es buscar el tile (x, y) de la pantalla n. Primero nos posicionamos al principio de la pantalla n (n * MAPSCREENWIDTH * MAPSCREENHEIGHT, como vimos en la rutina que pintaba una pantalla del mapa), luego descendemos y lineas (sumamos y * MAPSCREENWIDTH) y avanzamos x tiles (sumamos x). Devolvemos el valor que haya en esa posición del array mapa y listos.

Siguiente paso: una función que te diga cuál es el comportamiento del tile que está en la posición (x, y) a nivel de tiles de la pantalla n del mapa. Por supuesto, habrá que consultar el array de comportamientos que definimos antes. Y, por supuesto, usaremos la función getTileAt, que para eso la hemos hecho:

' Devuelve el comportamiento del tile en x, y de la pantalla n
' x, y = coordenadas de tile
Function getTileBehaviourAt (x as uByte, y as uByte, n as uInteger) as uByte
   return comportamientoTiles (getTileAt (x, y, n))
End Function

Básicamente, primero miramos qué tile hay en (x, y) a nivel de tiles de la pantalla n usando getTileAt, y luego, con ese número, consultamos nuestro array comportamientoTiles. Devolvemos ese valor, que será 0 o 4 dependiendo del tile que hubiera en (x, y).

Ya casi hemos terminado. Todas estas funciones trabajan a nivel de tiles, pero nosotros nos vamos a mover de caracter en caracter. Nuestro muñeco puede estar alineado con los tiles (cuando sus coordenadas sean números pares) o no, por lo que no podemos usar estas funciones directamente. Por lo tanto, nos inventeramos una función que nos diga qué comportamiento tiene el caracter que está en la posición que le pasemos, dentro de la pantalla n del mapa. Lo que hará esta función es mirar a qué tile pertenece ese carácter, y devolverá el comportamiento de dicho tile. Como los tiles son de 2×2 caracteres, para saber a qué tile pertence un carácter no habrá más que dividir entre dos. Veámoslo “gráficamente”:

XX XX ·· XX
XX XX ·· XX

XX ·· ·· ··
XX ·· ·· ··

Imaginemos que lo de arriba es un cacho de pantalla de 4×2 tiles de 2×2 caracteres cada uno. El caracter de arriba a la izquierda tiene posición (0, 0). Vemos que pertenece al tile de posición (0, 0), ya que 0/2 = 0. El siguiente caracter, el que tiene posición (1, 0), también pertenece al tile de posición (0, 0), ya que 1/2 = 0. El caracter en la posición (3, 1) (cuenta) pertenece al tile en la posición (1, 0), ya que 3/2 = 1 y 1/2 = 0. ¿queda claro?

Por tanto, solo tendremos que dividir entre 2 las coordenadas recibidas para pasar de coordenadas de caracter a coordenadas de tile, y seguidamente llamar a getTileBehaviourAt con el resultado.

' Devuelve el comportamiento del caracter en x, y de la pantalla n
' x, y = coordenadas de caracter
Function getCharBehaviourAt (x as uByte, y as uByte, n as uInteger) as uByte
   return getTileBehaviourAt (x >> 1, y >> 1, n)
End Function

x >> 1 equivale a x / 2, pero es mucho más rápido. Es un desplazamiento a la derecha. Cada desplazamiento a la derecha divide entre dos, y cada desplazamiento a la izquierda multiplica por 2.

Vamos a ilustrar esto para ver que todo funciona: vamos a imprimir una pantalla del mapa y luego imprimiremos, encima, el comportamiento de cada carácter del área de juego, para que veáis como coincide y cómo funcionan las funciones. Nos vamos de nuevo a feoncio.bas para escribir un poco de código de prueba que luego eliminaremos. Es solo para probar que getCharBehaviourAt funciona correctamente. Pegamos este código justo debajo de ‘ Empezar:

'' Esto es una prueba. Eliminar luego
Dim x, y, nPant as uByte
nPant = 1
pintaMapa (MAPOFFSETX, MAPOFFSETY, nPant)
For y = 0 To 23
   For x = 0 TO 23
      Print At y + MAPOFFSETY, x + MAPOFFSETX; getCharBehaviourAt (x, y, nPant)
   Next x
Next y
Pause 0
'' Fin de la prueba.

¿Qué estamos haciendo? Pues lo dicho: primero, pintar la pantalla 1 (pintaMapa (MAPOFFSETX, MAPOFFSETY, nPant)). Luego hacemos un bucle para todos los caracteres de la pantalla de juego (0 a 23 en y, 0 a 23 en x), y llamamos a getCharBehaviourAt de cada caracter y lo imprimimos en la pantalla. Así comprobamos que realmente nos está detectando bien qué caracter de la pantalla de juego se puede traspasar y qué caracter no.

Fíjaos que aquí se aplica lo que dijimos antes de la pantalla de juego y la pantalla del ordenador: fíjaos cómo se aplican los offsets en pintaMapa y en el PRINT AT.

Quedaría así:

Podéis bajaros el paquete correspondiénte a este capítulo pulsando aquí. Para el próximo, veremos cómo leer el teclado y explicaremos cómo funciona, por aquello de la cultura general.

Tutorial de ZX Basic + Fourspriter #13: Empezando el proyecto

Bueno, ya que tenemos las cosas básicas (fondos y sprites) listos, vamos a gastar este capítulo corto en empezar montar el proyecto para hacer nuestro güego. Vamos a crear el esqueleto del programa principal, que grabaremos como feoncio.bas, pero antes creemos engine.bas con nuestras dos subrutinas útiles y las constantes necesarias:

'' engine.bas
''
'' Rutinas del juego

Const MAPSCREENWIDTH As uByte = 12
Const MAPSCREENHEIGHT As uByte = 12
Const TILESIZE As uByte = 2

Sub pintaTile (x as uByte, y as uInteger, n as uByte)
   Dim addr as uInteger
   addr = 22528 + x + (y << 5)
   Poke addr, tileset (n, 0): Poke addr + 1, tileset (n, 2)
   Poke addr + 32, tileset (n, 4): Poke addr + 33, tileset (n, 6)
   Print Paper 8; Ink 8; _
      At y, x; Chr (tileset (n, 1)); Chr (tileset (n, 3)); _
      At y + 1, x; Chr (tileset (n, 5)); Chr (tileset (n, 7));
End Sub

Sub pintaMapa (x as uByte, y as uByte, n as uInteger)
   Dim idx as uInteger
   Dim i as uByte
   Dim screenSize as uByte
   Dim mapScreenWidthInChars as uByte
   Dim xx, yy as uByte
   ' Size in bytes:
   screenSize = MAPSCREENWIDTH * MAPSCREENHEIGHT
   mapScreenWidthInChars = TILESIZE * MAPSCREENWIDTH
   ' Read from here:
   idx = n * screenSize
   ' Loop
   xx = x
   yy = y
   For i = 1 To screenSize
      pintaTile (xx, yy, mapa (idx))
      xx = xx + TILESIZE
      If xx = x + mapScreenWidthInChars Then
         xx = x
         yy = yy + TILESIZE
      End If
      idx = idx + 1
   Next i
End Sub

Ahora crearemos feoncio.bas con esta estructura básica: los includes para incluirlo todo (gráficos, mapa, bibliotecas, funciones…), una sección de variables (vacía por ahora), el código principal con su inicialización (por lo pronto, ponemos la pantalla a negro, activamos los UDG y definimos el spriteset para fourspriter) y una sección para funciones y subrutinas (aún vacío).

'' feoncio.bas
''
'' Archivo principal

'' includes
#include once "fsp2.1.bas"
#include once "spriteset.bas"
#include once "udg.bas"
#include once "tileset.bas"
#include once "mapa.bas"
#include once "engine.bas"

'' variables

'' main

' Inicializarlo todo:

Border 0: Paper 0: Ink 7: Bright 0: Cls
Poke uInteger 23675, @udg (0)
fsp21SetGfxAddress (@spriteset (0))

' Empezar

End

'' subs

¡Ya estamos listos para empezar!

Tutorial de ZX Basic + Fourspriter #12: Dibujando pantallas

Ahora que tenemos nuestro mapa.bas en /dev, lo primero que tendremos que hacer será incluirlo en nuestro código. En la lista de includes de test2.bas añadimos el archivo con el mapa como siempre:

'' test2.bas
''
'' Nuestro segundo programa con Fourspriter.

#include once "fsp2.1.bas"
#include once "spriteset.bas"
#include once "udg.bas"
#include once "tileset.bas"
#include once "mapa.bas"

Ahora, para que nuestro código sea reutilizable, vamos a crear unas cuantas constantes con las diferentes dimensiones de la pantalla. ¿Por qué constantes y no variables? Pues porque las constantes se sustituyen por el numerico en sí en tiempo de compilación (ya que nunca van a cambiar) y, por tanto, funcionan más rápido. Nos creamos un área para constantes justo debajo de los includes, para ser ordenados:

'' Constantes

Const MAPSCREENWIDTH As uByte = 12
Const MAPSCREENHEIGHT As uByte = 12
Const TILESIZE As uByte = 2

Hemos creado tres constantes: las dos primeras almacenan el tamaño en tiles de cada pantalla, a lo ancho y a lo alto. La tercera constante indica el tamaño de cada tile (2×2 caracteres, en nuestro caso).

Escribir una rutina que imprima una pantalla es muy sencillo, como hemos dicho. Si echáis un vistazo a mapa.bas, veréis que el mapa es un array con un montón de números. Cada linea de ese array es una pantalla completa (habrá, en nuestro caso, 12×12=144 numeritos por linea), y cada numerito es un tile, ordenados de izquierda a derecha y de arriba a abajo.

Para imprimir una pantalla, por tanto, habrá que dibujar MAPSCREENWIDTH * MAPSCREENHEIGHT (12×12 = 144) tiles a partir del índice n * MAPSCREENWIDTH * MAPSCREENHEIGHT (n * 144) del array, siendo n el número de pantalla.

He escrito esta pequeña Sub que se encarga de hacer eso, y dibuja la pantalla “n” del mapa en la posición (x, y) que le indiques. Como ves, antes de empezar precalcula algunas cosas para ir un poco más rápido, aunque ya veremos que la velocidad tampoco va muy allá. Para conseguir un mapeador rápido habrá que hacerlo en ensamblador, pero eso ahora mismo se escapa un poco del objetivo del tutorial.

El código es el siguiente:

Sub pintaMapa (x as uByte, y as uByte, n as uInteger)
   Dim idx as uInteger
   Dim i as uByte
   Dim screenSize as uByte
   Dim mapScreenWidthInChars as uByte
   Dim xx, yy as uByte
   ' Size in bytes:
   screenSize = MAPSCREENWIDTH * MAPSCREENHEIGHT
   mapScreenWidthInChars = TILESIZE * MAPSCREENWIDTH
   ' Read from here:
   idx = n * screenSize
   ' Loop
   xx = x
   yy = y
   For i = 1 To screenSize
      pintaTile (xx, yy, mapa (idx))
      xx = xx + TILESIZE
      If xx = x + mapScreenWidthInChars Then
         xx = x
         yy = yy + TILESIZE
      End If
      idx = idx + 1
   Next i
End Sub

No es difícil de seguir. Es un bucle de MAPSCREENWIDTH * MAPSCREENHEIGHT iteraciones que va dibujando los tiles en la pantalla. Antes de empezar se calcula idx, un índice del array, y en cada vuelta del bucle se va incrementando para leer el tile correcto. Con cada tile, se llama a pintaTile para ponerlo en la pantalla. Y poco más.

Si llamamos a pintaMapa (4, 0, 1), por ejemplo, pintaremos la pantalla 1 del mapa en la posición (4, 0) (o sea, centrado para nuestro ejemplo).

 

Como siempre, pulsad aquí para descargar todo en el paquetico de este capítulo. Para el siguiente, vamos a empezar a empaquetar todas estas funciones en un archivo engine.bas, ya que vamos a empezar a hacer un güego, y a nosotros nos gusta mucho reutilizar rutinas, ¿no?

Tutorial de ZX Basic + Fourspriter #11: Un mapa

Manejar el Mappy ya sabemos (o deberíamos saber), pero antes de ponerse a lo loco hay que pensar varias cosas. Principalmente el tamaño de nuestro mapa en pantallas y el tamaño de cada pantalla en tiles. Esto es importante porque luego echarse atrás y cambiar cosas es un coñazo.

Nosotros para probar vamos a hacer un mapa en el que cada pantalla sea cuadrada, de 12×12 tiles, y todas formen un rectángulo de 6×5 pantallas, en total 30. Esto, a lo basto, ocupará 12x12x6x5 = 4320, 144 bytes por pantalla. Esto es muy comprimible del mil formas, pero por ahora vamos a pasar de estos temas, y vamos a ir poco a poco con un mapa normal sin comprimir ni gaitas.

Pues nada, abrimos mappy, creamos un nuevo mapa con las dimensiones que queramos (para mi ejemplo, de tiles de 16×16 y con 12×6=72 tiles de ancho y 12×5=60 tiles de alto), cargamos nuestro tileset, y nos ponemos manos a la obra con nuestro mapita (no olviden activar las lineas azulicas que te marcan cuando acaba una pantalla con dividers -> enable dividers y definiéndolos en su sitio: 16 * nº tiles; 192 y 192 en nuestro ejemplo). Cuando terminemos lo grabamos como mapa.fmp para conservar y editar y como mapa.map para importarlo. Todo esto lo metemos en una subcarpeta /map de nuestro proyecto.

Para empezar vamos a hacer un mapa de estos de vista cenital, que luego es más fácil programar el motor, y para empezar es más mejor.

Cuando ya esté petón petón, tendremos que convertirlo a código BASIC. Para ello usamos el mapcnv adaptado a ZX Basic que hice para el Suppafoam (del que ya hablaremos en este blog) y que viene muy bien para estas cosas, ya que saca código BASIC del tirón y no hay que andar convirtiendo mierda. Descargadlo de aquí y descomprimidlo en la carpeta /map.

Para usarlo, sólo tendremos que pasarle como parámetros el archivo mapa.map que hemos exportado con Mappy, las dimensiones del mapa en pantallas, las dimensiones de las pantallas en tiles, y si el mapa es packed. Nuestro mapa no será packed, de hecho ni siquiera sabemos qué carajo es eso, así que nos vale con:

mapcnvZXB.exe mapa.map 6 5 12 12

Si todo va bien, tras un misterioso proceso, tendremos un mapa.bas generado. Ese mapa.bas hay que llevárselo a /dev. Y en el próximo capítulo veremos como pintar las pantallas del mapa (aunque es algo bastante trivial).

Tutorial de ZX Basic + Fourspriter #10: De charsets

Aunque para seguir con el ejemplo a nosotros nos sobra y nos basta con un set de UDG, vamos a hablar sobre una forma de tener más de 21 bloques (en concreto, hasta 96 más), así como de definir un nuevo tipo de letra para los textos de nuestro güego.

La idea es usar un set de caracteres completo en vez de un set de UDG. Un set de caracteres es prácticamente lo mismo que un set de UDG, con la diferencia de que define todos los carácteres con ASCII 32 a 127 (en total, 96 caracteres), que son estos:

Lo ideal y deseable es usar un set completo para definir un tipo de letra, y otro set completo para definir un set de gráficos. Cuando vayamos a imprimir un texto, activaremos el set del tipo de letra. Cuando vayamos a pintar las pantallas del mapa, activaremos el set de gráficos.

El tema se hace de una forma muy parecida a la de los UDG. ¿Recordáis como definíamos el tileset, con un byte para el color, seguido por el código ascii del UDG correspondiente? ¿Recordáis como usábamos también el 32, que es el ASCII del espacio? Pues igual, pero con el código correspondiente de 32 a 127 del carácter que tenemos que imprimir. Porque, recordemos, el set de caracteres y el set de UDG son completamente independientes. Además, nada nos impide combinar un set de caracteres con otro de UDG para tener 96+21 = 117 bloques.

Empezamos creando dos imagenes de 256×24 píxeles, una con el charset y otra con nuestro set de gráficos. Para hacer el set de gráficos, vamos diseñando tiles de 16×16, y vamos recortando los bloques de 8×8 que no estén todavía y pegando en el set… Vamos, como los UDG, solo que con 96 en lugar de 21.

Como véis, hay huecos libres en ambos. Bueno, da igual, no tengo tiempo de hacer más. El tema es convertir estos gráficos de la misma forma que convertimos el spriteset, o los UDG, y salvarlos en dos archivos charset-graficos.bas y charset-textos.bas. El primero debería definir un array tal que…

Dim charsetGraficos (767) as uByte => { _

Y el segundo uno muy parecido:

Dim charsetTextos (767) as uByte => { _

Para activar un charset u otro, hay que hacer uso de un POKE especial, de la misma pinta que el POKE para activar un set de UDG u otro, pero con dos particularidades: es a 23606 (esta vez la variable del sistema es CHARS) en vez de a 23675, y además tenemos que restar 256 de la dirección del array que estemos pokeando. Esto tiene un por qué, por supuesto: el primer gráfico que estamos definiendo es el 32. 32*8=256… El set de gráficos “verdadero” (con definiciones para los caracteres de 0 a 31, que en Spectrum no son imprimibles, sino que representan códigos de control) debería empezar, pues, 256 bytes más atrás… Paranoias. Pasa del tema. Acuérdate:

Poke uInteger 23606, @array (0) - 256

Donde array es el array con el set que queramos activar (charsetGraficos, o charsetTextos). Por cierto, el set estándar del Spectrum está en 15616 (en la ROM), por lo que para activarlo se haría un sencillo:

Poke uInteger 23606, 15616 - 256

Vamos a hacer una prueba. Escribe este programa y grábalo como test3.bas:

#include once "charset-graficos.bas"
#include once "charset-textos.bas"

Cls
Poke uInteger 23606, @charsetGraficos (0) - 256
imprimeSet ()
Print
Print
Poke uInteger 23606, @charsetTextos (0) - 256
imprimeSet ()
Print
Print
Poke uInteger 23606, 15616 - 256
imprimeSet ()
Print
Print
End

Sub imprimeSet ()
    Dim i as uByte
    For i = 32 To 127: Print Chr (i);: Next i
End Sub

Como véis, siempre se imprimen los mismos códigos de caracteres, pero según qué set esté activo, se mostrarán unos diseños u otros.

A la hora de hacer el tileset, se hace igual que antes, pero teniendo en cuenta que los códigos del charset van de 32 hasta 127. Por último, hay que acordarse de activar el charset gráfico antes de imprimir tiles hechos con caracteres modificados (obviamente).

Como hemos dicho, en este tutorial sólo usaremos nuestros UDG para este primer güego que estamos haciendo, pero nada te impide a tí seguir el tutorial usando charsets.

Tutorial de ZX Basic + Fourspriter #9: Creando un tileset

Un tileset no es más que un conjunto de gráficos que se usarán para pintar pantallas basadas en tiles, y que diseñaremos con el Mappy. Usaremos tiles de 16×16, por ejemplo. En nuestro caso, cada tile no sería más que cuatro UDG y sus respectivos colores, colocados dos encima de otros dos.

Ahora mismo no hay ninguna herramienta que yo sepa que sea capaz de autogenerarte el tileset. Hay subterfugios, como en la churrera: como solo hay 16 tiles, podemos permitirnos generar el tileset automáticamente. El tile N estará formado por los caracteres N*4, N*4+1, N*4+2 y N*4+3, y listo. Hay caracteres de sobra para tan pocos tiles, y podemos hacer esa burrez. Pero con solo 21 UDG, o si queremos tener muchos tiles (como en el Severin Sewers) no hay más narices que ponernos a crear los tiles a mano para aprovechar bien los caracteres disponibles.

Es muy fácil. Un tile no será más que una ristra de 8 bytes: 1 byte de color y 1 byte con el UDG correspondiente para cada una de las cuatro casillas. Así de simple.

Para montárnoslo, lo más fácil es empezar diseñando el tileset en nuestro editor gráfico favorito, repegando UDG y dándoles color. Esto, además, nos servirá para luego usarlo como tileset en el Mappy para hacer el mapa. Cuando los tengamos, con paciencia, haremos a manubrio nuestro array de tiles.

Empezaremos con un tileset sencillo de pocos tiles empleando los UDG del ejemplo anterior. Voy a hacer 16 tiles, paso de hacer más, pero en teoría sería factible hacer hasta 256. El primero, para estar a bien con Mappy, será el tile vacío negro feo. Lo llamaremos tileset-mappy.png y lo grabaremos en /gfx. Más que nada porque lo usaremos con el Mappy, además de como guía.

Ahora nos armamos de paciencia para empezar un archivo tileset.bas. Como tenemos 16 tiles, crearemos un array de dos dimensiones: 16 tiles de 8 bytes:

'' tileset.bas

Dim tileset (15, 7) As uByte => { _

Sí, 15 y 7 en vez de 16 y 8. BASIC es así. Se indica el valor del último índice (se empieza en 0), y no el tamaño del Array. Sí, es un lío de cojones. Pero hay que joderse.

Pues nada, empezaremos con el primero. Se trata de cuatro espacios en blanco. Bueno, en negro. El ASCII del espacio es 32. Para ponerlos negros, usaremos PAPER 0, INK 0. El atributo se calcula como INK + 8 * PAPER + 64 * BRIGHT. Por tanto, 0 + 8*0 + 64*0 = 0. Así, vamos intercalando color y ASCII para los cuatro caracteres que forman el primer tile:

    {0, 32, 0, 32, 0, 32, 0, 32}, _

Ea, ya tenemos el primero. Ánimo, solo quedan 15 más. Veamos, el siguiente es el de los ladrillicos cyan. Este es sencillo: los cuatro carácteres son el primer UDG. O sea, el ASCII 144. Para no liarte, puedes imprimir el set de UDG y escribir los numeritos de 144 a 164 debajo con boli, eso te ayuda:

Tres de los caracteres son de color cyan (5), menos el de la esquina que tiene BRIGHT, o sea 5 + 64 = 69. El segundo tile nos quedaría, por tanto:

    {69, 144, 5, 144, 5, 144, 5, 144}, _

¡Yuju! Ahora quedan 14. Vamos a por el próximo. El segundo usa, alternados, los UDG tercero y cuarto, o sea, el 146 y el 147, alternando también sus colores entre rojo intenso (2 + 64 = 66) y rojo (2). Pues al lío:

    {66, 146, 2, 147, 2, 147, 66, 146}, _

Venga, otro más. Este es de los fáciles: solo usa el quinto UDG: el 148. Los dos de arriba, amarillo intenso (6 + 64 = 70), los dos de abajo, amarillo normal (6):

    {70, 148, 70, 148, 6, 148, 6, 148}, _

El siguiente es el de la seta, que usa cuatro UDGs diferentes: 149, 150, 151 y 152. Los dos de arriba, rojo intenso (2 + 64 = 66). Abajo amarillo intenso (70) y amarillo (6):

    {66, 149, 66, 150, 70, 151, 6, 152}, _

Vamos acelerando… Los siguientes tres son muy sencillos:

    {71, 154, 69, 154, 69, 154, 5, 154}, _
    {68, 155, 68, 155, 4, 155, 4, 155}, _
    {68, 156, 4, 156, 68, 156, 4, 156}, _

Luego dos más, los del cesped y la tierra, con los UDG 157 y 158 el primero y 159 el segundo. Luego el rosita (3 + 64 = 67) con el UDG 162:

    {68, 157, 4, 157, 2, 158, 2, 158}, _
    {66, 159, 66, 159, 66, 159, 66, 159}, _
    {67, 162, 67, 162, 67, 162, 67, 162}, _

Para los dos siguientes, los del agua, usaremos PAPER 1, INK 7 y BRIGHT 1: 7 + 8 * 1 + 64 = 79. Ambos usan el carácter espacio, además del UDG 163:

    {79, 163, 79, 163, 79, 32, 79, 32}, _
    {79, 32, 79, 32, 79, 32, 79, 32}, _

Y para terminar (anda, que no ha sido para tanto), los tres tiles de hojitas que emplean el UDG número 164 con diferentes combinaciones de amarillos y verdes, además del espacio vacío (32) de color 0:

    {70, 164, 68, 164, 0, 32, 68, 164}, _
    {68, 164, 4, 164, 68, 164, 68, 164}, _
    {4, 164, 0, 32, 4, 164, 4, 164} _
}

Para probarlos, vamos a crear un test2.bas que los imprima todos. Así matamos dos pájaros de un tiro: escribimos el código que pinta un tile en (x, y), y además vemos si nos hemos equivocado con los numericos.

'' test2.bas
''
'' Nuestro segundo programa con Fourspriter.

#include once "fsp2.1.bas"
#include once "spriteset.bas"
#include once "udg.bas"
#include once "tileset.bas"

'' Variables

Dim i as uInteger

'' Main

Border 0: Paper 0: Ink 7: Cls

Poke uInteger 23675, @udg (0)

For i = 0 To 15
   pintaTile (i * 2, 11, i)
Next i

End

'' Subs

Sub pintaTile (x as uByte, y as uInteger, n as uByte)
   Dim addr as uInteger
   addr = 22528 + x + (y << 5)
   Poke addr, tileset (n, 0): Poke addr + 1, tileset (n, 2)
   Poke addr + 32, tileset (n, 4): Poke addr + 33, tileset (n, 6)
   Print Paper 8; Ink 8; _
      At y, x; Chr (tileset (n, 1)); Chr (tileset (n, 3)); _
      At y + 1, x; Chr (tileset (n, 5)); Chr (tileset (n, 7));
End Sub

La función pintaTitle tiene un poco de hueva. Podéis usarla como caja negra, ya que en un futuro la estaré pasando a ensamblador, porque así, aunque está muy optimizada, es un poco lenta. Básicamente, los POKEs del principio ponen los colores en la memoria de video, y el PRINT de abajo pone los cuatro caracteres. Se usa PAPER 8 e INK 8 para que el PRINT no modifique los atributos que hemos puesto antes con los POKEs.

Compilamos test2.bas como siempre. Si todo ha ido bien, debería salir esto:

Pulsa aquí para descargar el paquetito con todo.

Tutorial de ZX Basic + Fourspriter #8: UDG más virgueros

Vamos a hacer esto de una forma más virguera. Vamos a definir nuestros 21 UDG (8×21 = 168 bytes) en un array y le vamos a decir al Spectrum que los UDG están justo donde está nuestro array. Básicamente definiremos un array con todos los UDG y luego estableceremos INICIO_UDG a la dirección de memoria donde está dicho array. Y los UDG los haremos en nuestro editor gráfico favorito, los grabaremos en png, y luego los convertiremos con SevenuP.

Empezamos creando una imagen de 168×8 píxels donde pintaremos nuestros 21 UDG, y la grabamos en /gfx como udg.png. En el ejemplo, he usado bloques que luego podamos emplear para crear escenarios:

Ahora abrimos SevenuP y seguimos los mismos pasos que cuando convertimos spriteset-import.png, o sea:

  1. Abrir SevenuP.
  2. Pulsar I de IMPORTAR. Seleccionar udg.png y abrirlo.
  3. File->Output Options y dejarlo todo así: 
  4. File->Export Data, Cambiar el tipo a “C” y grabar un udg.c.

Una vez obtenido udg.c, habrá que editarlo igual que el spriteset para convertirlo a código BASIC y grabarlo en /dev como udg.bas. Debería haberte quedado más o menos así: udg.bas.

De acuerdo. Ya tenemos nuestro array con los UDG. Ahora lo que tenemos que hacer es incluirlo en nuestro test1.bas para que el array esté disponible. Añadimos un include once al principio de test1.bas… Ahora mismo lo tendríamos que tener más o menos así (el principio del archivo):

'' test1.bas
''
'' Nuestro primer programa con Fourspriter.

#include once "fsp2.1.bas"
#include once "spriteset.bas"
#include once "udg.bas"

Dim x, y, mx, my as uByte
... etc

El siguiente paso es, como dijimos, cambiar el INICIO_UDG que consulta PRINT a la hora de imprimir un UDG. Ese INICIO_UDG no es más que una dirección de memoria, como dijimos: la dirección de memoria donde está el primer byte de nuestros gráficos. Por tanto, tendremos que hacer que INICIO_UDG sea igual a la dirección de memoria donde está nuestro array “udg“.

¿Cómo hacemos eso? Muy sencillo. INICIO_UDG es un número de 16 bits que está almacenado en las posiciones de memoria 23675 y 23676 del Spectrum. De hecho, es lo que se llama una variable del sistema, en concreto se llama UDG (si te interesan las variables del sistema, mira aquí). Hay muchas variables del sistema que pueden sernos muy útiles, de hecho en siguientes capítulos descubriremos otras.

ZX Basic tiene una forma de escribir de una tacada un número de 16 bits en una posición de memoria y la que le sigue: Poke uInteger. Lo que haremos será ejecutar Poke uInteger pasándole la dirección de la variable del sistema UDG (23675) y la dirección de inicio de nuestro array “udg” (o sea, @udg(0), como en el caso de los sprites).

Ni cortos ni perezosos, activaremos nuestros UDG con un simple:

Poke uInteger 23675, @udg (0)

Una vez hecho esto, los siguientes PRINT que impriman UDGs lo harán usando nuestro set de gráficos definido en el array udg. Así de fácil.

Briconsejo

Esto nos permite tener varios sets de gráficos. Imaginaos que los hemos hecho en los arrays set1 y set2. Entonces actuaríamos así:

Poke uInteger 23675, @set1 (0)
Print "Esto es del set1: \a\b\c\d"
Poke uInteger 23675, @set2 (0)
Print "Esto es del set2: \a\b\c\d"

El primer PRINT usaría los UDG de set1, porque son los que están activos a la hora de ejecutarse. Lo mismo con el segundo PRINT: imprimiría los UDG de set2, que es el que está activo en ese momento.

Con esto, podemos adornar nuestro ejemplo haciendo un marco de ladrillicos:

Dim i as uByte

...

For i = 0 To 31
   Print At 0, i; "\{p4}\{i1}\a"
   Print At 23, i; "\{p4}\{i1}\a"
Next i

For i = 0 To 23
   Print At i, 0; "\{p4}\{i1}\a"  
   Print At i, 31; "\{p4}\{i1}\a"
Next i

Esos \{pX} e \{iX} son códigos de control muy útiles: el primero establece un PAPER y el segundo un INK. \{p4} significa PAPER 4. \{i1} significa INK 1. Estos PRINT imprimirán el udg \a (el primero: los ladrillicos) en tinta azul sobre papel verde.

Esto nos va quedando así:

Fijaos como cuando se topa con los ladrillos, el PAPER de fondo del sprite cambia para adaptarse. Para descargar el paquetito con los ejemplos de este capítulo, haz click aquí.

Tutorial de ZX Basic + Fourspriter #7: Pintando fondos: UDG

Hay muchas formas de pintar fondos sobre los que mover nuestros sprites. De hecho, incluso podemos cargar una pantalla completa de cinta. Pero lo más básico, al igual que cuando uno programa en BASIC normal de Spectrum, es aprender a usar los UDG (gráficos definidos por el usuario).

¿Qué son los UDG? Esta explicación me hubiera venido muy bien cuando estaba aprendiendo. Es muy sencillo. Cuando el PRINT de BASIC se encuentra que tienen que imprimir un carácter con código ASCII que vaya de 144 a 164 (ambos inclusive, en total 21), sabe que tiene que pintar un gráfico definido por el usuario. En concreto, tendrá que pintar en pantalla 8 bytes, los que estén en INICIO_UDG + (c – 144) * 8, siendo c el código ASCII del UDG. Inicialmente, INICIO_UDG está situado justo en el tope de la RAM, esto es, en 65536 – 21 * 8 = 65368. Por tanto, si PRINT se encuentra una A en modo gráfico (el carácter 144), sabe que tiene que pintar los 8 bytes que haya en 65368 + 0 * 8, o sea, 65368. Si nosotros POKEamos 8 bytes justo a partir de ahí, cada vez que PRINT encuentre una A en modo gráfico imprimirá nuestro diseño.

Lo más básico, por tanto, para crear un diseño es hacer esto:

' Las direcciones de memoria tienen 16 bits:
Dim direccion as uInteger

direccion = 65368
Poke direccion    , Bin 00011000
Poke direccion + 1, Bin 00011000
Poke direccion + 2, Bin 01111110
Poke direccion + 3, Bin 11011011
Poke direccion + 4, Bin 11011011
Poke direccion + 5, Bin 00111100
Poke direccion + 6, Bin 01100110
Poke direccion + 7, Bin 01100110

Print Chr (144)

Si compilamos y ejecutamos este programa, veremos el muñequito ese en la pantalla. ¿Qué hemos hecho? Hemos modificado un trozo de la memoria que Print usa para mirar qué gráficos tiene que pintar cuando se encuentra un UDG, en este caso, el primero (el 144).

Hay una forma más intuitiva de imprimirlos: los carácteres 144 a 164 se introducían en el propio Spectrum usando las letras de la A a la U en modo gráfico. Imitando un poco esto, podemos imprimirlas así usando el compilador:

Print "\a"

Que es lo mismo que el Print Chr de arriba. Atención a la barra invertida.

Esto de aquí arriba está muy bien, pero imaginate definir así los 21 carácteres. Es un puto coñazo. Nosotros lo vamos a hacer de una forma mucho más virguera que, además, nos permite tener más de un set de UDG – más que nada porque ese INICIO_UDG del que hablamos antes se puede colocar donde queramos.

En el próximo capítulo.