Portando de Spectrum a CPC, #1

A las buenas. Voy a empezar una pequeña serie de tutoriales orientados a convertir juegos para ZX Spectrum 48K hechos en z88dk con splib2 a juegos para CPC 464 hechos con z88dk con CPCRSLIB. Esto incluye la gran mayoría de los juegos de los Mojon Twins y todos los lanzamientos que otras personas han hecho hasta la fecha empleando la Churrera.

¿De qué estamos hablando?

Si estás leyendo esto es probable que tengas nociones de programación. Las necesitas, necesitas conocer C. Si le das a los 8 bits y conoces C sabrás que z88dk es un paquete de compilador, ensamblador y linker que genera código para máquinas con un Z80, entre ellas el ZX Spectrum y el Amstrad CPC.

Allá por 2003, Alvin Albrecht lanzó splib2, una biblioteca de funciones orientada a escribir juegos. Esta biblioteca funciona siguiendo una filosofía muy parecida al hardware basado en tiles de fondo y sprites hardware, un enfoque seguido en casi todas las consolas de tercera y cuarta generación y muchos micros de 8 bits como el C64 o el MSX. En estos sistemas, el chip gráfico mantiene un fondo compuesto por tiles de 8×8 pixels y, sobre él, dibuja cierta cantidad de sprites que puede mover sin afectar al fondo. La biblioteca splib2 simula ese comportamiento por software y emplea una forma de actualización que permite no tener que ir sincronizados con la pantalla para evitar parpadeos, lo que la hace idónea para programar juegos en C, donde el control preciso sobre la temporización no es fácil.

Hace años, Artaburu añadió funcionalidad a su biblioteca de funciones CPCRSLIB para poder funcionar de un modo muy similiar a splib, empleando un buffer de tiles sobre el que componer los sprites y una actualización de la pantalla que funciona de forma muy similar. Este hecho hace que portar juegos de splib2 a CPCRSLIB sea una tarea relativamente sencilla (aunque, como veremos, laboriosa).

¿Qué necesitamos?

He hecho dos paquetes con los tiestos que necesitamos. En el primer paquete, he comprimido mi copia de z88dk 1.10 que contiene splib2, CPCRSLIB y CPCWYZLIB, que no es más que el player de WYZ adaptado para funcionar de forma integrada con CPCRSLIB. Sólo hay que descomprimir este paquete en C:\ y tendremos todo listo para compilar.

En el segundo paquete he recopilado las herramientas de conversión y manipulación de datos que necesitaremos para convertir e importar los gráficos y otras porciones para poder usarlos con nuestro juego en su versión de CPC.

Podéis descargarlos dándole [aquí] y [aquí].

Primeros pasos

Lo primero que hay que hacer es tener listos los tiestos. Y cuando digo tiestos, me refiero a gráficos y música. Esto no es un port directo. Aunque vayamos a aprovechar el 90% del código fuente, los gráficos y la música tendremos que hacerlos desde 0 para CPC. Necesitaremos componer un tileset y un spriteset equivalentes a los del juego original en modo 0 o modo 1 (o modo 2, el que se sienta aventurero), necesitaremos dibujar los marcos, pantalla de título y final, y también necesitaremos echar mano del tracker de Augusto Ruiz para componer música. Es un crimen no aprovechar el chip AY del CPC para tener música durante el juego.

Una vez tengamos todos los tiestos, habrá que emplear las diferentes utilidades para convertirlos a un formato usable desde el programa, como ya explicaremos.

¿Y luego?

Luego toca limpiar el código original, si vamos a portar un Churrera, por ejemplo. En los juegos de la Churrera el código es modular y todo está lleno de secciones que se añaden o no dependiendo del contenido de config.h. Esto hace el código bastante ilegible, así que es buena idea (aunque no es indispensable) «preprocesar» el código eliminando todas las partes que no apliequen y quedándonos sólo con el que nos interesa. Por ejemplo, si en config.h  hay un #define DEACTIVATE_KEYS, por ejemplo, y en el código del juego nos encontramos un #ifndef DEACTIVATE_KEYS … #endif, podemos cargarnos ese trozo de código.

Ya está el código listo

Ahora es cuando hacemos la adaptación. Tenemos que identificar todos los manejes que se hagan dependiendo del hardware y cambiarlos. Por suerte, casi todo serán llamadas a splib2 o llamadas a funciones que toquen sonido con el beeper, con lo que sólo habrá que adaptarlo para que se emplée CPCRSLIB y CPCWYZLIB. Lo veremos paso a paso y explicaremos las diferentes cosas que vamos haciendo.

¿Ya hemos terminado?

No, lo último es montar la música y los efectos. Lo dejaremos para la parte final del tutorial.

Empecemos, entonces.

Antes tengo que prepararme algo de material. Lo suyo es que explique todo de forma empírica mientras voy realizando un port. Así será todo más interactivo y veremos las diferentes situaciones a las que nos tendremos que enfrentar.

En el próximo capítulo empezamos.

Dedicado a todos mis amigos Commodoreros

¡¡Chin-cha ra-biiii-ñaaaaaa!! Mua ja ja ja ja ja.

¡No os he abandonado!

Lo que pasa es que llevamos toda la semana de puesta en producción en el curro (mucho estrés), y desde el miércoles pasado ya no tengo artículos escritos «en reserva» y tengo que «vivir al día». A eso hay que sumarle que me encuentro dándole los últimos retoques a un juego para ZX81 que vamos a sacar los Mojon Twins en muy breve. Pero no temáis, el curso de ZX Basic y fourspriter seguirá en breve (la semana que viene, seguramente). Ya queda muy poco para completar el juego. Calculo que el curso durará hasta el capítulo 30 o algo así. Aún quedan los últimos capítulos de dejar el juego bonito y si eso meter objetos o algo sencillo para redondear.

Luego no sé que hacer, así que voy a hacer una pequeña encuesta a ver qué os apetecería hacer cuando se acabe el curso actual. Podemos:

  • Hacer otro juego de otro tipo en ZX Basic usando fourspriter. No sé, un juego de plataformas o quizás de naves con scroll falso (ya veréis qué tontería)… O una mezcla, con fases de naves y fases de plataformas todo mezclado.
  • Hacer una aventura gráfica en ZX Basic con fourspriter y SUVLEIR 3. No os emocionéis demasiado, no va a ser como el Maniac Mansion, pero algo se puede intentar.
  • Explicar cómo hacer un juego en C con z88dk y splib2 usando la Churrera último modelo (que no es más que una colección de rutinas y de utilidades cutres). Como Sir Ababol, Zombie Calavera o Cheril of the Bosque.
  • Explicar cómo hacer un juego para ZX81 usando la biblioteca mojona de hacer güegos en ZX81, la zx81mtlib.

¿Qué os apetecería más?

Tutorial de ZX Basic + Fourspriter #26: Adaptando Fourspriter

Vamos a adaptar fourspriter a nuestro juego ya del todo. Ahora mismo lo que nos mosquea es tener que borrar los sprites y luego volver a inicializarlos cada vez que quitamos o ponemos uno. ¿Por qué es esto?

Veamos. Fourspriter sólo actualizará un sprite (restaurar fondo, capturar nuevo fondo, duplicar coordenadas, pintar sprite) si éste está activo. Esto se hace por motivos obvios: no vas a pintar un sprite que no esté activo. Esto funciona bien si vas a tener N sprites siempre activos en pantalla: sólo se tarda el tiempo necesario, y no más. Si sólo tienes 1, ocuparás muy poco tiempo actualizándolo y tendrás más para tu programa.

Sin embargo, esto nos viene muy mal para nuestro programa, ya que el número de sprites activos está fructuando continuamente. Esto provoca que la velocidad del juego sea variable: cuantos menos sprites activos haya (por ejemplo, al entrar en una pantalla sólo hay uno: el del protagonista) más rápido irá el juego. Además está el tema del parpadeo cuando creamos/eliminamos sprites. Como las acciones de tomar fondo, restaurar fondo, etcétera sólo se hacen con los sprites activos, cada vez que activamos un sprite nuevo tenemos que dejar la pantalla «como estaba» (sin sprites) para que los nuevos capturen bien el fondo, y no capturen el cacho de otro sprite en pantalla.

A nosotros estas cosas, como hemos dicho, nos vienen fatal. Por tanto, vamos a modificar nuestro programa y la biblioteca. La modificación que le haremos a fourspriter será muy sencilla: todas las acciones, excepto la de pintar el sprite en sí, se ejecutarán siempre, sin importar que los sprites estén o no activos. O sea, para todos los sprites se estará continuamente tomando y restaurando el fondo, y si están activos, además, se pintarán. Con esto ya no necesitamos borrarlos todos cada vez que introducimos uno nuevo para capturar su fondo: al activarlo, simplemente se empezará a pintar. Las demás operaciones ya se han estado ejecutando y, por tanto, tendrá un fondo correcto siempre. Por tanto, tendremos que quitar casi todos los fsp21BorraSprites y fsp21InitSprites del programa. Lo veremos a continuación.

