Tutorial de ZX Basic + Fourspriter #18: Cambiando de pantalla

Tenemos un mapa grande con un montón de pantallas. ¿Cómo movernos de una a otra? Hay varios métodos, pero a mí me gusta eso de comprobar que estemos pulsando la tecla adecuada en el lugar adecuado: básicamente, si estamos pulsando O en el borde izquierdo de la pantalla deberemos ir a la pantalla que está justo a la izquierda. Tenemos una rutina que pinta la pantalla n que le pasemos, así que habrá que ver también como modificar esa n para movernos por el mapa.

Lo primero es hacernos una rutina que sea la que se encargue de inicializar una nueva pantalla. Por ahora, simplemente llamará a la función que pinta la pantalla e incializará el sprite del prota, pero en un futuro ahí habría que colocar la incialización de los objetos, enemigos, etc. de la pantalla a la que vamos a entrar.

Escribimos la primera Sub de feoncio.bas en su sección de subrutinas, al final del archivo:

Sub initScreen ()
   ' Pintar pantalla

   pintaMapa (MAPOFFSETX, MAPOFFSETY, nPant)

   ' Configurar sprite

   fsp21MoveSprite (3, pX, pY)
   fsp21DuplicateCoordinatesSprite (3)
   fsp21InitSprites ()
End Sub

Ok – Ahora necesitamos algo que aún no hemos especificado: las dimensiones del mapa en pantallas que, como veremos, nos serán utiles para movernos por el mismo. Nos vamos a engine.bas y añadimos dos constantes a las que ya hay, en la zona de constantes:

Const MAPWIDTH As uByte = 6
Const MAPHEIGHT As uByte = 5

Esto define el tamaño de nuestro mapa en pantallas. Vamos a usar la primera de ellas para desplazarnos por el mapa. Aunque nuestro mapa sea un rectángulo, nosotros estamos almacenando todas las pantallas seguidas en una ristra: una fila de pantallas detrás de la otra, en un continuo. El rectángulo es, por tanto, una ilusión. Necesitamos saber el ancho de nuestro mapa en pantallas para poder movernos a la pantalla de arriba (restando el ancho a la pantalla actual) o a la pantalla de abajo (sumando el alto a la pantalla actual). Esto lo vemos fácil con un diagramilla. Imaginate un mapa de 3×3 = 9 pantallas. Estas estarán numeradas de 0 a 8:

0 1 2
3 4 5
6 7 8

Imagínate que estamos en la pantalla 4, la del centro. Para movernos a la pantalla de la izquierda, restamos 1: así nos movemos a la pantalla 3. Para ir a la pantalla de la derecha, sumamos 1: así nos movemos a la pantalla 5. Para ir a la pantalla de arriba, tenemos que restar el ancho del mapa, o sea, 3: 4 – 3 = 1. Para ir a la pantalla de abajo, tenemos que sumar el ancho del mapa: 4 + 3 = 7. ¿Véis como funciona?

Recordemos, además, otro concepto: las comprobaciones hay que hacerlas si el muñeco llega al borde de la pantalla y pulsa la dirección correcta (por ejemplo, si pulsa abajo en el borde inferior de la pantalla). ¿Cuándo está el jugador en los bordes de la pantalla? Los bordes de la izquierda y de arriba son fáciles: cuando, respectivamente, pX = 0 y pY = 0. ¿Cuándo estará en el borde de la derecha? Pues cuando pX = MAPSCREENWIDTH + MAPSCREENWIDTH – TILESIZE. ¿Y en el de abajo? Cuando pY = MAPSCREENHEIGHT + MAPSCREENHEIGHT – TILESIZE. Ya lo vimos en el capítulo anterior.

Recordad que multiplicamos por dos (sumamos las dimensiones con ellas mismas, que es lo mismo, pero más rapido) porque MAPSCREENWIDTH y MAPSCREENHEIGHT están en tiles, y nosotros nos movemos de caracter en caracter. El – TILESIZE del final es porque es lo que ocupa un tile (y lo que ocupa el sprite) y estaremos mirando el borde desde dentro. Como siempre, ante la duda te pillas una hoja de cuadritos y te lo pintas todo (un cuadrito por caracter). Contando cuadritos se aprende una barbaridad.

