Sprites en Z88DK (III): La librería SpritePack

En artículos anteriores de la revista hemos visto como utilizar las funciones proporcionadas de base con la librería z88dk para la creación de sprites, aplicándolas para crear la semilla de lo que podría ser nuestro propio juego de coches. Sin embargo, nos encontrábamos con un par de contratiempos que, aunque no se indicaron de forma explícita, los podríamos descubrir a la hora de crear nuestras propias aplicaciones. Por una parte, la complejidad del diseño de los propios sprites; de hecho, nos teníamos que basar en una aplicación (sólo para Linux) para la creación de los mismos. Por otra parte, cambiar los atributos de color y fondo de los sprites, así como transparencias, es un tema no especialmente sencillo que ni siquiera se trató.

Para hacernos la vida más fácil a la hora de programar usando sprites disponemos de la librería Sprite Pack; la página principal del proyecto es http://www.geocities.com/aralbrec/. Se trata de piezas de código escritas en ensamblador, con una interfaz que puede ser usada desde C para su compilación con las herramientas de z88dk. Esto en cristiano quiere decir que podremos llamar a las funciones de la librería Sprite Pack desde nuestros programas z88dk de forma totalmente transparente, pero teniendo la seguridad de que el código será lo más eficiente posible.

Sprite Pack es una librería multipropósito, no centrada únicamente en la creación de sprites. Se compone de varios módulos distintos: creación de sprites, rutinas de entrada, intersecciones de rectángulos, tipos abstractos de datos, compresión de datos, API de interrupciones, etc. No solo podemos utilizarla con el Spectrum, sino que en teoría podríamos emplear todo este código en cualquier plataforma Z80, aunque existen dos módulos demasiado específicos que solo dispondremos para Spectrum y Timex, el de creación de sprites y el de lectura de dispositivos de entrada.

Durante los siguientes artículos nos centraremos especialmente en el módulo de sprites, con la idea de ser utilizado en el seno de nuestras aplicaciones z88dk para Spectrum, aunque durante el desarrollo de nuestros ejemplos tendremos que ir usando también algunas pequeñas partes del resto de los módulos. Vamos a dar un pequeño paso atrás con respecto a entregas anteriores, pues vamos a aprender de nuevo a dibujar sprites desde el principio, aunque utilizando Sprite Pack en esta ocasión. Podría parecer que esto es un poco inútil, pero ya veremos más adelante que las ventajas de utilizar esta capa por encima de z88dk no tardarán en llegar.

Lo primero que debemos hacer es descargar la librería de su página (en el momento de escribir este artículo, la última versión era la 2.2):

http://www.geocities.com/aralbrec/

Tras obtener el fichero comprimido, lo descomprimimos en cualquier directorio de trabajo. El resultado será la carpeta z88dk, en cuyo interior encontraremos a su vez otro directorio llamado work, en el que en último lugar descubriremos un nuevo directorio llamado splib2, en el que nos deberemos situar para realizar la compilación.

Al tratarse de una librería para el manejo, entre otras cosas, de sprites de la pantalla, debemos comprobar que el código de la librería se va a compilar para las características peculiares del Spectrum antes de continuar. Lo único que tendremos que hacer será editar el fichero SPconfig.def, que es el fichero de configuración de la compilación, y asegurarnos de que el valor de DISP_SPECTRUM es 1 y el valor del resto de variables DISP_ (DISP_HICOLOUR, DISP_HIRES y DISP_TMXDUAL) es 0.

Sprite Pack incluye un fichero Makefile.bat que creará la librería en una máquina Windows. Solamente sería necesario en este tipo de entorno que se abriera una ventana de MS-DOS, se acudiera al directorio donde está el código fuente, y tras inicializar las variables de entorno que permiten compilar con z88dk, teclear Makefile. En Linux es un poco más complicado, y deberemos modificar ese archivo para que funcione. Para ahorrar trabajo al lector, se incluye un fichero Makefile.sh, equivalente al Makefile.bat para Windows, que puede ser usado en Linux junto a este artículo. No se debe olvidar el proporcionar permiso de ejecución a dicho fichero para que lo podamos utilizar. El fichero sp.lst, que se encuentra en el directorio raíz del código fuente de la librería (junto a makefile.bat) tambien debe ser modificado para Linux; junto a este artículo se adjunta igualmente este archivo para poder ser utilizado en este sistema.