Lo primero es modificar fourspriter. Abrimos fsp2.1.bas. La idea es eliminar las comprobaciones de «sprite activo» en las funciones que capturan el fondo y borran los sprites.

La rutina de capturar el fondo es init_sprites. Nos vamos con el editor de texto a la misma (puedes usar la función «Buscar» e introducir init_sprites hasta que des con ella si la quieres buscar tú), que empieza en la linea 246. Si miramos por ahí veremos un comentario como este:

;; Primero vemos si el sprite está activo

Y luego hay algunas operaciones en ensamblador. Tenemos que comentar la comprobación: está en las tres siguientes lineas. Tenemos que dejar esta zona del código así:

;; Primero vemos si el sprite está activo
;ld      a,  (de)
;cp      0
;jr      z,  init_adv    ; Si no está activo, nos lo saltamos
inc     de

Ahora tendremos que hacer exactamente lo mismo para la función que borra los sprites (restaura el fondo, esto es). La rutina se llama borra_sprites. Nos vamos a la misma, empieza en la linea 330. Un poco más abajo vemos exactamente el mismo cacho de código para detectar si el sprite está activo que había en la anterior rutina. Igualmente, lo comentamos.

;; Primero vemos si el sprite está activo
;ld      a,  (de)
;cp      0
;jr      z,  borra_adv   ; Si no está activo, nos lo saltamos
inc     de

¡Y listo! Tenemos nuestra fourspriter modificada. Ahora tendremos que modificar nuestro código. Básicamente, eliminaremos todos los fsp21BorraSprites y fsp21InitSprites que no sean los que se hacen al inicializar la pantalla. Hay unos cuantos, pero están todos en bicharracos.bas y boomerang.bas. También hay que cargarse los fsp21DuplicateCoordinatesSprite ya que todo esto se hace automáticamente.

Empezamos por bicharracos.bas. Tenemos la primera ocurrencia al final de la creación de un nuevo bicharraco en el estado BICHESTIDLE. Lo dejamos así (linea 94 y siguientes):

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

Como vemos, sólo hemos dejado la llamada que activa el sprite y la que lo coloca en su sitio. Seguimos. Hay un fsp21BorraSprites al terminar el estado BICHESTMURIENDO, para quitarlo de la pantalla (linea 134). Lo dejamos así:

enState (i) = BICHESTIDLE
'fsp21BorraSprites ()
fsp21DeactivateSprite (i)

Y ya hemos terminado con bicharracos.bas. Nos vamos a boomerang.bas. Tenemos llamadas al final de la rutina fireBoomerang, cuando creamos el boomerang. Lo dejamos así (linea 58 y siguientes):

If booMX + booMY <> 0 Then
    booX = pX
    booY = pY
    booFrame = 136
    booState = BOOESTAVANZANDO
    booCt = BOOAVANCE
    'fsp21BorraSprites ()
    fsp21ActivateSprite (2)
    fsp21MoveSprite (2, booX, booY)
    'fsp21DuplicateCoordinatesSprite (2)
    fsp21ColourSprite (2, 71, 71, 71, 71)
    'fsp21InitSprites ()
End If

Como véis, de nuevo hemos comentado la llamada a fsp21BorraSprites, a fsp21DuplicateCoordinatesSprite y a fsp21InitSprites. Ahora sólo nos queda una ocurrencia más: cuando eliminamos el boomerang (lineas 135 y siguientes), que debería quedar así:

If booX = pX And booY = pY Then
    booState = BOOESTIDLE
    'fsp21BorraSprite (2)
    fsp21DeactivateSprite (2)
End If

Si ahora compilamos y ejecutamos veremos que todo el parpadeo ha desaparecido: ahora todo se hace automáticamente de verdad. Soy consciente de que este capítulo es un poco liante, pero me parece interesante que tengamos bien claro que, en muchas ocasiones, tendremos que modificar el código de las bibliotecas que usemos para adaptarlas mejor a nuestro güego. Es lo que tiene programar para 8 bits.

Recapitulando

Vamos a recapitular un poco para que veamos cómo funciona ahora la biblioteca y comprendamos cómo se adapta mejor a nuestro escenario (en el que el número de sprites activos está continuamente cambiando).

Originalmente, la biblioteca trabajaba así:

  1. Inicialmente, sólo el sprite 3 está activo. Los demás sprites, los 0, 1 y 2, están inactivos. Al llamar a fsp21UpdateSprites, la biblioteca sólo restaura el fondo del sprite 3, captura el nuevo fondo, y lo dibuja. No hace nada para los sprites 0, 1 y 2 porque están inactivos.
  2. Si ahora aparece un enemigo, se activará el sprite 0. ¿Qué es lo que ocurre? Pues que el sprite 0 no tiene el fondo que hay en sus coordenadas almacenado, y por tanto si llamamos directamente a fsp21UpdateSprites, la parte de la rutina que restaura el fondo pintara «mierda» (un fondo anterior, o vacío, o lo que hubiera almacenado). Es por eso por lo que, al crear un sprite nuevo, estamos llamando a fsp21InitSprites. Pero, espera: fsp21InitSprites captura el fondo, pero si estamos creando al sprite en una zona donde hay otro sprite, en una o más de sus casillas no habrá fondo: habrá un cacho otro sprite. Es por eso que antes de llamar a fsp21InitSprites tenemos que eliminar los sprites de pantalla con fsp21BorraSprites. Eso ocasiona el pequeño parpadeo que vemos.

Tras las modificaciones, la biblioteca funciona así:

  1. Inicialmente, sólo el sprite 3 está activo. Los demás sprites, los 0, 1 y 2, están inactivos. Sin embargo, al llamar a fsp21UpdateSprites, la biblioteca modificada está restaurando el fondo de los cuatro sprites, capturando el nuevo fondo de los cuatro sprites, pero sólo dibuja el sprite 3.
  2. Si ahora activamos el sprite 0 (cuando aparece un enemigo), este sprite ya tendrá su fondo capturado correctamente, ya que esto se ha estado haciendo continuamente aunque dicho sprite estuviera desactivado. Por tanto, no hay que hacer nada. La próxima llamada a fsp21UpdateSprites lo seguirá procesando correctamente, y ahora también se dibujará el sprite 0, ya que ahora está activo.

¿Véis la diferencia y la conveniencia de realizar estos cambios? Espero que sí. Si no, preguntad.

Podéis descargar el paquete que trae la fourspriter y los archivos bicharracos.basboomerang.bas ya modificados y el feoncio.tap generado para que veáis lo bien que se ve ahora. En el próximo capítulo vamos a ver cómo meter pantallas completas comprimidas, y así nos podremos hacer un bonito marco para el juego y una pantalla de presentación y otra para el final.

Tutorial de ZX Basic + Fourspriter #25: Moviendo el boomerang

Bueno, como dijimos, ahora vamos a rellenar la cáscara con chicha bien apretá para que nuestro boomerang deje de ser un amasijo de variables, constantes, y esqueletos y se convierta en un arma mortífera de matar con el boomerang de matar.

Empecemos por el principio: crear el boomerang. Si recordamos, al pulsar SPACE o M (detección que se hace en el bucle principal del programa, en feoncio.bas), llamamos a una función fireBoomerang en boomerang.bas para que, si es posible, crée el boomerang y lo ponga en estado BOOESTAVANZANDO. El boomerang tendrá una dirección u otra dependiendo de adónde esté mirando nuestro muñecajo. Además, habrá que comprobar que podemos lanzar el boomerang en esa dirección (por ejemplo, ¿para qué lanzarlo si estamos mirando a la pared?). Además, todo esto lo haremos si y sólo si el boomerang está en estado BOOESTIDLE (o sea, que no está volando por ahí). Recordemos que para saber adónde mira el muñeco tenemos que mirar la variable pFacing, que valdrá 0 para abajo, 16 para arriba, 32 para la derecha y 48 para la izquierda.

El código podría ser algo parecido a esto:

Sub fireBoomerang ()
    ' Lanzaremos el boomerang sólo si está en estado Idle
    ' Lo crearemos en una posición y en una dirección que 
    ' vendrán dadas por pFacing.
    ' Además, sólo lo crearemos si hay espacio libre en 
    ' la dirección a la que mire el player.
    
    If booState = BOOESTIDLE Then
        booMX = 0
        booMY = 0
        If pFacing = 0 Then
            ' Abajo
            If getCharBehaviourAt (pX, pY + 2, nPant) < 4 And _
                getCharBehaviourAt (pX + 1, pY + 2, nPant) < 4 Then
                booMY = 1
            End If
        ElseIf pFacing = 16 Then
            ' Arriba
            If getCharBehaviourAt (pX, pY - 1, nPant) < 4 And _
                getCharBehaviourAt (pX + 1, pY - 1, nPant) < 4 Then
                booMY = -1
            End If
        ElseIf pFacing = 32 Then
            ' Derecha
            If getCharBehaviourAt (pX + 2, pY, nPant) < 4 And _
                getCharBehaviourAt (pX + 2, pX + 2, nPant) < 4 Then
                booMX = 1
            End If
        Else
            ' Izquierda
            If getCharBehaviourAt (pX - 1, pY, nPant) < 4 And _
                getCharBehaviourAt (pX - 1, pY + 1, nPant) < 4 Then
                booMX = -1
            End If
        End If
        
        ' Si hemos cambiado algo, es que se lanza el boomerang:
        If booMX + booMY <> 0 Then
            booX = pX
            booY = pY
            booFrame = 136
            booState = BOOESTAVANZANDO
            booCt = BOOAVANCE
            fsp21BorraSprites ()
            fsp21ActivateSprite (2)
            fsp21MoveSprite (2, booX, booY)
            fsp21DuplicateCoordinatesSprite (2)
            fsp21ColourSprite (2, 71, 71, 71, 71)
            fsp21InitSprites ()
        End If
    End If
End Sub

Como vemos, hay dos secciones: la primera inicializa booMX y booMY a 0, y acto seguido hace las comprobaciones pertinentes dependiendo de adónde esté mirando el personaje principal (recordad nuestro muy trillado diagramilla). En el caso de que sea posible lanzar el boomerang en la dirección concreta, daremos los valores pertinentes a booMX o booMY.

La segunda sección del código detecta si booMX o booMY tienen ahora un valor. Eso significa que hemos podido lanzar el boomerang. En ese caso, se pasa al estado BOOESTAVANZANDO, se inicializa el contador booCt y las coordenadas del boomerang booX y booY para que salga de donde está el muñeco, y finalmente se inicializa el sprite 2, que es el que vamos a usar para el boomerang.

Ahora toca programar la máquina de estados. Como hicimos con los bicharracos, sencillamente veremos qué debemos hacer en cada estado, y lo iremos codificando.

En primer lugar nos damos cuenta de que para el estado BOOESTIDLE no tenemos que hacer nada, ya que en este estado el boomerang ni siquiera está en la pantalla. Por ello lo quitamos del IF que planteamos en el anterior capítulo y empezamos directamente con BOOESTAVANZANDO:

Sub mueveBoomerang ()
    ' Máquina de estados!
    If booState = BOOESTAVANZANDO Then
        ' Podemos avanzar?
        If booMX = 1 Then
            If booX < MAPSCREENWIDTH + MAPSCREENWIDTH - TILESIZE And _
                getCharBehaviourAt (booX + 2, booY, nPant) < 4 And _
                getCharBehaviourAt (booX + 2, booY + 1, nPant) < 4 Then
                booX = booX + 1
            Else
                booState = BOOESTVOLVIENDO
            End If
        ElseIf booMX = -1 Then
            If booX > 0 And _
                getCharBehaviourAt (booX - 1, booY, nPant) < 4 And _
                getCharBehaviourAt (booX - 1, booY + 1, nPant) < 4 Then
                booX = booX - 1
            Else
                booState = BOOESTVOLVIENDO
            End If
        End If
        
        If booMY = 1 Then
            If booY < MAPSCREENHEIGHT + MAPSCREENHEIGHT - TILESIZE And _
                getCharBehaviourAt (booX, booY + 2, nPant) < 4 And _
                getCharBehaviourAt (booX + 1, booY + 2, nPant) < 4 Then
                booY = booY + 1
            Else
                booState = BOOESTVOLVIENDO
            End If
        ElseIf booMY = -1 Then
            If booY > 0 And _
                getCharBehaviourAt (booX, booY - 1, nPant) < 4 And _
                getCharBehaviourAt (booX + 1, booY + 2, nPant) < 4 Then
                booY = booY - 1
            Else
                booState = BOOESTVOLVIENDO
            End If
        End If
        
        ' Fin de la cuenta
        booCt = booCt - 1
        If booCt = 0 Then
            booState = BOOESTVOLVIENDO
        End If

¿Qué estamos haciendo? Muy sencillo. Hemos dicho que el boomerang va a avanzar sólo hasta que se cumpla la cuenta, colisionemos con el escenario, o colisionemos con un enemigo. La primera parte del código sirve para comprobar las colisiones con el escenario. Si os fijáis, dependiendo de la dirección en la que avanza el boomerang, y que nos dictan las variables booMX y booMY, miramos unas casillas u otras (de nuevo, consultando el diagramilla más famoso de la historia de los videogüegos). Si chocamos con algo, simplemente pasamos al siguiente estado (BOOESTVOLVIENDO). Si no, avanzamos en la dirección que sea.

Posteriormente, decrementamos el contador booCt para que, si llega a cero, pasemos también al estado BOOESTVOLVIENDO. Esto significa que, en ausencia de obstáculos, el boomerang avanzará booCt casillas en la dirección en la que se haya lanzado.

La tercera condición para pasar a BOOESTVOLVIENDO (colisión con un bicharraco) la vamos a colocar luego en el manejador de bicharracos, y así nos ahorramos tener que poner un bucle aquí para recorrerlos todos y comprobar uno por uno. Como en el manejador de bicharracos ya tenemos un bucle que los recorre, simplemente colocaremos la comprobación de colisión en el estado activo de los mismos y a otra cosa, mariposa.

Dando el estado BOOESTAVANZANDO por concluido, pasamos al estado BOOESTVOLVIENDO. En este estado, simplemente tendremos que avanzar hasta donde esté el personaje, sin fijarnos en colisiones ni nada por el estilo. Este comportamiento ya lo hemos programado: corresponde con el de los bicharracos de tipo BICHTIPOACOSADOR pero simplificado (no comprobamos colisiones), con lo que no nos detendremos demasiado en explicar esto:

    ElseIf booState = BOOESTVOLVIENDO Then
        ' Seguir al jugador:
        If booX < pX Then
            booX = booX + 1
        ElseIf booX > pX Then   
            booX = booX - 1
        End If
        
        If booY < pY Then
            booY = booY + 1
        ElseIf booY > pY Then
            booY = booY - 1
        End If
        
        If booX = pX And booY = pY Then
            booState = BOOESTIDLE
            fsp21BorraSprites ()
            fsp21DeactivateSprite (2)
        End If
    End If

Primero avanzamos en la dirección que nos dicte la posición del boomerang con respecto a la del personaje principal (para perseguirle) y seguidamente comprobamos si hemos llegado. En ese caso, volvemos al estado idle y desactivamos el sprite del boomerang.

Sólo queda actualizar el frame de animación. Avanzamos de 4 en 4 como hemos visto otras veces. Mirando al spriteset, vemos que el primer frame del boomerang empieza en el bloque 136 y el último en el 152. Por tanto:

    booFrame = booFrame + 4
    If booFrame >= 152 Then booFrame = 136: End If
End Sub

De acuerdo, ya hemos terminado de programar el boomerang. Ahora vamos a integrarlo. Lo primero es darle sentido al boomerang: integrarlo con los bicharracos, de forma que mueran cuando choquen con él. Como hemos dicho antes, esto lo haremos en el manejador de los bicharracos, por lo que nos vamos a bicharracos.bas y añadimos las comprobaciones en el estado BICHESTACTIVO: no tiene sentido matar a los bicharracos si están en otro estado.

Recordad la comprobación de colisión entre jugador y bicharraco que hicimos hace un par de capítulos: es lo mismo. La rama del IF del manejador de bicharracos correspondiente al estado BICHESTACTIVO debería quedar así:

        ElseIf enState (i) = BICHESTACTIVO Then
            ' Llamamos a una subrutina que contenga la IA
            ' Según el valor de enType (i), y le pasamos
            ' "i", el número de enemigo que estamos procesando.
            If enType (i) = BICHTIPOREBOTANTE Then
                iaRebotante (i)
            ElseIf enType (i) = BICHTIPOACOSADOR Then
                iaAcosador (i)
            Else
                iaPasoDeTuCulo (i)
            End If
            
            ' Colisión con el boomerang!
            If booState <> BOOESTIDLE Then 
                If booX >= enX (i) - 1 And booX <= enX (i) + 1 And _
                    booY >= enY (i) - 1 And booY <= enY (i) + 1 Then
                    mataBicharraco (i)
                    booState = BOOESTVOLVIENDO
                End If
            End If

¿Qué hacemos? En primer lugar, comprobar que el boomerang está activo (o sea, no está en estado BOOESTIDLE). En ese caso, comprobamos la colisión. Si existe colisión, llamaremos a la función de matar al bicharraco que hicimos hace poco y haremos que el estado del boomerang pase a BOOESTVOLVIENDO.