Por tanto, añadiremos 4 IFs al final del bucle principal, en feoncio.bas, justo después de la llamada a fsp21UpdateSprites ():

   ' Cambiar de pantalla

   ' O en el borde izquierdo:
   If (In (57342) bAnd 2) = 0 And pX = 0 Then
      nPant = nPant - 1
      pX = MAPSCREENWIDTH + MAPSCREENWIDTH - TILESIZE
      initScreen ()
   End If

   ' P en el borde derecho:
   If (In (57342) bAnd 1) = 0 And pX = MAPSCREENWIDTH + MAPSCREENWIDTH - TILESIZE Then
      nPant = nPant + 1
      pX = 0
      initScreen ()
   End If

   ' Q en el borde superior
   If (In (64510) bAnd 1) = 0 And pY = 0 Then
      nPant = nPant - MAPWIDTH
      pY = MAPSCREENHEIGHT + MAPSCREENHEIGHT - TILESIZE
      initScreen ()
   End If

   ' A en el borde inferior
   If (In (65022) bAnd 1) = 0 And pY = MAPSCREENHEIGHT + MAPSCREENHEIGHT - TILESIZE Then
      nPant = nPant + MAPWIDTH
      pY = 0
      initScreen ()
   End If

Como véis, en cada caso, actualizamos nPant, que es el número de pantalla actual, y además actualizamos una de las coordenadas. Por ejemplo: si vas a salir por la derecha (pX = MAPSCREENWIDTH + MAPSCREENWIDTH – TILESIZE), tendremos que entrar por la izquierda en la siguiente pantalla (pX = 0). Finalmente, llamamos a nuestra nueva rutina initScreen, que pintará la pantalla especificada por nPant e inicializará correctamente nuestro sprite.

¡Ya podemos explorar el mapa! Por cierto: no hacemos comprobaciones, así que si te sales del mapa pasarán cosas divertidas. Procura que tu mapa esté cerrado (no puedas salirte).

Descárgate el paquetillo y date una vuelta.

Tutorial de ZX Basic + Fourspriter #17: Esto ya va pareciéndose a un juego

¡Y sólo hemos tardado 17 capítulos! Para seguir, necesitamos un spriteset que contenga la animación de un personaje en las cuatro direcciones, ya que vamos a programar un juego de vista cenital. He reaprovechado este spriteset para ello. Lo reordeno, lo exporto, y lo paso a BASIC… Como ya hemos explicado en anteriores capítulos:

 

Lo primero que haremos será el bucle principal. Aquí hay que llamar a subrutinas que aún no tenemos programadas, pero que haremos enseguida. El bucle principal lo vamos a colocar en feoncio.bas, justo donde teníamos el código de las pruebas. En el bucle principal esperaremos un poco (con halt), llamaremos a una rutina para mover al protagonista, llamaremos a otra rutina que actualizará el gráfico del sprite con el frame de animación correcto, moveremos el sprite a su lugar en la pantalla (acordándonos de usar MAPOFFSETX y MAPOFFSETY), y por último llamaremos a fsp21UpdateSprites para que se vean todos los cambios.

Pero antes tenemos que inicializar algunas cosas. Por lo pronto, necesitamos las variables que representarán la pantalla actual y las coordenadas de nuestro protagonista. Colocamos estas definiciones en engine.bas, al principio, en la zona de variables, donde teníamos nuestro array de comportamiento de tiles:

' Pantalla
Dim nPant as uByte

' Nuestro muñequito
Dim pX, pY, pStep, pFacing as uByte

Usaremos pX y pY como coordenadas, pStep para saber el frame de animación actual (0, 1, 2 o 3) y pFacing como offset al primer frame de cada dirección. Si nos fijamos en nuestro spriteset, pFacing valdrá 0 para abajo, 16 para arriba, 32 para derecha y 48 para izquierda. En efecto, pFacing apunta al primer bloque de el primer frame de animación en cada dirección.

Una vez definido esto, nos volvemos a nuestro feoncio.bas para inicializarlo todo, encender los sprites, y pintar la pantalla:

' Empezar

nPant = 0
pX = 2
pY = 2
pFacing = 0
pStep = 0

' Pintar pantalla

pintaMapa (MAPOFFSETX, MAPOFFSETY, nPant)

