TUTORIAL 3: SPRITESPara la Game Boy, un sprite es una imagen de
8x8 u 8x16 pixels que se puede mover por la pantalla de forma independiente al fondo y a otros sprites. La consola presenta ciertas limitaciones además del tamaño, solo puede haber
40 sprites en pantalla y como mucho 10 en la misma linea horizontal (scanline) o se producirá parpadeo (flicker). Además, dentro de que la consola solo trabaja con
4 colores (Blanco, Negro, Gris claro y Gris oscuro), se puede usar
2 paletas de 4 colores para los sprites (siendo el primero el transparente). Para la Game Boy COLOR, estás limitaciones varían ligeramente, por ejemplo, poseemos 8 paletas de 4 colores para los sprites.
Ahora que ya sabemos esto, podemos pasar a dibujar los tiles que formaran nuestros sprites (imagenes de 8x8 pixels). Un sprite de 8x8 pixels usará un tile y otro de 8x16 pues usará dos. Para crear nuestros tiles usaremos el programa de Harry Mulder incluido en la carpeta TOOLS llamado
GBTD (Game Boy Tile Designer). El programa es muy sencillo de usar, contiene las típicas herramientas de lápiz, cubo de pintura, espejados, etc.

De todas formas, siempre podéis dibujar vuestros tiles en cualquier programa de dibujo, copiar en el portapapeles y pegarlos en el GBTD. Siempre que respetéis esta paleta que os enseño no habrá problema al importar.

Cuando tengamos nuestros tiles listos, deberemos exportarlos. Para ello, vamos al menu
"FILE->EXPORT TO". En "TYPE" elegimos "GBDK C FILE" y en en "FILENAME", pues la ruta donde vayamos a escribir nuestro código, por ejemplo "C:\gbdk\examples\03_sprites_A". La sección de "SETTINGS" debereis elegir el nombre al que le vamos a poner al vector de salida, la cantidad de tiles a guardar (en mi caso de 0 a 3), el formato de "GAME BOY 4 COLOR" y que se exporten los tiles como una unidad.