¡Sólo nos queda un detalle! Actualizar el sprite del boomerang. Esto lo vamos a hacer en el bucle principal de feoncio.bas, en la sección donde movemos y actualizamos los sprites. Por ejemplo, justo debajo del bloque que actualiza los sprites de los bicharracos. En primer lugar, comprobamos que no estamos en el estado BOOESTIDLE. Luego, movemos el sprite y le actualizamos el frame de animación.

    ' Actualizar sprite del boomerang
    If booState <> BOOESTIDLE Then
        fsp21MoveSprite (2, MAPOFFSETX + booX, MAPOFFSETY + booY)
        fsp21SetGfxSprite (2, booFrame, booFrame + 1, booFrame + 2, booFrame + 3)
    End If

¡Y listo! Ya tenemos que tener a nuestro boomerang activo. Probadlo, está super chulo.

Ahora nos damos cuenta de que cada vez que aparece o desaparece un sprite de pantalla hay un leve parpadeo en los demás. Esto queda muy feo, y en el próximo capítulo veremos cómo solucionarlo. ¿habrá que modificar fourspriter? ¿Será cuestión de mover cosas de sitio? ¡Próximo episodio en tu casa!

Mientras tanto, puedes jugar un poco con el archivillo de este capítulo.

Tutorial de ZX Basic + Fourspriter #24: Boomerang

Lo siguiente que vamos a ver es una posible manera de darles caña a los enemigos porculeros que hemos creado. Podríamos hacerlo de mil formas, pero ya que nos queda un sprite libre, vamos a implementar un arma chula: un boomerang.

Nuestro boomerang tendrá un comportamiento sencillo, pero más complejo que el de un sencillo proyectil: cuando lo lancemos, avanzará en la dirección hacia la que esté mirando nuestro personaje un número determinado de casillas, y luego volverá en plan magia adonde nosotros estemos (vamos, que si nos movemos, el boomerang no se va a perder). El boomerang también volverá si choca contra algún obstáculo (aunque, volviendo, ignorará los obstáculos, por aquello de simplificar un poco) o si alcanza algún enemigo, al cual asesinará vilmente.

¿Os sugiere algo el párrafo de antes? Hay que aprender a pensar como programador de güegos. Examina el párrafo y verás algo interesante: el boomerang tiene que tener dos comportamientos diferentes: avanzando y volviendo. ¿A qué te suena eso? Exacto: a una máquina de estados. Otra.

Las máquinas de estados se usan mucho haciendo güegos. De hecho, aprender a diseñarlas es clave si quieres comportamientos más o menos elaborados y cosas que ocurran a la vez.

Intuitivamente, nos damos cuenta de que la máquina de estados que describe el comportamiento del boomerang tendrá tres estados:

  • Estado idle, BOOESTIDLE: no se hace nada. El boomerang no aparece en pantalla siquiera.
  • Estado avanzando, BOOESTAVANZANDO: Si el boomerang está en BOOESTIDLE y pulsamos «fire» (SPACE o M), pasaremos al estado BOOESTAVANZANDO. En este estado, decrementaremos un contador y haremos que el boomerang avance. Cuando el contador llegue a cero, golpeemos el escenario, o choquemos contra un enemigo pasaremos al siguiente estado.
  • Estado volviendo, BOOESTVOLVIENDO. En este estado, el boomerang avanza sin remedio hasta el jugador. Cuando lo alcanza, volvemos a BOOESTIDLE.

También hay que tener en cuenta que al entrar en una nueva pantalla habrá que colocar al boomerang en estado idle.

Con todo esto y un bizcocho, vamos crear e importar los gráficos y a plantear el esqueleto de nuestro manejador de boomerangs, el cual rellenaremos vilmente en el próximo capítulo. Por eso empezamos dibujando cuatro frames de rotatoria animación para nuestra siniestra arma:

 

Empecemos con el esqueleto de nuestro manejaboomeranes. Lo primero es crear un nuevo archivo boomerang.bas, que no tenemos que olvidar incluir en feoncio.bas a golpe de #include once. En nuestro nuevo boomerang.bas empezaremos definiendo alguna que otra constante:

'' Boomerang.bas
'' Controla el boomerang de feoncio
''

'' Constantes
Const BOOAVANCE as uByte = 12

Const BOOESTIDLE as uByte = 0
Const BOOESTAVANZANDO as uByte = 1
Const BOOESTVOLVIENDO as uByte = 2

La primera constante, BOOAVANCE, es la que indica cuántos caracteres avanzará nuestro boomerang. Empezaremos por 12… Luego siempre podemos afinar. Luego tenemos tres constantes para llamar a los estados de la máquina de estados finitos. Así el código es más legible (como ocurría con el tema bicharraquístico). Una vez definido todo esto, vamos con un set básico de variables:

'' Variables
Dim booX, booY as uByte
Dim booMX, booMY as Byte
Dim booFrame as uByte
Dim booState as uByte
Dim booCt as uByte

Lo de siempre: nuestras coordenadas (booX y booY), una pareja de variables para indicar la dirección del movimiento del boomerang (booMX y booMY), booFrame para indicar el frame de animación, booState para indicar en qué estado está la máquina de estados y booCt para contar pasos en el estado BOOESTAVANZANDO.

Acto seguido planteamos una función que servirá para ver si podemos lanzar el boomerang, y si eso, lanzarlo. Por ahora la dejaremos vacía, sólo codificaremos la cáscara:

Lo de siempre: nuestras coordenadas (booX y booY), una pareja de variables para indicar la dirección del movimiento del boomerang (booMX y booMY), booFrame para indicar el frame de animación, booState para indicar en qué estado está la máquina de estados y booCt para contar pasos en el estado BOOESTAVANZANDO.

Acto seguido planteamos una función que servirá para ver si podemos lanzar el boomerang, y si eso, lanzarlo. Por ahora la dejaremos vacía, sólo codificaremos la cáscara:

Sub fireBoomerang ()
   ' Lanzaremos el boomerang sólo si está en estado Idle
   ' Lo crearemos en una posición y en una dirección que 
   ' vendrán dadas por pFacing.
   ' Además, sólo lo crearemos si hay espacio libre en 
   ' la dirección a la que mire el player.
   
   If booState = BOOESTIDLE Then
      If pFacing = 0 Then
         ' Abajo
         
      ElseIf pFacing = 16 Then
         ' Arriba
         
      ElseIf pFacing = 32 Then
         ' Derecha
         
      Else
         ' Izquierda
         
      End If
   End If
End Sub

¿Qué hará esto? En primer lugar, comprobar que el boomerang esté en estado idle (no podemos lanzar el boomerang si no está en nuestras manos ¿no?). En caso positivo, mira para dónde mira el personaje principal, con el fin de asignar una dirección concreta al boomerang, y además comprobar que se puede lanzar el boomerang en esa dirección (por si se nos ocurre lanzar el boomerang mirando a la pared). Esta función será la que llamemos desde el bucle principal cuando el jugador pulse FIRE.

La siguiente función que plantearemos será la que implementa la máquina de estados finitos del boomerang y a la cual llamaremos en cada frame (como ya hacemos con muevePive y mueveBicharracos):

Sub mueveBoomerang ()
   ' Máquina de estados!
   If booState = BOOESTIDLE Then
   
   ElseIf booState = BOOESTAVANZANDO Then
   
   ElseIf booState = BOOESTVOLVIENDO Then
   
   End If
End Sub

Por ahora no hay nada, es solo el esqueleto básico y cascaroso de la máquina de estados. Queda rellenar de código cada estado.

Antes de terminar, vamos a integrar todo esto en nuestro engine. Abrimos feoncio.bas. Lo primero es, como hemos dicho, incluir el nuevo archivo al principio (lo repito porque, por ejemplo, a mí siempre se me olvida). Como vamos a necesitar las variables del boomerang en el módulo de los enemigos para detectar las colisiones (añadiremos código al estado activo de los enemigos), tenemos que incluir boomerang.bas ANTES que bicharracos.bas.

'' 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"
#include once "boomerang.bas"
#include once "bicharracos.bas"

Ya son unos cuantos ¿eh? De todos modos, yo prefiero tener muchos archivos de código cortos que pocos archivos tochaquers. Cuestión de comodidad y orden, la verdad. Al compilador le da exactamente lo mismo y al Spectrum ya no te digo.

Seguimos. Tenemos que acordarnos de poner el boomerang a idle cada vez que entremos en una pantalla. Por tanto, modificamos nuestra rutina initScreen:

Sub initScreen ()
   ' Boomerang
   booState = BOOESTIDLE

   ' Bicharracos
   apagaBicharracos ()

   ' Pintar pantalla
   pintaMapa (MAPOFFSETX, MAPOFFSETY, nPant)
   
   ' Configurar sprite
   fsp21MoveSprite (3, pX, pY)
   fsp21DuplicateCoordinatesSprite (3)
   fsp21InitSprites ()
End Sub

Y lo último que queda es modificar el bucle principal. Lo primero es llamar a la función que mueve el boomerang en cada frame del juego. Tras la llamada que mueve a los bicharracos (o sea, mueveBicharracos), llamamos a:

   ' Mover boomerang
   mueveBoomerang ()