Sea cual sea el sistema operativo que utilicemos, como resultado obtendremos un nuevo fichero splib2.lib, que deberemos copiar al interior del directorio lib\clibs dentro de nuestro directorio de instalación de z88dk. A su vez, deberemos copiar el fichero spritepack.h en el directorio include, también dentro del directorio de isntalación de z88dk. ¡Ya tenemos la librería lista para ser usada!. Solo será necesario añadir la línea #include “spritepack.h” en cada uno de los archivos .c en los que queramos hacer uso de las funcionalidades de la libreria, y compilarlos añadiendo -lsplib2 como uno de los parámetros de zcc.

Sprite Pack tiene una forma particular de actualizar la pantalla. Realizar una actualización completa y simultánea de toda ella simultáneamente haría necesario el uso de rutinas en ensamblador demasiado específicas, lo cual chocaría con el objetivo real de la librería, que se pretende que sea multiplataforma. Es por ello que lo que realmente se hace es actualizar únicamente las porciones de la pantalla donde se ha producido algún cambio. Se dirá que aquellas partes de la pantalla que queramos redibujar deberán ser invalidadas. Aquellas partes que no queramos redibujar serán regiones validadas. Luego lo veremos con un ejemplo y lo entenderemos mejor, pero de momento nos debemos hacer a la idea de que nunca podremos hacer juegos con scroll total, ya sea horizontal o vertical, con esta librería. Deberemos centrarnos en juegos de plataformas o juegos limitados donde toda la acción se desarrolla sin scroll.

Otros conceptos que deberemos tener claros son el de backtile y sprite. Comprender la diferencia entre ambos es necesario para desarrollar nuestras aplicaciones. La pantalla está dividida en un array de tamaño 32×24 de celdas de caracteres (a su vez de tamaño 8×8). Cada una de estas celdas podrá contener sólo un backtile y uno o más sprites. Los backtiles se pueden entender como el “fondo”, y en realidad se trata de UDGs coloreados de tamaño 8×8, que son escritos en la pantalla de la misma forma en la que lo son los UDGs en BASIC. Por otra parte, los sprites podrán ocupar cualquier posición de la pantalla, en incrementos de un pixel, y su tamaño podrá ser mayor que 8×8, aunque no tendremos tanta libertad como con z88dk y únicamente podrán tener tamaños múltiplos, tanto en número de filas como de columnas, de 8.

De momento nos centraremos en los backtiles. La función básica para dibujarlos es sp_PrintAtInv, que además de dibujar el carácter con el color de tinta y papel que deseemos, invalidará la celda donde haya sido dibujado para que se redibuje al actualizar la pantalla. Existe otra función, sp_PrintAt, que hace exactamente lo mismo, pero sin invalidar la celda donde el backtile es dibujado. Todos los cambios en la pantalla se realizarán de forma simultánea al llamar a la función sp_updateNow.

Veamos un ejemplo. El siguiente código se encarga de dibujar letras 'x' en posiciones al azar de la pantalla, cambiando el color de la tinta y el papel, también al azar.

#include <stdlib.h>
#include <spritepack.h>
 
#pragma output STACKPTR=61440
 
main()
{
   #asm
   di
   #endasm
   sp_InitIM2(0xf1f1);
   sp_CreateGenericISR(0xf1f1);
   #asm
   ei
   #endasm
 
 
   sp_Initialize(INK_BLACK | PAPER_WHITE, ' ');
   while(1) {
           sp_UpdateNow();
           if (rand()%10 > 5)
                   sp_PrintAtInv(rand()%24, rand()%32, INK_RED | PAPER_CYAN, 'x');
           else
                   sp_PrintAtInv(rand()%24, rand()%32, INK_CYAN | PAPER_RED, 'x');
           sp_Pause(20);
   }
}

Veamos línea por línea de qué va todo esto. Las dos primeras se corresponden con las sentencias #include necesarias; en este caso la librería estándar y el archivo de cabecera de la librería Sprite Pack.

La sentencia #pragma deberemos incluirla siempre al principio de nuestros programas escritos para la librería Sprite Pack, antes del método main. Lo que se dice con ella es que se desea comenzar la ejecución del programa con el puntero de la pila del Z80 apuntando a la dirección 61440. Al hacerlo de esta forma, evitaremos que la función sp_initialize destruya cierta información de la memoria.

Las primeras líneas de código en el interior de main(), entre el primer #asm y el último #endasm tendremos que ponerlas también siempre en nuestros programas que usen Sprite Pack. No es necesario entrar en detalle, pero diremos que este código tiene algo que ver con deshabilitar interrupciones para que ciertas funciones como sp_Invalidate funcionen.

