Tutorial de ZX Basic + Fourspriter #20: Máquina de estados

Ahora que tenemos nuestros gráficos y sabemos qué vamos a hacer, vamos a empezar a crear la máquina de estados en un nuevo archivo bicharracos.bas. Vamos a plantear todo el esqueleto de la máquina de estados y luego iremos rellenando el código necesario para cada estado. Por último, haremos las modificaciones pertinentes en feoncio.bas para llamar a las rutinas de crear y mover bicharracos.

Lo único que nos dejaremos para el último capítulo es el estado activo, donde irán las tres IA que hemos diseñado para los enemigos. Por ahora los enemigos aparecerán, pero todavía no harán nada.

Antes que nada, tendremos que crear la lista de constantes y variables necesarias para manejar todo el cotarro. Primero crearemos algunas constantes que definirán algún que otro comportamiento y que harán el resto del código más legible. Empezamos a escribir nuestro bicharracos.bas:

'' bicharracos.bas
'' todo lo que usted necesita para mover sus bicharracos

'' Constantes

' Mínimo y máximo de pasos que da en una dirección 
' un enemigo pasodetuculista.
Const BICHMINPASOS as uByte = 4
Const BICHMAXPASOS as uByte = 8

' Estados
Const BICHESTIDLE as uByte = 0
Const BICHESTAPARECIENDO as uByte = 1
Const BICHESTACTIVO as uByte = 2
Const BICHESTMURIENDO as uByte = 3

' Tipos
Const BICHTIPOREBOTANTE as uByte = 0
Const BICHTIPOACOSADOR as uByte = 1
Const BICHTIPOPASODETUCULISTA as uByte = 2

' Número de enemigos en pantalla (max = 3!!)
Const BICHNUMENEMS as uByte = 2

' 1 / Frecuencia de salida. Cuanto más alto,
' menos frecuentemente se "despierta" un enemigo
Const BICHFREQUENCY as uByte = 16

Veamos qué hemos definido aquí, que son un montón de cosas. En primer lugar, BICHMINPASOS y BICHMAXPASOS nos servirán para definir cuántos espacios andará en una dirección un enemigo del tipo porculero. Por ahora no las usaremos, lo haremos cuando implementemos la IA en el siguiente capítulo.

Posteriormente tenemos una constante para definir cada uno de los estados de la máquina de estados. Hablamos de ellos en el capítulo anterior, pero recordemos a medida que las citamos: BICHESTIDLE será el estado inicial, llamado “Idle“, en el que el bicharraco está inactivo. Bajo determinadas circunstancias, que describiremos seguidamente, el bicharraco pasará al siguiente estado, BICHESTAPARECIENDO. En este estado el bicharraco estará “apareciendo“. Nosotros crearemos un efecto en el que el bicharraco empieza siendo negro y va aclarándose hasta alcanzar su color. Cuando alcanza su color, se pasa al estado BICHESTACTIVO, que es donde se aplicará la IA concreta según el tipo de enemigo que sea. Finalmente (y esto es algo que aún no haremos), si el jugador mata (como sea) al bicharraco, este entrará en un estado BICHESTMURIENDO, donde podremos poner una animación que dure unos cuantos frames, tras lo cual se volverá al principio, al estado BICHESTIDLE.

Como véis, se trata de una máquina de estados super sencilla, totalmente circular y sin bifurcaciones. ¿Por qué tenemos que utilizar una máquina de estados? Es obvio: porque los bicharracos tienen definidos diferentes comportamientos según en qué estado estén. Básicamente, en cada frame del juego llamaremos a una función que actualizará cada enemigo según el estado en el que se encuentre y que saltará al estado siguiente si se cumple cierta condición. Así, toda la acción del juego ocurrirá “a la vez“.

Sigamos con las constantes. Las siguientes simplemente definen el tipo de IA que tendrá cada bicharraco. De BICHTIPOREBOTANTE, BICHTIPOACOSADOR y BICHTIPOPASODETUCULISTA ya hablamos en el capítulo anterior.

Posteriormente, definiremos el número total de enemigos que puede llegar a haber en pantalla. Nosotros hemos elegido 2 para nuestro juego. Por último, BICHFREQUENCY define con qué frecuencia se crea un nuevo enemigos en la pantalla. Cuanto mayor el número, menos frecuencia. Luego veremos por qué.