Justo después colocaremos el código que intenta lanzar el boomerang. Echamos mano de nuestra chuleta de detección de teclas para detectar la M y el SPACE y, si eso, llamar a la rutina correspondiente:

   ' Disparar boomerang
   If (In (32766) bAnd 1) = 0 Or (In (32766) bAnd 4) = 0 Then
      fireBoomerang ()
   End If

Y poco más. Podéis descargar el paquetito, aunque en rigor no haga nada nuevo… Pero para los completistas. En el próximo capítulo rellenaremos todo el esqueleto y a otra cosa, mariposa.

Tutorial de ZX Basic + Fourspriter #23: Colisiones

Una vez resueltos los problemas del parpadeo (aunque, como os adelanto, no será la última vez que nos pongamos a modificar la biblioteca fourspriter), lo siguiente es ponerse a detectar colisiones entre bicharracos y el protagonista principal del güego. Actuaremos de la siguiente forma: cuando un enemigo y el protagonista principal choquen (sus sprites se toquen), pasaremos el bicharraco al estado de morirse, para que explote y desaparezca, y restaremos 1 de la vida del jugador.

Por lo tanto, lo primero que necesitamos es almacenar la vida del jugador. Nos vamos a la zona de variables de engine.bas, y justo debajo de donde definimos pX y compañía añadimos una variable más:

Dim pEn as uByte

A esa variable habrá que darle un valor inicial. Nos vamos a feoncio.bas y, en la parte donde inicializamos a nuestro jugador (a partir de la linea 30), añadimos

pEn = 10

Ponemos 10 porque está bien para empezar. Luego ya afinaremos… Es lo típico: si el juego te queda muy facilorro ponemos menos vidas y listo. Eso se llama «diseño de gameplay cutre de cojones«, pero funciona. Ahora hacemos una subrutina para imprimir la vida que tenemos. Por ahora iremos a lo sencillo, creando esta rutina en la zona de subrutinas de feoncio.bas. Ya lo pondremos más bello cuando diseñemos un marco de juego en condiciones. Por ahora nos vale así:

Sub imprimeVida ()
   Print At 0,0; "E"; pEn; " ";
End Sub

Y llamamos a esta subrutina al principio del juego, antes de entrar en el bucle principal. Además, nos vamos a cargar nuestro bucle infinito, ya que ahora podremos morirnos. En la zona de variables de feoncio.bas creamos una nueva:

Dim gameOver as uByte

Esta variable actuará como bandera, de forma que justo antes de empezar el bucle tendremos que ponerla a 0. Cambiamos, por tanto, nuestro WHILE 1 original por:

gameOver = 0
While Not gameOver

Bien. Pongámonos al lío de verdad. Lo que tendremos que hacer, básicamente, es detectar una colisión en el bucle principal del juego. Si esto ocurre, mataremos al enemigo en implicado. Luego, en nuestra máquina de estados de los bicharracos, añadiremos código para gestionar el estado «muriendo». Lo que haremos será que se muestre una pequeña animación con una explosión durante un tiempo, y luego se pase al estado Idle, con lo que, al poco rato, el enemigo volverá a aparecer de nuevo en otro sitio (y con otro color, sprite y comportamiento).

Lo primero que necesitamos, por tanto, es ampliar un poco nuestro spriteset. Nosotros hemos dibujado dos frames de explosión que quedarán realmente aparentes en el juego (ya lo veréis). Haremos lo de siempre: pintamos los frames, reordenamos la imagen, importamos con SevenuP, pasamos a BASIC.

 

Hecho esto, vamos a añadir el código. Lo primero será detectar la colisión. Si cogemos una hoja de cuadritos y nos pintamos los dos sprites, veremos que la colisión se dará si el enemigo (en realidad, su casilla superior izquierda, que es la que lleva su posición real) está dentro del cuadrado que va desde (pX – 1, pY – 1) hasta (pX + 1, pY + 1). Como véis, hacer juegos en 2D y pintarse 300 diagramillas son uno. Por lo menos hasta que no tengamos lo conceptos grabados a fuego.

Metemos esa comprobación dentro de un bucle que recorra todos los enemigos, y sólo la haremos si el enemigo en cuestión está en estado activo (para que no nos mate si está apareciendo, idle, o muriéndose). Metemos, por tanto, este bloque de código en el bucle principal. Por ejemplo, justo antes de las comprobaciones de cambio de pantalla:

   '' Colisiones
   
   For i = 0 To BICHNUMENEMS - 1
      If enState (i) = BICHESTACTIVO Then
         If enX (i) >= pX - 1 And enX (i) <= pX + 1 And _
            enY (i) >= pY - 1 And enY (i) <= pY + 1 Then
            mataBicharraco (i)
            mataPersonaje ()
         End If
      End If
   Next i

Como vemos, si se cumplen todas las condiciones, llamamos a dos subrutinas: mataBicharraco (pasándole el bicharraco indicado) y mataPersonaje. Empecemos por matar al personaje:

Sub mataPersonaje ()
   If pEn > 0 Then
      pEn = pEn - 1
   Else
      gameOver = 1
   End If
   imprimeVida ()
End Sub

Sencillo y directo ¿verdad? Si tenemos más de 0 vidas, restamos 1. En caso contrario, ponemos a cierto nuestra bandera de gameOver, con lo que terminaría el bucle principal (recordad el WHILE NOT gameOver que hemos puesto al principio).

Ahora toca matar al bicharraco. Nos vamos a bicharracos.bas para crear la subrutina mataBicharraco. En esta función nos prepararemos el estado BICHESTMURIENDO: pondremos el color a blanco, seleccionaremos el primer frame de explosión (que, en nuestro spriteset, empieza en el bloque número 128) y reutilizaremos el contador de los enemigos tipo «paso de tu culo» para contar 4 frames de explosión. Además, reiniciaremos el contador de frame de animación:

Sub mataBicharraco (i as uByte)
   enCt (i) = 4
   enState (i) = BICHESTMURIENDO
   enColorReal (i) = 7
   enSprite (i) = 128
   enFrame (i) = 0
End Sub

Ok – esto prepara el estado «muriendo» y lo activa. Sólo queda, por tanto, definir el comportamiento de dicho estado en nuestra máquina de estados. Básicamente cambiaremos de frame de animación mientras no se consuma el contador en enCt. Cuando esto ocurra, volveremos al estado BICHESTIDLE, con lo que volveríamos a empezar (el enemigo desaparece, y ya volverá a crearse). Como verás, se cumple la regla no escrita de que siempre que vayamos a activar o desactivar un sprite, hay que borrar los que estén en pantalla. Bueno, ya está escrita. Y tampoco permanecerá escrita mucho tiempo. Esto se hace así ahora porque fourspriter aún necesita una modificación que haremos dentro de poco:

        ElseIf enState (i) = BICHESTMURIENDO Then
            enCt (i) = enCt (i) - 1
            enFrame (i) = 4 - enFrame (i)
            If enCt (i) = 0 Then
               enState (i) = BICHESTIDLE
               fsp21BorraSprites ()
               fsp21DeactivateSprite (i)
            End If
        End If

Lo que hemos dicho: decrementar el contador enCt, cambiar de frame (pasa de 0 a 4, de 4 a 0, etc.), y comprobar si se ha agotado el contador. En ese caso, desactivar el sprite y volver el bicharraco al estado idle.

¡Probadlo, probadlo, veréis qué chulada! Lo próximo será hacer que nuestro prota sea un poco más beligerante. El paquetico, como siempre, para descargar.

 

Tutorial de ZX Basic + Fourspriter #22: Parpadeando

¡Houston, tenemos un problema! Si os habéis fijado, cuando hay más de dos sprites activos aparece un cierto parpadeo en la parte superior de la pantalla que queda super mal. Joder, vaya basuraca ¿no? Entonces te preguntarás ¿para qué carajo estoy usando fourspriter si luego se ve mal y parpadea?

Ay, amigo, cuánta razón. Pero no te preocupes. Primero vamos a explicar por qué ocurre el parpadeo. Y no, no es para camelarte y que te olvides de él. Es para culturizarte, querido lector.

Veamos. Para empezar hablemos de rasters. La imagen que ves en tu pantalla no se forma por arte de magia, no. Lo sentimos, pero el famoso cuento del Hada Video es mentira, y los electroduendes son los padres. En el Spectrum hay un chip llamado ULA que hace un montón de cosas. Una de las cosas que hace, y que, además, es tela de importante, consiste en crear la imagen que ves en la pantalla. Para ello va enviando señales de video que construye ella misma. Las señales de video consisten en una onda analógica que luego la TV interpreta y utiliza para modular un haz de electrones que impacta contra la pantalla, iluminando ciertos puntos. O al menos era así antes, con las pantallas de tubo. Lo importante de esto es que esa señal lineal ha de convertirse en un rectángulo. Y para ello, lo que se hace es empezar por la esquina superior izquierda, e ir rellenando hacia la derecha hasta que llegamos al borde, tras lo cual bajamos una linea y volvemos a empezar a rellenar de izquierda a derecha, y así hasta que hayamos completado toda la imagen. Esto no es así al 100% (antes de que me salte el típico friki premio novel en televisores rajando en los comentarios), pero bueno, a efectos prácticos nos vale.