Y por fin comienzan las líneas interesantes. La función sp_Initialize es necesaria para que el módulo de sprites de la librería comience a funcionar. En este caso, los parámetros hacen que la pantalla se inicialice escribiendo espacios en blanco, con color de fondo (paper) blanco y tinta negra (ink).

A continuación nos introducimos en el bucle principal, que se va a ejecutar de forma ininterrumpida por toda la eternidad (a menos que detengamos la ejecución del programa). La estructura es muy sencilla. Primero se llama a sp_UpdateNow para que se redibujen las celdas de la pantalla donde hubo algún cambio. A continuación se obiene un número al azar entre 1 y 10 (mediante la función rand()), y en función de su valor llamaremos a sp_PrintAtInv con unos parámetros u otros.

El método sp_PrintAtInv dibuja un backtile en la pantalla e invalida la posición donde dicho backtile ha sido colocado, para que la celda correspondiente sea redibujada tras llamar a sp_UpdateNow. Los dos primeros parámetros indican la posición del backtile (que también los obtenemos al azar), a continuación el color de papel y de tinta (con la misma sintaxis que en el caso de la función sp_Initialize) y por último indicamos el carácter a escribir. Este carácter se corresponde con un UDG, como en BASIC. De momento no hemos asociado ningún UDG a la letra x, por lo que se mostrará en pantalla será la letra x apareciendo en posiciones al azar, teniendo o bien color de tinta rojo y papel cyan o al revés.

La última línea del bucle, sp_Pause, introduce un pequeño retardo. Actúa exactamente igual que el comando PAUSE de BASIC.

Espectaculares efectos gráficos conseguidos con la librería Sprite Pack

En esta sección vamos a dibujar un sprite simple sobre un fondo un poco más complejo. Para comprender cómo podemos crear un fondo más complejo, hemos de recordar que los backtiles pueden entenderse como si fueran UDGs de BASIC; sin embargo, hasta ahora solo hemos utilizado caracteres alfanuméricos. ¿De qué forma definimos gráficos para estos backtiles?

Mediante la función sp_TileArray asociamos un determinado UDG 8×8 a un carácter, de tal forma que al escribir dicho carácter como backtile en la pantalla, aparecerá su UDG correspondiente. Esta función recibe como parámetro el carácter al que queremos asociar el UDG y un array de tipo uchar con la definición del UDG. Ese array contendrá un valor hexadecimal por cada fila del UDG, utilizando la misma notación que vimos en capítulos anteriores para la creación de sprites de tamaño 8×8 con z88dk. Por ejemplo, observemos el siguiente código:

uchar fondo[] = {0x55,0xaa,0x55,0xaa,0x55,0xaa,0x55,0xaa};
 
sp_TileArray(' ', fondo);
sp_Initialize(INK_WHITE | PAPER_BLACK, ' ');

Con la primera línea creamos un array llamado fondo que contendrá la definición de un UDG de tipo “tablero de ajedrez” (un pixel negro, un pixel blanco, un pixel negro, un pixel blanco, y así sucesivamente). Con la instrucción sp_TileArray asociamos este UDG al carácter de espacio en blanco, de tal forma que cuando en la línea siguiente llamamos a sp_Initialize, el fondo se llenará de copias del UDG asociado a dicho carácter. De esta forma tan sencilla podremos tener fondos más detallados.

¿Y cómo definimos un sprite? De forma mucho más fácil a la vista en artículos anteriores, en los que se usaba z88dk sin ningún añadido. Nada más simple que una notación como la siguiente:

#asm
 
._sprite1
defb @00111100, @11000011
defb @01000010, @10000001
defb @10000001, @00000000
defb @10100101, @00000000
defb @10000001, @00000000
defb @10011001, @00000000
defb @01011010, @10000001
defb @00111100, @11011011
 
#endasm

Como se puede observar, volvemos a hacer uso de código ensamblador empotrado en el interior de nuestro código C, entre las directivas #asm y #endasm. En este caso concreto estamos definiendo un único sprite 8×8, para el cual necesitaremos dos bloques de bits de ese tamaño. El de la izquierda es el dibujo en sí mismo del sprite, que en nuestro caso en una pequeña carita con la boca abierta. El bloque de la derecha indica las transparencias; en aquellas posiciones donde el valor sea 1, el sprite será transparente y se verá lo que haya en el fondo. Evidentemente, es necesario que dichas posiciones tengan un valor 0 en el bloque de la izquierda.

