› Foros › Retro y descatalogado › Consolas clásicas
#include <genesis.h>
int main()
{
VDP_drawText("Hello World!", 10, 13);
return (0);
}
#include <genesis.h>
void myJoyHandler( u16 joy, u16 changed, u16 state)
{
//Si el botón pulsado corresponde al pad en el puerto 1
if (joy == JOY_1)
{
//La sintaxis del código para comprobar el estado
//del botón será la que sigue variando el valor
//con el que se compararán sendos atributos:
//state y change, state correspondiéndose con la
//pulsación del botón y change con la liberación
//del mismo
if (state & BUTTON_START) //Si se pulsa START
{
//Que específicamente elijamos estas determinadas
//coordenadas (x=5, y=13) se debe a que en la
//función main vamos a mostrar el rótulo
//correspondiente al estado del botón START en
//dichas coordenadas. Lo que lo que estaremos
//haciendo será sobreescribir el rótulo cada vez
//que se pulse o suelte el botón, ya sea mostrando
//"START button 1" o "START button 0" según el caso
VDP_drawText("START button 1", 5, 13);
//El área para disponer texto en pantalla en
//megadrive se corresponde exactamente con el área
//de tiles visibles en pantalla (no hay razón para
//no considerar cara caracter un tile, que es lo
//que también son). Por lo tanto, en el modo PAL
//"normal" en que disponemos de una resolución de
//320x224 tendremos un área de 40x28 caracteres;
//La primera fila y la primera columna toma valor
//cero, por lo que para comenzar un texto en la
//esquina superior izquierda lo haríamos tal que
//así: VDP_drawText("texto", 0, 0);
}
else if (changed & BUTTON_START) //Si se suelta
{
VDP_drawText("START button 0", 5, 13);
}
if (state & BUTTON_A) //Si se pulsa A
{
VDP_drawText("A button 1", 5, 14);
}
else if (changed & BUTTON_A) //Si se suelta A
{
VDP_drawText("A button 0", 5, 14);
}
if (state & BUTTON_B)
{
VDP_drawText("B button 1", 5, 15);
}
else if (changed & BUTTON_B)
{
VDP_drawText("B button 0", 5, 15);
}
if (state & BUTTON_C)
{
VDP_drawText("C button 1", 5, 16);
}
else if (changed & BUTTON_C)
{
VDP_drawText("C button 0", 5, 16);
}
if (state & BUTTON_X)
{
VDP_drawText("X button 1", 17, 14);
}
else if (changed & BUTTON_X)
{
VDP_drawText("X button 0", 17, 14);
}
if (state & BUTTON_Y)
{
VDP_drawText("Y button 1", 17, 15);
}
else if (changed & BUTTON_Y)
{
VDP_drawText("Y button 0", 17, 15);
}
if (state & BUTTON_Z)
{
VDP_drawText("Z button 1", 17, 16);
}
else if (changed & BUTTON_Z)
{
VDP_drawText("Z button 0", 17, 16);
}
if (state & BUTTON_UP)
{
VDP_drawText("UP button 1", 5, 17);
}
else if (changed & BUTTON_UP)
{
VDP_drawText("UP button 0", 5, 17);
}
if (state & BUTTON_DOWN)
{
VDP_drawText("DOWN button 1", 5, 18);
}
else if (changed & BUTTON_DOWN)
{
VDP_drawText("DOWN button 0", 5, 18);
}
if (state & BUTTON_LEFT)
{
VDP_drawText("LEFT button 1", 5, 19);
}
else if (changed & BUTTON_LEFT)
{
VDP_drawText("LEFT button 0", 5, 19);
}
if (state & BUTTON_RIGHT)
{
VDP_drawText("RIGHT button 1", 5, 20);
}
else if (changed & BUTTON_RIGHT)
{
VDP_drawText("RIGHT button 0", 5, 20);
}
}
// otras funciones interesante
// JOY_update() refresca el estado del pad, se llama en cada refresco de la pantalla
// JOY_readJoypad( joy ) ¨devuelve el estado del pad1
// JOY_waitPressBtn() espera a que se pulse un boton (no direcciones)
// JOY_waitPress(joy, BUTTON_A | BUTTON_UP) espera a pulsar un boton indicado en un pad especifico
}//end myJoyHandler()
////////////////////////////////////////////////////////////////////
// MAIN
////////////////////////////////////////////////////////////////////
int main( )
{
// resetea el manejador y lee la informacion de los mandos conectados
JOY_init();
//Establecemos que el programa dará soporte al pad de 6 botones
//conectado en el puerto 1
JOY_setSupport(PORT_1, JOY_SUPPORT_6BTN);
//Los otros valores correspondientes al resto de controles soportados
//en las librerías SGDK son JOY_SUPPORT_OFF, JOY_SUPPORT_3BTN,
//JOY_SUPPORT_MOUSE, JOY_SUPPORT_TRACKBALL, JOY_SUPPORT_TEAMPLAYER,
//JOY_SUPPORT_EA4WAYPLAY, JOY_SUPPORT_MENACER, JOY_SUPPORT_JUSTIFIER_BLUE,
//JOY_SUPPORT_JUSTIFIER_BOTH, JOY_SUPPORT_ANALOGJOY, JOY_SUPPORT_KEYBOARD
//A través de la función siguiente conseguimos que nuestra funcion
//creada anteriormente sea llamada cada vez que se cambia el estado de un boton
JOY_setEventHandler( &myJoyHandler );
//Dibujamos con dos líneas de texto una cabecera austera
VDP_drawText("Ejemplo de control del PAD", 5, 10);
VDP_drawText("--------------------------", 5, 11);
//Y rellenamos el resto del espacio escribiendo cadenas que indiquen
//que inicialmente todos los estados están a cero (nada pulsado)
VDP_drawText("START button 0", 5, 13);
VDP_drawText("A button 0", 5, 14);
VDP_drawText("B button 0", 5, 15);
VDP_drawText("C button 0", 5, 16);
VDP_drawText("X button 0", 17, 14);
VDP_drawText("Y button 0", 17, 15);
VDP_drawText("Z button 0", 17, 16);
VDP_drawText("UP button 0", 5, 17);
VDP_drawText("DOWN button 0", 5, 18);
VDP_drawText("LEFT button 0", 5, 19);
VDP_drawText("RIGHT button 0", 5, 20);
//BUCLE PRINCIPAL
//En este caso sólo tendrá en cuenta el manejador del input que
//hemos creado y establecido con JOY_setEventHandler anteriormente
while(1){
// Sincroniza la pantalla de modo que el barrido vertical no
// interfiera con el dibujo de tiles, sprites o nuestro texto,
// evitándonos algunos efectos indeseados de flickering
// principalmente
VDP_waitVSync();
}
return (0); // Sin efecto funcional en nuestro juego o programa,
// pero de obligada aparición al final de una función
// main que no se ha declarado como "void main()"
}//end main()
#include <genesis.h>
#include "moon.h"
const u32 tile[8]=
{
0x00111100, // Línea 1: pixels 1 a 8
0x01144110, // Línea 2
0x11244211, // Línea 3
0x11244211, // Línea 4
0x11222211, // Línea 5
0x11222211, // Línea 6
0x01122110, // Línea 7
0x00111100 // Línea 8: píxels 57 a 64
};
struct genresTiles
{
u16 *pal; // Puntero a los datos de la paleta
u32 *tiles; // Puntero a los datos de los tiles
u16 width; // Ancho en tiles
u16 height; // Alto en tiles
u16 compressedSize; // 0 en esta demo, mas proximamente
};
extern struct genresTiles luna;
////////////////////////////////////////////////////////////////////
// MAIN
////////////////////////////////////////////////////////////////////
int main( ){
VDP_drawText("Ejemplo de dibujo de TILES", 5, 1);
VDP_drawText("--------------------------", 5, 2);
// VDP_loadTileData(const u32 *data, u16 index, u16 num, u8 use_dma):
// - data: puntero a los datos de nuestro tile
// (o al primero de nuestra ristra tiles si es el caso)
// - index: en qué posición de la memoria de vídeo (VRAM) vamos a
// almacernarlo (o a partir de qué dirección si son varios)
// - num: cuántos tiles vamos a almacenar a partir de esa posición
// - use_dma: ¿usaremo o no "Acceso Directo a Memoria"?
VDP_loadTileData( (const u32 *)tile, 1, 1, 0);
VDP_setTileMap(APLAN, 1, 5, 5);
VDP_setTileMap(BPLAN, TILE_ATTR_FULL(PAL2, 0, 1, 0, 1), 6, 5);
VDP_setTileMap(APLAN, TILE_ATTR_FULL(PAL1, 1, 0, 0, 1), 7, 7);
VDP_setTileMap(BPLAN, TILE_ATTR_FULL(PAL2, 0, 0, 0, 1), 7, 7);
VDP_setTileMap(APLAN, TILE_ATTR_FULL(PAL1, 0, 0, 0, 1), 8, 7);
VDP_setTileMap(BPLAN, TILE_ATTR_FULL(PAL2, 1, 0, 0, 1), 8, 7);
// Rellena un cuadrado de 8x4 del tile 1 con paleta azul en (12,12)
VDP_fillTileMapRect(BPLAN, TILE_ATTR_FULL(PAL3, 0, 0, 0, 1), 12, 12, 8, 4);
u16 w = moon[0] / 8;
u16 h = moon[1] / 8;
VDP_setPalette(PAL1, &moon[2]);
VDP_loadBMPTileData((u32*) &moon[18], 2, w, h, w);
VDP_fillTileMapRectInc(BPLAN, TILE_ATTR_FULL(PAL1, 0, 0, 0, 2), 23, 6, w, h);
VDP_setPalette(PAL2, luna.pal);
VDP_loadTileData(luna.tiles, 100, luna.width*luna.height, 0);
VDP_fillTileMapRectInc(BPLAN, TILE_ATTR_FULL(PAL2, 0, 0, 0, 100), 23, 12, luna.width, luna.height);
while(1){
// sincroniza la pantalla
VDP_waitVSync();
}
****************************************************/
// LIBRERIAS ////////////////////////////////////////
#include <genesis.h>
// DATOS GLOBALES ///////////////////////////////////
const u32 spriteTiles[4*8]=
{
0x00001111, // Tile izquierda arriba
0x00001111,
0x00111144,
0x00111144,
0x11112244,
0x11112244,
0x11112244,
0x11112244,
0x11112222, // Tile izquierda abajo
0x11112222,
0x11112222,
0x11112222,
0x00111122,
0x00111122,
0x00001111,
0x00001111,
0x11110000, // Tile derecha arriba
0x11110000,
0x44111100,
0x44111100,
0x44221111,
0x44221111,
0x44221111,
0x44221111,
0x22221111, // Tile derecha abajo
0x22221111,
0x22221111,
0x22221111,
0x22111100,
0x22111100,
0x11110000,
0x11110000
};
struct genresSprites
{
u16 *pal; // Puntero a los datos de la paleta
u32 **sprites; // Puntero a los datos de los sprites
u16 count; // Numero de sprites
u16 width; // Ancho de cada sprite en pixels
u16 height; // Ancho de cada sprite en pixels
u16 size; // como usamos el ancho/alto en pixels, informacion importante en el tamaño del sprite
};
extern struct genresSprites sonic;
SPRITE sonic "data/sonic.bmp" 24 32 0 7
/////////////////////////////////////////////////////////////
// MAIN
/////////////////////////////////////////////////////////////
int main( ){
// Dibujamos la cabecera austera de turno con VDP_drawText()
VDP_drawText("Ejemplo de dibujo de SPRITES", 5, 1);
VDP_drawText("----------------------------", 5, 2);
// VDP_loadTileData(const u32 *data, u16 index, u16 num, u8 use_dma):
// - data: puntero a los datos de nuestro tile
// (o al primero de nuestra ristra tiles si es el caso)
// - index: en qué posición de la memoria de vídeo (VRAM) vamos a
// almacernarlo (o a partir de qué dirección si son varios)
// - num: cuántos tiles vamos a almacenar a partir de esa posición
// - use_dma: ¿usaremos o no "Acceso Directo a Memoria"?
// "spriteTiles" es el nombre que le dimos al array con la información
// gráfica de nuestro sprite-bola de 2x2 tiles.
VDP_loadTileData( (const u32 *)spriteTiles, 1, 4, 1);
VDP_resetSprites();
// void VDP_setSprite(u16 index, u16 x, u16 y, u8 size, u16 tile_attr, u8 link)
// - index: índice numérico que identifica al sprite (desde 0 a 79)
// - x: coordenada x en pantalla en la que se situará
// - y: coordenada y en pantalla en la que se situará
// - size: tamaño en tiles (desde 1x1 a 4x4 tiles). Nos valdremos de la
// macro "SPRITE_SIZE()" para indicarlo
// - tile_attr: atributos de los tile(s), (paleta, prioridad, vflip, hflip, pos_tile_VRAM)
// - link: índice del sprite que sucede al actual, cada sprite debe enlazar
// al siguiente, teniendo el último que enlazar con el primero
VDP_setSprite(0, 40, 40, SPRITE_SIZE(2,2), TILE_ATTR_FULL(PAL0,1,0,0,1), 1);
SpriteDef spriteBola2;
// typedef struct
// {
// s16 posx, posy;
// u16 tile_attr;
// u8 size, link;
// } SpriteDef;
// Los atributos se corresponden con cada uno de los parámetros requeridos por
// VDP_setSprite() salvando la ausencia de uno para el índice del sprite
spriteBola2.posx = 0;
spriteBola2.posy = 0;
spriteBola2.size = SPRITE_SIZE(2,2);
spriteBola2.tile_attr = TILE_ATTR_FULL(PAL0,1,0,0,1);
spriteBola2.link = 2;
// void VDP_setSpriteP(u16 index, const SpriteDef *sprite)
// - index: índice numérico que identifica al sprite (desde 0 a 79)
// - sprite: dirección de memoria donde se encuentra el struct
// con la información de nuestro sprite
// (nos valemos del operador & para ello)
VDP_setSpriteP(1, &spriteBola2);
u16 nbTiles = (sonic.height>>3) * (sonic.width>>3);
extern struct genresSprites sonic;
SpriteDef spriteErizo;
VDP_setPalette(PAL1, sonic.pal);
spriteErizo.posx = 128;
spriteErizo.posy = 128;
spriteErizo.size = sonic.size>>8;
spriteErizo.tile_attr = TILE_ATTR_FULL(PAL1,1,1,1,5);
spriteErizo.link = 0; // Como éste va a ser el último sprite que definiremos
// lo hemos de enlazar ya con el primero cuya id es 0.
// Si se te olvida hacer esto no veremos sprite alguno.
VDP_setSpriteP(2, &spriteErizo);
u8 frame = 0;
// BUCLE PRINCIPAL ==================================================
while(1){ // Bucle infinito al canto
spriteBola2.posx++;
spriteBola2.posy++;
VDP_setSpriteP(1, &spriteBola2);
VDP_loadTileData(sonic.sprites[frame + 1], 5, nbTiles, 1);
frame++;
frame%=3; // solo tenemos 3 frames, volvemos al primero
spriteErizo.posx+=10;
VDP_setSpriteP(2, &spriteErizo);
VDP_updateSprites();
VDP_waitVSync();
VDP_waitVSync();
VDP_waitVSync();
VDP_waitVSync();
VDP_waitVSync();
VDP_waitVSync();
VDP_waitVSync();
VDP_waitVSync();
VDP_waitVSync();
}
return (0); // Return cero, y hasta aquí hemos llegado en esta ocasión.
}
Antes que nada comentar algunos detalles:
- Puede parecer un poco largo y complejo, pero la verdad es que escrito y leído parece mucho más de lo que es una vez sabes los pasos a seguir, todo se hace mucho más rápido cuando se vuelve automático pero ya sabéis lo que dicen de las primeras veces...
- Descargar la última versión de Gimp en este enlace: Gimp 2.8.
- Al igual que en la wiki, sólo he probado y usado archivos BMP.
- Este tutorial se aplica tanto para bitmaps cualquiera como para sprites (usaré la palabra "imágenes" para generalizar).
- El máximo número de colores de una imagen debe ser 16.
- El ancho y largo de las imágenes deben ser múltiplo de 8. Los sprites tienen un tamaño máximo de altura y anchura de 32 píxeles (GenRes aún no soporta sprites mayores).
- Las imágenes deben tener 4 bits por píxel.
Se puede aplicar el siguiente tutorial para crear bitmaps de cualquier tipo o sprites para GenRes, pero lo haré con una tabla de sprites ya que un bitmap se puede considerar una tabla de 1x1 (sin olvidar que los bitmaps no tienen la limitación de 32 píxeles).
El primer paso es crear nuesta tabla de sprites, que para los que no hayan oído antes este concepto decir que no es más que una imagen que contiene cada uno de los sprites que queremos usar colocados a modo de tabla, ordenados en filas y columnas. Normalmente una única tabla contiene todos los sprites de un personaje o enemigo, pero se puede usar para cualquier objeto.
Aclarar que en otros entornos de programación de videojuegos el hecho de cargar un único archivo de imagen que incluye a todos los sprites ofrece mejor rendimiento que cargar un archivo por sprite, pero desconozco como afecta esto a la Megadrive.
Empecemos con el Gimp:
- Debemos elegir el tamaño de nuestra tabla en función del número de sprites que vamos a necesitar. Por ejemplo supongamos que queremos hacer al prota de nuestro juego y hemos decidido que estará compuesto de 14 sprites distintos, entonces la elección óptima para el tamaño de la tabla sería 1x14, 14x1, 2x7 o 7x2, pero para el ejemplo vamos a escoger una tabla 3x5 (3 filas con 5 columnas -sprites- cada una) para explicar lo que pasa cuando queda alguna casilla vacía (también nos valdría una tabla de 4x4, 10x2... pero todas ocupan un espacio extra innecesario). La casilla 15 -posición 3,5- estará vacía y no tendrá sentido acceder a ella, aunque no produce error alguno. Simplemente se deja en blanco o se rellena con el color que indica transparencia (esto lo explico más adelante).
- Miramos el tamaño en píxeles que necesitamos para nuestros sprites. Deben ser suficientes para que quepan el sprite "más ancho" y el sprite "más largo o alto". Por ejemplo 24x32.
- Ahora sabemos que debemos crear una imagen de tamaño 120x96 (5 columnas por 24 píxeles de ancho y 3 filas por 32 píxeles de altura).
- Vamos al Gimp y le damos a Archivo->Nuevo. En la ventana ponemos el tamaño que decidimos en el paso anterior. Le damos a Opciones avanzadas y seleccionamos Color de fondo en Rellenar con. Sin cerrar esta ventana vamos a la ventana Caja de Herramientas y elegimos un color de fondo (de los 2 rectángulos superpuestos, el de debajo). Este color debe ser uno que NO aparezca en ninguno de los sprites de su tabla ya que será el color que el compilador interpretará como transparente. Volvemos a la primera ventana y pulsamos Aceptar.
- Ahora tenemos un archivo monocolor 120x96. Para situar correctamente los sprites vamos a colocar una rejilla (esta rejilla no aparecerá en la imagen final). Vamos a Vista->Mostrar rejilla. Ahora la ajustamos a nuestro caso: vamos a Imagen->Configurar rejilla.
Se abre una nueva ventana donde la configuramos a nuestro gusto (es recomendable que los colores no interfieran visualmente con nuestros sprites para poder colocarlos bien). En Espaciado colocamos la resolución de nuestros sprites: 24x32. Pulsad el icono de la cadena para que el ancho y la altura puedan tomar valores distintos.
Empecé haciendo el tutorial sin pensar con qué sprites haría las imágenes... finalmente elegí los sprites de Sonic y la verdad que el fondo verde claro y la rejilla roja le sienta a Sonic como a un cristo dos pistolas, por eso luego de repente cambio los colores pero recordad que no importa, a la hora de programar cualquier color vale, sólo debe cumplir que el color de fondo no aparezca en ningún sprite de la tabla y que este color no complique visualmente a la hora de colocar el sprite en la celda, como pasaría aquí con la rejilla y botas rojas.- Ya podemos empezar a pegar los sprites en cada celda de la tabla. Tened en cuenta que la posición del sprite dentro de cada casilla es importante. Hay que procurar que si dos sprites se diferencian por ejemplo en el movimiento de la cabeza del personaje, que la posición relativa del resto del cuerpo dentro de la celda sea la misma en ambas casillas.
Quedará más claro con el ejemplo de las siguientes viñetas: tenemos dos tablas de 1x2 donde en la inferior los sprites se han colocado mal horizontal y verticalmente mientras que en la superior están situados correctamente. Como los pies no se mueven deben estar en la misma posición dentro de sus respectivas casillas. En caso de que la diferencia entre dos sprites sea total (Sonic de pie y echo una bola por ejemplo) se debe procurar centrar ambas imágenes en la celda para evitar animaciones bruscas. A la derecha de ambas tablas se muestran las animaciones tal cual se verían en la Megadrive en cada caso (he dejado el color de fondo para que se vea mejor el movimiento dentro de la celda).- Una vez tenemos ya lista nuestra tabla de sprites tenemos que guardarla en un formato que pueda leer SGDK o GenRes. Para ello vamos a Imagen->Modo->Indexado:
Marcamos Generar paleta óptima, ponemos en Número máximo de colores 16 y pulsamos Convertir:
Ya tenemos una imagen a 16 colores (no tiene por qué ser 16, este número indica el número máximo. En mi ejemplo se queda en 13). Ahora tenemos que ver la paleta de colores de la imagen para el tema de las transparencias. Vamos a Colores->Mapa->Reordenar el mapa de colores. En mi caso obtengo el siguiente resultado:
En esta pantalla debemos apuntar el número al que corresponde el color de fondo de nuestra tabla, en mi caso el verde es el 8. En su momento diré para qué sirve este número.- Nuestra tabla de sprites está construida, ahora vamos a guardarla en formato BMP. Vamos a Archivo->Exportar... ponemos el nombre que queramos a nuestro archivo + ".bmp". Luego pulsamos el botón Exportar y en la ventana que se nos abre le damos a Opciones de compatibilidad y por último marcamos la casilla de No escribir la información del espacio de colores y exportamos.
Tiles
Antes de meternos con los sprites y bitmaps, empecemos entendiendo un poco como maneja la Megadrive los tiles:Otros datos interesantes a conocer:
- La Megadrive redibuja 2 planos en cada actualización de pantalla (más un tercero para los sprites).
- Cada plano está compuesto por tiles de 8x8 píxeles.
- Cada plano puede cargar en memoria tiles de entre 32x32 y 128x128 (aunque sólo hasta 40x28 son visibles en la pantalla).
- Cada plano se rellena de izquierda a derecha y de arriba a abajo.
- Cada tile se puede reusar varias veces en cualquier plano con distintas paletas.
- Cada tile se puede usar con cualquiera de las 4 paletas disponibles.
- Cada tile se puede dibujar volteada sin un consumo extra de memoria.
- Una cantidad importante de tiles se pueden cargar usando DMA.
Entonces para dibujar un tile en la pantalla:
- Un tile está hecho de 32 bytes, 4 bytes por línea.
- Cada píxel del tile tiene un valor de 4 bits que indica el color, entre 0x0 y 0xF.
- El primer tile (el 0) en la VRAM se usa para colorear el fondo del tile.
- SGDK reserva espacio en la VRAM para 1310 tiles + 96 para texto.
- Un tile se considera un patrón o unidad de diseño.
- Un tile en la pantalla no se borra en cada actualización, no es necesario dibujarlo cada vez.
- Cada paleta esta compuesta de 16 colores.
- Cargamos el tile en la VRAM (el DMA no lo usaremos en este tutorial).
- Cargamos su paleta si no ha sido cargada antes.
- Lo dibujamos en el plano que indiquemos en (x,y) con esa paleta.
Ahora que ya sabemos más sobre los tiles, necesitamos crear uno para practicar un poco. Existen programas para convertir bitmaps de 16 colores en tiles y paletas para la Megadrive (Genitile, B2T, GenRes, Mega-Happy-Sprite...) pero vamos a empezar haciendo uno en código C mediante un array:const u32 tile[8]=
{
0x00111100,
0x01144110,
0x11244211,
0x11244211,
0x11222211,
0x11222211,
0x01122110,
0x00111100
};
Tenemos un tile de 4 colores: 0, 1, 2 y 4. Veamos el código para dibujarlo en pantalla:// Cargamos nuestro tile en la posición 1 de la VRAM
// argumento 1: el tile a cargar.
// argumento 2: posición en la VRAM.
// argumento 3: número de tiles a cargar.
// argumento 4: usar DMA (1) o no (0).
VDP_loadTileData( (const u32 *)tile, 1, 1, 0);
// No cargamos una paleta en este ejemplo, dejamos la paleta por defecto.
// Dibujamos nuestro tile.
// argumento 1: el plano donde se dibuja: APLAN o BPLAN.
// argumento 2: índice del tile a dibujar.
// argumento 3: Coordenada X.
// argumento 4: Coordenada Y.
VDP_setTileMap(APLAN, 1, 5, 5);
while(1)
{
VDP_waitVSync();
}
Esto dibujará en la pantalla nuestro tile gris degradado.
El argumento 2 de VDP_setTileMap puede ser algo más que el índice del tile. Veamos cómo sería dibujarlo en verde y volteado:
// Argumentos de TILE_ATTR_FULL:
// argumento 1: paleta del tile (PAL2 = paleta por defecto de tonos verdes).
// Prioridad: prioridad baja (0) o alta (1).
// Volteo vertical (1) o no (0).
// Volteo horizontal (1) o no (0).
// Tile a dibujar: tile 1.
VDP_setTileMap(BPLAN, TILE_ATTR_FULL(PAL2, 0, 1, 0, 1), 6, 5);
Zoom X4
La prioridad indica qué plano se dibuja sobre el otro. Esto se regula a nivel de tiles, lo que significa que por ejemplo el plano A puede estar delante del B en el punto (x,y) pero estará detrás en el punto (m,n). Esto es útil para hacer planos de scroll que se mueven a distintas velocidades. Veamos un ejemplo del uso de las prioridades://El mismo tile dibujado en 2 planos y con paletas distintas, ¿cuál se superpone?
VDP_setTileMap(APLAN, TILE_ATTR_FULL(PAL1, 1, 0, 0, 1), 7, 7);
VDP_setTileMap(BPLAN, TILE_ATTR_FULL(PAL2, 0, 0, 0, 1), 7, 7);
VDP_setTileMap(APLAN, TILE_ATTR_FULL(PAL1, 0, 0, 0, 1), 8, 7);
VDP_setTileMap(BPLAN, TILE_ATTR_FULL(PAL2, 1, 0, 0, 1), 8, 7);
En (7,7) A está frente a B al tener prioridad 1 frente a 0, en cambio en (8,7) B tiene mayor prioridad por lo que se superpone a A. Si ambos tuvieran la misma prioridad se mantiene el orden por defecto: A queda por delante de B.
De izquierda a derecha: prioridad A>B, B>A, A=B.
Zoom X4
Ahora supongamos que queremos rellenar un gran espacio (una zona o la pantalla por ejemplo) con nuestro tile, entonces deberíamos dibujarlos uno a uno lo que sería un trabajo tedioso. Por suerte SGDK nos permite usar la función VDP_fillTileMapRect:// Rellena con tiles azules un cuadrado de tamaño 8x8 en la posición (12,12).
// argumento1: dibujamos en el plano A o B.
// argumento2: tile inicial.
// argumento3: coordenada X.
// argumento4: coordenada Y.
// argumento5: anchura del rectángulo.
// argumento6: altura del rectángulo.
VDP_fillTileMapRect(BPLAN, TILE_ATTR_FULL(PAL3, 0, 0, 0, 1), 12, 12, 8, 8);
Zoom X2
Multi-Tile
Como no es muy difícil de entender, no podemos hacer un juego con un único tile. Probemos a dibujar esta luna en la pantalla:
SGDK puede manejar varios ficheros de bits a la vez si están en la subcarpeta res. Entonces SGDK creará dos archivos para cada bitmap:
- nombre_del_archivo_bmp.o es la forma de objeto del bitmap.
- nombre_del_archivo_bmp.h permite acceder a los datos del bmp en código fuente.
El código para cargar nuestro bitmap es el siguiente:// Carca el código de nuestro bitmap.
#include "moon.h"
int main( )
{
// Obtiene el ancho de la imagen en píxeles (múltiplo de 8).
u16 w = moon[0];
// Obtiene la altura de la imagen en píxeles (múltiplo de 8).
u16 h = moon[1];
// Obtiene la paleta de la imagen (de 2 a 17)
VDP_setPalette(PAL1, &moon[2]);
// Carga la imagen (18....) in VRAM
// w/8 = ancho en tiles que queremos cargar.
// h/8 = altura en tiles que queremos cargar.
// w/8 = Ancho en tiles del bitmap (se necesita porque se puede cargar sólo una parte del bitmap si se quiere pero[i] SGDK[/i] necesita la anchura como referencia).
VDP_loadBMPTileData((u32*) &moon[18], 1, w / 8, h / 8, w / 8 );
while(1)
{
VDP_waitVSync();
}
return 0;
}
Para dibujarlo ahora podríamos hacerlo manualmente usando VDP_setTileMap pero resulta muy trabajoso dibujar uno a uno cada tile. Por suerte SGDK trae la función VDP_fillTileMapRectInc que dibuja desde el índice del tile en el argumento hasta el último necesario para rellenar el rectángulo cuyas coordenadas y tamaño pasamos como argumentos:// Llena la pantalla con los tiles desde el primero hasta rellenar el rectángulo con origen (12,12) y como tamaño la anchura y altura indicadas.
// argumento1: dibujamos en el plano A o B.
// argumento2: tile inicial.
// argumento3: coordenada X.
// argumento4: coordenada Y.
// argumento5: anchura del rectángulo (w / 8 = anchura de la imágen en tiles).
// argumento6: altura del rectángulo (h / 8 = altura de la imágen en tiles).
VDP_fillTileMapRectInc(BPLAN, TILE_ATTR_FULL(PAL1, 0, 0, 0, 1), 12, 12, w / 8, h / 8);
GenRes para manejar bitmaps
SGDK trae otra manera de cargar tiles: usar GenRes. GenRes es una herramienta que convierte bitmaps, animaciones y maps para el desarrollo de juegos en la Megadrive. En este tutorial sólo hablaremos del modo BITMAP.
GenRes se basa en la declaración de un fichero de recursos en la carpeta raiz del proyecto llamado resource.rc donde cada línea define el modo de conversión, el nombre de la variable de salida, el fichero de bitmaps y otros parámetros. Usamos el modo de conversión de BITMAP:BITMAP variable_salida1 "directorio/fichero1.bmp" 0
BITMAP variable_salida2 "directorio/fichero2.bmp" 0
....
....
....
; La última línea de este archivo debe ser una línea en blanco o un comentario.
Los archivos BMP no deben guardarse en la carpeta "res" ya que serían compilados por el GenRes y el compilador de bitmaps del SGDK al mismo tiempo.
Escribiendo esto en nuestro archivo resource.rc, SGDK invocará GenRes para compilar los ficheros directorio/ficheroX.bmp a un archivo enlazado resource.o.
Desde la versión 0.7d, el formato de salida de GenRes no está definido en SGDK por lo que debemos escribir nuestra propia estructura, como ésta:struct genresTiles
{
u16 *pal; // Puntero a la paleta.
u32 *tiles; // Puntero a los tiles.
u16 width; // Anchura en tiles.
u16 height; // Altura en tiles.
u16 compressedSize; // 0.
};
A diferencia del soporte de bitmaps nativo de SGDK, GenRes no genera un archivo de cabecera pero podemos acceder a ellos declarando las variables variable_salidaX de la siguiente manera:extern struct genresTiles variable_salida1, variable_salida2...;
VDP_loadBMPTileData se usa para los ficheros BMP, pero como GenRes convierte un bitmap en tiles necesitamos otra función: VDP_loadTileData. Así quedaría el ejemplo anterior de la luna usando GenRes:
resource.rcBITMAP moon "data/moon.bmp" 0
; La última línea de este archivo debe ser una línea en blanco o un comentario.
main.cstruct genresTiles
{
u16 *pal;
u32 *tiles;
u16 width;
u16 height;
u16 compressedSize;
};
extern struct genresTiles moon;
int main( )
{
VDP_setPalette(PAL1, moon.pal);
// Carga los tiles en la VRAM.
// argumento 1: los tiles.
// argumento 2: índice del primer tile.
// argumento 3: número de tiles a cargar.
// argumento 4: usa DMA (1) o no (0).
VDP_loadTileData(moon.tiles, 1, moon.width*moon.height, 0);
VDP_fillTileMapRectInc(BPLAN, TILE_ATTR_FULL(PAL1, 0, 0, 0, 1), 12, 12, moon.width, moon.height);
while(1)
{
VDP_waitVSync();
}
return 0;
}
Sobre si es mejor usar el soporte nativo para bitmaps de SGDK o el GenRes... que cada uno pruebe ambos y se quede con el que más le guste.
Bitmaps comprimidos
Si se usa una gran cantidad de tiles es muy recomendable comprimir los bitmaps con programas como RLE, Huffman... SGDK no incluye un método para cargar bitmaps comprimidos por lo que es cosa del usuario hacerse su propio algoritmo de des/compresión. GenRes puede usar la compresión RLE, habrá más detalles en próximos tutoriales. Podéis mirar si el programa que usáis puede exportar bitmaps comprimidos.
Miscelánea
Tenemos disponibles las utilidades del emulador Gens KMod para comprobar como funcionan vuestros tiles. Podéis comprobar la VRAM y detectar cualquier posible anomalía.
Sprites
Antes que nada comentar que existe una diferencia entre los tiles de los planos y los tiles de los sprites:Por esto no podemos usar las mismas funciones para dibujar los tiles de los planos y sprites. Por suerte la forma de cargarlas en memoria si es la misma. Ambos usan los tiles cargados en la VRAM por lo que podremos usar un mismo tile para dibujar en ambas cosas a la vez si lo necesitamos.
- Los planos dibujan los tiles de izquierda a derecha y luego de arriba a abajo (siguiendo las filas).
- Los sprites dibujan sus tiles de arriba a abajo y después de izquierda a derecha (recorriendo las columnas).
Información básica que debemos saber sobre los sprites:Los pasos para dibujar un sprite en la pantalla:
- El "plano" de los sprites, a diferencia de los otros dos, no es desplazable (no admite scrolling) y tiene un tamaño fijo, normalmente 512x512.
- Esto significa que solo una parte del plano de los sprites es visible. Esta parte comienza en (128,128).
- Un sprite fuera de este área de visualización no es visible.
- La posición de los sprites es cíclica. Por ejemplo el punto (300, 812) es el mismo que (300, 300) ya que 812 mod 512 = 300.
- Se puede controlar el orden en el que se dibujan los sprites mediante el atributo link.
- Un máximo de 80 sprites se pueden definir en modo PAL.
- Un máximo de 20 sprites se pueden dibujar en una misma línea en modo PAL.
- Los sprites están hechos de 1x1 a 4x4 tiles (por lo que los sprites mas grandes son de hecho multi-sprites).
- Cargar los tiles en la VRAM.
- Cargar su paleta (si no ha sido cargada aún).
- Definir el sprite.
- Definir los demás, si existen.
- Solicitar dibujarlo en pantalla
Crear y dibujar un sprite definido en C
Como antes, vamos a crear primero un sprite mediante un array de tiles. Queremos un sprite de 2x2, entonces necesitamos 4 tiles:const u32 spriteTiles[4*8]=
{
0x00001111, //Tile de arriba-izquierda.
0x00001111,
// ...
0x11112222, //Tile de abajo-izquierda.
0x11112222,
// ...
0x11110000, //Tile de arriba-derecha.
0x11110000,
// ...
0x22221111, //Tile de abajo-derecha.
0x22221111,
// ...
};
Este sprite tiene la misma forma que usamos para crear un tile. Este es el código para cargarlo en memoria y visualizarlo correctamente:// Carga los tiles en la VRAM.
VDP_loadTileData( (const u32 *)spriteTiles, 1, 4, 0);
// Usamos una paleta por defecto por ahora.
// Definimos el sprite:
// argumento 1: el índice del sprite (desde 0 a 79).
// argumento 2: coordenada X.
// argumento 3: coordenada Y.
// argumento 4: tamaño (de 1x1 a 4x4 tiles).
// argumento 5: atributos de el/los tile/s.
// argumento 6: propiedad link.
VDP_setSprite(0, 40, 40, SPRITE_SIZE(2,2), TILE_ATTR_FULL(PAL0,1,0,0,1), 0);
// Solicitar que se dibuje el sprite.
VDP_updateSprites();
while(1)
{
VDP_waitVSync();
}
Otros aspectos importantes:
- Puedes definir tantos sprites como necesites (80 máximo) antes de solicitar que se dibujen en pantalla.
- Usando SGDK, la posición de los sprites está basada en el área de la pantalla, no en el plano de los sprites, lo que quiere decir que (0,0) significa (0,0) en la pantalla y (128,128) en el plano de los sprites.
- SPRITE_SIZE es necesario para pasar un valor correcto (0000b para 1x1, 0101b para 2x2, etc...). Al parecer hay algunos problemas con la función SPRITE_SIZE, aunque yo de momento no he tenido ninguno. En caso de producirse cambiar "SPRITE_SIZE(X,X)" por "variable_salidaX.size>>8".
- TILE_ATTR_FULL es el mismo macro que usamos con los tiles.
Mover un sprite
Un sprite se usa principalmente para objetos que se mueven por lo que hay que actualizar la x e y del sprite constantemente. Usando la función VDP_setSprite pondremos en problemas a la Megadrive ya que tendremos que restablecer el sprite en cada actualización. Una manera útil de hacerlo es:
- Usar una estructura para mantener las propiedades del sprite: un objeto SpriteDef (en el tutorial en inglés aparece _spritedef porque está desactualizado).
- Una función para cambiar y actualizar lo que necesitemos: VDP_setSpriteP
SpriteDef mySprite;
mySprite.posx = 0;
mySprite.posy = 0;
mySprite.size = SPRITE_SIZE(2,2);
mySprite.tile_attr = TILE_ATTR_FULL(PAL0,1,0,0,1);
mySprite.link = 0;
VDP_setSpriteP(0, &mySprite);
.....y para hacer que se mueva:while(1)
{
mySprite.posx++;
mySprite.posy++;
VDP_setSpriteP(0, &mySprite);
VDP_updateSprites();
VDP_waitVSync();
}
Puedes usar VDP_setSprite o VDP_setSpriteP indistintamente sin problemas, SDGK te ofrece VDP_setSpriteP para hacerlo más fácilmente.
Propiedad link
Hasta ahora no habíamos hablado de la propiedad link. Sin conocer su uso no seremos capaces de dibujar más de un sprite. Esta variable contiene el índice del siguiente sprite a dibujar (el último debe apuntar de vuelta al sprite 0). Volviendo al ejemplo anterior, sólo dibujamos el sprite 0 ya que su link vale 0.mySprite.link = 0;
Para dibujar dos sprites tendremos que hacerlo de la siguiente manera:mySprite0.link = 1; // Enlaza al siguiente sprite, el 1.
VDP_setSpriteP(0, &mySprite0); // Dibuja el sprite 0.
mySprite1.link = 0; // Como mySprite1 es el último, volvemos al sprite 0.
VDP_setSpriteP(1, &mySprite1); // Dibujamos el sprite 1.
La propiedad link es usa también para definir el orden de superposición de los sprites: el primero estará por debajo del siguiente, es decir, se van superponiendo a medida que se van dibujando. Entonces te permite controlar qué sprite y cuándo éste puede ser dibujado.
De la siguiente manera puedes fácilmente definir un sprite pero no dibujarlo:mySprite0.link = 2; // Enlaza al sprite 2.
VDP_setSpriteP(0, &mySprite0); // Dibuja el sprite 0.
mySprite1.link = 2; // Enlaza al sprite 2.
VDP_setSpriteP(1, &mySprite1); // Dibuja el sprite 1 (esto nunca ocurre).
mySprite2.link = 0; // Vuelve al sprite 0 por lo que el sprite 1 no se dibuja.
VDP_setSpriteP(2, &mySprite2); // Dibuja el sprite 2.
Por supuesto el sprite 0 nunca se puede evitar.
PRECAUCIÓN: un mal uso de la propiedad link es la principal razón para que aparezcan bugs en el juego. Hay que comprobar estas 3 cosas:
- Todos tus sprites deben enlazar a otro.
- Que no se forme un enlace cíclico (por ejemplo: 1->2, 2->3, 3->1 ...).
- El último sprite tiene que apuntar al sprite 0
Manejo de sprites con GenRes
A diferencia de los tiles para planos, SGDK no trae soporte nativo para sprites en bitmaps de 16 colores, pero podemos usar GenRes en modo SPRITE.
Como ya hemos comentado, GenRes se basa en la declaración de un fichero de recursos donde cada línea define el modo de conversión, el nombre de la variable de salida, el fichero de bitmaps y otros parámetros. Ahora con SPRITE se puede hacer de esta manera:SPRITE variable_salida1 "directorio/fichero1.bmp" <ancho_sprite_fichero1> <altura_sprite_fichero1> <argumento*> <posición del color de transparencia en la paleta de fichero1.bmp>
SPRITE variable_salida2 "directorio/fichero2.bmp" <ancho_sprite_fichero2> <altura_sprite_fichero2> <argumento*> <posición del color de transparencia en la paleta de fichero2.bmp>
....
....
; La última línea de este archivo debe ser una línea en blanco o un comentario.
argumento*: desconozco la utilidad y función de este argumento. el que nos interesa es el 4ª que es el número que apuntamos antes en el Gimp, la posición donde está el color de la transparencia en nuestra paleta de colores.
Recordar de nuevo que los archivos no deben guardarse en la carpeta res.
Escribimos esto en un fichero resource.rc. SGDK invocará a GenRes para compilar los directorio/archivoX.bmp al fichero resource.o.
Desde la versión GenRes 0.7d este formato de salida de estas variables no está definido en SGDK por lo que de nuevo tendremos que crear nosotros mismos una estructura con ese formato:struct genresSprites
{
u16 *pal; //Puntero a la paleta.
u32 **sprites; //Puntero hacia los sprites.
u16 count; //El número de sprites que ha encontrado [i]GenRes [/i]en nuestro archivo.
u16 width; //Anchura de cada sprite en píxeles, no en tiles.
u16 height; //Altura de cada sprite en píxeles, no en tiles.
u16 size; //Tamaño del sprite (luego veremos más acerca de [i]size[/i]).
};
Luego para acceder a los datos usando las variable_salidaX del fichero resource.sc:extern struct genresSprites variable_salida1, variable_salida2, .... ;
Nos fijamos en que la variable sprites ya no es "u32 *sprites", ahora es "u32 **sprites". Esto es porque GenRes convierte tablas de sprites en un Array de sprites (incluso si la tabla sólo contiene 1 sprite), no en un único sprite.
Podemos retomar ahora nuestra tabla de sprites que hicimos con Gimp. Si te saltaste la parte 1 de este tutorial puedes seguir adelante usando esta tabla (la rejilla la pone el editor de imágenes, no aparece en el fichero):
Para nuestro proyecto crearíamos un archivo resource.rc de la siguiente manera (suponemos que nuestro archivo se llama sonic.bmp y está guardado en una carpeta dentro de nuestro proyecto llamada data):SPRITE sonic "data/sonic.bmp" 24 32 0 T
; La última línea de este archivo debe ser una línea en blanco o un comentario.
T es la posición del color de transparencia en la paleta de nuestra tabla (en la parte 1 del tutorial, apartado 7, podéis ver cómo averiguar esta posición con el Gimp). El de la imagen anterior es la posición 7.
Ahora tenemos nuestro archivo BMP diseccionado en un array de sprites en la variable sprites. Los sprites están ordenados en el array recorriendo cada fila de la tabla hasta la última columna y luego saltando a la siguiente fila y repitiendo el proceso, así hasta terminar.
Cada posición del array contiene los datos estándar de un tile por lo que se puede acceder a ella usando la función VDP_loadTileData pasándole el número de tiles. Para dibujar uno de los sprites de nuestra tabla el código sería el siguiente:// Cada sprite es altura/8 * anchura/8 (porque es tamaño en píxel, no en tiles)
// Es el número de 8x8 tiles que necesitas para dibujar un sprite.
u16 nbTiles = (sonic.height>>3) * (sonic.width>>3);
VDP_loadTileData( sonic.sprites[0], 1, nbTiles, 0);
// Carga la paleta de sonic.bmp en PAL1.
VDP_setPalette(PAL1, sonic.pal);
// Es opcional pero recomendable.
VDP_resetSprites();
// argumento1: índice del sprite (entre 0 y 79).
// argumento2: posición en el eje X.
// argumento3: posición en el eje y.
// argumento4: tamaño: tiles de 1x1 a 4x4, el nuestro es 3x4 (24x32 entre 8).
// argumento5: atributos de el/los tile/s:
// argumento6: propiedad link.
VDP_setSprite(0, 0, 0, SPRITE_SIZE(3,4), TILE_ATTR_FULL(PAL1,1,0,0,1), 0);
VDP_updateSprites();
Animaciones
Sonic no sería Sonic si no corriese. Ya sabemos como hacer que se mueva usando SpriteDef, pero nos falta algo: la animación de sprites.
Hay 2 maneras básicas de hacer una animación:El primer método necesita mucha VRAM, el segundo realiza una carga continua que puede ralentizar el juego. Vamos a usar el segundo: actualizar los frames.
- Cargar cada frame y declarar el sprite desde el primer tile en cada actualización.
- Declarar un sprite y cargar cada frame en el primer tile en cada actualización.
Los pasos son simples: en cada actualización cargar los tiles del frame con VDP_loadTileData y luego dibujar el frame usando VDP_setSpriteP.
Hagamos que Sonic corra, usamos los sprites 1, 2 & 3:u8 frame = 0;
// definimos el sprite (usando un SpriteDef para mover a Sonic).
mySprite.posx = 40;
mySprite.posy = 40;
mySprite.size = SPRITE_SIZE(3,4);
mySprite.tile_attr = TILE_ATTR_FULL(PAL1,1,0,0,1);
mySprite.link = 0;
VDP_setSpriteP(0, &mySprite);
while(1)
{
// Seguimos usando nbTiles porque todos los sprites tienen el mismo tamaño en una tabla de sprites.
// Frame vale 0, 1, 2 por lo que carga sprites 1, 2, 3; el 0 es Sonic parado de pie.
VDP_loadTileData( sonic.sprites[frame + 1], 1, nbTiles, 0);
frame++; // Siguiente frame
frame%=3; // Va rotando porque sólo necesitamos 3 frames.
// Sonic se mueve.
mySprite.posx+=10;
VDP_setSpriteP(0, &mySprite);
//flush
VDP_updateSprites();
VDP_waitVSync();
}
Miscelánea
Para probar nuestro motor de sprites es útil el emulador Gens KMod. Podemos explorar la lista de sprites y rastrear posibles problemas.
#include <genesis.h>
void myJoyHandler( u16 joy, u16 changed, u16 state)
{
//Si el botón pulsado corresponde al pad en el puerto 1
if (joy == JOY_1)
{
//La sintaxis del código para comprobar el estado
//del botón será la que sigue variando el valor
//con el que se compararán sendos atributos:
//state y change, state correspondiéndose con la
//pulsación del botón y change con la liberación
//del mismo
if (state & BUTTON_START) //Si se pulsa START
{
//Que específicamente elijamos estas determinadas
//coordenadas (x=5, y=13) se debe a que en la
//función main vamos a mostrar el rótulo
//correspondiente al estado del botón START en
//dichas coordenadas. Lo que lo que estaremos
//haciendo será sobreescribir el rótulo cada vez
//que se pulse o suelte el botón, ya sea mostrando
//"START button 1" o "START button 0" según el caso
VDP_drawText("START button 1", 5, 13);
//El área para disponer texto en pantalla en
//megadrive se corresponde exactamente con el área
//de tiles visibles en pantalla (no hay razón para
//no considerar cara caracter un tile, que es lo
//que también son). Por lo tanto, en el modo PAL
//"normal" en que disponemos de una resolución de
//320x224 tendremos un área de 40x28 caracteres;
//La primera fila y la primera columna toma valor
//cero, por lo que para comenzar un texto en la
//esquina superior izquierda lo haríamos tal que
//así: VDP_drawText("texto", 0, 0);
}
else if (changed & BUTTON_START) //Si se suelta
{
VDP_drawText("START button 0", 5, 13);
}
if (state & BUTTON_A) //Si se pulsa A
{
VDP_drawText("A button 1", 5, 14);
}
else if (changed & BUTTON_A) //Si se suelta A
{
VDP_drawText("A button 0", 5, 14);
}
if (state & BUTTON_B)
{
VDP_drawText("B button 1", 5, 15);
}
else if (changed & BUTTON_B)
{
VDP_drawText("B button 0", 5, 15);
}
if (state & BUTTON_C)
{
VDP_drawText("C button 1", 5, 16);
}
else if (changed & BUTTON_C)
{
VDP_drawText("C button 0", 5, 16);
}
if (state & BUTTON_X)
{
VDP_drawText("X button 1", 17, 14);
}
else if (changed & BUTTON_X)
{
VDP_drawText("X button 0", 17, 14);
}
if (state & BUTTON_Y)
{
VDP_drawText("Y button 1", 17, 15);
}
else if (changed & BUTTON_Y)
{
VDP_drawText("Y button 0", 17, 15);
}
if (state & BUTTON_Z)
{
VDP_drawText("Z button 1", 17, 16);
}
else if (changed & BUTTON_Z)
{
VDP_drawText("Z button 0", 17, 16);
}
if (state & BUTTON_UP)
{
VDP_drawText("UP button 1", 5, 17);
}
else if (changed & BUTTON_UP)
{
VDP_drawText("UP button 0", 5, 17);
}
if (state & BUTTON_DOWN)
{
VDP_drawText("DOWN button 1", 5, 18);
}
else if (changed & BUTTON_DOWN)
{
VDP_drawText("DOWN button 0", 5, 18);
}
if (state & BUTTON_LEFT)
{
VDP_drawText("LEFT button 1", 5, 19);
}
else if (changed & BUTTON_LEFT)
{
VDP_drawText("LEFT button 0", 5, 19);
}
if (state & BUTTON_RIGHT)
{
VDP_drawText("RIGHT button 1", 5, 20);
}
else if (changed & BUTTON_RIGHT)
{
VDP_drawText("RIGHT button 0", 5, 20);
}
}
// otras funciones interesante
// JOY_update() refresca el estado del pad, se llama en cada refresco de la pantalla
// JOY_readJoypad( joy ) ¨devuelve el estado del pad1
// JOY_waitPressBtn() espera a que se pulse un boton (no direcciones)
// JOY_waitPress(joy, BUTTON_A | BUTTON_UP) espera a pulsar un boton indicado en un pad especifico
}//end myJoyHandler()
////////////////////////////////////////////////////////////////////
// MAIN
////////////////////////////////////////////////////////////////////
int main( )
{
// resetea el manejador y lee la informacion de los mandos conectados
JOY_init();
//Establecemos que el programa dará soporte al pad de 6 botones
//conectado en el puerto 1
JOY_setSupport(PORT_1, JOY_SUPPORT_6BTN);
//Los otros valores correspondientes al resto de controles soportados
//en las librerías SGDK son JOY_SUPPORT_OFF, JOY_SUPPORT_3BTN,
//JOY_SUPPORT_MOUSE, JOY_SUPPORT_TRACKBALL, JOY_SUPPORT_TEAMPLAYER,
//JOY_SUPPORT_EA4WAYPLAY, JOY_SUPPORT_MENACER, JOY_SUPPORT_JUSTIFIER_BLUE,
//JOY_SUPPORT_JUSTIFIER_BOTH, JOY_SUPPORT_ANALOGJOY, JOY_SUPPORT_KEYBOARD
//A través de la función siguiente conseguimos que nuestra funcion
//creada anteriormente sea llamada cada vez que se cambia el estado de un boton
JOY_setEventHandler( &myJoyHandler );
//Dibujamos con dos líneas de texto una cabecera austera
VDP_drawText("Ejemplo de control del PAD", 5, 10);
VDP_drawText("--------------------------", 5, 11);
//Y rellenamos el resto del espacio escribiendo cadenas que indiquen
//que inicialmente todos los estados están a cero (nada pulsado)
VDP_drawText("START button 0", 5, 13);
VDP_drawText("A button 0", 5, 14);
VDP_drawText("B button 0", 5, 15);
VDP_drawText("C button 0", 5, 16);
VDP_drawText("X button 0", 17, 14);
VDP_drawText("Y button 0", 17, 15);
VDP_drawText("Z button 0", 17, 16);
VDP_drawText("UP button 0", 5, 17);
VDP_drawText("DOWN button 0", 5, 18);
VDP_drawText("LEFT button 0", 5, 19);
VDP_drawText("RIGHT button 0", 5, 20);
//BUCLE PRINCIPAL
//En este caso sólo tendrá en cuenta el manejador del input que
//hemos creado y establecido con JOY_setEventHandler anteriormente
while(1){
// Sincroniza la pantalla de modo que el barrido vertical no
// interfiera con el dibujo de tiles, sprites o nuestro texto,
// evitándonos algunos efectos indeseados de flickering
// principalmente
VDP_waitVSync();
}
return (0); // Sin efecto funcional en nuestro juego o programa,
// pero de obligada aparición al final de una función
// main que no se ha declarado como "void main()"
}//end main()
Manveru Ainu escribió:No es por romper la magia o parecer tocapelotas, pero existen librerías como Bennu o Allegro que sirven para hacer juegos en 2D sin las dificultades y limitaciones de hacerlo para la Megadrive. Yo mismo tengo por ahí iniciados juegos como Golden Axe, Sonic, Thunder Force IV o Mercs.
Saludos.
bertobp escribió:Y que?
No son juegos para la Megadrive, el objetivo de esto es programar para la Megadrive, que puedas crear tu juego y grabarlo en un cartucho y jugarlo en la mitica consola.
programar juegos para PC es otra historia...
A mi me encanta programar para la GB, es como cumplir el sueño de cuando tenia 10 años.
pocket_lucho escribió:Si tus juegos no usan la paltea de 512 colores de la megadrive, ni sus 2 planos, ni su limite de sprites ni de colroes, ni su cpu motorola 68000... no son juegos 100% megadrive, ¿programar un pc con chorrocientos ghz y gigas de ram?¿que gracia tiene eso? Luego la gente se queja de que los juegos de hoy dia no están optimizados
No me lo tomo a mal, tu piensas que es una tontería hacer juegos para megadrive... yo pienso que es una tontería hacer juegos con las limitaciones de megadrive en un pc con la de cosas que puede hacer...
Manveru Ainu escribió:No es por romper la magia o parecer tocapelotas, pero existen librerías como Bennu o Allegro que sirven para hacer juegos en 2D sin las dificultades y limitaciones de hacerlo para la Megadrive. Yo mismo tengo por ahí iniciados juegos como Golden Axe, Sonic, Thunder Force IV o Mercs.
Saludos.
pocket_lucho escribió:Te aseguro que programar la super nintendo es un infierno pero la mega en C se da bastante bien, si te animas ya sabes donde esta el tuto
¿Alguna demo tuya que se pueda ver?
Un tile para que nos entendamos es la medida de memoria de video más pequeña que puede manejar una Megadrive.
Conceptos previos:
- Un tile esta formado por un total de 8x8 pixeles.
- Cada linea de tile tiene 4 bytes. Cada pixel del tile tiene un valor de 4 bits, por lo que trabajaremos en valores hexadecimales.
- Megadrive tiene dos capas de tiles que aceptan scrolling independiente.
- Hay que entender que los tiles no tienen referencia de pixeles, si no simplemente nos referimos en una referencia segun el numero de tiles.
___________________________________
| 1,1 | 1,2 | 1,3 | 1,4 | .. | 1,N |
| 2,1 | 2,2 | 2,3 | 2,4 | .. | 2,N |
| 3,1 | ... | | | | | Y lo que venga por debajo...
| N,1 | ... | | | | |
¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯
Hacemos la declaración del tile de la siguiente manera:#DEFINE TILE1 1 /* Creamos una referencia para recordar la referencia en memoria del TILE */
const u32 tile[8]= /* Definimos 4 bytes para cada linea, y añadimos 8 lineas al array. Usamos const en lugar de una variable, por que esto lo aloja en la memoria del cartucho en vez de en RAM*/
{
0x00088000,/* Dibujamos pixel por pixel lo que queremos, cuanto más alto es el numero mayor es la intensidad del color */
0x00888800,
0x08888880,
0x88888888,
0x88888888,
0x08888880,
0x00888800,
0x00088000
}
Ahora para cargar el tile en la memoria de vídeo usaremos la función VDP_loadTileData y para diubjar usaremos las funciones VDP_setTileMap y VDP_fillTileMapRect.
VDP_loadTileData( (const u32 *)tile, TILE1, 1, 0); /*Escribimos en el bloque TILE1 (1) de memoria, si escribimos en el bloque 0 sucede algo curioso, y se dibuja todo el fondo con el mismo sprite*/
VDP_setTileMap(APLAN, TILE1, 5, 5); /*Dibujamos la memoria TILE1 en la posición 5,5 en el plano A
VDP_setTileMap(BPLAN, TILE1, 8, 8); /*Dibujamos la memoria TILE1 en la posición 8,8 en el plano B
VDP_fillTileMapRect(BPLAN, TILE1, 12, 12, 8, 8); /*Dibujamos la memoria TILE1 repetidas veces en un rectangulo desde el plano 12,12 hasta el plano 8,8
pocket_lucho escribió:No solo no es molestia sino que se agradece la aportacion
Siento la tardanza con el tuto de lso tiles pero es que es de lejos el mas lioso (que si genres, que si imagenesis) y ahora mismo estoy onfire con lo del oh mummy para que esté para octubre
A ver si este finde...
Manveru Ainu escribió:pocket_lucho escribió:No solo no es molestia sino que se agradece la aportacion
Siento la tardanza con el tuto de lso tiles pero es que es de lejos el mas lioso (que si genres, que si imagenesis) y ahora mismo estoy onfire con lo del oh mummy para que esté para octubre
A ver si este finde...
Yo entre el wiki y la ayuda en su foro ya he terminado de pillar bien todo el tema del GenRes respecto al manejo de sprites, hojas de sprites o convertir las imágenes y editar su paleta de colores para que el SGDK las pille. Si puedo echarte un cable no tienes más que decírmelo
pocket_lucho escribió:...ahora mismo estoy onfire con lo del oh mummy para que esté para octubre
Antes que nada comentar algunos detalles:
- Puede parecer un poco largo y complejo, pero la verdad es que escrito y leído parece mucho más de lo que es una vez sabes los pasos a seguir, todo se hace mucho más rápido cuando se vuelve automático pero ya sabéis lo que dicen de las primeras veces...
- Descargar la última versión de Gimp en este enlace: Gimp 2.8.
- Al igual que en la wiki, sólo he probado y usado archivos BMP.
- Este tutorial se aplica tanto para bitmaps cualquiera como para sprites (usaré la palabra "imágenes" para generalizar).
- El máximo número de colores de una imagen debe ser 16.
- El ancho y largo de las imágenes deben ser múltiplo de 8. Los sprites tienen un tamaño máximo de altura y anchura de 32 píxeles (GenRes aún no soporta sprites mayores).
- Las imágenes deben tener 4 bits por píxel.
Se puede aplicar el siguiente tutorial para crear bitmaps de cualquier tipo o sprites para GenRes, pero lo haré con una tabla de sprites ya que un bitmap se puede considerar una tabla de 1x1 (sin olvidar que los bitmaps no tienen la limitación de 32 píxeles).
El primer paso es crear nuesta tabla de sprites, que para los que no hayan oído antes este concepto decir que no es más que una imagen que contiene cada uno de los sprites que queremos usar colocados a modo de tabla, ordenados en filas y columnas. Normalmente una única tabla contiene todos los sprites de un personaje o enemigo, pero se puede usar para cualquier objeto.
Aclarar que en otros entornos de programación de videojuegos el hecho de cargar un único archivo de imagen que incluye a todos los sprites ofrece mejor rendimiento que cargar un archivo por sprite, pero desconozco como afecta esto a la Megadrive.
Empecemos con el Gimp:
- Debemos elegir el tamaño de nuestra tabla en función del número de sprites que vamos a necesitar. Por ejemplo supongamos que queremos hacer al prota de nuestro juego y hemos decidido que estará compuesto de 14 sprites distintos, entonces la elección óptima para el tamaño de la tabla sería 1x14, 14x1, 2x7 o 7x2, pero para el ejemplo vamos a escoger una tabla 3x5 (3 filas con 5 columnas -sprites- cada una) para explicar lo que pasa cuando queda alguna casilla vacía (también nos valdría una tabla de 4x4, 10x2... pero todas ocupan un espacio extra innecesario). La casilla 15 -posición 3,5- estará vacía y no tendrá sentido acceder a ella, aunque no produce error alguno. Simplemente se deja en blanco o se rellena con el color que indica transparencia (esto lo explico más adelante).
- Miramos el tamaño en píxeles que necesitamos para nuestros sprites. Deben ser suficientes para que quepan el sprite "más ancho" y el sprite "más largo o alto". Por ejemplo 24x32.
- Ahora sabemos que debemos crear una imagen de tamaño 120x96 (5 columnas por 24 píxeles de ancho y 3 filas por 32 píxeles de altura).
- Vamos al Gimp y le damos a Archivo->Nuevo. En la ventana ponemos el tamaño que decidimos en el paso anterior. Le damos a Opciones avanzadas y seleccionamos Color de fondo en Rellenar con. Sin cerrar esta ventana vamos a la ventana Caja de Herramientas y elegimos un color de fondo (de los 2 rectángulos superpuestos, el de debajo). Este color debe ser uno que NO aparezca en ninguno de los sprites de su tabla ya que será el color que el compilador interpretará como transparente. Volvemos a la primera ventana y pulsamos Aceptar.
- Ahora tenemos un archivo monocolor 120x96. Para situar correctamente los sprites vamos a colocar una rejilla (esta rejilla no aparecerá en la imagen final). Vamos a Vista->Mostrar rejilla. Ahora la ajustamos a nuestro caso: vamos a Imagen->Configurar rejilla.
Se abre una nueva ventana donde la configuramos a nuestro gusto (es recomendable que los colores no interfieran visualmente con nuestros sprites para poder colocarlos bien). En Espaciado colocamos la resolución de nuestros sprites: 24x32. Pulsad el icono de la cadena para que el ancho y la altura puedan tomar valores distintos.
Empecé haciendo el tutorial sin pensar con qué sprites haría las imágenes... finalmente elegí los sprites de Sonic y la verdad que el fondo verde claro y la rejilla roja le sienta a Sonic como a un cristo dos pistolas, por eso luego de repente cambio los colores pero recordad que no importa, a la hora de programar cualquier color vale, sólo debe cumplir que el color de fondo no aparezca en ningún sprite de la tabla y que este color no complique visualmente a la hora de colocar el sprite en la celda, como pasaría aquí con la rejilla y botas rojas.- Ya podemos empezar a pegar los sprites en cada celda de la tabla. Tened en cuenta que la posición del sprite dentro de cada casilla es importante. Hay que procurar que si dos sprites se diferencian por ejemplo en el movimiento de la cabeza del personaje, que la posición relativa del resto del cuerpo dentro de la celda sea la misma en ambas casillas.
Quedará más claro con el ejemplo de las siguientes viñetas: tenemos dos tablas de 1x2 donde en la inferior los sprites se han colocado mal horizontal y verticalmente mientras que en la superior están situados correctamente. Como los pies no se mueven deben estar en la misma posición dentro de sus respectivas casillas. En caso de que la diferencia entre dos sprites sea total (Sonic de pie y echo una bola por ejemplo) se debe procurar centrar ambas imágenes en la celda para evitar animaciones bruscas. A la derecha de ambas tablas se muestran las animaciones tal cual se verían en la Megadrive en cada caso (he dejado el color de fondo para que se vea mejor el movimiento dentro de la celda).- Una vez tenemos ya lista nuestra tabla de sprites tenemos que guardarla en un formato que pueda leer SGDK o GenRes. Para ello vamos a Imagen->Modo->Indexado:
Marcamos Generar paleta óptima, ponemos en Número máximo de colores 16 y pulsamos Convertir:
Ya tenemos una imagen a 16 colores (no tiene por qué ser 16, este número indica el número máximo. En mi ejemplo se queda en 13). Ahora tenemos que ver la paleta de colores de la imagen para el tema de las transparencias. Vamos a Colores->Mapa->Reordenar el mapa de colores. En mi caso obtengo el siguiente resultado:
En esta pantalla debemos apuntar el número al que corresponde el color de fondo de nuestra tabla, en mi caso el verde es el 8. En su momento diré para qué sirve este número.- Nuestra tabla de sprites está construida, ahora vamos a guardarla en formato BMP. Vamos a Archivo->Exportar... ponemos el nombre que queramos a nuestro archivo + ".bmp". Luego pulsamos el botón Exportar y en la ventana que se nos abre le damos a Opciones de compatibilidad y por último marcamos la casilla de No escribir la información del espacio de colores y exportamos.
Tiles
Antes de meternos con los sprites y bitmaps, empecemos entendiendo un poco como maneja la Megadrive los tiles:Otros datos interesantes a conocer:
- La Megadrive redibuja 2 planos en cada actualización de pantalla (más un tercero para los sprites).
- Cada plano está compuesto por tiles de 8x8 píxeles.
- Cada plano puede cargar en memoria tiles de entre 32x32 y 128x128 (aunque sólo hasta 40x28 son visibles en la pantalla).
- Cada plano se rellena de izquierda a derecha y de arriba a abajo.
- Cada tile se puede reusar varias veces en cualquier plano con distintas paletas.
- Cada tile se puede usar con cualquiera de las 4 paletas disponibles.
- Cada tile se puede dibujar volteada sin un consumo extra de memoria.
- Una cantidad importante de tiles se pueden cargar usando DMA.
Entonces para dibujar un tile en la pantalla:
- Un tile está hecho de 32 bytes, 4 bytes por línea.
- Cada píxel del tile tiene un valor de 4 bits que indica el color, entre 0x0 y 0xF.
- El primer tile (el 0) en la VRAM se usa para colorear el fondo del tile.
- SGDK reserva espacio en la VRAM para 1310 tiles + 96 para texto.
- Un tile se considera un patrón o unidad de diseño.
- Un tile en la pantalla no se borra en cada actualización, no es necesario dibujarlo cada vez.
- Cada paleta esta compuesta de 16 colores.
- Cargamos el tile en la VRAM (el DMA no lo usaremos en este tutorial).
- Cargamos su paleta si no ha sido cargada antes.
- Lo dibujamos en el plano que indiquemos en (x,y) con esa paleta.
Ahora que ya sabemos más sobre los tiles, necesitamos crear uno para practicar un poco. Existen programas para convertir bitmaps de 16 colores en tiles y paletas para la Megadrive (Genitile, B2T, GenRes, Mega-Happy-Sprite...) pero vamos a empezar haciendo uno en código C mediante un array:const u32 tile[8]=
{
0x00111100,
0x01144110,
0x11244211,
0x11244211,
0x11222211,
0x11222211,
0x01122110,
0x00111100
};
Tenemos un tile de 4 colores: 0, 1, 2 y 4. Veamos el código para dibujarlo en pantalla:// Cargamos nuestro tile en la posición 1 de la VRAM
// argumento 1: el tile a cargar.
// argumento 2: posición en la VRAM.
// argumento 3: número de tiles a cargar.
// argumento 4: usar DMA (1) o no (0).
VDP_loadTileData( (const u32 *)tile, 1, 1, 0);
// No cargamos una paleta en este ejemplo, dejamos la paleta por defecto.
// Dibujamos nuestro tile.
// argumento 1: el plano donde se dibuja: APLAN o BPLAN.
// argumento 2: índice del tile a dibujar.
// argumento 3: Coordenada X.
// argumento 4: Coordenada Y.
VDP_setTileMap(APLAN, 1, 5, 5);
while(1)
{
VDP_waitVSync();
}
Esto dibujará en la pantalla nuestro tile gris degradado.
El argumento 2 de VDP_setTileMap puede ser algo más que el índice del tile. Veamos cómo sería dibujarlo en verde y volteado:
// Argumentos de TILE_ATTR_FULL:
// argumento 1: paleta del tile (PAL2 = paleta por defecto de tonos verdes).
// Prioridad: prioridad baja (0) o alta (1).
// Volteo vertical (1) o no (0).
// Volteo horizontal (1) o no (0).
// Tile a dibujar: tile 1.
VDP_setTileMap(BPLAN, TILE_ATTR_FULL(PAL2, 0, 1, 0, 1), 6, 5);
Zoom X4
La prioridad indica qué plano se dibuja sobre el otro. Esto se regula a nivel de tiles, lo que significa que por ejemplo el plano A puede estar delante del B en el punto (x,y) pero estará detrás en el punto (m,n). Esto es útil para hacer planos de scroll que se mueven a distintas velocidades. Veamos un ejemplo del uso de las prioridades://El mismo tile dibujado en 2 planos y con paletas distintas, ¿cuál se superpone?
VDP_setTileMap(APLAN, TILE_ATTR_FULL(PAL1, 1, 0, 0, 1), 7, 7);
VDP_setTileMap(BPLAN, TILE_ATTR_FULL(PAL2, 0, 0, 0, 1), 7, 7);
VDP_setTileMap(APLAN, TILE_ATTR_FULL(PAL1, 0, 0, 0, 1), 8, 7);
VDP_setTileMap(BPLAN, TILE_ATTR_FULL(PAL2, 1, 0, 0, 1), 8, 7);
En (7,7) A está frente a B al tener prioridad 1 frente a 0, en cambio en (8,7) B tiene mayor prioridad por lo que se superpone a A. Si ambos tuvieran la misma prioridad se mantiene el orden por defecto: A queda por delante de B.
De izquierda a derecha: prioridad A>B, B>A, A=B.
Zoom X4
Ahora supongamos que queremos rellenar un gran espacio (una zona o la pantalla por ejemplo) con nuestro tile, entonces deberíamos dibujarlos uno a uno lo que sería un trabajo tedioso. Por suerte SGDK nos permite usar la función VDP_fillTileMapRect:// Rellena con tiles azules un cuadrado de tamaño 8x8 en la posición (12,12).
// argumento1: dibujamos en el plano A o B.
// argumento2: tile inicial.
// argumento3: coordenada X.
// argumento4: coordenada Y.
// argumento5: anchura del rectángulo.
// argumento6: altura del rectángulo.
VDP_fillTileMapRect(BPLAN, TILE_ATTR_FULL(PAL3, 0, 0, 0, 1), 12, 12, 8, 8);
Zoom X2
Multi-Tile
Como no es muy difícil de entender, no podemos hacer un juego con un único tile. Probemos a dibujar esta luna en la pantalla:
SGDK puede manejar varios ficheros de bits a la vez si están en la subcarpeta res. Entonces SGDK creará dos archivos para cada bitmap:
- nombre_del_archivo_bmp.o es la forma de objeto del bitmap.
- nombre_del_archivo_bmp.h permite acceder a los datos del bmp en código fuente.
El código para cargar nuestro bitmap es el siguiente:// Carca el código de nuestro bitmap.
#include "moon.h"
int main( )
{
// Obtiene el ancho de la imagen en píxeles (múltiplo de 8).
u16 w = moon[0];
// Obtiene la altura de la imagen en píxeles (múltiplo de 8).
u16 h = moon[1];
// Obtiene la paleta de la imagen (de 2 a 17)
VDP_setPalette(PAL1, &moon[2]);
// Carga la imagen (18....) in VRAM
// w/8 = ancho en tiles que queremos cargar.
// h/8 = altura en tiles que queremos cargar.
// w/8 = Ancho en tiles del bitmap (se necesita porque se puede cargar sólo una parte del bitmap si se quiere pero[i] SGDK[/i] necesita la anchura como referencia).
VDP_loadBMPTileData((u32*) &moon[18], 1, w / 8, h / 8, w / 8 );
while(1)
{
VDP_waitVSync();
}
return 0;
}
Para dibujarlo ahora podríamos hacerlo manualmente usando VDP_setTileMap pero resulta muy trabajoso dibujar uno a uno cada tile. Por suerte SGDK trae la función VDP_fillTileMapRectInc que dibuja desde el índice del tile en el argumento hasta el último necesario para rellenar el rectángulo cuyas coordenadas y tamaño pasamos como argumentos:// Llena la pantalla con los tiles desde el primero hasta rellenar el rectángulo con origen (12,12) y como tamaño la anchura y altura indicadas.
// argumento1: dibujamos en el plano A o B.
// argumento2: tile inicial.
// argumento3: coordenada X.
// argumento4: coordenada Y.
// argumento5: anchura del rectángulo (w / 8 = anchura de la imágen en tiles).
// argumento6: altura del rectángulo (h / 8 = altura de la imágen en tiles).
VDP_fillTileMapRectInc(BPLAN, TILE_ATTR_FULL(PAL1, 0, 0, 0, 1), 12, 12, w / 8, h / 8);
GenRes para manejar bitmaps
SGDK trae otra manera de cargar tiles: usar GenRes. GenRes es una herramienta que convierte bitmaps, animaciones y maps para el desarrollo de juegos en la Megadrive. En este tutorial sólo hablaremos del modo BITMAP.
GenRes se basa en la declaración de un fichero de recursos en la carpeta raiz del proyecto llamado resource.rc donde cada línea define el modo de conversión, el nombre de la variable de salida, el fichero de bitmaps y otros parámetros. Usamos el modo de conversión de BITMAP:BITMAP variable_salida1 "directorio/fichero1.bmp" 0
BITMAP variable_salida2 "directorio/fichero2.bmp" 0
....
....
....
; La última línea de este archivo debe ser una línea en blanco o un comentario.
Los archivos BMP no deben guardarse en la carpeta "res" ya que serían compilados por el GenRes y el compilador de bitmaps del SGDK al mismo tiempo.
Escribiendo esto en nuestro archivo resource.rc, SGDK invocará GenRes para compilar los ficheros directorio/ficheroX.bmp a un archivo enlazado resource.o.
Desde la versión 0.7d, el formato de salida de GenRes no está definido en SGDK por lo que debemos escribir nuestra propia estructura, como ésta:struct genresTiles
{
u16 *pal; // Puntero a la paleta.
u32 *tiles; // Puntero a los tiles.
u16 width; // Anchura en tiles.
u16 height; // Altura en tiles.
u16 compressedSize; // 0.
};
A diferencia del soporte de bitmaps nativo de SGDK, GenRes no genera un archivo de cabecera pero podemos acceder a ellos declarando las variables variable_salidaX de la siguiente manera:extern struct genresTiles variable_salida1, variable_salida2...;
VDP_loadBMPTileData se usa para los ficheros BMP, pero como GenRes convierte un bitmap en tiles necesitamos otra función: VDP_loadTileData. Así quedaría el ejemplo anterior de la luna usando GenRes:
resource.rcBITMAP moon "data/moon.bmp" 0
; La última línea de este archivo debe ser una línea en blanco o un comentario.
main.cstruct genresTiles
{
u16 *pal;
u32 *tiles;
u16 width;
u16 height;
u16 compressedSize;
};
extern struct genresTiles moon;
int main( )
{
VDP_setPalette(PAL1, moon.pal);
// Carga los tiles en la VRAM.
// argumento 1: los tiles.
// argumento 2: índice del primer tile.
// argumento 3: número de tiles a cargar.
// argumento 4: usa DMA (1) o no (0).
VDP_loadTileData(moon.tiles, 1, moon.width*moon.height, 0);
VDP_fillTileMapRectInc(BPLAN, TILE_ATTR_FULL(PAL1, 0, 0, 0, 1), 12, 12, moon.width, moon.height);
while(1)
{
VDP_waitVSync();
}
return 0;
}
Sobre si es mejor usar el soporte nativo para bitmaps de SGDK o el GenRes... que cada uno pruebe ambos y se quede con el que más le guste.
Bitmaps comprimidos
Si se usa una gran cantidad de tiles es muy recomendable comprimir los bitmaps con programas como RLE, Huffman... SGDK no incluye un método para cargar bitmaps comprimidos por lo que es cosa del usuario hacerse su propio algoritmo de des/compresión. GenRes puede usar la compresión RLE, habrá más detalles en próximos tutoriales. Podéis mirar si el programa que usáis puede exportar bitmaps comprimidos.
Miscelánea
Tenemos disponibles las utilidades del emulador Gens KMod para comprobar como funcionan vuestros tiles. Podéis comprobar la VRAM y detectar cualquier posible anomalía.
Sprites
Antes que nada comentar que existe una diferencia entre los tiles de los planos y los tiles de los sprites:Por esto no podemos usar las mismas funciones para dibujar los tiles de los planos y sprites. Por suerte la forma de cargarlas en memoria si es la misma. Ambos usan los tiles cargados en la VRAM por lo que podremos usar un mismo tile para dibujar en ambas cosas a la vez si lo necesitamos.
- Los planos dibujan los tiles de izquierda a derecha y luego de arriba a abajo (siguiendo las filas).
- Los sprites dibujan sus tiles de arriba a abajo y después de izquierda a derecha (recorriendo las columnas).
Información básica que debemos saber sobre los sprites:Los pasos para dibujar un sprite en la pantalla:
- El "plano" de los sprites, a diferencia de los otros dos, no es desplazable (no admite scrolling) y tiene un tamaño fijo, normalmente 512x512.
- Esto significa que solo una parte del plano de los sprites es visible. Esta parte comienza en (128,128).
- Un sprite fuera de este área de visualización no es visible.
- La posición de los sprites es cíclica. Por ejemplo el punto (300, 812) es el mismo que (300, 300) ya que 812 mod 512 = 300.
- Se puede controlar el orden en el que se dibujan los sprites mediante el atributo link.
- Un máximo de 80 sprites se pueden definir en modo PAL.
- Un máximo de 20 sprites se pueden dibujar en una misma línea en modo PAL.
- Los sprites están hechos de 1x1 a 4x4 tiles (por lo que los sprites mas grandes son de hecho multi-sprites).
- Cargar los tiles en la VRAM.
- Cargar su paleta (si no ha sido cargada aún).
- Definir el sprite.
- Definir los demás, si existen.
- Solicitar dibujarlo en pantalla
Crear y dibujar un sprite definido en C
Como antes, vamos a crear primero un sprite mediante un array de tiles. Queremos un sprite de 2x2, entonces necesitamos 4 tiles:const u32 spriteTiles[4*8]=
{
0x00001111, //Tile de arriba-izquierda.
0x00001111,
// ...
0x11112222, //Tile de abajo-izquierda.
0x11112222,
// ...
0x11110000, //Tile de arriba-derecha.
0x11110000,
// ...
0x22221111, //Tile de abajo-derecha.
0x22221111,
// ...
};
Este sprite tiene la misma forma que usamos para crear un tile. Este es el código para cargarlo en memoria y visualizarlo correctamente:// Carga los tiles en la VRAM.
VDP_loadTileData( (const u32 *)spriteTiles, 1, 4, 0);
// Usamos una paleta por defecto por ahora.
// Definimos el sprite:
// argumento 1: el índice del sprite (desde 0 a 79).
// argumento 2: coordenada X.
// argumento 3: coordenada Y.
// argumento 4: tamaño (de 1x1 a 4x4 tiles).
// argumento 5: atributos de el/los tile/s.
// argumento 6: propiedad link.
VDP_setSprite(0, 40, 40, SPRITE_SIZE(2,2), TILE_ATTR_FULL(PAL0,1,0,0,1), 0);
// Solicitar que se dibuje el sprite.
VDP_updateSprites();
while(1)
{
VDP_waitVSync();
}
Otros aspectos importantes:
- Puedes definir tantos sprites como necesites (80 máximo) antes de solicitar que se dibujen en pantalla.
- Usando SGDK, la posición de los sprites está basada en el área de la pantalla, no en el plano de los sprites, lo que quiere decir que (0,0) significa (0,0) en la pantalla y (128,128) en el plano de los sprites.
- SPRITE_SIZE es necesario para pasar un valor correcto (0000b para 1x1, 0101b para 2x2, etc...). Al parecer hay algunos problemas con la función SPRITE_SIZE, aunque yo de momento no he tenido ninguno. En caso de producirse cambiar "SPRITE_SIZE(X,X)" por "variable_salidaX.size>>8".
- TILE_ATTR_FULL es el mismo macro que usamos con los tiles.
Mover un sprite
Un sprite se usa principalmente para objetos que se mueven por lo que hay que actualizar la x e y del sprite constantemente. Usando la función VDP_setSprite pondremos en problemas a la Megadrive ya que tendremos que restablecer el sprite en cada actualización. Una manera útil de hacerlo es:
- Usar una estructura para mantener las propiedades del sprite: un objeto SpriteDef (en el tutorial en inglés aparece _spritedef porque está desactualizado).
- Una función para cambiar y actualizar lo que necesitemos: VDP_setSpriteP
SpriteDef mySprite;
mySprite.posx = 0;
mySprite.posy = 0;
mySprite.size = SPRITE_SIZE(2,2);
mySprite.tile_attr = TILE_ATTR_FULL(PAL0,1,0,0,1);
mySprite.link = 0;
VDP_setSpriteP(0, &mySprite);
.....y para hacer que se mueva:while(1)
{
mySprite.posx++;
mySprite.posy++;
VDP_setSpriteP(0, &mySprite);
VDP_updateSprites();
VDP_waitVSync();
}
Puedes usar VDP_setSprite o VDP_setSpriteP indistintamente sin problemas, SDGK te ofrece VDP_setSpriteP para hacerlo más fácilmente.
Propiedad link
Hasta ahora no habíamos hablado de la propiedad link. Sin conocer su uso no seremos capaces de dibujar más de un sprite. Esta variable contiene el índice del siguiente sprite a dibujar (el último debe apuntar de vuelta al sprite 0). Volviendo al ejemplo anterior, sólo dibujamos el sprite 0 ya que su link vale 0.mySprite.link = 0;
Para dibujar dos sprites tendremos que hacerlo de la siguiente manera:mySprite0.link = 1; // Enlaza al siguiente sprite, el 1.
VDP_setSpriteP(0, &mySprite0); // Dibuja el sprite 0.
mySprite1.link = 0; // Como mySprite1 es el último, volvemos al sprite 0.
VDP_setSpriteP(1, &mySprite1); // Dibujamos el sprite 1.
La propiedad link es usa también para definir el orden de superposición de los sprites: el primero estará por debajo del siguiente, es decir, se van superponiendo a medida que se van dibujando. Entonces te permite controlar qué sprite y cuándo éste puede ser dibujado.
De la siguiente manera puedes fácilmente definir un sprite pero no dibujarlo:mySprite0.link = 2; // Enlaza al sprite 2.
VDP_setSpriteP(0, &mySprite0); // Dibuja el sprite 0.
mySprite1.link = 2; // Enlaza al sprite 2.
VDP_setSpriteP(1, &mySprite1); // Dibuja el sprite 1 (esto nunca ocurre).
mySprite2.link = 0; // Vuelve al sprite 0 por lo que el sprite 1 no se dibuja.
VDP_setSpriteP(2, &mySprite2); // Dibuja el sprite 2.
Por supuesto el sprite 0 nunca se puede evitar.
PRECAUCIÓN: un mal uso de la propiedad link es la principal razón para que aparezcan bugs en el juego. Hay que comprobar estas 3 cosas:
- Todos tus sprites deben enlazar a otro.
- Que no se forme un enlace cíclico (por ejemplo: 1->2, 2->3, 3->1 ...).
- El último sprite tiene que apuntar al sprite 0
Manejo de sprites con GenRes
A diferencia de los tiles para planos, SGDK no trae soporte nativo para sprites en bitmaps de 16 colores, pero podemos usar GenRes en modo SPRITE.
Como ya hemos comentado, GenRes se basa en la declaración de un fichero de recursos donde cada línea define el modo de conversión, el nombre de la variable de salida, el fichero de bitmaps y otros parámetros. Ahora con SPRITE se puede hacer de esta manera:SPRITE variable_salida1 "directorio/fichero1.bmp" <ancho_sprite_fichero1> <altura_sprite_fichero1> <argumento*> <posición del color de transparencia en la paleta de fichero1.bmp>
SPRITE variable_salida2 "directorio/fichero2.bmp" <ancho_sprite_fichero2> <altura_sprite_fichero2> <argumento*> <posición del color de transparencia en la paleta de fichero2.bmp>
....
....
; La última línea de este archivo debe ser una línea en blanco o un comentario.
argumento*: desconozco la utilidad y función de este argumento. el que nos interesa es el 4ª que es el número que apuntamos antes en el Gimp, la posición donde está el color de la transparencia en nuestra paleta de colores.
Recordar de nuevo que los archivos no deben guardarse en la carpeta res.
Escribimos esto en un fichero resource.rc. SGDK invocará a GenRes para compilar los directorio/archivoX.bmp al fichero resource.o.
Desde la versión GenRes 0.7d este formato de salida de estas variables no está definido en SGDK por lo que de nuevo tendremos que crear nosotros mismos una estructura con ese formato:struct genresSprites
{
u16 *pal; //Puntero a la paleta.
u32 **sprites; //Puntero hacia los sprites.
u16 count; //El número de sprites que ha encontrado [i]GenRes [/i]en nuestro archivo.
u16 width; //Anchura de cada sprite en píxeles, no en tiles.
u16 height; //Altura de cada sprite en píxeles, no en tiles.
u16 size; //Tamaño del sprite (luego veremos más acerca de [i]size[/i]).
};
Luego para acceder a los datos usando las variable_salidaX del fichero resource.sc:extern struct genresSprites variable_salida1, variable_salida2, .... ;
Nos fijamos en que la variable sprites ya no es "u32 *sprites", ahora es "u32 **sprites". Esto es porque GenRes convierte tablas de sprites en un Array de sprites (incluso si la tabla sólo contiene 1 sprite), no en un único sprite.
Podemos retomar ahora nuestra tabla de sprites que hicimos con Gimp. Si te saltaste la parte 1 de este tutorial puedes seguir adelante usando esta tabla (la rejilla la pone el editor de imágenes, no aparece en el fichero):
Para nuestro proyecto crearíamos un archivo resource.rc de la siguiente manera (suponemos que nuestro archivo se llama sonic.bmp y está guardado en una carpeta dentro de nuestro proyecto llamada data):SPRITE sonic "data/sonic.bmp" 24 32 0 T
; La última línea de este archivo debe ser una línea en blanco o un comentario.
T es la posición del color de transparencia en la paleta de nuestra tabla (en la parte 1 del tutorial, apartado 7, podéis ver cómo averiguar esta posición con el Gimp). El de la imagen anterior es la posición 7.
Ahora tenemos nuestro archivo BMP diseccionado en un array de sprites en la variable sprites. Los sprites están ordenados en el array recorriendo cada fila de la tabla hasta la última columna y luego saltando a la siguiente fila y repitiendo el proceso, así hasta terminar.
Cada posición del array contiene los datos estándar de un tile por lo que se puede acceder a ella usando la función VDP_loadTileData pasándole el número de tiles. Para dibujar uno de los sprites de nuestra tabla el código sería el siguiente:// Cada sprite es altura/8 * anchura/8 (porque es tamaño en píxel, no en tiles)
// Es el número de 8x8 tiles que necesitas para dibujar un sprite.
u16 nbTiles = (sonic.height>>3) * (sonic.width>>3);
VDP_loadTileData( sonic.sprites[0], 1, nbTiles, 0);
// Carga la paleta de sonic.bmp en PAL1.
VDP_setPalette(PAL1, sonic.pal);
// Es opcional pero recomendable.
VDP_resetSprites();
// argumento1: índice del sprite (entre 0 y 79).
// argumento2: posición en el eje X.
// argumento3: posición en el eje y.
// argumento4: tamaño: tiles de 1x1 a 4x4, el nuestro es 3x4 (24x32 entre 8).
// argumento5: atributos de el/los tile/s:
// argumento6: propiedad link.
VDP_setSprite(0, 0, 0, SPRITE_SIZE(3,4), TILE_ATTR_FULL(PAL1,1,0,0,1), 0);
VDP_updateSprites();
Animaciones
Sonic no sería Sonic si no corriese. Ya sabemos como hacer que se mueva usando SpriteDef, pero nos falta algo: la animación de sprites.
Hay 2 maneras básicas de hacer una animación:El primer método necesita mucha VRAM, el segundo realiza una carga continua que puede ralentizar el juego. Vamos a usar el segundo: actualizar los frames.
- Cargar cada frame y declarar el sprite desde el primer tile en cada actualización.
- Declarar un sprite y cargar cada frame en el primer tile en cada actualización.
Los pasos son simples: en cada actualización cargar los tiles del frame con VDP_loadTileData y luego dibujar el frame usando VDP_setSpriteP.
Hagamos que Sonic corra, usamos los sprites 1, 2 & 3:u8 frame = 0;
// definimos el sprite (usando un SpriteDef para mover a Sonic).
mySprite.posx = 40;
mySprite.posy = 40;
mySprite.size = SPRITE_SIZE(3,4);
mySprite.tile_attr = TILE_ATTR_FULL(PAL1,1,0,0,1);
mySprite.link = 0;
VDP_setSpriteP(0, &mySprite);
while(1)
{
// Seguimos usando nbTiles porque todos los sprites tienen el mismo tamaño en una tabla de sprites.
// Frame vale 0, 1, 2 por lo que carga sprites 1, 2, 3; el 0 es Sonic parado de pie.
VDP_loadTileData( sonic.sprites[frame + 1], 1, nbTiles, 0);
frame++; // Siguiente frame
frame%=3; // Va rotando porque sólo necesitamos 3 frames.
// Sonic se mueve.
mySprite.posx+=10;
VDP_setSpriteP(0, &mySprite);
//flush
VDP_updateSprites();
VDP_waitVSync();
}
Miscelánea
Para probar nuestro motor de sprites es útil el emulador Gens KMod. Podemos explorar la lista de sprites y rastrear posibles problemas.
#include <genesis.h>
#include "moon.h"
const u32 tile[8]=
{
0x00111100, // Línea 1: pixels 1 a 8
0x01144110, // Línea 2
0x11244211, // Línea 3
0x11244211, // Línea 4
0x11222211, // Línea 5
0x11222211, // Línea 6
0x01122110, // Línea 7
0x00111100 // Línea 8: píxels 57 a 64
};
struct genresTiles
{
u16 *pal; // Puntero a los datos de la paleta
u32 *tiles; // Puntero a los datos de los tiles
u16 width; // Ancho en tiles
u16 height; // Alto en tiles
u16 compressedSize; // 0 en esta demo, mas proximamente
};
extern struct genresTiles luna;
////////////////////////////////////////////////////////////////////
// MAIN
////////////////////////////////////////////////////////////////////
int main( ){
VDP_drawText("Ejemplo de dibujo de TILES", 5, 1);
VDP_drawText("--------------------------", 5, 2);
// VDP_loadTileData(const u32 *data, u16 index, u16 num, u8 use_dma):
// - data: puntero a los datos de nuestro tile
// (o al primero de nuestra ristra tiles si es el caso)
// - index: en qué posición de la memoria de vídeo (VRAM) vamos a
// almacernarlo (o a partir de qué dirección si son varios)
// - num: cuántos tiles vamos a almacenar a partir de esa posición
// - use_dma: ¿usaremo o no "Acceso Directo a Memoria"?
VDP_loadTileData( (const u32 *)tile, 1, 1, 0);
VDP_setTileMap(APLAN, 1, 5, 5);
VDP_setTileMap(BPLAN, TILE_ATTR_FULL(PAL2, 0, 1, 0, 1), 6, 5);
VDP_setTileMap(APLAN, TILE_ATTR_FULL(PAL1, 1, 0, 0, 1), 7, 7);
VDP_setTileMap(BPLAN, TILE_ATTR_FULL(PAL2, 0, 0, 0, 1), 7, 7);
VDP_setTileMap(APLAN, TILE_ATTR_FULL(PAL1, 0, 0, 0, 1), 8, 7);
VDP_setTileMap(BPLAN, TILE_ATTR_FULL(PAL2, 1, 0, 0, 1), 8, 7);
// Rellena un cuadrado de 8x4 del tile 1 con paleta azul en (12,12)
VDP_fillTileMapRect(BPLAN, TILE_ATTR_FULL(PAL3, 0, 0, 0, 1), 12, 12, 8, 4);
u16 w = moon[0] / 8;
u16 h = moon[1] / 8;
VDP_setPalette(PAL1, &moon[2]);
VDP_loadBMPTileData((u32*) &moon[18], 2, w, h, w);
VDP_fillTileMapRectInc(BPLAN, TILE_ATTR_FULL(PAL1, 0, 0, 0, 2), 23, 6, w, h);
VDP_setPalette(PAL2, luna.pal);
VDP_loadTileData(luna.tiles, 100, luna.width*luna.height, 0);
VDP_fillTileMapRectInc(BPLAN, TILE_ATTR_FULL(PAL2, 0, 0, 0, 100), 23, 12, luna.width, luna.height);
while(1){
// sincroniza la pantalla
VDP_waitVSync();
}
realbrucest escribió:Lo de ir subiéndolo a la wiki de Stef (AKA KanedaFr si no ando desencaminado) era algo que yo mismo quería proponerle también a Pocket_lucho que hiciéramos en algún momento (con el consentimiento previo del francés, claro). Pero entre que no sabría ni cómo empezar a organizarlo y que sé que el de tiempo libre de Pocket_lucho es un bien escaso lo dejé correr. A ver si poco a poco le vamos dando forma a la idea ...
-------------- Build: default in tiles ---------------
Using makefile: C:\sgdk\makefile.gen
make: *** [resource.asm] Error 53
Process terminated with status 2 (0 minutes, 0 seconds)
0 errors, 0 warnings
weirdzod escribió:Me he puesto ya, me hace ilusión hacer algo para la mega. El primer tutorial y el segundo muy bien, pero me bajo el de los tiles y a la hora de compilar me da el siguiente error:-------------- Build: default in tiles ---------------
Using makefile: C:\sgdk\makefile.gen
make: *** [resource.asm] Error 53
Process terminated with status 2 (0 minutes, 0 seconds)
0 errors, 0 warnings
Seguramente se me escapa algo. ¿Qué puede ser?