Por tanto, la ULA lo que hace es lo siguiente: primero lanza una interrupción, y acto seguido empieza a generar el borde superior de la imagen. Cuando digo borde me refiero a lo que se pinta de un color cuando haces un BORDER en BASIC. Cuando termina el borde superior, empieza con la imagen que hay en la memoria gráfica del Spectrum: pinta un cacho de borde (el de la izquierda), lee la memoria de video (los 32 de los 6912 bytes que hay a partir de 16384 en la RAM del Spectrum) mientras dibuja los colores correspondientes, pinta otro cacho de borde (el de la derecha), y vuelta a empezar. Así hasta que se acaba la imagen de la memoria de video, tras lo cual pinta el borde de abajo. Cuando acaba, vuelve a empezar por el principio.

La operación halt en ensamblador lo que hace es esperarse a que haya una interrupción enmascarable. En un Spectrum esto solo ocurre cuando la ULA va a empezar a dibujar la pantalla. Por eso se suele usar para sincronizarse con la pantalla.

Fourspriter no usa ningún buffer. Esto significa que hace todo directamente sobre la pantalla. Lo que hace es restaurar el fondo del sprite en su ubicación anterior, almacenar el fondo del sprite en su actual ubicación, y pintar el sprite encima. Una y otra vez. La rutina de actualización a la que llamamos en cada frame del juego, fsp21updateSprites, llama al siguiente código en ensamblador (archivo fsp2.1.bas, linea 524 y siguientes):

        upd_sprites:
                    halt
                    
                    call    borra_sprites
                    call    init_sprites
                    call    pinta_sprites
                    call    upd_coord
                    
                    ret

¿Veis el halt del principio? Eso significa que la rutina empieza a ejecutarse justo cuando la ULA empieza a dibujar la imagen. De hecho, empieza a ejecutarse mientras la ULA dibuja el borde superior.

El parpadeo es debido a que, si incrementamos el número de sprites activos, la rutina tarda más en terminar su tarea, y la ULA ha terminado con el borde superior y ha llegado ya a la pantalla propiamente dicha antes de que hayamos terminado de actualizar los sprites. Si los sprites implicados que aún no han terminado de actualizarse se encuentran en la parte superior y se cruzan con la ULA actualizando la pantalla, pues veremos como se borran y se vuelven a imprimir: los vemos parpadear.

Básicamente, fourspriter no tiene tiempo de hacerlo todo mientras la ULA pinta el borde, y se le ven las bragas. ¡Mierda!

Afortunadamente, esto tiene una solución muy sencilla (aunque en realidad se me ocurrió hace poquísimo, tonto de mí, porque además luego me entero de que hay otra gente que lleva meses usando este «truco»). Si lo piensas, en total estamos haciendo tres halts en nuestro juego: uno en fourspriter para sincronizarse con la ULA (el que hemos visto más arriba), y dos más en el bucle del juego para perder tiempo y que no vaya todo tan rápido. La idea es la siguiente: eliminar uno de los halts, y sustituirlo por un bucle dentro de la rutina en ensamblador upd_sprites que simplemente pierda el tiempo, que no haga nada hasta que la ULA toque el borde inferior. Entonces empezaríamos a ejecutar las rutinas de actualización. ¿Os dais cuenta? de esa forma, tendremos mucho más tiempo para hacer cosas en la pantalla sin que se vean: el tiempo que la ULA esté dibujando el borde inferior, más el que esté dibujando el borde superior.

Huelga decir que esto se me ocurrió cagando, como todas las grandes ideas de la historia de la humanidad.

Se me ocurrió pero no sabía cuánto esperar. Tras un par de sugerencias que me hicieron en el foro de WOS me dispuse a medirlo de forma empírica. Simplemente haces halt, pones el borde de un color (azul, que es muy bonito), metes un bucle, y lo vuelves a poner a negro. Así, poco a poco, vas ajustando la duración del bucle hasta que el color del borde cambie a negro justo al llegar al borde inferior. Luego quitas los cambios de color del borde, y te quedas con la duración del bucle.

La rutina upd_sprites, con el bucle con la duración correcta, queda así. Así que no tardes en abrir fsp2.1.bas y cambiar ese trozo de código… O descárgate el paquetico de este capítulo pulsando aquí mismo.

        upd_sprites:
                    halt
                    
                    ld bc, 1900
        loop_waste: dec bc
                    ld a, b
                    cp 0
                    jp nz, loop_waste
                    ld a, c
                    cp 0
                    jp nz, loop_waste
                    
                    call    borra_sprites
                    call    init_sprites
                    call    pinta_sprites
                    call    upd_coord
                    
                    ret

Recuerda que tendremos que irnos al bucle principal en feoncio.bas y eliminar un halt de los dos que aparencen al principio, si no el juego irá más lento.

Y seguramente os preguntaréis por qué no hago yo el cambio y distribuyo fourspriter con el cambio ya hecho… Buena pregunta. Es muy sencillo: este cambio nos viene bien ahora a nosotros en este caso, pero es posible que alguien quiera ocupar el tiempo que nosotros simplemente estamos esperando a que la ULA llegue al borde de abajo haciendo otras cosas… ¿Quién sabe? En ese tiempo podríamos tocar un poco de música, por ejemplo.

Aún tenemos un par de modificaciones más que hacer a fourspriter, pero antes vamos a interactuar un poco con esos enemigos ¿no? ¡en el próximo capítulo!

Tutorial de ZX Basic + Fourspriter #21: Estupidez Artificial

Vamos a implementar ahora las tres rutinas de IA diferentes. Como hemos dicho, enType, que valdrá 0, 1 o 2 para cada enemigo, decidirá que IA aplica. Cada IA la encapsularemos en una subrutina que modifique el bicharraco «i». Por lo pronto, ya podemos empezar a «poblar» la rama del IF de la máquina de estados correspondiente al estado activo. Básicamente, miramos qué vale enType, y llamamos a una rutina u otra:

        ElseIf enState (i) = BICHESTACTIVO Then
            ' Llamamos a una subrutina que contenga la IA
            ' Según el valor de enType (i), y le pasamos
            ' "i", el número de enemigo que estamos procesando.
            If enType (i) = BICHTIPOREBOTANTE Then
                iaRebotante (i)
            ElseIf enType (i) = BICHTIPOACOSADOR Then
                iaAcosador (i)
            Else
                iaPasoDeTuCulo (i)
            End If

Bien, ahora toca el turno de implementar estas tres subrutinas: iaRebotante, iaAcosador y iaPasoDeTuCulo. Vamos a ello. Empecemos por el principio…

Bicharracos rebotantes

Esta IA es muy sencilla. Se basa en mover al bicharraco sumando a sus coordenadas un 1 o un -1 en cada frame, con lo que se moverá en diagonal. Si llegamos al borde de la pantalla o nos chocamos con un obstáculo, cambiaremos el sentido del movimiento en el eje que haya producido el choque. Esto se ve más fácilmente en código que explicándolo de palabra, creo. Como tenemos que detectar colisiones con el escenario, usaremos nuestra función getCharBehaviourAt.

Como en muchas otras ocasiones, el tratamiento del eje X y del eje Y se hacen por separado. Primero, el eje X:

Sub iaRebotante (i as uByte)
    If enMx (i) = 1 Then
        If enX (i) = MAPSCREENWIDTH + MAPSCREENWIDTH - 2 _
            Or getCharBehaviourAt (enX (i) + 2, enY (i), nPant) >= 4 _
            Or getCharBehaviourAt (enX (i) + 2, enY (i) + 1, nPant) >= 4 Then
            enMx (i) = -1
        End If
    ElseIf enMx (i) = -1 Then
        If enX (i) = 0 _
            Or getCharBehaviourAt (enX (i) - 1, enY (i), nPant) >= 4 _
            Or getCharBehaviourAt (enX (i) - 1, enY (i) + 1, nPant) >= 4 Then
            enMx (i) = 1
        End If
    End If
    
    enX (i) = enX (i) + enMx (i)

Veamos qué estamos haciendo aquí: en primer lugar comprobamos si nos estamos moviendo a la derecha (enMx = 1) o a la izquierda (enMx = -1), principalmente para saber qué comprobaciones tenemos que hacer. Recordemos el diagramilla de las colisiones para guiarnos:

Si nos movemos a la derecha (enMx = 1), tenemos que comprobar una colisión a la derecha del muñeco. Tendremos, pues, que comprobar las casillas en amarillo del diagrama. Además, comprobaremos que enX no se sale de la pantalla. Si ocurre cualquiera de estas dos cosas (que haya un obstáculo en cualquiera de las casillas amarillas o que estemos en el borde de la pantalla) lo que hacemos es cambiar el sentido: enMx = -1. Hacemos lo mismo al movernos a la izquierda (enMx = -1), pero comprobando esta vez, como es lógico, que enX esté en el borde izquierdo o que alguna las casillas rojas sea un obstáculo. Si algo de eso se cumple, cambiamos el sentido: enMx = 1.

Después de las comprobaciones, simplemente hacemos avanzar al bicharraco en el sentido correcto (con enX (i) = enX (i) + enMx (i)).

Luego se hace exactamente lo mismo para el eje vertical. En este caso entran en juego los bordes superior e inferior de la pantalla y las casillas verdes y azules del diagramilla:

    If enMy (i) = 1 Then
        If enY (i) = MAPSCREENHEIGHT + MAPSCREENHEIGHT - 2 _
            Or getCharBehaviourAt (enX (i), enY (i) + 2, nPant) >= 4 _
            Or getCharBehaviourAt (enX (i) + 1, enY (i) + 2, nPant) >= 4 Then
            enMy (i) = -1
        End If
    ElseIf enMy (i) = -1 Then
        If enY (i) = 0 _
            Or getCharBehaviourAt (enX (i), enY (i) - 1, nPant) >= 4 _
            Or getCharBehaviourAt (enX (i) + 1, enY (i) - 1, nPant) >= 4 Then
            enMy (i) = 1
        End If
    End If
    
    enY (i) = enY (i) + enMy (i)

Por último, y para terminar, animamos el sprite cambiando de un frame a otro. enFrame debería ir cambiando de 0 a 4 para ir seleccionando los bloques correctos en cada frame. Como enFrame inicialmente valdrá 0, es muy sencillo conseguirlo con un «one-liner«:

    enFrame (i) = 4 - enFrame (i)
End Sub

Bicharracos acosadores

Los bicharracos acosadores son también muy sencillos: lo que hay que hacer es que enX y enY se «acerquen» a pX y pY en cada frame… siempre que no haya un obstáculo de por medio. Una vez más, separamos los ejes horizontal y vertical. Además, sólo nos moveremos si enHl = 0. Si recordáis, enHl va cambiando de 0 a 1 continuamente en cada frame. Así, por tanto, conseguiremos que el enemigo se mueva a la mitad de la velocidad que nuestro protagonista.

Otra cosa que queremos conseguir es que el sprite sólo se anime (cambie de frame) si nos hemos movido. Para ello vamos a recordar los valores iniciales de enX y enY para que al final, si ha cambiado cualquiera de ellos, cambiemos de frame. Hagamos esto lo primero:

Sub iaAcosador (i as uByte)
    Dim enCx, enCy as uByte
    
    enCx = enX (i): enCy = enY (i)

Empecemos. Vamos a tratar primero el eje horizontal. Dependiendo de la relación entre pX y enX intentaremos movernos a la derecha (si pX > enX) o a la izquierda (si pX < enX). Para ello (tirando de diagramilla) habrá que comprobar que las casillas correspondientes sean traspasables:

    If enHl (i) = 1 Then
        If pX > enX (i) _
            And getCharBehaviourAt (enX (i) + 2, enY (i), nPant) < 4 _
            And getCharBehaviourAt (enX (i) + 2, enY (i) + 1, nPant) < 4 Then
            enX (i) = enX (i) + 1
        ElseIf pX < enX (i) _
            And getCharBehaviourAt (enX (i) - 1, enY (i), nPant) < 4 _
            And getCharBehaviourAt (enX (i) - 1, enY (i) + 1, nPant) < 4 Then
            enX (i) = enX (i) - 1
        End If

Seguidamente, hacemos lo mismo con el eje vertical:

        If pY > enY (i) _
            And getCharBehaviourAt (enX (i), enY (i) + 2, nPant) < 4 _ 
            And getCharBehaviourAt (enX (i) + 1, enY (i) + 2, nPant) < 4 Then
            enY (i) = enY (i) + 1
        ElseIf pY < enY (i) _
            And getCharBehaviourAt (enX (i), enY (i) - 1, nPant) < 4 _ 
            And getCharBehaviourAt (enX (i) + 1, enY (i) - 1, nPant) < 4 Then
            enY (i) = enY (i) - 1
        End If
    End If

Finalmente, comprobamos si enX o enY han cambiado para animar el sprite (cambiar de frame):

    If enCx <> enX (i) Or enCy <> enY (i) Then
        enFrame (i) = 4 - enFrame (i)
    End If
End Sub

Bicharracos que pasan de tu culo

Estos bicharracos se moverán enCt pasos en la dirección que indique enDir. Hemos decidido que enDir = 0 significa que no se moverá, enDir = 1 será hacia arriba, 2 hacia abajo, 3 hacia la izquierda, y 4 hacia la derecha. enCt se decrementará en cada frame del juego y cuando llegue a 0 se volverá a elegir una dirección en enDir y una nueva cuenta en enCt. Como enCt comienza valiendo 0, esto será precisamente lo primero que se haga. El nuevo valor de enCt será un número aleatorio entre BICHMINPASOS y BICHMAXPASOS, como dijimos anteriormente.

Lo que hay que hacer, por tanto, es ver si enCt vale 0, en cuyo caso habrá que elegir nueva dirección en enDir y una nueva cuenta en enCt, o vale distinto de 0, en cuyo caso habrá que avanzar en la dirección de enDir y decrementar enCt:

Sub iaPasoDeTuCulo (i as uByte)
    If enCt (i) = 0 Then
        enCt (i) = Int (Rnd * (BICHMAXPASOS - BICHMINPASOS)) + BICHMINPASOS
        enDir (i) = Int (Rnd * 5)

La expresión en enCt sirve para calcular un número al azar entre BICHMINPASOS y BICHMAXPASOS. Apuntároslo: es un truco BASIC muy util.

En la otra rama del IF, simplemente decrementaremos enCt y posteriormente, según el valor de enDir, intentaremos avanzar en la dirección correspondiente siempre que sea posible (de nuevo: comprobando las colisiones según el diagramilla):

    Else 
        enCt (i) = enCt (i) - 1
        If enDir (i) = 1 Then
            ' Arriba
            If enY (i) > 0 _
                And getCharBehaviourAt (enX (i), enY (i) - 1, nPant) < 4 _
                And getCharBehaviourAt (enX (i) + 1, enY (i) - 1, nPant) < 4 Then
                enY (i) = enY (i) - 1
            Else
                enCt (i) = 0
            End If
        ElseIf enDir (i) = 2 Then
            ' Abajo
            If enY (i) < MAPSCREENHEIGHT + MAPSCREENHEIGHT - 2 _
                And getCharBehaviourAt (enX (i), enY (i) + 2, nPant) < 4 _
                And getCharBehaviourAt (enX (i) + 1, enY (i) + 2, nPant) < 4 Then
                enY (i) = enY (i) + 1
            Else
                enCt (i) = 0
            End If
        ElseIf enDir (i) = 3 Then
            ' Izquierda
            If enX (i) > 0 _
                And getCharBehaviourAt (enX (i) - 1, enY (i), nPant) < 4 _
                And getCharBehaviourAt (enX (i) - 1, enY (i) + 1, nPant) < 4 Then
                enX (i) = enX (i) - 1
            Else
                enCt (i) = 0
            End If
        ElseIf enDir (i) = 4 Then
            ' Derecha
            If enX (i) > 0 _
                And getCharBehaviourAt (enX (i) + 2, enY (i), nPant) < 4 _
                And getCharBehaviourAt (enX (i) + 2, enY (i) + 1, nPant) < 4 Then
                enX (i) = enX (i) + 1
            Else
                enCt (i) = 0
            End If
        End If

Como vemos, las comprobaciones de colisiones/borde de la pantalla avanzan al muñeco en la dirección correcta si es posible y, en caso contrario, ponen enCt a 0 para volver a elegir una dirección.

Finalmente tendremos que animar al sprite (cambiar de frame). Esto lo haremos solo si la dirección en enDir es diferente de 0, ya que si es 0 no nos estaremos moviendo:

        If enDir (i) <> 0 Then
            enFrame (i) = 4 - enFrame (i)
        End If
    End If
End Sub

¡Y listo! Ya tenemos la inteligencia artificial lista y funcionando. Cuando lo probéis, os daréis cuenta de que existe cierto parpadeo de los sprites si hay al menos tres y si están en la parte superior de la pantalla. Eso queda feo. Pero no os preocupéis: en el siguiente capítulo explicaremos por qué ocurre esto y modificaremos la biblioteca fourspriter para solucionarlo.

Pulsa aquí para descargar el paquetito de este capítulo.

SUVLEIR 3.0 (Super Ultra Vector Library Experience Inspire Redux 3.0)