Seguimos con las variables. Necesitamos una buena ristra de arrays, y no de variables simples, simplemente porque tendremos X enemigos que procesaremos usando un bucle. He añadido comentarios al código para que se vea para qué vamos a usar cada variable:

'' Variables
' Coordenadas, etc, de los enemigos. 
Dim enX (2) as uByte            ' Coordenada X del sprite
Dim enY (2) as uByte            ' Coordenada Y del sprite
Dim enMx (2) as Byte            ' Movimiento en X del sprite (BICHTIPOREBOTANTE)
Dim enMy (2) as Byte            ' Movimiento en Y del sprite (BICHTIPOREBOTANTE)
Dim enCt (2) as uByte           ' Contador del sprite (BICHTIPOPASODETUCULISTA)
Dim enDir (2) as uByte          ' Dirección del sprite (BICHTIPOPASODETUCULISTA)
Dim enState (2) as uByte        ' Estado de la máquina de estados
Dim enFrame (2) as uByte        ' Frame de animación
Dim enHl (2) as uByte           ' flip-flop (pasará de 0 a 1 y viceversa continuamente).
Dim enColor (2) as uByte        ' Color del bicharraco.
Dim enColorReal (2) as uByte    ' Color real (para aparecer poco a poco)
Dim enSprite (2) as uByte       ' Sprite del bicharraco.
Dim enType (2) as uByte         ' Tipo del bicharraco.

¡Ojo! ¡fíjate que enMx y enMy son de tipo BYTE, no UBYTE, porque necesitamos que tengan signo!

Listo. Podemos empezar a programar nuestro manejador de enemigos. Lo primero que haremos será una subrutina que llamaremos al entrar en cada pantalla (en nuestra función initScreen) para que ponga todos los enemigos en estado idle y desactive sus sprites relacionados:

' "apaga" todos los enemigos (los pone en estado "idle").
Sub apagaBicharracos ()
   Dim i as uByte
   For i = 0 To BICHNUMENEMS
      enState (i) = BICHESTIDLE
      fsp21DeactivateSprite (i)
   Next i
End Sub

Acto seguido escribiremos el esqueleto de nuestra máquina de estados, y luego lo iremos rellenando. Vamos a hacer un bucle que recorra los enemigos, y luego un IF dentro para actuar de una forma u otra dependiendo del estado:

' Gestor principal de enemigos.
' Implementación de la máquina de estados.
Sub mueveBicharracos ()
   Dim i as uByte
   Dim myRand as uByte
   For i = 0 To BICHNUMENEMS - 1
      ' Flip flop:
      enHl (i) = 1 - enHl (i)

      ' Máquina de estados
      If enState (i) = BICHESTIDLE Then

      ElseIf enState (i) = BICHESTAPARECIENDO Then

      ElseIf enState (i) = BICHESTACTIVO Then

      ElseIf enState (i) = BICHESTMURIENDO Then

      End If
   Next i
End Sub

El código es sencillo. El enHl(i) = 1 – enHl (i) sirve para que, en cada frame, enHl (i) valga 1, luego 0, luego 1… Eso nos va a servir luego, programando la IA, para hacer que algunos enemigos se muevan más lentamente. Si hacemos que un enemigo se mueva solo si enHl (i) = 1, estamos consiguiendo que se actualice solo en los frames impares y que, por tanto, se mueva a la mitad de velocidad que el protagonista. Pero eso, luego.

El resto de la subrutina no es más que un bucle que recorre todos los enemigos, y luego un IF dentro que hará que hagamos una cosa u otra dependiendo de su estado.

Empecemos por el primer estado, el estado BICHESTIDLE. En este estado, generaremos un número aleatorio de 0 a BICHFREQUENCY. Si dicho número aleatorio vale 0, haremos que el enemigo se active. Por eso, cuanto mayor sea BICHFREQUENCY, más tardará en activarse un enemigo.

Para activar un enemigo, habrá que pasarlo al siguiente estado, BICHESTAPARECIENDO, inicializar sus variables, decidir el número de gráfico, el color, el tipo de IA, y colocar el enemigo en un sitio libre de la pantalla.

Empecemos a escribir el código de esa parte del IF:

      If enState (i) = BICHESTIDLE Then
         myRand = Int (Rnd * BICHFREQUENCY)
         If myRand = 0 Then