A este bloque de datos que marcan un sprite de 8×8 le hemos llamado sprite1. Este nombre nos servirá para referenciar este código ensamblador desde el código C. Para ver cómo dibujar el sprite en la pantalla, nada mejor que un pequeño ejemplo:

#include <spritepack.h>
#pragma output STACKPTR=61440
 
extern struct sp_Rect *sp_ClipStruct;
#asm
LIB SPCClipStruct
._sp_ClipStruct         defw SPCClipStruct
#endasm
 
 
extern uchar sprite1[];
uchar fondo[] = {0x55,0xaa,0x55,0xaa,0x55,0xaa,0x55,0xaa};
 
 
void *my_malloc(uint bytes)
{
           return sp_BlockAlloc(0);
}
 
 
void *u_malloc = my_malloc;
void *u_free = sp_FreeBlock;
 
 
 
main()
{
       struct sp_SS *bicho;
 
       #asm
       di
       #endasm
       sp_InitIM2(0xf1f1);
       sp_CreateGenericISR(0xf1f1);
       #asm
       ei
       #endasm
 
       sp_TileArray(' ', fondo);
       sp_Initialize(INK_WHITE | PAPER_BLACK, ' ');
       sp_Border(BLACK);
       sp_AddMemory(0, 255, 14, 0xb000);
 
       bicho = sp_CreateSpr(sp_MASK_SPRITE, 1, sprite1, 1, TRANSPARENT);
       sp_MoveSprAbs(bicho, sp_ClipStruct, 0, 10, 15, 0, 0);
 
       sp_UpdateNow();
 
       while(1);
}
 
#asm
 
._sprite1
defb @00111100, @11000011
defb @01000010, @10000001
defb @10000001, @00000000
defb @10100101, @00000000
defb @10000001, @00000000
defb @10011001, @00000000
defb @01011010, @10000001
defb @00111100, @11011011
 
#endasm

Comentemos línea por línea el programa. La sentencia #pragma ya la conocemos del ejemplo anterior, así que no hace falta que nos detengamos en ella. A continuación se define una estructura de la forma siguiente:

extern struct sp_Rect *sp_ClipStruct;
#asm
LIB SPCClipStruct
._sp_ClipStruct         defw SPCClipStruct
#endasm

Esta estructura tan confusa está sacada de spritepack.h y define un rectángulo que cubre toda la superficie de la pantalla. Existen muchas definiciones como ésta dentro de dicho fichero de cabecera. Para qué sirven lo veremos mucho más adelante, cuando tratemos el tema de las colisiones. De momento deberemos saber que lo necesitamos para poder dibujar el sprite en la pantalla.

La línea extern uchar sprite1[]; crea el array que contendrá la información del sprite. Este array se llena con los datos definidos entre las cláusulas #asm y #endasm al final del programa, después de la etiqueta ._sprite1, y contendrá la definición del sprite, de la misma forma en la que en la línea siguiente se crea un array de tipo uchar llamado fondo con la definición de un UDG para asociarlo al backtile de fondo.

El siguiente código:

void *my_malloc(uint bytes)
{
           return sp_BlockAlloc(0);
}
 
 
void *u_malloc = my_malloc;
void *u_free = sp_FreeBlock;

entra dentro de lo que estaremos obligados a insertar en nuestro programa cada vez que queramos añadirle sprites, aunque no entendamos muy bien de qué se trata. Se obtiene a partir del módulo de manejo de memoria de Sprite Pack y sirve para que el programa pueda obtener memoria bajo demanda. Esto es necesario debido a la forma en que la pantalla es actualizada al usar esta librería. Además de las estructuras creadas por el programador, la librería creará otras propias durante la ejecución y la memoria para dichas estructuras se obtendrá por medio de esta función.

Y por fin comienza el método main. En primer lugar definimos la variable que va a contener el sprite que vamos a mostrar por pantalla. Esta variable es de tipo struct sp_SS, una estructura que contiene diversa infomación sobre un sprite, como su localización y su tamaño. A continuación, entre las directivas #asm y #endasm, el código ensamblador necesario en cada uno de nuestros programas Sprite Pack del que hablamos en la sección anterior.

Y continuamos con las dos líneas siguientes, cuyo funcionamiento ha sido explicado anteriormente:

p_TileArray(' ', fondo);
sp_Initialize(INK_WHITE | PAPER_BLACK, ' ');

