LA LIBRERIA SPRITE PACK (II)
INTRODUCCIÓN
En la entrega anterior de la revista comenzamos a examinar la librería Sprite
Pack y aprendimos a dibujar elementos gráficos sobre la pantalla. También aprendimos
que la pantalla estaba dividida en 32x24 "celdas", cada una de las cuales podía contener
un tile (cada uno de los elementos que forman parte del fondo) y/o uno o más
sprites (gráficos que representaban los diferentes personajes o elementos de nuestro
juego). Incluso llegamos a crear un sprite que se movía al azar por la pantalla.
En esta ocasión vamos a añadir un par de sencillos detalles más antes de comenzar a
escribir nuestro propio juego; un código que luego podrá formar parte de un producto más
elaborado. En concreto veremos cómo hacer reaccionar nuestro programa ante la pulsación
de teclas por parte del usuario, y cómo mover un sprite utilizando este dispositivo de
entrada. También aprenderemos como añadir color a los sprites. Para nuestras
explicaciones haremos uso como base del código sprite2.c que se creó en la
anterior entrega, y en el que se definía un sprite de tamaño 2x1 que se desplazaba al
azar.
MOVIENDO LOS SPRITES CON EL TECLADO
Si queremos mover un sprite por la pantalla utilizando el teclado, lo primero que
deberemos hacer en nuestro programa es declarar una estructura del siguiente tipo:
La variable keys nos permitirá asociar teclas de nuestro teclado con los
diferentes controles de un joystick virtual haciendo uso de la función
sp_Lookup_Key, tal como se puede observar a continuación:
keys.up = sp_LookupKey('q');
keys.down = sp_LookupKey('a');
keys.right = sp_LookupKey('p');
keys.left = sp_LookupKey('o');
keys.fire = sp_LookupKey(' ');
|
Según este ejemplo, cada vez que pulsáramos 'q' en el teclado es como si estuviéramos
moviendo el joystick hacia arriba, al pulsar 'a' simulamos la dirección de abajo del
joystick, y así sucesivamente. Varias de estas teclas podrían ser pulsadas
simultáneamente sin ningún problema.
La función sp_JoyKeyboard devuelve una máscara de bits indicándonos qué controles
del joystick están accionados en ese momento debido a que sus correspondientes teclas
están pulsadas. La forma de interpretar el valor devuelto por esta función es igual que
con cualquier máscara de bits en C, y se puede contemplar en el siguiente ejemplo, que
consiste en el archivo sprite2.c de la anterior entrega modificado de tal forma
que movamos el sprite precisamente con la combinación de teclas indicada anteriormente.
Para ello realizamos movimientos relativos del sprite, cambiando cómo desplazamos el
sprite en x y en y (dx y dy respectivamente) según los controles de nuestro joystick
virtual que estén siendo accionados. El código nuevo respecto a la versión anterior del
archivo se muestra en rojo:
#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[];
struct sp_UDK keys;
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(BLUE);
sp_AddMemory(0, 255, 14, 0xb000);
keys.up = sp_LookupKey('q');
keys.down = sp_LookupKey('a');
keys.right = sp_LookupKey('p');
keys.left = sp_LookupKey('o');
keys.fire = sp_LookupKey(' ');
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();
i = sp_JoyKeyboard(&keys);
if ((i & sp_FIRE) == 0) {
dx = dy = 1;
} else {
dx = dy = 8;
}
if ((i & sp_LEFT) == 0)
dx = -dx;
else if ((i & sp_RIGHT) != 0)
dx = 0;
if ((i & sp_UP) == 0)
dy = -dy;
else if ((i & sp_DOWN) != 0)
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
|
Al probar el ejemplo nos habremos encontrado con un problema bastante grave: al no
controlar cuándo el sprite sobrepasa los límites de la pantalla, es posible desplazar
nuestro "bicho" al exterior y perder totalmente su control. Una forma de solucionarlo
sería añadir un nuevo control de tal forma que al accionarlo volviéramos a colocar a
nuestro bicho en el centro de la pantalla. Esto se puede conseguir realizando mapeados
adicionales de teclado a los del joystick virtual. Mediante sp_LookupKey se puede
asociar una tecla a una variable uint, de tal forma que más adelante, mediante el
uso de otra función adicional llamada sp_KeyPressed podremos saber si dicha tecla
está siendo pulsada en este momento.
|
¡Ya podemos mover a nuestro bicho con el
teclado!
|
El código anterior se ha modificado para añadir dos nuevas funcionalidades. Al pulsar la
tecla r, nuestro "bicho" volverá al centro de la pantalla. Por otra parte, la
tecla b va a permitir que cambiemos el color del borde. Las modificaciones se
muestran en rojo:
#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[];
struct sp_UDK keys; //NUEVO (TECLAS)
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;
uint reset,cambioBorde;
int borde = 1;
#asm
di
#endasm
sp_InitIM2(0xf1f1);
sp_CreateGenericISR(0xf1f1);
#asm
ei
#endasm
sp_TileArray(' ', hash);
sp_Initialize(INK_WHITE | PAPER_BLACK, ' ');
sp_Border(BLUE);
sp_AddMemory(0, 255, 14, 0xb000);
keys.up = sp_LookupKey('q');
keys.down = sp_LookupKey('a');
keys.right = sp_LookupKey('p');
keys.left = sp_LookupKey('o');
keys.fire = sp_LookupKey(' ');
reset = sp_LookupKey('r');
cambioBorde = sp_LookupKey('b');
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();
// TODO DENTRO DE ESTE WHILE ES NUEVO (TECLADO) MENOS EL UPDATE
i = sp_JoyKeyboard(&keys);
if ((i & sp_FIRE) == 0) {
dx = dy = 1;
} else {
dx = dy = 8;
}
if ((i & sp_LEFT) == 0)
dx = -dx;
else if ((i & sp_RIGHT) != 0)
dx = 0;
if ((i & sp_UP) == 0)
dy = -dy;
else if ((i & sp_DOWN) != 0)
dy = 0;
if (sp_KeyPressed(reset))
sp_MoveSprAbs(spriteBicho, sp_ClipStruct, 0, 10, 15, 0, 0);
else
sp_MoveSprRel(spriteBicho, sp_ClipStruct, 0, 0, 0, dx, dy);
if (sp_KeyPressed(cambioBorde))
if (borde == 1)
{
borde = 2;
sp_Border(RED);
}
else
{
borde = 1;
sp_Border(BLUE);
}
}
}
#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
|
Como veremos más adelante, deberemos implementar algún mecanismo para limitar la zona por
donde nuestros sprites se van a desplazar. La forma de hacer esto es mediante simples
comparaciones, comprobando que el lugar al que vamos a desplazar el sprite no este fuera
de la pantalla (o de la zona donde queremos que permanezca).
AÑADIENDO COLOR
Un paso importante para poder tener como resultado un videojuego vistoso y que entre por
los ojos es aprovechar los colores de nuestro Spectrum y disponer de una combinación
agradable de cromaticidad en los sprites de nuestro juego. Los colores también
permitirán distinguir con mayor facilidad nuestro personaje de los enemigos y el resto
de elementos de la pantalla. Ya vimos en entregas anteriores que en el caso de los tiles
era bastante sencillo modificar la tonalidad de la tinta y el papel:
sp_TileArray(' ', hash);
sp_Initialize(INK_WHITE | PAPER_BLACK, ' ');
|
pero en el caso de los sprites la cosa se complica un poco más y vamos a tener que dar
unas cuantas "vueltas" para llegar a nuestro objetivo. En concreto, deberemos hacer uso
de una función llamada sp_IterateSprChar, que recibe como parámetro una variable
de tipo struct sp_SS, que representa el sprite que queremos modificar, y el
nombre de una función que habremos definido anteriormente. Dicha función, al ser
llamada, obtendrá como parámetro una variable de tipo struct sp_CS, una
estructura muy interesante que nos va a permitir modificar diversas características del
sprite, entre ellas el color.
El siguiente código muestra, en color rojo, las modificaciones realizadas al programa
anterior para poder añadirle color al sprite de nuestro bicho, y que pasaremos a
explicar a continuación:
#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 *sp_NullSprPtr;
#asm
LIB SPNullSprPtr
._sp_NullSprPtr defw SPNullSprPtr
#endasm
extern uchar bicho1[];
extern uchar bicho2[];
extern uchar bicho3[];
struct sp_UDK keys;
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;
uchar n;
void addColour(struct sp_CS *cs)
{
if (n == 0) // Se rellena de arriba a abajo y de izquierda
// a derecha, incluyendo partes vacias del sprite
cs->colour = INK_BLACK | PAPER_WHITE;
else if (n == 1)
cs->colour = INK_BLUE | PAPER_BLACK;
else if (n == 2)
cs->colour = INK_RED | PAPER_GREEN;
else if (n == 3)
cs->colour = INK_YELLOW | PAPER_BLACK;
else if (n == 4)
cs->colour = INK_GREEN | PAPER_WHITE;
else
cs->colour = TRANSPARENT;
if (n > 5)
cs->graphic = sp_NullSprPtr;
n++;
return;
}
main()
{
char dx,dy,i
struct sp_SS *spriteBicho;
uint reset,cambioBorde;
int borde = 1;
#asm
di
#endasm
sp_InitIM2(0xf1f1);
sp_CreateGenericISR(0xf1f1);
#asm
ei
#endasm
sp_TileArray(' ', hash);
sp_Initialize(INK_WHITE | PAPER_BLACK, ' ');
sp_Border(BLUE);
sp_AddMemory(0, 255, 14, 0xb000);
keys.up = sp_LookupKey('q');
keys.down = sp_LookupKey('a');
keys.right = sp_LookupKey('p');
keys.left = sp_LookupKey('o');
keys.fire = sp_LookupKey(' ');
reset = sp_LookupKey('r');
cambioBorde = sp_LookupKey('b');
spriteBicho = sp_CreateSpr(sp_MASK_SPRITE, 3, bicho1, 1, TRANSPARENT);
sp_AddColSpr(spriteBicho, bicho2, TRANSPARENT);
sp_AddColSpr(spriteBicho, bicho3, TRANSPARENT);
sp_IterateSprChar(spriteBicho, addColour);
sp_MoveSprAbs(spriteBicho, sp_ClipStruct, 0, 10, 15, 0, 0);
while(1) {
sp_UpdateNow();
i = sp_JoyKeyboard(&keys);
if ((i & sp_FIRE) == 0) {
dx = dy = 1;
} else {
dx = dy = 8;
}
if ((i & sp_LEFT) == 0)
dx = -dx;
else if ((i & sp_RIGHT) != 0)
dx = 0;
if ((i & sp_UP) == 0)
dy = -dy;
else if ((i & sp_DOWN) != 0)
dy = 0;
if (sp_KeyPressed(reset))
sp_MoveSprAbs(spriteBicho, sp_ClipStruct, 0, 10, 15, 0, 0);
else
sp_MoveSprRel(spriteBicho, sp_ClipStruct, 0, 0, 0, dx, dy);
if (sp_KeyPressed(cambioBorde))
if (borde == 1)
{
borde = 2;
sp_Border(RED);
}
else
{
borde = 1;
sp_Border(BLUE);
}
}
}
#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
|
Como se puede comprobar, tras crear el sprite del bicho, hacemos uso de la siguiente
instrucción:
sp_IterateSprChar(spriteBicho, addColour);
|
Esto significa que vamos a usar una función llamada addColour, y que deberemos
haber definido anteriormente dentro del archivo con el código, para modificar las
propiedades de spriteBicho. Aunque la instrucción sólo aparece una vez, es como
si estuviéramos llamando a la función addColour una vez por cada bloque de 8x8
que forma el sprite del bicho (recordemos que el sprite de nuestro bicho está formado
por 3 columnas de 3 bloques de 8x8, por lo que la función addColour será llamada
un total de 9 veces). La forma que tiene está función es la siguiente:
uchar n;
void addColour(struct sp_CS *cs)
{
if (n == 0) // Se rellena de arriba a abajo y de izquierda
// a derecha, incluyendo partes vacias del sprite
cs->colour = INK_BLACK | PAPER_WHITE;
else if (n == 1)
cs->colour = INK_BLUE | PAPER_BLACK;
else if (n == 2)
cs->colour = INK_RED | PAPER_GREEN;
else if (n == 3)
cs->colour = INK_YELLOW | PAPER_BLACK;
else if (n == 4)
cs->colour = INK_GREEN | PAPER_WHITE;
else
cs->colour = TRANSPARENT;
if (n > 5)
cs->graphic = sp_NullSprPtr;
n++;
return;
}
|
Justo antes se define una variable global llamada n, y que no será más que un
contador que nos permitirá saber en cuál de las nueve llamadas a la función
addColour nos encontramos. Ya dentro de dicho método se observa como su valor se
incrementa de uno en uno en cada llamada.
Como hemos repetido varias veces, la función addColour será llamada una vez por
cada bloque 8x8 que forme nuestro sprite, recibiendo como parámetro un struct de tipo
sp_CS que nos va a permitir modificar diversas características de dicho bloque del
sprite. Uno de los campos de ese struct es colour, que como su propio nombre
indica, es el indicado para añadir color. Gracias al valor de n, podremos conocer
en cuál de todos los bloques del sprite nos encontramos (empezando por el 0, los bloques
están ordenados de arriba a abajo y de izquierda a derecha, por lo que en el caso de
nuestro bicho, aun estando compuesto por 3x3 bloques, sólo nos interesará colorear
aquellos para los que n vale 0,1,3 y 4, que son los que no están vacíos) y asignarle un
color de tinta y papel modificando el valor del campo colour del struct
sp_CS, tal como se puede observar en el código anterior.
Sólo deberemos colorear los bloques 0,1,3 y 4 de nuestro bicho, pues el bloque 2 se
corresponde con el último de la primera columna (que está vacío), el bloque 5 con el
último de la segunda columna (que también está vacío) y los bloques 6,7 y 8 con la
última columna de todas, también vacía, y que se añadió para que no hubiera problemas al
desplazar el sprite. En el caso de los bloques 2 y 5 lo más correcto hubiera sido
utilizar el valor TRANSPARENT para el campo colour (aunque en nuestro
ejemplo hemos sido un poco transgresores y el bloque 2 no lo hemos hecho transparente).
Para la última columna (bloques para los que n vale más que 5), sin embargo,
asignamos el valor sp_NullSprPtr al campo colour. Este valor, que ha sido
definido anteriormente en el programa de la siguiente forma:
extern uchar *sp_NullSprPtr;
#asm
LIB SPNullSprPtr
._sp_NullSprPtr defw SPNullSprPtr
#endasm
|
evitará que esa columna vacía "moleste" al resto de los sprites con los que nuestro bicho
entre en contacto.
Y ya está, ya le hemos dado color a nuestro bicho (eso sí, una combinación bastante
psicodélica). Cada vez que queramos hacer lo mismo con cualquier otro sprite, tan solo
deberemos seguir la receta anterior, pues es algo mecánico.
|
Nuestro sprite a color. Evidentemente, la
combinación de colores puede ser mejorada
|
UN NUEVO JUEGO
A continuación, y empleando el conocimiento adquirido tanto en este número como en el
número anterior del magazine, vamos a comenzar a desarrollar nuestro propio juego. Al
hacerlo nos encontraremos con una serie de problemas que iremos resolviendo en próximos
artículos. En nuestro juego controlaremos los movimientos de un esquiador que deberá
descender una montaña esquivando todas las rocas que se interpongan en su camino. El
esquiador estará situado en la parte superior de la pantalla y podrá desplazarse a
izquierda y derecha. Las rocas subirán desde la parte inferior de la pantalla, simulando
el descenso por la pendiente nevada de la montaña.
|
Aspecto final del juego |
El código completo, que comentaremos a lo largo de esta sección (aunque no en detalle,
sólo aquellas partes que difieran de lo visto hasta ahora o introduzcan conceptos
nuevos), es el siguiente:
#include <spritepack.h>
#include <stdlib.h>
#pragma output STACKPTR=61440
#define NUM_ROCAS 8
extern struct sp_Rect *sp_ClipStruct;
#asm
LIB SPCClipStruct
._sp_ClipStruct defw SPCClipStruct
#endasm
extern uchar *sp_NullSprPtr;
#asm
LIB SPNullSprPtr
._sp_NullSprPtr defw SPNullSprPtr
#endasm
extern uchar roca1[];
extern uchar roca2[];
extern uchar roca3[];
extern uchar banderin1[];
extern uchar banderin2[];
extern uchar skiCentrado1[];
extern uchar skiCentrado2[];
extern uchar skiIzquierda1[];
extern uchar skiIzquierda2[];
extern uchar skiDerecha1[];
extern uchar skiDerecha2[];
struct sp_UDK keys;
void *my_malloc(uint bytes)
{
return sp_BlockAlloc(0);
}
void *u_malloc = my_malloc;
void *u_free = sp_FreeBlock;
uchar n;
void addColourRoca(struct sp_CS *cs)
{
if (n >= 0 && n <= 5)
cs->colour = INK_RED | PAPER_WHITE;
else
cs->colour = TRANSPARENT;
if (n > 5)
cs->graphic = sp_NullSprPtr;
n++;
return;
}
void addColourSki(struct sp_CS *cs)
{
if (n == 0)
cs->colour = INK_BLUE | PAPER_WHITE;
else if (n == 2)
cs->colour = INK_BLUE | PAPER_WHITE;
else
cs->colour = TRANSPARENT;
if (n>2)
cs->graphic = sp_NullSprPtr;
n++;
return;
}
main()
{
char dx,dy,i
struct sp_SS *spriteRoca[NUM_ROCAS], *spriteSkiCentrado,
*spriteSkiIzquierda, *spriteSkiDerecha, *ski;
short int posicion = 0;
int roca = 0;
#asm
di
#endasm
sp_InitIM2(0xf1f1);
sp_CreateGenericISR(0xf1f1);
#asm
ei
#endasm
sp_Initialize(INK_BLACK | PAPER_WHITE, ' ');
sp_Border(WHITE);
sp_AddMemory(0, 255, 14, 0xb000);
keys.up = sp_LookupKey('q');
keys.down = sp_LookupKey('a');
keys.fire = sp_LookupKey(' ');
keys.right = sp_LookupKey('p');
keys.left = sp_LookupKey('o');
for (i=0;i<NUM_ROCAS;i++)
{
n = 0;
spriteRoca[i] = sp_CreateSpr(sp_OR_SPRITE, 3, roca1, 1, TRANSPARENT);
sp_AddColSpr(spriteRoca[i], roca2, TRANSPARENT);
sp_AddColSpr(spriteRoca[i], roca3, TRANSPARENT);
sp_IterateSprChar(spriteRoca[i], addColourRoca);
sp_MoveSprAbs(spriteRoca[i],sp_ClipStruct,0,0,-20,-20,0);
}
n = 0;
spriteSkiCentrado = sp_CreateSpr(sp_MASK_SPRITE, 2, skiCentrado1, 1, TRANSPARENT);
sp_AddColSpr(spriteSkiCentrado, skiCentrado2, TRANSPARENT);
sp_IterateSprChar(spriteSkiCentrado, addColourSki);
n = 0;
spriteSkiIzquierda = sp_CreateSpr(sp_MASK_SPRITE, 2, skiIzquierda1, 1, TRANSPARENT);
sp_AddColSpr(spriteSkiIzquierda, skiIzquierda2, TRANSPARENT);
sp_IterateSprChar(spriteSkiIzquierda, addColourSki);
n = 0;
spriteSkiDerecha = sp_CreateSpr(sp_MASK_SPRITE, 2, skiDerecha1, 1, TRANSPARENT);
sp_AddColSpr(spriteSkiDerecha, skiDerecha2, TRANSPARENT);
sp_IterateSprChar(spriteSkiDerecha, addColourSki);
ski = spriteSkiCentrado;
sp_MoveSprAbs(ski, sp_ClipStruct, 0, 0, 15, 0, 0);
while(1) {
sp_UpdateNow();
i = sp_JoyKeyboard(&keys);
dx = 0;
dy = 0;
if ((i & sp_LEFT) == 0 && ski->col > 0)
{
if (posicion != -1)
{
sp_MoveSprAbs(spriteSkiIzquierda,sp_ClipStruct,0,ski->row,ski->col,0,0);
sp_MoveSprAbs(ski,sp_ClipStruct,0,0,-10,0,0);
ski = spriteSkiIzquierda;
posicion = -1;
}
dx = -3;
}
else if ((i & sp_RIGHT) == 0 && ski->col < 30)
{
if (posicion != 1)
{
sp_MoveSprAbs(spriteSkiDerecha,sp_ClipStruct,0,ski->row,ski->col,0,0);
sp_MoveSprAbs(ski,sp_ClipStruct,0,0,-10,0,0);
ski = spriteSkiDerecha;
posicion = 1;
}
dx = 3;
}
else
{
if (posicion != 0)
{
sp_MoveSprAbs(spriteSkiCentrado,sp_ClipStruct,0,ski->row,ski->col,0,0);
sp_MoveSprAbs(ski,sp_ClipStruct,0,0,-10,0,0);
ski = spriteSkiCentrado;
posicion = 0;
}
}
if (dx != 0) sp_MoveSprRel(ski, sp_ClipStruct, 0, 0, 0, dx, dy);
if (spriteRoca[roca]->row != -10)
{
dx = 0;
dy = -4;
sp_MoveSprRel(spriteRoca[roca],sp_ClipStruct,0,0,0,dx,dy);
}
else
if (rand()%100>98)
{
sp_MoveSprAbs(spriteRoca[roca],sp_ClipStruct,0,23,rand()%29+1,0,4);
}
roca ++;
if (roca >= NUM_ROCAS)
roca = 0;
}
}
#asm
defb @00000000, @11111111
defb @00000000, @11111111
defb @00000000, @11111111
defb @00000000, @11111111
defb @00000000, @11111111
defb @00000000, @11111111
defb @00000000, @11111111
._roca1
defb @00000000, @11111111
defb @00000000, @11111111
defb @00000000, @11111111
defb @00000000, @11111111
defb @00000001, @11111110
defb @00000011, @11111100
defb @00000011, @11111100
defb @00000111, @11111000
defb @00001111, @11110000
defb @00001111, @11110000
defb @00011111, @11100000
defb @00111111, @11000000
defb @00111111, @11000000
defb @00111110, @11000000
defb @01111110, @10000000
defb @01111110, @10000000
defb @00000000, @11111111
defb @00000000, @11111111
defb @00000000, @11111111
defb @00000000, @11111111
defb @00000000, @11111111
defb @00000000, @11111111
defb @00000000, @11111111
defb @00000000, @11111111
._roca2
defb @00000000, @11111111
defb @00000000, @11111111
defb @00000000, @11111111
defb @01100000, @10011111
defb @11110000, @00001111
defb @11111000, @00000111
defb @11111000, @00000111
defb @10111000, @00000111
defb @10111100, @00000011
defb @10111100, @00000011
defb @10111100, @00000011
defb @01111110, @00000001
defb @01111110, @00000001
defb @11111110, @00000001
defb @11111111, @00000000
defb @11111111, @00000000
defb @00000000, @11111111
defb @00000000, @11111111
defb @00000000, @11111111
defb @00000000, @11111111
defb @00000000, @11111111
defb @00000000, @11111111
defb @00000000, @11111111
defb @00000000, @11111111
._roca3
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
._skiCentrado1
defb @00111110, @11000001
defb @01101011, @10000000
defb @00111110, @11000001
defb @00011100, @11100011
defb @00010100, @11101011
defb @00100010, @11011101
defb @00100010, @11011101
defb @01000001, @10111110
defb @00000000, @11111111
defb @00000000, @11111111
defb @00000000, @11111111
defb @00000000, @11111111
defb @00000000, @11111111
defb @00000000, @11111111
defb @00000000, @11111111
defb @00000000, @11111111
._skiCentrado2
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
._skiIzquierda1
defb @00011111, @11100000
defb @00110101, @11000000
defb @00011111, @11100000
defb @00001110, @11110001
defb @00010010, @11101101
defb @00100100, @11011011
defb @01001000, @10110111
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
._skiIzquierda2
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
._skiDerecha1
defb @11111000, @00000111
defb @10101100, @00000011
defb @11111000, @00000111
defb @01110000, @10001111
defb @01001000, @10110111
defb @00100100, @11011011
defb @00010010, @11101101
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
._skiDerecha2
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
|
Se han definido un total de cuatro sprites. El sprite de tipo roca (dividido en
tres columnas denominadas roca1, roca2 y roca3) será el que representará
los obstáculos que encontrará nuestro intrépido esquiador en su descenso. Para el
protagonista de nuestro juego se han definido tres sprites divididos cada uno de ellos
en dos columnas: skiCentrado que será el que se mostrará cuando el esquiador esté
descendiendo sin que lo movamos a izquierda o derecha, skiIzquierda que
representa nuestro esquiador desplazándose hacia la izquierda, y skiDerecha que a
su vez representa al mismo esquiador desplazándose a la derecha.
Como debemos reservar memoria para los sprites que vayamos a mostrar por pantalla, lo
haremos para cada una de las tres posiciones del esquiador (de tal forma que mostraremos
la adecuada según el movimiento del jugador) y un número fijo de rocas. Es probable que
no todas las rocas sean visibles simultáneamente durante el juego, pero hacerlo de esta
forma simplifica en gran medida el diseño, sin tener que hacer grandes esfuerzos para
reservar o liberar memoria cuando sea necesario. En concreto, la constante NUM_ROCAS es
la que vamos a usar para indicar el número de rocas que se van a crear. Se define un
array llamado spriteRoca con tantas posiciones como las indicadas por la
constante anteriormente mencionada, y en cada posición de dicho array se reserva memoria
para un sprite de tipo roca que es coloreado con su correspondiente función
addColour:
for (i=0;i<NUM_ROCAS;i++)
{
n = 0;
spriteRoca[i] = sp_CreateSpr(sp_OR_SPRITE, 3, roca1, 1, TRANSPARENT);
sp_AddColSpr(spriteRoca[i], roca2, TRANSPARENT);
sp_AddColSpr(spriteRoca[i], roca3, TRANSPARENT);
sp_IterateSprChar(spriteRoca[i], addColourRoca);
sp_MoveSprAbs(spriteRoca[i],sp_ClipStruct,0,0,-20,-20,0);
}
|
Un par de comentarios sobre la definición de los sprites en este programa: como primer
parámetro para los sprites de tipo roca en la función sp_CreateSpr se pasa el
valor sp_OR_SPRITE en lugar de sp_MASK_SPRITE. Esto permite mayor
velocidad en el dibujado de las rocas por la pantalla a cambio de renunciar a que el
sprite pueda tener píxeles transparentes en su interior. Por otra parte, antes de llamar
a la correspondiente función addColour para cada sprite se vuelve a dar a la variable
global n el valor 0, de tal forma que se pueda iterar de nuevo por todos los
bloques de cada nuevo sprite que queramos colorear. Esto se puede ver también en el caso
de los tres sprites para el esquiador.
Una vez creados los sprites se utilizan las siguientes instrucciones:
i = spriteSkiCentrado;
sp_MoveSprAbs(ski, sp_ClipStruct, 0, 0, 15, 0, 0);
|
El puntero ski apuntará en todo momento al sprite del esquiador que deba ser
mostrado por pantalla. El esquiador comenzará mirando hacia abajo, ya que no se está
moviendo ni a izquierda ni a derecha; por lo tanto, ski apuntará al sprite spriteSkiCentrado,
que es el que deberá ser visualizado. Cada vez que el esquiador se mueva a izquierda o
derecha, ski apuntará, respectivamente, a spriteSkiIzquierda y spriteSkiDerecha.
En el bucle principal iniciado por la sentencia while(1) y que se ejecutará de
forma infinita, vemos como se resuelve el tema de la animación del protagonista, la
limitación de sus movimientos por la pantalla, y el movimiento de las rocas. Comencemos
por la primera parte de dicho bucle, que es donde encontramos el código referido al
movimiento del esquiador:
i = sp_JoyKeyboard(&keys);
dx = 0;
dy = 0;
if ((i & sp_LEFT) == 0 && ski->col > 0)
{
if (posicion != -1)
{
sp_MoveSprAbs(spriteSkiIzquierda,sp_ClipStruct,0,ski->row,ski->col,0,0);
sp_MoveSprAbs(ski,sp_ClipStruct,0,0,-10,0,0);
ski = spriteSkiIzquierda;
posicion = -1;
}
dx = -3;
}
else if ((i & sp_RIGHT) == 0 && ski->col < 30)
{
if (posicion != 1)
{
sp_MoveSprAbs(spriteSkiDerecha,sp_ClipStruct,0,ski->row,ski->col,0,0);
sp_MoveSprAbs(ski,sp_ClipStruct,0,0,-10,0,0);
ski = spriteSkiDerecha;
posicion = 1;
}
dx = 3;
}
else
{
if (posicion != 0)
{
sp_MoveSprAbs(spriteSkiCentrado,sp_ClipStruct,0,ski->row,ski->col,0,0);
sp_MoveSprAbs(ski,sp_ClipStruct,0,0,-10,0,0);
ski = spriteSkiCentrado;
posicion = 0;
}
}
if (dx != 0) sp_MoveSprRel(ski, sp_ClipStruct, 0, 0, 0, dx, dy);
|
Cada vez que el esquiador cambia de posición, se debe hacer apuntar a ski hacia el
sprite adecuado. Esto sólo se debe hacer en la primera iteración en la que estemos
moviéndonos en esa dirección. Para ello se observa que se utiliza una variable posicion,
inicializada a cero, y que valdrá precisamente 0 si el sprite a mostrar por pantalla
debe ser spriteSkiCentrado, y -1 o 1 si se debe mostrar el esquiador
desplazándose hacia la izquierda o la derecha, respectivamente. Este valor nos va a
permitir saber cuándo debemos hacer que el puntero ski apunte hacia otro sprite
diferente del que lo estaba haciendo hasta ese momento.
Veámoslo con un ejemplo. Supongamos que el esquiador se está deslizando ladera abajo, sin
que el jugador lo mueva a izquierda o derecha. En este caso, ski apunta a spriteSkiCentrado,
y posicion vale 0. A continuación, el jugador pulsa la tecla de la derecha y la
mantiene presionada. En la primera iteración en la que esto sucede, se realizan las
siguientes acciones:
- Se mueve spriteSkiDerecha, que representa al esquiador desplazándose en esa
dirección, a la misma posición de la pantalla donde se encuentra el sprite del
esquiador centrado, que es el que se estaba mostrando hasta ahora. Para conocer esa
posición se hace uso de los campos row y col de la estructura
ski.
- A continuación, el sprite apuntado por ski se mueve fuera de la pantalla,
pues no va a ser mostrado más.
- La variable ski apunta a spriteSkiDerecha, para poder mostrarlo por la
pantalla.
- Se cambia el valor de posicion a 1, indicando que ya hemos estado al menos
una iteración desplazándonos hacia la derecha y que no es necesario repetir todas
estas operaciones en el caso en el que sigamos moviéndonos en esta dirección.
Sólo podremos desplazarnos hacia la izquierda o la derecha siempre que nos encontremos
dentro de los límites de la pantalla. Esto se controla de la siguiente forma:
if ((i & sp_LEFT) == 0 && ski->col > 0)
else if ((i & sp_RIGHT) == 0 && ski->col < 30)
|
Con respecto a las rocas, moveremos tan solo una en cada iteración del bucle principal.
Esto no es más que una medida preliminar para conseguir un mínimo de velocidad y algo de
sincronización, pero es evidente que deberemos modificar este apartado del código más
adelante. Para poder desplazar tan solo una roca en cada iteración, hacemos uso de la
variable roca, inicializada a cero, que nos va a indicar cual de todas las rocas
va a ser movida. Esta variable se incrementa en 1 cada vez, volviéndole a asignar el
valor 0 cuando almacena un entero superior al valor indicado por NUM_ROCAS. Finalmente,
y como se puede observar en el código siguiente, esta variable roca se utiliza
como indice del array de sprites de tipo roca, determinando qué roca se mueve.
if (spriteRoca[roca]->row != -10)
{
dx = 0;
dy = -4;
sp_MoveSprRel(spriteRoca[roca],sp_ClipStruct,0,0,0,dx,dy);
}
else
if (rand()%100>98)
{
sp_MoveSprAbs(spriteRoca[roca],sp_ClipStruct,0,23,rand()%29+1,0,4);
}
roca ++;
if (roca >= NUM_ROCAS)
roca = 0;
|
Cada roca se desplazará hacia arriba en la pantalla usando un valor de desplazamiento dy
= -4, hasta llegar a un valor de -10 en su coordenada y. En ese momento, la roca ya
habrá desaparecido de la pantalla por su parte superior. Si una roca está fuera de la
pantalla, volveremos a sacarla por debajo de nuevo en una posición x al azar (como si
fuera otra roca distinta) con una probabilidad del 98% (para que una roca que ha salido
por la parte superior de la pantalla no vuelva a aparecer inmediatamente por la parte
inferior y que efectivamente parezca una roca diferente). Se podría modificar el valor
de la constante NUM_ROCAS para cambiar el número de obstáculos, pero se ha
escogido un valor que hace que el juego no vaya demasiado lento.
Una vez que compilamos y ejecutamos nuestro juego, nos encontramos con tres problemas,
que trataremos de resolver en artículos sucesivos:
- No hay colisiones. Nuestro esquiador parece un fantasma, pues puede atravesar las
rocas sin que éstas le afecten. Eso es porque no hemos establecido un control de
colisiones.
- No hay sincronización. Esto quiere decir que la velocidad del juego es diferente
según el número de rocas que se estén mostrando en determinado momento por la
pantalla. Haciendo uso de las interrupciones podremos conseguir que haya el número
de rocas que haya, la velocidad de descenso se mantenga constante.
- Colour clash. Cuando nuestro esquiador y las rocas entran en contacto, se produce un
choque de atributos bastante desagradable (los colores de los sprites parecen
"mezclarse"). Esto también se puede medio resolver.
Por último, un apunte sobre la liberación de memoria. Como hemos visto, cada vez que
queremos mostrar un sprite por pantalla, debemos reservar memoria para el mismo. En el
caso en el que un sprite no vaya a ser mostrado nunca más, lo más adecuado es liberar la
memoria correspondiente, tras haber movido dicho sprite fuera de la pantalla para
evitar efectos no deseados.
Y EN EL PRÓXIMO NUMERO
¡La programación de nuestro juego ha comenzado! Sin embargo, nos encontramos con varios
problemas, tal como se ha comentado anteriormente; problemas que intentaremos resolver
en sucesivas entregas del curso centrándonos en tres aspectos: cómo solucionar el
problema conocido como colour clash, el uso de IM2 (interrupciones), y la intersección
de rectángulos para detección de colisiones. Gracias a estos elementos nuestro juego
quedará mucho más completo y jugable. Con unos pocos añadidos más podría incluso
compararse a las grandes producciones comerciales de 1983 ;).
LINKS
|
SIEW |
|