Paramos aquí. Sencillo: generamos un número aleatorio entre 0 y BICHFREQUENCY, y luego actuamos si este número vale 0. Seguimos con el código:

            ' Despertar al bicharraco:
            enState (i) = BICHESTAPARECIENDO
            enFrame (i) = 0
            enColor (i) = Int (Rnd * 7) + 1
            enColorReal (i) = 0
            enSprite (i) = 64 + Int (Rnd * 8) * 8
            enType (i) = Int (Rnd * 3)

Lo dicho: pasamos el estado de este enemigo a BICHESTAPARECIENDO y lo inicializamos: ponemos su frame de animación a 0, decidimos un color aleatorio, inicializamos su color real a 0 (negro), elegimos su número de sprite (los bloques de los bicharracos empiezan a partir del 64, cada uno ocupa 8 bloques y estamos seleccionando uno de los ocho que hay, ver el anterior capítulo y fijarse en el spriteset). Por último, elegimos un tipo de IA de entre los tres disponibles. Seguimos:

            ' Para los BICHTIPOREBOTANTE, dirección al azar:
            If enType (i) = BICHTIPOREBOTANTE Then
               enMx (i) = 2 * Int (Rnd * 2) - 1
               enMy (i) = 2 * Int (Rnd * 2) - 1
            End If

            ' Para los BICHTIPOPASODETUCULISTA, quieto paraos:
            If enType (i) = BICHTIPOPASODETUCULISTA Then
               enCt (i) = 0
            End If

Seguidamente, según el tipo de IA que nos haya salido, tendremos que inicializar otras tantas variables. En el caso de los enemigos rebotadores, definimos enMx y enMy. Esas expresiones eligen un número al azar que puede ser -1 o 1 (fíjate como funciona: Int (Rnd * 2) saca 0 ó 1, lo multiplicamos por 2, eso lo convierte en 0 ó 2, y finalmente le restamos 1, eso lo convierte en -1 ó 1).

En el caso de los enemigos que pasan de tu culo, inicializamos su contador de pasos enCt a 0, para que en cuanto se empiece a ejecutar su estado activo la IA le asigne una dirección y un número de pasos (eso lo veremos en el próximo capítulo). Seguimos con el código:

            ' Buscar un sitio libre para crear al enemigo
            Do
               enX (i) = TILESIZE * Int (Rnd * MAPSCREENWIDTH)
               enY (i) = TILESIZE * Int (Rnd * MAPSCREENHEIGHT)
            Loop While getCharBehaviourAt (enX (i), enY (i), nPant) >= 4

Aquí lo que estamos haciendo es buscar una posición al azar (alineada a tiles) que no sea obstáculo. Elegimos valores al azar para enX y enY de 0 a la dimensión de la pantalla en ancho y alto (que se expresa en número de tiles), y lo multiplicamos por el tamaño de cada tile para obtener coordenadas a nivel de carácter (recuerda: 1 tile = 2 carácteres). Si en esa posición hay un carácter obstáculo no nos vale, y volvemos a elegir otra posición.

Ya, para terminar:

            ' Mover y activar el enemigo
            fsp21BorraSprites ()
            fsp21ActivateSprite (i)
            fsp21MoveSprite (i, enX (i), enY (i))
            fsp21DuplicateCoordinatesSprite (i)
            fsp21InitSprites ()
         End If

Las llamadas de rigor a fourspriter. Básicamente: borramos los sprites activos, activamos el nuevo, lo colocamos en su sitio, duplicamos sus coordenadas, y volvemos a inicializar todos los sprites (para obtener el fondo que hay debajo). Lo de borrar los sprites activos causará un pequeño parpadeo, pero es necesario porque puede que haya algún solapamiento y si no lo borramos, al capturarse el fondo en InitSprites se capturará un cacho de otro sprite y empezará a rondar mierda por la pantalla.

Con esto hemos terminado de definir el primer estado. Pasemos al siguiente estado. En el estado BICHESTAPARECIENDO, simplemente incrementaremos el color del sprite (enColorReal) hasta que llegue a valer el color definitivo que tomará el sprite (enColor). Cuanto esto ocurra, pasaremos al estado activo:

      ElseIf enState (i) = BICHESTAPARECIENDO Then
         If enColorReal (i) = enColor (i) Then 
            ' Ha terminado de aparecer:
            enState (i) = BICHESTACTIVO
         Else
            ' Aún apareciendo:
            enColorReal (i) = enColorReal (i) + 1
         End If