La línea posterior es nueva, y permite definir el color del borde de la pantalla (como la instrucción BORDER de BASIC). A continuación otra línea un tanto complicada, correspondiente a la llamada a la función sp_AddMemory, con la que se reserva memoria para los sprites. En concreto estamos reservando 255 bloques de 14 bytes a partir de la dirección 0xb000 que se sabe que está libre. Para cada sprite deberíamos reservar un par de bloques de 14 bytes, por lo que 255 es algo desproporcionado para nustro ejemplo. Si nos vemos apurados de memoria podemos reservar menos en este paso.

Las líneas que más nos interesan de este ejemplo son las siguientes:

bicho = sp_CreateSpr(sp_MASK_SPRITE, 1, sprite1, 1, TRANSPARENT);
sp_MoveSprAbs(bicho, sp_ClipStruct, 0, 10, 15, 0, 0);

Con la primera de ellas creamos el sprite en si mismo. El resultado de la llamada se almacenará en la variable de tipo struct sp_SS bicho declarada anteriormente, que usaremos en el resto de métodos con los que queramos manipular dicho sprite. El primer parámetro (para el que nosotros hemos usado MASK, pero que podría ser XOR, OR o LOAD) indica la forma en la que el sprite se dibuja en la pantalla. El tipo MASK es el más lento de dibujar, y utiliza una máscara para determinar que porciones del sprite serán transparentes (tal como se ha visto anteriormente). Los tipos OR y XOR se corresponderían con los ya vistos en artículos anteriores. El siguiente parámetro indica el número de rejillas 8×8 que forman el sprite (se hablará de esto más adelante). A continuación, el nombre del array de tipo uchar que contiene la definición del sprite. En tercer lugar, el plano que ocupará el sprite en pantalla. El valor de este parámetro podrá valer entre 0 y 63. Cuanto mas bajo sea este valor más cerca del usuario se encontrará el sprite, de tal forma que sprites con valores bajos del tercer parámetro serán situados “encima” de sprites con valores altos, tapándolos. El cuarto parámetro tiene que ver con el color, y lo veremos también más adelante. Hemos usado el valor TRANSPARENT para que no influya en el color del fondo.

Situaremos el sprite creado sobre la pantalla mediante el uso de la función sp_MoveSprAbs, que desplaza nuestro personaje a una posición absoluta de la pantalla. Podríamos utilizar sp_MoveSprRel, que acepta el mismo número de parámetros, con la diferencia de mover el sprite a una posición relativa a la actual. Por lo tanto, siempre usaremos sp_MoveSprAbs para colocar al sprite en su posición inicial.

Como primer parámetro, el sprite a mover. Como segundo parámetro, el rectángulo que hace referencia a toda la pantalla definido anteriormente. El tercer parámetro hace referencia a la animación, y tampoco hablaremos de él en esta ocasión. El cuarto y el quinto, al bloque de la pantalla donde situaremos el sprite. Decíamos anteriormente que la pantalla estaba dividida en bloques de 8×8; pues bien, con estos dos parámetros indicamos la celda donde colocaremos nuestro sprite. Para conseguir una colocación más exacta, podemos usar los dos últimos parámetros, correspondientes al offset. Indican cuantos píxeles mover hacia la derecha y hacia abajo, respectivamente, a partir de la celda cuya posición es la indicada por los dos parámetros anteriores.

Por último, con sp_UpdateNow() redibujamos las porciones de la pantalla que lo necesiten, y con while(1) dejamos nuestro programa en constante ejecución, hasta el resto de los días, o hasta que lo detengamos. El resultado se puede observar en la siguiente captura de pantalla.

Un terrible ser intergaláctico espera ansioso la orden de atacar

En este apartado veremos un pequeño ejemplo que introduce algunos conceptos interesantes, como la creación de sprites de un tamaño superior a 8×8 y el uso del movimiento relativo. Vamos a hacer aparecer una criatura en nuestra pantalla que se mueva de forma nerviosa y aleatoria.

Pero comencemos por el principio. ¿Cómo definimos sprites que contengan más de un bloque 8×8? Con Sprite Pack, los sprites grandes se definen por columnas. Podemos definir una columna de un sprite grande, compuesta por uno o más bloques de 8×8, bajo una misma etiqueta de código ensamblador. Un sprite de más de una columna se formará a partir de varias de estas definiciones en ensamblador.

Por ejemplo, mostramos como quedaría un sprite con un tamaño de dos bloques de alto por dos bloques de ancho listo para ser usado con Sprite Pack:

#asm
 