Vamos a hacer un pequeño paréntesis en las entregas del tutorial que tenemos entre manos para hablar del lanzamiento de este producto, una biblioteca para ZX Spectrum que permite mostrar sencillas imagenes vectoriales en pantalla, que acaba de llegar a su tercera versión con dos novedades importantes:

  • Nueva interfaz ZX Basic. Britlion ha portado a ZX Basic la rutina PFill de Alvin Albrecht, rutina de splib2 que SUVLEIR empleaba para realizar los rellenos con patrón empleados en los diseños. Esto me ha permitido crear un gfxparser nativo de ZX Basic, con lo que emplear estos dibujos en programas de ZX Basic es un juego de niños.
  • Nueva capa de durezas. No, no tiene nada que ver con los callos en la planta del pie, sino con una capa «oculta» asociada a nuestra imagen en la que podemos especificar qué caracteres son traspasables y qué caracteres no lo son. Esto nos sirve para poder usar los diseños que creemos como fondos de un juego.

Como tenemos muy frescas las rutinas de movimiento cenital del tutorial de programación en ZX Basic (ver este capítulo), vamos a emplear esos conceptos para explicar cómo podemos usar SUVLEIR 3.0 para generar las pantallas de fondo de nuestros juegos.

Antes que nada, nos vamos a pasar por los foros de Mojonia para descargar SUVLEIR 3.0.

Vemos que el paquete de la biblioteca viene con tres directorios. Nosotros vamos a necesitar el contenido de dos de ellos: cdraw, donde está el programa para diseñar los gráficos y asociarles un mapa de durezas, y gfxparser_Bas, donde está el código que necesitaremos para interpretar los diseños desde nuestro programa ZX Basic.

Como nos gusta ser ordenados, crearemos una carpeta de proyecto con subcarpetas /dev, /gfx y /util. Por lo pronto, copiaremos en /dev los archivos BASIC de gfxparser_Bas (los archivos gfxparser.bas y SPPFill.bas). En /gfx copiaremos un spriteset, que tendremos que diseñar de un modo especial (invertido) que explicaremos más abajo. Por último, en /util copiaremos el contenido de la carpeta cdraw, esto es, el programa para diseñar las pantallas.

También copiaremos en /dev una versión especial de fourspriter, fsp2.1.xor.bas, que podéis descargar desde aquí. Es una modificación de fourspriter 2.1 con la particularidad de que mezcla con XOR los sprites con el contenido de la pantalla.

Haciendo un spriteset

Hemos dicho que el spriteset iba a ser un poco particular: normalmente, los diseños de cdraw se mostrarán con un PAPER claro y un INK oscuro y, por lo tanto, tendremos que diseñar nuestros sprites de acuerdo con esto. Lo más sencillo es pintarlos así en tu programa de gráficos preferido, y, cuando termines, invertir la imagen para que quede el fondo negro y la tinta blanca. Así, la importación con SevenuP no dará ningún problema (podremos hacerla directamente sin tener que tocar nada). El spriteset debería ser algo parecido a esto (usaremos los 64 primeros bloques para animar a nuestro protagonista):

Lo reordenamos con reordenator.exe (que puedes encontrar aquí) y lo importamos con SevenuP y convertimos a BASIC tal y como se explica en este capítulo del tutorial de ZX Basic y fourspriter.

Dibujando las pantallas con cdraw

Cdraw es un editor bastante rudimentario. Lo programé en dos patás sin echarle demasiada cuenta porque detesto programar aplicaciones. Desde aquí hago un llamado a un alma caritativa que se ofrezca para hacer un cdraw mejor. Yo, sinceramente, paso de mejorarlo.

Podemos usar cdraw para dibujar las pantallas de nuestro juego. El editor está preparado para mostrar una imagen fondo.pcx cuando pulsamos la tecla «D». Por defecto, este fondo.pcx es una rejilla de 8×8 que nos viene de perlas. Lo activamos, y dibujamos nuestro fondo fijándonos en la rejilla, más o menos, ya que luego la colisión tendrá la resolución de dicha rejilla.

Posteriormente, desactivamos el fondo (volviendo a pulsar «D») y rellenamos nuestra imagen. Recordemos que cada color equivale a un tramado, cuanto más oscuro sea el color, más oscuro será el tramado. El 4, por ejemplo, es el tramado al 50%, o sea, el del tablero de ajedrez. Cualquier número mayor será más claro (con más blanco y menos negro) y cada número menor será más oscuro (con más negro y menos blanco), salvo la trama 5 que es un diseño de ladrillos. Las tramas se pueden cambiar muy fácilmente modificando gfxparser.bas, por cierto.

Para terminar, tenemos que crear la capa de durezas. Para ello, activamos el modo de durezas pulsando G, con lo que nos aparecerá una rejilla sobre nuestro dibujo. En este modo, haciendo click con el botón izquierdo marcamos una celda como «obstáculo», y con el botón derecho la marcamos como «traspasable». Así, con paciencia, vamos construyendo el tema…

Cuando hayamos terminado, pulsamos «S», que generará import.bin en el mismo directorio, y luego ESC para salir. Ahora cogemos ese import.bin, lo renombramos a pant_01.bin, por ejemplo, y lo copiamos en /dev. Repetiremos el proceso para todas las pantallas que queramos hacer.

Montando las pantallas en nuestro proyecto

Lo primero será crear el archivo principal del proyecto, que llamaremos test_suvleir.bas, por ejemplo. Lo primero que tiene que haber en este archivo es una ristra de include once para incluir la versión modificada de fourspriter, el módulo gfxparser.bas, nuestro spriteset.bas y un archivo pantallas.bas que crearemos seguidamente.

' Prueba SUVLEIR 3.0 + Fourspriter XOR
#include once "fsp2.1.xor.bas"
#include once "gfxparser.bas"
#include once "spriteset.bas"
#include once "pantallas.bas"

Ahora vamos a introducir las pantallas en nuestro proyecto. Para ello creamos pantallas.bas, que será donde incluyamos los binarios de cada pantalla, cada uno de ellos precedido de una etiqueta para poder referenciarlas luego desde el código. Como nosotros sólo tenemos una pantalla, nuestro pantallas.bas tendría esta pinta:

' Pantallas bas
Sub scrDummyContainer
pantalla01:
Asm
   Incbin "pant_01.bin"
End Asm
End Sub

Para añadir más pantallas, solo hay que añadir más etiquetas y más bloques asm dentro del Sub scrDummyContainer. La etiqueta que colocamos (pantalla01) será la que empleemos para apuntar a la pantalla que queramos mostrar cuando llamemos al parseador.

Mostrando una pantalla

Para mostrar una pantalla hay que llamar a la función gpParseBinary, que se encuentra en gfxparser.bas. Vamos a probarlo. Nos vamos a nuestro archivo principal del proyecto, test_suvleir.bas, y escribimos este código:

' Prueba
Border 0: Paper 7: Ink 0: Cls
gpParseBinary (@pantalla01)

Como veis, simplemente tenemos que llamar a la función gpParserBinary pasándole la dirección (con @) de la etiqueta que hemos colocado justo antes del bloque Asm que contiene el Incbin que importa nuestra pantalla. Si compilamos y ejecutamos nuestro proyecto (con un zxb -t -B -a test_suvleir.bas), veremos nuestro diseño en pantalla.

Cómo se usa la capa de durezas

La capa de durezas se incluye al final de la imagen, comprimida en RLE. Cuando llamamos a gpParseBinary, la capa asociada a la imagen que se muestra se descomprime y se almacena en un buffer interno. A partir de ese instante, podremos consultar la dureza de la casilla situada en (x, y) haciendo una llamada a gpisHard:

If gpisHard (x, y) Then Print "obstáculo en ";x;",";y;".": End If

Empleando esta función, por lo tanto, podremos modificar la rutina de movimiento que hemos visto en el tutorial para que use esta función a la hora de comprobar si nuestro muñeco puede o no avanzar. Quedaría algo así como:

Sub muevePibe ()
    '' Detectar "O": Sexta semifila, bit 1
    If (In (57342) bAnd 2) = 0 Then
        doStep ()
        pFacing = 48
        ' Puedo moverme?
        If pX > 0 And Not gpisHard (pX - 1, pY) _
            And Not gpisHard (pX - 1, pY + 1) 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 Not gpisHard (pX + 2, pY) _
            And Not gpisHard (pX + 2, pY + 1) 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 Not gpisHard (pX, pY - 1) _
            And Not gpisHard (pX + 1, pY - 1) 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 Not gpisHard (pX, pY + 2) _
            And Not gpisHard (pX + 1, pY + 2) Then
            pY = pY + 1
        End If
    End If
End Sub

Como vemos, la adaptación es nimia. Con esto y un bizcocho, tendremos a nuestro muñeco andando por la pantalla. Bueno, antes tenemos que hacer los típicos manejes y escribir un bucle principal, pero todo eso lo podéis ver en el paquetito que podéis descargar pulsando aquí.

Espero que esto os sirva para probar una nueva manera de generar las pantallas de un juego.