El siguiente estado, el estado activo, es el que moverá a cada sprite según su IA (definida en enType). Por ahora vamos a dejarlo en blanco. Lo rellenaremos en el próximo capítulo:

      ElseIf enState (i) = BICHESTACTIVO Then
         '''
         ''' Aquí es donde iremos implementando las IA!
         '''

Por último, el estado BICHESTMURIENDO, al que no pasaremos nunca hasta que implementemos alguna forma de matar a los enemigos. En él haremos una animación de morirse, por ejemplo, y luego pasaremos de nuevo al estado idle. Por ahora lo dejamos esbozado:

      ElseIf enState (i) = BICHESTMURIENDO Then
         ' Nada por ahora. Ya lo haremos luego.
         enState (i) = BICHESTIDLE
         fsp21DeactivateSprite (i)
      End If
   Next i
End Sub

Bueno, ¡no ha sido para tanto! Ya tenemos definida nuestra máquina de estados finitos. Ahora vamos a integrar todo esto con nuestro juego. Vámonos de nuevo a feoncio.bas.

En primer lugar, como dijimos antes, habrá que modificar la rutina initScreen, que ejecutamos al entrar en cada pantalla, para que llame a apagaBicharracos. Lo hacemos antes que nada, para que los sprites estén debidamente apagados antes de empezar. Dicha rutina nos quedaría así, por tanto:

Sub initScreen ()
   ' Bicharracos
   apagaBicharracos ()

   ' Pintar pantalla
   pintaMapa (MAPOFFSETX, MAPOFFSETY, nPant)

   ' Configurar sprite
   fsp21MoveSprite (3, pX, pY)
   fsp21DuplicateCoordinatesSprite (3)
   fsp21InitSprites ()
End Sub

Ahora, habrá que hacer las pertinentes llamadas en el bucle principal del juego, y además habrá que mover y colorear los sprites relacionados con los enemigos. Por tanto, en el bucle principal, justo antes de la llamada a fsp21UpdateSprites, introducimos el siguiente código:

   ' Mover bicharracos
   mueveBicharracos ()

   ' Actualizar sprites de los bicharracos
   For i = 0 To BICHNUMENEMS - 1
      If enState (i) <> BICHESTIDLE Then
         fsp21MoveSprite (i, MAPOFFSETX + enX (i),  MAPOFFSETY + enY (i))
         fsp21SetGfxSprite (i, _
                           enSprite (i) + enFrame (i), _
                           enSprite (i) + enFrame (i) + 1, _
                           enSprite (i) + enFrame (i) + 2, _
                           enSprite (i) + enFrame (i) + 3)
         fsp21ColourSprite (i, _
                           64 + enColorReal (i), _
                           enColorReal (i), _
                           enColorReal (i), _
                           enColorReal (i))
      End If
   Next i

Como véis, hacemos una llamada a nuestro gestor de bicharracos (mueveBicharracos), y luego hacemos un bucle para todos los enemigos, y si alguno no está en estado idle movemos su sprite, le ponemos el gráfico correcto, y lo coloreamos.

¡Ah! ¡Y que no se os olvide incluir bicharracos.bas en nuestro proyecto! Ponemos esto como el último de los include once:

#include once "bicharracos.bas"

Listo… Si andamos por el mapa, veremos que van apareciendo bicharracos al poco de entrar en las pantallas. (También veremos cómo los sprites parpadean si están en la parte superior de la pantalla y hay tres… Esto es porque fourspriter no lo suficientemente rápida y “nos pilla el retrazo”, pero tranquilidad: muy pronto explicaremos detalladamente qué ocurre y cómo solucionarlo). Si queremos que tarden más en aparecer, tendremos que aumentar el valor de BICHFREQUENCY.

Y, ahora sí, puedes descargarte todo el tiesto en el paquetito tradicional haciendo click en este enlace. En el próximo capítulo haremos que esos bicharracos muevan sus culos.

Anuncios

Tutorial de ZX Basic + Fourspriter #19: Bicharracos porculosos

Ahora lo que toca es meter algo de chicha en el juego, algo que nos de un poquito de por saco. Hay muchas formas de meter enemigos, y de muchos tipos. Para este tutorial, vamos a implementar un tipo de enemigo basados en máquinas de estados finitos sencillas, que son tontos del culo pero molestan que es un primor.

Este tipo de enemigos tendrá un estado inicial idle, en el cual ni siquiera aparecerán en pantalla. Un número aleatorio hará que despierten y pasen al estado apareciendo, con lo que se decidirá (al azar también) qué IA llevarán y qué gráfico tendrán y aparecerán en pantalla en un lugar aleatorio. Una vez hayan aparecido, pasarán al estado activo y empezarán a dar por saco. En este estado activo aplicaremos su IA en cada frame del juego para moverlo (IA por decir algo, más bien EA, de Estupidez Artificial, porque tampoco es que vayan a ser muy listos). Posteriormente, cuando dotemos al protagonista de disparar o de hacer algo que haga pupa (por decidir), tendrán un estado muriendo en el que se mostrará una animación de puf, y posteriormente volverán al estado idle. ¡Voie-la! Una máquina de estados finitos. Todo esto lo vamos a explicar en este capítulo y los tres o cuatro que le siguen.

En este capítulo introductorio simplemente ampliaremos el tileset con los gráficos de los bicharracos y explicaremos cómo funciona cada una de las tres IA que vamos a implementar. En el siguiente capítulo implementaremos paso a paso la máquina de estados finitos y, posteriormente, las IA que explicaremos en este. ¡Prestad atención!

Los enemigos, con vistas a poder reusarlos en otros güegos, los implementaremos íntegramente en un archivo bicharracos.bas, que contendrá todas las funciones, variables y constantes necesarias para manejarse con estos personajes de mala calaña.

Para simplificar, sólo he creado dos frames de animación por cada tipo de bicho. He hecho (mentira, he robado de otros juegos de los Mojon Twins) ocho enemigos diferentes. Los he pegado en el spriteset, justo debajo de los frames del protagonista. El frame se calculará, por tanto, como 64 + 8 * tipoEnemigo + 4 * frame, con tipoEnemigo de 0 a 7 y frame de 0 a 1 (el frame de animación).

Lo de siempre: reordenamos, importamos en SevenuP, exportamos como código C, y convertimos a BASIC.

 

Hablemos ahora de los tres tipos de IA que vamos a implementar:

Enemigos rebotantes: ¿Os acordáis del primer ejemplo de fourspriter que vimos en este tutorial? Pues eso mismo, pero teniendo en cuenta la traspasabilidad (también conocida como obstaculescencia) de los tiles de la pantalla. En cada frame, veremos si es posible mover a nuestro enemigo con dos desplazamientos mx y my que podrán valer -1 o 1. Si no es posible en alguno de los ejes, rebotaremos al enemigo en ese eje (cambiando el signo del m? implicado).

Enemigos acosadores: En los frames impares (para que el mastuerzo se mueva a la mitad de velocidad de nuestro prota), y siempre que sea posible teniendo en cuenta los tiles de la pantalla (ya sabéis, su traspasabilidad u obstaculescencia), el bicharraco se dirigirá hacia donde estemos nosotros. Esto se logra decrementando su posición X si la posición X del muñeco es menor, o incrementándola si es mayor, y lo mismo en el eje Y. Esto, en un laberinto cerrado, funciona como el culo, pero en sitios más abiertos da bastante el pego.

Enemigos pasodetuculistas: Estos enemigos pasan de tu culo, pero dan bastante por el idem en lugares estrechos. Estos comienzan eligiendo al azar una dirección (arriba, abajo, izquierda o derecha) y un número de pasos (de 4 a 8, por poner, aunque esto lo meteremos en un buen par de… constantes), y luego en cada frame del juego harán justo eso: moverse en esa dirección. Cuando hayan agotado el número de pasos o se hayan topado con un obstáculo, vuelta a empezar: eligen otra dirección y otro número de pasos, y etcétera etcétera.

Con estos tres tipos de IA, un par de enemigos en pantalla apareciendo al azar, cada enemigo con un gráfico y un color aleatorios, y un yogur de pera, nos quedará un desarrollo de lo más variado. ¿que no?

Lamentablemente, en este capítulo no hay paquetico. En el siguiente tendremos más chicha.

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).