._bicho1
defb @00000011, @11111100
defb @00000100, @11111000
defb @00001000, @11110000
defb @00001011, @11110000
defb @00001011, @11110000
defb @00001000, @11110000
defb @00001000, @11110000
defb @00000100, @11111000
 
defb @00000011, @11111100
defb @00001100, @11110011
defb @00001100, @11110011
defb @00011000, @11100111
defb @00011000, @11100111
defb @01111100, @10000011
defb @01111100, @10000011
defb @00000000, @11111111
 
._bicho2
defb @11100000, @00011111
defb @00010000, @00001111
defb @00001000, @00000111
defb @01101000, @00000111
defb @01101000, @00000111
defb @00001000, @00000111
defb @10001000, @00000111
defb @10010000, @00001111
 
defb @11100000, @00011111
defb @00011000, @11100111
defb @00011000, @11100111
defb @00001100, @11110011
defb @00001100, @11110011
defb @00111110, @11000001
defb @00111110, @11000001
defb @00000000, @11111111
 
#endasm

Como se puede observar, definimos dos columnas para el sprite, cada una de ellas formada a su vez por dos sprites de tamaño 8×8, incluyendo su máscara de transparencias. Para utilizar el sprite en el código deberíamos hacer algo similar a esto:

extern uchar bicho1[];
extern uchar bicho2[];
 
main()
{
	struct sp_SS *spriteBicho;
 
         spriteBicho = sp_CreateSpr(sp_MASK_SPRITE, 2, bicho1, 1, TRANSPARENT);
         sp_AddColSpr(spriteBicho, bicho2, TRANSPARENT);
         sp_MoveSprAbs(spriteBicho, sp_ClipStruct, 0, 10, 15, 0, 0);
}

Se debe hacer uso, en primer lugar, de sp_CreateSpr para asignar la primera columna del sprite a la variable de tipo struct sp_SS, y en segundo lugar, de sp_AddColSpr, tantas veces como columnas adicionales debamos añadir. En este caso, se ha usado un valor de 2 para el segundo parámetro de sp_CreateSpr, indicando que cada columna del sprite tendrá un tamaño de dos bloques de 8×8. Efectivamente, este segundo parámetro indica el número de bloques que tendrá cada columna de nuestro sprite; por eso en nuestro primer ejemplo le dimos valor 1. Una vez se ha creado el sprite con sp_CreateSpr y se han añadido las columnas correspondientes con sp_AddColSpr, se podrá tratar la estructura sp_SS resultante como un todo, tal como se demuestra en la llamada a sp_MoveSprAbs que sigue a las dos líneas anteriores.

Es importante destacar que todas las columnas de un mismo sprite deben de ser definidas de forma contigua en la memoria. Esto se traduce en que tenemos que definirlas de forma contigua también en nuestro código.

Sin embargo, hay algo que hasta ahora no hemos tenido en cuenta, y es debido a que no hemos movido nuestros sprites por la pantalla. Cuando trasladamos sprites usando el pixel y no el bloque como unidad de medida (los dos últimos parámetros de sp_MoveSprAbs y sp_MoveSprRel servían para esto) veremos como los sprites no son correctamente dibujados; solo se redibuja la parte del sprite más a la izquierda que cabe dentro de una misma celdilla de la pantalla. Un truco para evitar esto es crear sprites un poco más anchos y más altos de lo que realmente necesitamos. Para ello, añadimos una nueva columna en blanco, y en cada columna, un nuevo bloque en blanco al final. En el caso concreto de nuestro sprite 2×2 anterior, deberíamos definirlo de esta forma:

#asm
 
._bicho1
defb @00000011, @11111100
defb @00000100, @11111000
defb @00001000, @11110000
defb @00001011, @11110000
defb @00001011, @11110000
defb @00001000, @11110000
defb @00001000, @11110000
defb @00000100, @11111000
 
defb @00000011, @11111100
defb @00001100, @11110011
defb @00001100, @11110011
defb @00011000, @11100111
defb @00011000, @11100111
defb @01111100, @10000011
defb @01111100, @10000011
defb @00000000, @11111111
 
defb @00000000, @11111111
defb @00000000, @11111111
defb @00000000, @11111111
defb @00000000, @11111111
defb @00000000, @11111111
defb @00000000, @11111111
defb @00000000, @11111111
defb @00000000, @11111111
 
._bicho2
defb @11100000, @00011111
defb @00010000, @00001111
defb @00001000, @00000111
defb @01101000, @00000111
defb @01101000, @00000111
defb @00001000, @00000111
defb @10001000, @00000111
defb @10010000, @00001111
 