Si abrimos el fichero
"export.c" creado, podremos ver el resultado de nuestros tiles convertidos:
unsigned char tile_data[] =
{
0xFF,0xFF,0x81,0xFF,0x8D,0xF3,0xBD,0xC3,
0x8D,0xF3,0x8D,0xF3,0x81,0xFF,0xFF,0xFF,
0xFF,0xFF,0x81,0xFF,0xBD,0xC3,0x8D,0xF3,
0xB1,0xCF,0xBD,0xC3,0x81,0xFF,0xFF,0xFF,
0xFF,0xFF,0x81,0xFF,0xBD,0xC3,0x8D,0xF3,
0x9D,0xE3,0x8D,0xF3,0xBD,0xC3,0xFF,0xFF,
0xFF,0xFF,0x81,0xFF,0xAD,0xD3,0xBD,0xC3,
0x8D,0xF3,0x8D,0xF3,0x81,0xFF,0xFF,0xFF
};
Recordad siempre añadir
"CONST" delante de todo para asegurarnos el compilador mete este vector en ROM y no en RAM.
Una vez que ya tenemos los tiles, podemos empezar con nuestro
"main.c", en mi caso, en la carpeta "C:\gbdk\examples\03_sprites_A"
#include <gb/gb.h>
// los tiles que usaran los sprites
const unsigned char tile_data[] =
{
0xFF,0xFF,0x81,0xFF,0x8D,0xF3,0xBD,0xC3,
0x8D,0xF3,0x8D,0xF3,0x81,0xFF,0xFF,0xFF,
0xFF,0xFF,0x81,0xFF,0xBD,0xC3,0x8D,0xF3,
0xB1,0xCF,0xBD,0xC3,0x81,0xFF,0xFF,0xFF,
0xFF,0xFF,0x81,0xFF,0xBD,0xC3,0x8D,0xF3,
0x9D,0xE3,0x8D,0xF3,0xBD,0xC3,0xFF,0xFF,
0xFF,0xFF,0x81,0xFF,0xAD,0xD3,0xBD,0xC3,
0x8D,0xF3,0x8D,0xF3,0x81,0xFF,0xFF,0xFF
};
//////////////////////////////////////////////////////
// punto de entrada
//////////////////////////////////////////////////////
void main(){
// modo de sprites a 8x8 pixels
SPRITES_8x8;
Elegimos el tamaño de sprites de 8x8, no se pueden mezclar, todos deben ser del mismo tamaño (8x8 u 8x16).
// carga los tiles de los sprites, posicion, cantidad de tiles y los tiles
set_sprite_data( 0, 4, tile_data);
Cargamos en VRAM los tiles de los sprites especificando posición en memoria, la cantidad de tiles y el vector de los tiles. Podemos almacenar
hasta 256 tiles para los sprites, hay que tener en cuenta que la zona de memoria de 128 a 255 es compartida con la memoria del fondo, lo veremos en detalle cuando lleguemos a estos.
// asigna a cada sprite un tile, sprite (0-39), posicion del tile
set_sprite_tile( 0, 0 );
set_sprite_tile( 1, 1 );
set_sprite_tile( 2, 2 );
set_sprite_tile( 3, 3 );
Asignamos a un sprite un tile, especificando el número de sprite (0-39) y la posición en VRAM del tile (0-255)
// mueve el sprite a la posicion x, y
move_sprite( 0, 20, 20 );
move_sprite( 1, 28, 20 );
move_sprite( 2, 36, 20 );
move_sprite( 3, 44, 20 );
Movemos los sprites a una posición de la pantalla, especificando el número de sprite (0-39) y la coordenada X/Y (la pantalla de la consola es de 160x144 pixels).
// muestra los sprites
SHOW_SPRITES;
while(1) ;
}
Por último mostramos los sprites y entramos en un bucle infinito. El código completo quedaría así:
#include <gb/gb.h>
const unsigned char tile_data[] =
{
0xFF,0xFF,0x81,0xFF,0x8D,0xF3,0xBD,0xC3,
0x8D,0xF3,0x8D,0xF3,0x81,0xFF,0xFF,0xFF,
0xFF,0xFF,0x81,0xFF,0xBD,0xC3,0x8D,0xF3,
0xB1,0xCF,0xBD,0xC3,0x81,0xFF,0xFF,0xFF,
0xFF,0xFF,0x81,0xFF,0xBD,0xC3,0x8D,0xF3,
0x9D,0xE3,0x8D,0xF3,0xBD,0xC3,0xFF,0xFF,
0xFF,0xFF,0x81,0xFF,0xAD,0xD3,0xBD,0xC3,
0x8D,0xF3,0x8D,0xF3,0x81,0xFF,0xFF,0xFF
};
//////////////////////////////////////////////////////
// punto de entrada
//////////////////////////////////////////////////////
void main(){
// modo de sprites a 8x8 pixels
SPRITES_8x8;
// carga los tiles de los sprites, posicion, cantidad de tiles y los tiles
set_sprite_data( 0, 4, tile_data);
// asigna a cada sprite un tile, sprite (0-39), posicion del tile
set_sprite_tile( 0, 0);
set_sprite_tile( 1, 1);
set_sprite_tile( 2, 2);
set_sprite_tile( 3, 3);
// mueve el sprite a la posicion x, y
move_sprite( 0, 20, 20 );
move_sprite( 1, 28, 20 );
move_sprite( 2, 36, 20 );
move_sprite( 3, 44, 20 );
// muestra los sprites
SHOW_SPRITES;
while(1) ;
}
Ya podríamos crear el "compile.bat" con el mismo texto de hasta ahora y obtener el siguiente resultado:
lcc -o rom.gb main.c
pause
bgb.exe rom.gb
COLOREl siguiente paso que vamos a abarcar es darle color a nuestros sprites.
Primero añadiremos las paletas que vamos a utilizar, recordad que podemos cargar
hasta 8 para los sprites.
const UWORD sprite_palette[] = {
RGB_RED, RGB_DARKRED, RGB_GREEN, RGB_DARKGREEN,
RGB_BLUE, RGB_DARKBLUE, RGB_YELLOW, RGB_DARKYELLOW,
RGB_CYAN, RGB_AQUA, RGB_PINK, RGB_PURPLE,
RGB_LIGHTGRAY, RGB_DARKGRAY, RGB_WHITE, RGB_LIGHTFLESH,
RGB_BLACK, RGB_BROWN, RGB_ORANGE, RGB_TEAL
};
En
"include\cgb.h" tenéis la siguiente definición de los colores basados en la paleta por defecto EGA:
#define RGB_RED RGB(31, 0, 0)
#define RGB_DARKRED RGB(15, 0, 0)
#define RGB_GREEN RGB( 0, 31, 0)
#define RGB_DARKGREEN RGB( 0, 15, 0)
#define RGB_BLUE RGB( 0, 0, 31)
#define RGB_DARKBLUE RGB( 0, 0, 15)
#define RGB_YELLOW RGB(31, 31, 0)
#define RGB_DARKYELLOW RGB(21, 21, 0)
#define RGB_CYAN RGB( 0, 31, 31)
#define RGB_AQUA RGB(28, 5, 22)
#define RGB_PINK RGB(11, 0, 31)
#define RGB_PURPLE RGB(21, 0, 21)
#define RGB_BLACK RGB( 0, 0, 0)
#define RGB_DARKGRAY RGB(10, 10, 10)
#define RGB_LIGHTGRAY RGB(21, 21, 21)
#define RGB_WHITE RGB(31, 31, 31)
#define RGB_LIGHTFLESH RGB(30, 20, 15)
#define RGB_BROWN RGB(10, 10, 0)
#define RGB_ORANGE RGB(30, 20, 0)
#define RGB_TEAL RGB(15, 15, 0)
Ya dentro de nuestro "main.c" colocamos el código siguiente:
if( _cpu == CGB_TYPE ){
// carga las paletas de los sprites, paleta inicial, numero de paletas a cargar y datos
set_sprite_palette( 0, 4, &sprite_palette[0] );
// carga la paleta de cada sprite (numero de sprite, numero de paleta)
set_sprite_prop( 0, 0 );
set_sprite_prop( 1, 1 );
set_sprite_prop( 2, 2 );
set_sprite_prop( 3, 3 );
}
Si detectamos que estamos en una Game Boy COLOR (CGB_TYPE), cargamos las paletas de los sprites y luego asignamos a cada sprite una paleta. Es recomendable hacer la comprobación para que nuestra rom sea 100% compatible con la Game Boy clásica. Con
"set_sprite_prop" no solo podemos elegir la paleta, tambien podemos cambiar otras propiedades de los sprites como su
espejado (flip) usando
S_FLIPX (bit 6 a 1 -> 00100000 o 0x20U) o
S_FLIPY (bit 7 a 1 -> 01000000 o 0x40U), su
prioridad respecto al fondo con
S_PRIORITY (bit 8 a 1 -> 10000000 o 0x80U) o en modo monocromo
elegir entre las dos posibles paletas para los sprites,
OBJ1PAL o OBJ0PAL. Hablando de paletas, por defecto, GBDK carga en modo Game Boy Clásica las paletas con la secuencia BLANCO, GRIS CLARO, GRIS OSCURO y NEGRO en las 3 paletas, podemos cambiar este orden haciendo:
BGP_REG = OBP0_REG = OBP1_REG = 0xE4U; // 0xE4U seria la normal -> 11100100, 0xFFU seria todo oscuro -> 11111111, etc.
Usando las opciones de debug de los emuladores incluidos podremos verlas.