' Configurar sprite

fsp21MoveSprite (3, pX, pY)
fsp21DuplicateCoordinatesSprite (3)
fsp21ColourSprite (3, 71, 70, 70, 70)
fsp21ActivateSprite (3)
fsp21InitSprites ()

Todo esto ya lo habíamos hecho antes: llamar a pintaMapa, y luego configurar el sprite número 3. Como veis, no estamos llamando a fsp21SetGfxSprite todavía, ya que eso lo haremos luego, en el bucle principal, según el valor de pStep y pFacing.

Justo después de esto, escribimos nuestro bucle principal, tal y como lo describimos más arriba:

' Bucle principal

While 1
   ' Wait 
   Asm
      halt
      halt
   End Asm

   ' Mover muñeco
   muevePibe ()

   ' Actualizar el frame del sprite
   actualizaFrameProta ()

   ' Mover el sprite
   fsp21MoveSprite (3, MAPOFFSETX + pX, MAPOFFSETY + pY)

   ' Actualizar pantalla
   fsp21UpdateSprites ()
Wend

Ese While 1 inicia un bucle infinito. ZX Basic se quejará y todo al compilar. Pero por ahora nos vale. Luego habrá que modificarlo, porque el bucle del juego tendrá que terminarse, por ejemplo, cuando nos maten del todo.

Como vemos, ahí hay dos funciones que no hemos hecho todavía: muevePibe y actualizaFrameProta. Como hemos dicho, la primera se usará para leer el teclado y mover al protagonista. La segunda, nos servirá para asignarle los gráficos correctos según su frame y orientación.

Necesitamos una tercera función auxiliar que colocaremos en engine.bas:

Sub doStep () 
   pStep = pStep + 1: If pStep = 4 Then pStep = 0: End If
End Sub

Simplemente hace el ciclo de 0 a 3 en pStep. Cada vez que se le llame, avanzará un frame de animación. Es para no repetir código, ya que necesitaremos hacer esto cada vez que avancemos en cada una de las direcciones.

Pasamos pues a la rutina MuevePibe. Empecemos por el principio. Vamos a detectar el teclado exactamente igual que hicimos en el ejemplo anterior. Vamos a ir por pasos para ver qué comprobaciones tendremos que hacer. Sobre todo, vamos a guiarnos por este diagramilla:

Vamos a empezar detectando la pulsación de “O” y moviendo al muñeco a la izquierda. Luego haremos la detección para el resto de las direcciones, que serán muy parecidas.

Lo primero es detectar la tecla “O”. Luego lo que haremos será avanzar un frame la animación y establecer la dirección correcta para el sprite:

   '' Detectar "O": Sexta semifila, bit 1
   If (In (57342) bAnd 2) = 0 Then 
      doStep ()
      pFacing = 48

   End If

Ahora miramos el diagramilla. Nos queremos mover hacia la izquierda. Entonces tendremos que comprobar que las posiciones marcadas en rojo son “traspasables”, ambas las dos. Además, vamos a comprobar que no nos salgamos de la pantalla, o sea, que pX > 0. Si miramos las coordenadas que hay que mirar en el diagramilla, podemos terminar de escribir nuestro IF:

   '' Detectar "O": Sexta semifila, bit 1
   If (In (57342) bAnd 2) = 0 Then 
      doStep ()
      pFacing = 48
      ' Puedo moverme?
      If pX > 0 And getCharBehaviourAt (pX - 1, pY, nPant) < 4 _ 
         And getCharBehaviourAt (pX - 1, pY + 1, nPant) < 4 Then
         pX = pX - 1
      End If
   End If

Como veis, estamos llamando a la función getCharBehaviourAt que escribimos hace poco. Si recordáis, esta función devolvía 0 para traspasable, y 4 para obstáculo. Si se cumple que los dos carácteres que están a la izquierda del muñeco, (pX – 1, pY) y (pX – 1, pY + 1), son traspasables, entonces avanzamos a la izquierda: pX = pX – 1.

De forma análoga, y mirando el diagramilla, podemos escribir las otras tres direcciones. Como véis, lo que cambia es qué tecla se detecta, la comprobación de no salirnos de la pantalla, y el comportamiento de qué caracteres se comprueba. Con esto habremos terminado la rutina muevePibe:

Sub muevePibe () 
    '' Detectar "O": Sexta semifila, bit 1
    If (In (57342) bAnd 2) = 0 Then 
        doStep ()
        pFacing = 48
        ' Puedo moverme?
        If pX > 0 And getCharBehaviourAt (pX - 1, pY, nPant) < 4 _ 
            And getCharBehaviourAt (pX - 1, pY + 1, nPant) < 4 Then
            pX = pX - 1
        End If
    End If
    
    '' Detectar "P": Sexta semifila, bit 0
    If (In (57342) bAnd 1) = 0 Then 
        doStep ()
        pFacing = 32
        ' Puedo moverme?
        If pX < MAPSCREENWIDTH + MAPSCREENWIDTH - TILESIZE _
            And getCharBehaviourAt (pX + 2, pY, nPant) < 4 _
            And getCharBehaviourAt (pX + 2, pY + 1, nPant) < 4 Then
            pX = pX + 1
        End If
    End If
    
    '' Detectar "Q": Tercera semifila, bit 0
    If (In (64510) bAnd 1) = 0 Then
        doStep ()
        pFacing = 16
        ' Puedo moverme?
        If pY > 0 And getCharBehaviourAt (pX, pY - 1, nPant) < 4 _
            And getCharBehaviourAt (pX + 1, pY - 1, nPant) < 4 Then
            pY = pY - 1
        End If
    End If
    
    '' Detectar "A": Segunda semifila, bit 0
    If (In (65022) bAnd 1) = 0 Then
        doStep ()
        pFacing = 0
        ' Puedo moverme?
        If pY < MAPSCREENHEIGHT + MAPSCREENHEIGHT - TILESIZE _ 
            And getCharBehaviourAt (pX, pY + 2, nPant) < 4 _
            And getCharBehaviourAt (pX + 1, pY + 2, nPant) < 4 Then
            pY = pY + 1
        End If
    End If
End Sub

Vemos que en las direcciones abajo y derecha se controla que no nos salgamos de la pantalla por esas direcciones empleando las expresiones MAPSCREENWIDTH + MAPSCREENWIDTH – TILESIZE y MAPSCREENHEIGHT + MAPSCREENHEIGHT – TILESIZE, respectivamente. Estas expresiones representan el máximo valor que pueden tomar las variables pX y pY para no salirnos de la pantalla. Si haces la cuenta, nuestro área de juego es de 12×12 tiles, o sea, 24×24 caracteres, cuyas coordenadas irán de 0 a 23 y de 0 a 23 en los ejes X e Y. Como nuestro sprite ocupa 2×2 tiles, las coordenadas máximas en las que podremos colocarlo serán 22 en X y 22 en Y. MAPSCREENWIDTH + MAPSCREENWIDTH – TILESIZE = 12 + 12 – 2 = 22, y lo mismo para la otra expresión. Podemos coger y escribir un 22 directamente (de hecho, será más rápido), pero estamos escribiendo código genérico. Si luego necesitamos optimizar el juego, es una de las cosas que deberíamos cambiar.

Ahora solo nos queda la rutina para establecer el gráfico correcto según los valores de pStep y pFacing… Nos vale con un sencillo IF. Como tenemos un ciclo de cuatro pasos de animación que emplean 3 gráficos diferentes (1,2,3,2,1,2,3…) nos lo montamos así:

Sub actualizaFrameProta ()
   If pStep = 0 Then
      fsp21SetGfxSprite (3, pFacing, pFacing + 1, pFacing + 2, pFacing + 3)
   ElseIf pStep = 1 Or pStep = 3 Then
      fsp21SetGfxSprite (3, pFacing + 4, pFacing + 5, pFacing + 6, pFacing + 7)
   Else
      fsp21SetGfxSprite (3, pFacing + 8, pFacing + 9, pFacing + 10, pFacing + 11)
   End If
End Sub

Con esto y un bizcocho, tenemos que tener a nuestro muñeco moviéndose por la pantalla respondiendo a las teclas y deteniéndose con los obstáculos.

Pulsa aquí para descargar el paquetito correspondiente a este capítulo. En el próximo, veremos como cambiar de pantalla.

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.

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.