defb @11100000, @00011111
defb @00011000, @11100111
defb @00011000, @11100111
defb @00001100, @11110011
defb @00001100, @11110011
defb @00111110, @11000001
defb @00111110, @11000001
defb @00000000, @11111111
 
defb @00000000, @11111111
defb @00000000, @11111111
defb @00000000, @11111111
defb @00000000, @11111111
defb @00000000, @11111111
defb @00000000, @11111111
defb @00000000, @11111111
defb @00000000, @11111111
 
._bicho3
defb @00000000, @11111111
defb @00000000, @11111111
defb @00000000, @11111111
defb @00000000, @11111111
defb @00000000, @11111111
defb @00000000, @11111111
defb @00000000, @11111111
defb @00000000, @11111111
 
defb @00000000, @11111111
defb @00000000, @11111111
defb @00000000, @11111111
defb @00000000, @11111111
defb @00000000, @11111111
defb @00000000, @11111111
defb @00000000, @11111111
defb @00000000, @11111111
 
defb @00000000, @11111111
defb @00000000, @11111111
defb @00000000, @11111111
defb @00000000, @11111111
defb @00000000, @11111111
defb @00000000, @11111111
defb @00000000, @11111111
defb @00000000, @11111111
 
#endasm

Por lo tanto, nuestro sprite 2×2 se convierte en un sprite 3×3 al añadir una nueva columna a la derecha y un nuevo bloque en la parte inferior de cada columna. Nuestro código a la hora de usar el sprite debería ser en este caso algo más parecido a esto:

extern uchar bicho1[];
extern uchar bicho2[];
extern uchar bicho3[];
 
main()
{
         struct sp_SS *spriteBicho;
 
         spriteBicho = sp_CreateSpr(sp_MASK_SPRITE, 3, bicho1, 1, TRANSPARENT);
         sp_AddColSpr(spriteBicho, bicho2, TRANSPARENT);
         sp_AddColSpr(SpriteBicho, bicho3, TRANSPARENT);
         sp_MoveSprAbs(spriteBicho, sp_ClipStruct, 0, 10, 15, 0, 0);
}

Como queda patente, indicamos que el tamaño en bloques de cada columna es 3 al llamar a la función sp_CreateSPr, y además se llama dos veces a sp_AddColSpr para añadir la segunda y la tercera columna a nuestro bicho.

A continuación se muestra un ejemplo completo, con nuestro sprite moviéndose al azar por la pantalla, por medio de la función sp_MoveSprRel:

#include <spritepack.h>
#include <stdlib.h>
 
#pragma output STACKPTR=61440
 
extern struct sp_Rect *sp_ClipStruct;
#asm
LIB SPCClipStruct
._sp_ClipStruct         defw SPCClipStruct
#endasm
 
extern uchar bicho1[];
extern uchar bicho2[];
extern uchar bicho3[];
uchar hash[] = {0x55,0xaa,0x55,0xaa,0x55,0xaa,0x55,0xaa};
 
void *my_malloc(uint bytes)
{
           return sp_BlockAlloc(0);
}
 
void *u_malloc = my_malloc;
void *u_free = sp_FreeBlock;
 
 
main()
{
           char dx, dy, i;
           struct sp_SS *spriteBicho;
 
            #asm
            di
            #endasm
            sp_InitIM2(0xf1f1);
            sp_CreateGenericISR(0xf1f1);
            #asm
            ei
            #endasm
 
           sp_TileArray(' ', hash);
           sp_Initialize(INK_WHITE | PAPER_BLACK, ' ');
           sp_Border(CYAN);
           sp_AddMemory(0, 255, 14, 0xb000);
 
 
           spriteBicho = sp_CreateSpr(sp_MASK_SPRITE, 3, bicho1, 1, TRANSPARENT);
           sp_AddColSpr(spriteBicho, bicho2, TRANSPARENT);
           sp_AddColSpr(spriteBicho, bicho3, TRANSPARENT);
           sp_MoveSprAbs(spriteBicho, sp_ClipStruct, 0, 10, 15, 0, 0);
 
           while(1) {
                 sp_UpdateNow();
 
                 dx = dy = 1;
 
                if (rand()%2 == 0) // izquierda
                        dx = -dx;
                else if (rand()%2 == 0) // derecha
                        dx = 0;
                if (rand()%2 == 0) // arriba
                        dy = -dy;
                else if (rand()%2 == 0) // abajo
                        dy = 0;
 
                sp_MoveSprRel(spriteBicho, sp_ClipStruct, 0, 0, 0, dx, dy);
           }
}
 