El código completo quedaría de la siguiente manera:
#include <gb/gb.h>
const unsigned char tile_data[] =
{
0xFF,0xFF,0x81,0xFF,0x8D,0xF3,0xBD,0xC3,
0x8D,0xF3,0x8D,0xF3,0x81,0xFF,0xFF,0xFF,
0xFF,0xFF,0x81,0xFF,0xBD,0xC3,0x8D,0xF3,
0xB1,0xCF,0xBD,0xC3,0x81,0xFF,0xFF,0xFF,
0xFF,0xFF,0x81,0xFF,0xBD,0xC3,0x8D,0xF3,
0x9D,0xE3,0x8D,0xF3,0xBD,0xC3,0xFF,0xFF,
0xFF,0xFF,0x81,0xFF,0xAD,0xD3,0xBD,0xC3,
0x8D,0xF3,0x8D,0xF3,0x81,0xFF,0xFF,0xFF
};
const UWORD sprite_palette[] = {
RGB_RED, RGB_DARKRED, RGB_GREEN, RGB_DARKGREEN,
RGB_BLUE, RGB_DARKBLUE, RGB_YELLOW, RGB_DARKYELLOW,
RGB_CYAN, RGB_AQUA, RGB_PINK, RGB_PURPLE,
RGB_LIGHTGRAY, RGB_DARKGRAY, RGB_WHITE, RGB_LIGHTFLESH,
RGB_BLACK, RGB_BROWN, RGB_ORANGE, RGB_TEAL
};
//////////////////////////////////////////////////////
// punto de entrada
//////////////////////////////////////////////////////
void main(){
// modo de sprites a 8x8 pixels
SPRITES_8x8;
// carga los tiles de los sprites, posicion, cantidad de tiles y los tiles
set_sprite_data( 0, 4, tile_data);
// asigna a cada sprite un tile, sprite (0-39), posicion del tile
set_sprite_tile( 0, 0);
set_sprite_tile( 1, 1);
set_sprite_tile( 2, 2);
set_sprite_tile( 3, 3);
if( _cpu == CGB_TYPE ){
// carga las paletas de los sprites, paleta inicial, numero y datos
set_sprite_palette( 0, 4, &sprite_palette[0] );
// carga la paleta de cada sprite (numero de sprite, numero de paleta)
set_sprite_prop( 0, 0 );
set_sprite_prop( 1, 1 );
set_sprite_prop( 2, 2 );
set_sprite_prop( 3, 3 );
}
// mueve el sprite a la posicion x, y
move_sprite( 0, 20, 20 );
move_sprite( 1, 28, 20 );
move_sprite( 2, 36, 20 );
move_sprite( 3, 44, 20 );
// muestra los sprites
SHOW_SPRITES;
while(1) ;
}
Por último debemos hacer un cambio más, en nuestro "compile.bat" debemos
modificar la linea de comandos del "lcc".
lcc -Wl-yp0x143=0x80 -o rom.gb main.c
pause
bgb.exe rom.gb
Con esto hemos dado valor a
0x143 para que sea CGB-compatible (0x80). Ya podemos compilar la rom y obtener el siguiente resultado:
BANCOSComo comenta
xzakox en su tutorial de ensamblador: "la memoria principal de la gameboy, mapeada en un espacio de 16 bit, nos permite direccionar directamente 64K (2^16 = 65536). En este espacio de direcciones, tenemos que direccionar todos los bloques de memoria a los que la gameboy necesita acceder, esto es, la RAM, la ROM del cartucho, la RAM interna del cartucho para los juegos que graban partidas, la memoria de vídeo, etc. Para la ello los diseñadores de la GameBoy mapearon la memoria en diferentes bloques necesarios, como la RAM interna o la memoria de video, dejando dos bloques de 16K para el acceso a la ROM de los juegos, y un bloque de 8K para el acceso a la RAM de los juegos (partidas guardadas). Como muchos juegos empezaron a requerir mas de 32K de ROM o de 8K de RAM de guardado, se empezó a emplear una técnica denominada “Banking”, en la que la ROM del juego se divide en diversos bloques que se puedan independizar (los gráficos o sonidos de cada pantalla por ejemplo), que se van mapeando en el bloque de acceso a la memoria según sean necesarios. En la GameBoy, esto se diseñó de la siguiente manera; tenemos un bloque fijo de 16K(donde programamos la lógica principal del juego), y luego, mediante ciertas instrucciones (dependiendo del chip de mapping que usemos en nuestro cartucho), podemos ir intercambiando bancos de 16K en el otro bloque disponible."
Debido a esto, es necesario pensar en ir guardando todo lo que no sea la lógica del juego, en bancos de memoria diferentes al primero, para ello usaremos el mapper
MBC1 soportado por las GBDK. En realidad no es tan complicado como suena.
Si recordais al principio cuando exportamos los tiles que
dibujamos en GBTD, se nos creó un fichero llamado
"export.c" que es donde estaba el vector de los tiles que copiamos en nuestro main. Lo que vamos a hacer es
renombrarlo a "tiles.c" y compilarlo aparte del "main.c" para a la hora de generar la rom, poder elegir en que banco de memoria guardarlo.
Simplemente debemos modificar en "main.c" donde copiamos el contenido del vector de esto:
const unsigned char tile_data[] =
{
0xFF,0xFF,0x81,0xFF,0x8D,0xF3,0xBD,0xC3,
0x8D,0xF3,0x8D,0xF3,0x81,0xFF,0xFF,0xFF,
0xFF,0xFF,0x81,0xFF,0xBD,0xC3,0x8D,0xF3,
0xB1,0xCF,0xBD,0xC3,0x81,0xFF,0xFF,0xFF,
0xFF,0xFF,0x81,0xFF,0xBD,0xC3,0x8D,0xF3,
0x9D,0xE3,0x8D,0xF3,0xBD,0xC3,0xFF,0xFF,
0xFF,0xFF,0x81,0xFF,0xAD,0xD3,0xBD,0xC3,
0x8D,0xF3,0x8D,0xF3,0x81,0xFF,0xFF,0xFF
};
A esto:
extern const unsigned char tile_data[];
Y que justo antes de cargar estos tiles en VRAM, debemos especificar en que banco hemos de acceder con:
// saltamos al banco de memoria 2
SWITCH_ROM_MBC1(2);
set_sprite_data( 0, 4, tile_data );
El fichero "compile.bat" si que sufrirá más cambios .
lcc -c -o main.o main.c
Ahora solo creamos el objeto de main.c
lcc -Wf-bo2 -c -o tiles.o tiles.c
Creamos el objeto de tiles.c especificando en que banco se colocará con
-Wf-boX (en este caso el banco 2)
lcc -Wl-yp0x143=0x80 -Wl-yt1 -Wl-yo4 -o rom.gb main.o tiles.o
Y ya podemos crear nuestra rom, con
-Wl-ytX especificamos que será una ROM con MBC1, los distintos tipos posibles son:
0 : ROM ONLY
1 : ROM+MBC1
2 : ROM+MBC1+RAM
3 : ROM+MBC1+RAM+BATTERY
5 : ROM+MBC2
6 : ROM+MBC2+BATTERYCon
-Wl-yoX especificamos el numero de bancos que tendrá nuestra ROM, en este caso, 4. Podemos poner hasta 32 (512 KB) con el mapper MBC1, siendo el primer banco el 1, así que empezar a meter vuestros datos en el dos.
pause
bgb.exe rom.gb
Acostumbraos a usar "Banking", no es difícil y muchos seguro que ya lo conoceis si programais para ordenadores de 8 bits, máquinas posteriores como Mega Drive pueden direccionar 32 megabits sin banking (4 megabytes), cosa que se agradece pero en cambio otras supuestamente más poderosas como Super Nintendo hay que volver a utilizarlo así que no está de más practicar.