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.