#asm
 
defb @00000000, @11111111
defb @00000000, @11111111
defb @00000000, @11111111
defb @00000000, @11111111
defb @00000000, @11111111
defb @00000000, @11111111
defb @00000000, @11111111
 
._bicho1
defb @00000011, @11111100
defb @00000100, @11111000
defb @00001000, @11110000
defb @00001011, @11110000
defb @00001011, @11110000
defb @00001000, @11110000
defb @00001000, @11110000
defb @00000100, @11111000
 
defb @00000011, @11111100
defb @00001100, @11110011
defb @00001100, @11110011
defb @00011000, @11100111
defb @00011000, @11100111
defb @01111100, @10000011
defb @01111100, @10000011
defb @00000000, @11111111
 
defb @00000000, @11111111
defb @00000000, @11111111
defb @00000000, @11111111
defb @00000000, @11111111
defb @00000000, @11111111
defb @00000000, @11111111
defb @00000000, @11111111
defb @00000000, @11111111
 
._bicho2
defb @11100000, @00011111
defb @00010000, @00001111
defb @00001000, @00000111
defb @01101000, @00000111
defb @01101000, @00000111
defb @00001000, @00000111
defb @10001000, @00000111
defb @10010000, @00001111
 
defb @11100000, @00011111
defb @00011000, @11100111
defb @00011000, @11100111
defb @00001100, @11110011
defb @00001100, @11110011
defb @00111110, @11000001
defb @00111110, @11000001
defb @00000000, @11111111
 
defb @00000000, @11111111
defb @00000000, @11111111
defb @00000000, @11111111
defb @00000000, @11111111
defb @00000000, @11111111
defb @00000000, @11111111
defb @00000000, @11111111
defb @00000000, @11111111
 
._bicho3
defb @00000000, @11111111
defb @00000000, @11111111
defb @00000000, @11111111
defb @00000000, @11111111
defb @00000000, @11111111
defb @00000000, @11111111
defb @00000000, @11111111
defb @00000000, @11111111
 
defb @00000000, @11111111
defb @00000000, @11111111
defb @00000000, @11111111
defb @00000000, @11111111
defb @00000000, @11111111
defb @00000000, @11111111
defb @00000000, @11111111
defb @00000000, @11111111
 
defb @00000000, @11111111
defb @00000000, @11111111
defb @00000000, @11111111
defb @00000000, @11111111
defb @00000000, @11111111
defb @00000000, @11111111
defb @00000000, @11111111
defb @00000000, @11111111
 
#endasm

Ya deberíamos entender la práctica totalidad de este código, que mueve un sprite al azar por la pantalla, por lo que tan solo haremos dos apuntes:

  • Los movimientos indicados con sp_MoveSprRel son movimientos relativos, por lo que si queremos desplazarnos tan solo un pixel, ya sea a la izquierda o a la derecha, ya sea arriba o abajo (penúltimo y último parámetros respectivamente) usaremos valores +1 y -1.
  • Se ha añadido un bloque vacío antes de definir nuestro sprite, al final del código anterior; esto es así porque debemos asegurarnos de que haya un bloque en blanco encima de cada columna (manías del ensamblador generado por Sprite Pack).

 El terrible ser intergaláctico se ha hecho mayor y es más nervioso que antes...

Hemos aprendido las más básicas funcionalidades de Sprite Pack para la creación de sprites. Ya somos capaces de crear nuestros propios sprites (de forma más sencilla a la que se ha realizado en artículos anteriores) y de moverlos, aunque sea de manera aleatoria, por la pantalla (sin necesidad de hacer llamadas de ensamblador para el manejo de interrupciones, como en el ejemplo de los coches visto en el número anterior de MagazineZX). Por lo tanto, ganamos en facilidad de uso.

Vemos que no es necesario borrar un sprite usando la máscara XOR antes de volver a dibujarlo para simular movimiento, tal como hacíamos con z88dk sin Sprite Pack, y también observamos como somos capaces de añadir transparencias en los sprites de forma muy simple. Volvemos a ganar en facilidad de uso.

En el siguiente artículo añadiremos colores a nuestros sprites y aprenderemos a moverlos con el teclado. También veremos como borrar de forma efectiva un sprite. Si el espacio lo permite (y si no es así, que no cunda el pánico, pues se verá en números posteriores de la revista) codificaremos un simple juego funcional con todo lo aprendido.