Programación en Megadrive

En construcción.png

Este artículo está en construcción.

Por esta razón, seguramente habrá lagunas en el contenido o en el formato. Por favor, antes de cambiar el contenido, consulta la página de discusión del artículo.


Contenido

Introducción

  • Tutoriales básicos de programación para la videoconsola doméstica Mega Drive de Sega.

SGDK

Para consultas y debates sobre el contenido de este artículo, visita el Hilo oficial.

  • Acrónimo de Sega Genesis Developmen Kit. Librerías y compilador basado en gcc que permite el desarrollo de software para la megadrive usando código en lenguaje C.

01 - Instalación del entorno

02 - Controles

  • Lo primero, como siempre, incluimos las librerías SGDK:
#include <genesis.h>

Función MYJOYHANDLER()

  • En este ejemplo, aparte de la función main vamos a tener otra, la MYJOYHANDLER().
  • Esta función se llamará en cada ocasión en que la videoconsola detecta la pulsación de un botón (para lo cual la pasaremos como parámetro más adelante a la JOY_setEventHandler()). Su cometido es interpretar los estados de entrada del pad y modificar estados o realizar acciones en nuestro juego, de momento nos conformaremos con dibujar distintas cadenas de texto en pantalla para durante la ejecución conocer los estados de los botones y direcciones del pad ;)
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 interesantes*/
/*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()

Función MAIN()

  • Ahora volvemos a la función main(), donde iniciamos el manejador de los controles, establecemos la función de control myJoyHandler() anterior que se ejecutará automáticamente y entramos en un bucle infinito.
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()*/
  • Y ya está, ya tenemos control de todos los botones del PAD1, aquí os dejo un fichero con el código por si no tenéis ganas de teclear.

03 - Tiles

  • Para aquel que no esté familiarizado con los "tiles", vamos a partir entonces de su traducción al castellano: "azulejos", "losetas" o "baldosas", pero nos quedaremos con el término "tiles" para no liarnos.
  • Todo lo que vemos plasmado en pantalla cuando encendemos una megadrive está generado en base a tiles (la excepción que usa la "magia" de una técnica llamada framebuffer quedará para otro día). Esto es, que todos los fondos que vemos están formados por trocitos de ocho por ocho píxels que en su conjunto forman el escenario y los sprites.
  • En modo PAL "estándar" el área visible se rellena con 40x28 tiles (horizontal x vertical). En la VRAM, la memoria de vídeo de la consola, habrán de almacenarse todos los patrones distintos de tiles que vayamos a usar. Por lo tanto trabajar con tiles no es más que componer escenarios (en el presente caso, otro dia sprites) usando repeticiones bien pensadas de pedacitos gráficos de ocho por ocho píxels.
  • Con eso tenemos para empezar sin perdernos demasiado, así que manos a la obra.
  • Incluimos las librerías SGDK como en otros tutos:
#include <genesis.h>
  • Incluimos los tiles generados con SGDK, colocamos la imagen que queremos convertir en tiles en la carpeta "res" de nuestro proyecto. Sus dimensiones han de ser ambas múltiplos de 8 y debe ser un bitmap de 16 colores (4 bits/pixel). Momento para echar un vistazo a la carpeta del proyecto, abrir "res" y comprobar lo comentado.
#include "moon.h"
  • Creamos "a pelo" un tile (ahora lo explicamos). Con el permiso de los que ya se manejan con C diremos que el trocito de código que sigue es para crear un dato de tipo array (AKA "vector", AKA "tabla" ...), que no es otra cosa que un dato que contiene a su vez una ristra de datos. Nuestro array de nombre "tile" contendrá ocho elementos, ocho datos de 32 bits, y cada uno de ellos se corresponderá con cada una de las líneas horizontales que forman el tile (1 tile = 8x8 píxels).
  • Los datos están representados en base hexadecimal (se preceden con 0x para indicarlo) y son separados por comas.
  • Llegó el momento de revelar una de las características que más quebraderos de cabeza darán al grafista de turno, los tiles en megadrive están limitados a 16 colores (siendo el cero transparente, así que ... 15). Lo que va a ser un poco jodienda cuando nos metamos en materia artística, por lo menos para representar los tiles en formato de array viene a ser una ventaja pues se puede ver con relativa claridad el contenido gráfico del tile mirando su código ("relativa")
  • Si nos acordamos de que 0x es sólo un prefijo para indicar valor hexadecimal (base numérica en la que al llegar a 9 seguimos A, B, C, D, E, F y luego ya 10 -que equivaldría al 16 decimal-) cada valor representa en orden de izquierda a derecha y de arriba a abajo los colores de los píxels que conforman nuestro tile (color cero, color cero, color uno, color uno ...) ¿Y qué colores son esos? ¡Ah, amigo! eso depende de la paleta de color, anticiparemos que el color cero es el "color transparente", el que no se pinta, vamos; el resto en nada lo vemos.
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*/
};
  • SGDK ofrece dos métodos de convertir imágenes a tiles sin tener que andarse con el trabajo de chinos que supone crear los tiles a pelo como hemos hecho anteriormente. Para ello o bien colocamos el bitmap que queramos "tilear" en la carpeta "res" de nuestro proyecto. O bien, haciendo uso de la herramienta "genres" también incluída que nos requiere editar el archivo "resource.rc" para indicar qué imágenes queremos en este caso convertir.
  • Si vamos a usar el método de "genres" precisamos contar con el siguiente struct para manejar los tiles. Para no alargarnos, recomiendo hacer una pausa aquí para echar una ojeada al archivo "resource.rc" de este ejemplo:
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*/
};
  • Como con los tiles generados con SGDK:
  • Las dimensiones de los bitmap han de ser múltiplos de 8
  • Deben ser un bitmaps de 16 colores (4 bits/pixel)
  • Y con esta línea "cargamos" en nuestro código los tiles que "genres" nos haya generado. (Los cargamos en una estructura de tipo "genresTiles" que para algo la creamos justo antes.
extern struct genresTiles luna;
  • Tambien existe la herramienta imagenesis que conoceréis si mirasteis el tutorial de basic de theelf, la pega es que el formato que exporta no nos sirve pero vamos a ver como solucionarlo. Abrimos la imagen (file, open image), elegimos el tipo de conversión (si queremos optimizar los tiles -> mode, 15 color 4bpp 1 plane 8x8 tiles optimized, si es un sprite tendremos que cambiar el orden x-y... lo veremos en el tutorial siguiente sobre estos últimos), el color transparente (enable transparency, select transparent color). Pulsamos Actions -> Quantize Now y tendremos la imagen convertida a la paleta de megadrive y lista para exportar. La paltea si la podemos exportar sin modificarla con Export Palette Data y eligiendo como salida C, el mapa de tiles lo mismo, ya veremos como usarlo en posteriores tutoriales. El problema son los tiles, para usaremos el pequeño conversor que he escrito, el megagfx. Exportamos los tiles como basic y nos fijamos en el número de tiles que se han generado, lo pegamos en el fichero output.txt que viene en el rar que adjunto, pulsamos run.bat, elegimos un nombre para el vector de salida y por último la cantidad de tiles a covertir que vimos al convertirla en imagenesis. Se generará un fichero llamado tiles.txt, cuyo contenido podréis copiar sin problema en vuestro código y que será completamente compatible con las funciones de manejo de tiles de las SGDK.
  • Seguimos, como en el tutorial anterior, cabecera austera con VDP_drawText
int main( ){
VDP_drawText("Ejemplo de dibujo de TILES", 5, 1);
VDP_drawText("--------------------------", 5, 2);
  • Cargamos nuestro tile "creado a pelo" en la posicion 1 de la VRAM:
/* 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);
  • En cuanto a la paleta de color, usaremos una de las paletas por defecto. Disponemos en megadrive de 4 paletas simultáneas de 15 colores cada una (+ transparente), por defecto la primera paleta (PAL0) contiene una escala de grises, PAL1: escala de rojos, PAL2: verdes, PAL3: azules.
  • Buen momento éste para regresar a donde creamos nuestro tile "a pelo", de "0 a F" van 16 valores y nuestras paletas son de quince colores más el "color transparente" (que se corresponde con el valor cero). Cada valor corresponde con el color que ha de tomar un píxel de acuerdo a los que estén disponibles en la paleta que corresponda a este tile, luego ya, si eso, puedes experimentar cambiando valores.
  • Si antes habíamos almacenado nuestro tile en la memoria de vídeo, ahora es el momento en el que lo sacamos de allí para ponerlo en pantalla.
  • Dibujamos:
  • el tile que se encuentre en la POSICIÓN 1 de la VRAM
  • sobre el "PLANO A"
  • en las COORDENADAS (tile 5 horizontal,tile 5 vertical) (origen: 0,0)
  • (con la paleta 0)
VDP_setTileMap(APLAN, 1, 5, 5);
  • "Plano A": la megadrive por hardware cuenta con dos planos de profundidad sobre los que, como norma general, dibujaremos los fondos rellenándolos de lustrosos tiles. El "plano A" (referenciado en SGDK con el nombre APLAN) es el más próximo, siendo BPLAN al que se le superpone éste (por norma general). En el futuro podremos modificar las prioridades de los sprites respecto a los planos o incluso usar el plano WINDOW, que no es más que un área del plano A que será fijo y no se moverá con el scroll, se suele utilizar para marcadores y cosas por el estilo.
  • Hacemos lo mismo pero asignándole parametros al tile ayudándonos de la macro TILE_ATTR_FULL() que insertaremos como segundo parámetro en la llamada a VDP_setTileMap().
  • Los valores de los atributos de la macro TILE_ATTR_FULL() son:
  • PAL2 = paleta predeterminada verde
  • 0 = baja prioridad (se dibujará bajo tilemaps de alta prioridad)
  • 1 = vflip (reflejo vertical)
  • 0 = no hflip (no se usa reflejo horizontal)
  • 1 = tile 1 de la VRAM
VDP_setTileMap(BPLAN, TILE_ATTR_FULL(PAL2, 0, 1, 0, 1), 6, 5);
  • Vamos a experimentar con las prioridades, un mismo tile puede tener distintas propiedades independientemente del plano en el que esté si tienen la misma prioridad, A prevalece sobre B:
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);
  • Y si hasta ahora VDP_setTileMap() nos ha servido para dibujar en pantalla nuestro tile de forma independiente en las coordenadas que indicásemos... "VDP_fillTileMapRect()" va a hacer lo propio rellenando un determinado área mediante repeticiones de ese tile.
/* 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);
  • Como veis, los parámetros son los mismos que en la función anterior, con el añadido de cuántos tiles en horizontal y en vertical se dispondrán en las coordenadas dadas
  • A lo mejor llegado a este punto necesitas una cerveza para seguir en predisposición de seguir adquiriendo conocimiento megadrivero, sin problema, busca en el frigo y ahora seguimos con lo propio para los tiles generados por SGDK, queda poco en todo caso. Lo más normal si hemos dejado una imagen para que nos la convierta SGDK en tiles utilizables en nuestro código, es que esa conversión nos ofrezca "varios tiles", por lo que los anteriores ejemplos usando uno solo van a variar un poquito.
  • Casi al principio de nuestro código escribíamos la siguiente línea: #include "moon.h"
  • Con ello disponemos de un array "moon[]" el cual contiene todos los datos necesarios para que hagamos uso de la imagen original ya oportunamente "tiletizada"
  • El primer elemento del array contiene el ancho en píxels de la imagen, lo guardamos en la variable "w" (width) pasado a tiles (dividiendo entre ocho):
u16 w = moon[0] / 8;
  • El segundo elemento del array contiene el alto en píxels de la imagen, lo guardamos en la variable "h" (height) pasado a tiles (dividiendo entre ocho) :
u16 h = moon[1] / 8;
  • Cargamos sobre PAL1 la paleta de la imagen que está almacenada en moon[2 a 17]
VDP_setPalette(PAL1, &moon[2]);
  • Cargamos los datos del bitmap en la VRAM usando para ello la función VDP_loadBMPTileData(const u32 *data, u16 index, u16 w, u16 h, u16 bmp_w)
  • Argumentos:
  • - data: dirección de memoria a partir de la cual se encuentra la información gráfica
  • - index: posición en VRAM a partir de la cual se almacenarán los tiles
  • - w: ancho (en tiles) de la región del bitmap que mandaremos a VRAM
  • - h: alto (en tiles) de la región del bitmap que mandaremos a VRAM
  • - bmp_w: ancho (en tiles) de la imagen original; éste último argumento es necesario porque podríamos cargar solo un a parte del bitmap pero SGDK necesita el ancho como referencia.
VDP_loadBMPTileData((u32*) &moon[18], 2, w, h, w);
  • Y finalmente dibujamos el bitmap en pantalla. De nuevo hacemos uso de TILE_ATTR_FULL() para indicar los atributos extras que queremos asignar a la imagen, principalmente nos interesa que haga uso de PAL1 por ser la paleta donde hemos cargado los colores de la imagen original.
  • (23, 6) son las coordenadas en pantalla (en tiles; "1 tile = 8 x 8 píxels")
  • (w, h) son el ancho y el alto (en tiles) respectivamente
VDP_fillTileMapRectInc(BPLAN, TILE_ATTR_FULL(PAL1, 0, 0, 0, 2), 23, 6, w, h);
  • Y ya sí, por fin. Lo propio para tiles generados por genres que habían sido incluidos en nuestro código mediante la siguiente línea "extern struct genresTiles luna;"
  • Guardamos la paleta de la imagen sobre PAL2, tomándola del atributo "pal"
VDP_setPalette(PAL2, luna.pal);
  • Cargamos en VRAM todos los tiles que han resultado de la conversión de la imagen:
  • Argumentos de VDP_loadTileData()
  • arg0 = dirección donde se encuentran los tiles a cargar
  • arg1 = indice (posición en VRAM) del primero de los tiles que conforman la imagen
  • arg2 = núero de tiles a enviar a la VRAM
  • arg3 = usar DMA (1) o no (0)
VDP_loadTileData(luna.tiles, 100, luna.width*luna.height, 0);
  • Y ya sólo nos queda dibujar la imagen en pantalla usando la misma función que en el ejemplo de tiles generados por el SGDK, VDP_fillTileMapRectInc(). En este caso usando PAL2, indicando que tomaremos tiles que se encuentran a partir de la posición 100 de la VRAM, en las coordenadas (23,12) de pantalla y con un ancho y un alto (en tiles) indicados por los atributos "width" y "heigh" del struct luna:
VDP_fillTileMapRectInc(BPLAN, TILE_ATTR_FULL(PAL2, 0, 0, 0, 100), 23, 12, luna.width, luna.height);
  • Bucle infinito
while(1){
/* sincroniza la pantalla */
VDP_waitVSync();
}
return(0);
  • Aquí os dejo el fichero con el código y los gráficos utilizados, además otro con el comentado megagfx.

04 - Sprites

  • Pues aquí andamos de nuevo con un nuevo tuto que en conjunción con el anterior, en el que tratamos los tiles, va a empezar a darle vidilla a las primeras demos que os animéis a hacer.
  • Eso sí, si aún no te ha quedado del todo claro el funcionamiento y uso de tiles en megadrive es MUY recomendable volver al tuto anterior y dedicarle el tiempo necesario. Tampoco hay por qué agobiarse, no tiene maś historia que:
  1. Crear los tiles con cualquier método disponible
  2. Mandarlos a VRAM en una determinada posición
  3. Dibujarlos en pantalla usando la función pertinente
  • Pero claro, hay que conocer las funciones necesarias y conviene tener claro también como funciona en líneas generales la memoria de vídeo. Se reitera, no tiene gran misterio, sólo requiere un poquito de paciencia y tal vez una pizca de concentración para pillar la idea de primeras.
  • Y tras habernos ido por las ramas para no pasar por alto que este tuto parte de la base de haber asimilado debidamente el anterior, damas y caballeros, con todos ustedes "Los Sprites".
  • "Esto ... ¿y si yo no sé que es un sprite?"
  • Si ese es el caso, vamos con unos ejemplos visuales la mar de efectivos y así nos ahorramos la definición técnica que para eso se tira de wikipedia y listo.
  • Primero recordaremos que megadrive dispone de dos planos de fondo por hardware: plano A, plano B. Para por ejemplo dibujar en el B un mapa de tiles que contengan las montañas en la lejanía y en el A usamos tiles para dibujar una cordillera cercana. Llegado el momento haremos que el plano B se desplace más despacio que el A, horizontalmente los dos (tómate tus segundos para visualizar la idea, no hay prisa).
  • Y entonces a lo mejor, luego de visualizar mentalmente el ejemplo, piensas: "Oye, ¿se supone que ibas a hablar sobre sprites?". Sí, pero me viene al pelo el ejemplo de los mapas de tiles. Hagámonos a la idea de que los "mapas de tiles" dibujados en los planos A y B nos servirán para dibujar fondos y escenarios de nuestros juegos. Los "sprites" suelen ser los objetos, personajes o elementos que aunque lleguen a interactuar con el escenario no forman parte de éste.
  • Ejemplo: Sonic, Green Hill Zone
  • "Mapa de tiles" en el plano A: el escenario plataformero en primer plano
  • "Mapa de tiles" en el plano B: el mar, bosquecillos cascadas, montañas, nubes y cielo en segundo plano (que se muevan independientemente es algo de lo tal vez hablaremos en otro tuto)
  • SPRITES: Sonic, los monitores, enemigos, anillos ...
  • En un juego de naves los sprites serán las propias naves, los bichejos enemigos, las explosiones, disparos y demás elementos que sean independientes del escenario.
  • Los sprites se dibujan sobre el "plano de sprites". Con lo cual ya tenemos que la megadrive dispone de:
  • 2 planos para fondo (A y B)
  • 1 plano para sprites
  • 1 ?? (trollface), "plano Window", que en realidad será una parte inmóvil del plano A que sirve normalmente para meter los marcadores, etc.
  • Y ya por último antes de meternos en faena, enumeraremos algunas características (o puñeterías) de los sprites en megadrive.
  • El plano destinado a los sprites tiene un tamaño fijo y no permite scroll. Normalmente 512x512 píxels, correspondiendo la coordenada 128,128 al primer píxel visible en la esquina superior izquierda de la pantalla. Con SGDK usaremos 0,0 como coordenada que indique el píxel de la esquina superior izquierda del plano de sprites, pero la consola en realidad no los maneja así internamente.
  • Cualquier sprite fuera del área visible de pantalla no será visible. Puesto de otro modo, si sitúas un sprite de 16 píxels de ancho en la coordenada (-20, 128) dicho sprite no aparecerá en pantalla pero estar.. está.
  • Si el valor dado a la coordenada X o Y del sprite excede el espacio del plano "vuelve" sobre el origen. Ejemplo: (300, 812) es lo mismo que (300, 300)
  • Todos los sprites están identificados con un índice numérico y cuentan con un atributo que indica cuál es el sprite que le sigue a la hora de ser dibujado ("link"). El atributo link de nuestro último sprite ha de tomar el valor del índice que tenga el primer sprite que vayamos a dibujar. Si no lo hacemos así obtendremos el bonito número de cero sprites en pantalla. Podemos modificarlos más tarde para indicar prioridades entre los sprites... cual se dibuja encima de otros, imaginad los dragones del Space Harrier, cuyas partes cambian de prioridad según se acercan o alejan de la pantalla, para eso sirve.
  • Máximo 80 sprites definidos simultáneamente.
  • Máximo 20 sprites sobre una misma scanline ("en línea"), si te pasas... parpadean.
  • No es necesario aprendérselas de memoria, pero si tu demo o tu juego hace algo raro y no sabes por qué, dales un repaso a ver si algo de eso va a tener la culpa.
  • Y la idea básica y más importante:
  • Los sprites están compuestos por tiles, podemos definir sprites desde 1x1 tile (8x8 píxels) hasta 4x4 tiles (32x32 píxels) usando cualquier valor intermedio, cosa que la todopoderosa snes no puede [poraki]
  • Por ejemplo (3x2 tiles = 24x16 píxels).
  • En el momento en que quieras usar sprites más grandes, ten en cuenta que megadrive cuenta con esos límites, por lo que tendrás que:
  • definir varios sprites hasta completar el tamaño que necesites
  • y tratar las coordenadas de esos sprites de manera adecuada para que en su conjunto formen la imagen que hayan de representar.
#include <genesis.h>
  • Cuando programando con SGDK definamos algún dato como const ("constante", el valor con que se inicialice ya no puede variar en todo el programa) dicho dato se amacenará en ROM en lugar de en la memoria RAM de 64Kb, así que los gráficos por ejemplo DEBEN declararse así.
  • Vamos a crear un array de cuatro tiles (2x2) que representarán la imagen de una bola. Éste será el gráfico de nuestro primer sprite. Pero aún queda mandanga después de definir el array. Paciencia que con esto simplemente tenemos la información gráfica, que ya es algo por otro lado.
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
};
  • Del mismo modo que en el tutorial anterior tuvimos que crear un struct "genresTiles" (sí, lo hicimos, si no te acuerdas, anda, búscalo y de le das el repasito) para manejar los tiles que nos creaba la herramienta "genres" provista por SGDK, en este caso haremos lo propio creando un struct que nos permitirá hacer buen uso de los sprites. Guarda gran similitud con "genresTiles" pero una importante diferencia a tener en cuenta es que los valores que usemos vendrán dados en píxels.
  • Le echamos un ojo:
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 */
};
  • Almacenamos los datos del exprite que nos haya generado "genres" sobre los atributos de un struct "genresSprites".
extern struct genresSprites sonic;
  • En el archivo "resource.rc" se habrá indicado el nombre de la variable de salida (sonic), la ruta del bitmap que contiene los frames de nuestro sprite y el tamaño en píxels en el que se han de dividir los distintos frames. En nuestro caso, de la siguiente manera:
SPRITE sonic "data/sonic.bmp" 24 32 0 7
  • Empecemos con el bucle principal, como en las otras ocasiones:
int main( ){
/* Dibujamos la cabecera austera de turno con VDP_drawText() */
VDP_drawText("Ejemplo de dibujo de SPRITES", 5, 1);
VDP_drawText("----------------------------", 5, 2);
  • Igual que hicimos en el tuto anterior, hay que empezar por escribir los tiles que vayamos a requerir en la VRAM de la consola usando VDP_loadTileData(). La recordamos:
/* 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);
  • La llamada a esta función es opcional, pero es recomendable usarla para asegurarnos de que ningún sprite arranque con algún valor que no le hayamos dado nosotros y el consecuente desespero
VDP_resetSprites();
  • Digamos que por sistema, tras mandar a VRAM los tiles de un sprite, lo siguiente por hacer es cargar su paleta. Nuestro sprite-bola usará una de las predefinidas sin embargo.
  • Y ahora... Y AHORA ... Y AHOOOORA... lo definimos.
  • Lo que viene a decir que le indicaremos a la megadrive que queremos crear un flamante sprite con las características que le indiquemos y ya que ella se encargue de rellanar los registros en una tabla contenida en la VRAM con los valores pertinentes.
  • Cada sprite que pensemos usar hemos de definirlo antes de poder hacer uso de él en el programa. Vamos a ver dos métodos de hacerlo y luego ya decides tú cuál te parece más conveniente (*cof*, el segundo, *cof*)
  • Primero veamos cómo definir un sprite usando la función básica que SGDK dispone para ello de modo, VDP_setSprite() :
/* 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);
  • Atendiendo a los valores que hemos pasado como parámetros podemos ver que nuestro primer sprite (sprite 0), tiene un tamaño de 2x2 tiles los cuales se tomarán a partir de la posición 1 de la VRAM. Ergo, nuestro primer sprite va a mostrar el gráfico de la bola que creamos al empezar el código. Una cosa a tener en cuenta, es que mientras que para dibujar tiles en uno de las capas A y B, la consola dibuja los tiles secuencialmente de izquierda a derecha y de arriba a abajo, para los sprites lo hace de arriba a abajo y de izquierda a derecha, hay que tenerlo en cuenta a la hora de meterlo en la vram, pero vamos, que de eso se encargará el conversor gráfico que usemos al especificar que es un sprite, ya sea genres, imagenesis, etc.
  • Veamos ahora otra manera distinta (¿más elegante?) para definir un sprite
SpriteDef spriteBola2;
  • "SpriteDef" es un struct disponible en SGDK destinado a acoger los distintos datos que precisa un sprite. El primer paso de este método es proceder como en la línea de código anterior: simplemente creando un dato de tipo SpriteDef que nombraremos según nos plazca. La definición de SpriteDef en SGDK es la siguiente:
/* 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 */
  • Creado el dato, inicializamos sus atributos.
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;
  • Y llegado este momento usamos la función VDP_setSpriteP() para definir nuestro segundo sprite:
/* 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);
  • Está siendo un poco tocho pero es por pretender dejar clara la forma de proceder para crear sprites. Ahora que tenemos ya definidos dos sprites, un par de bolas que se desplazarán (o no) por pantalla, vamos a recapitular a modo de resumen los pasos para crear y mostrar sprites en pantalla:
  1. Cargar los tiles necesarios en VRAM
  2. Cargar la paleta (si no ha sido cargada previamente)
  3. Definir el sprite
  4. Definir más sprites
  5. Solicitarle a la megadrive que nos los dibuje
  • Vamos a ver ya por último como definir us sprite creado desde genres y en unos minutos nos metemos POR FIN en cómo animarlos por pantalla, tanto en cuanto a desplazarlos por la pantalla como crear el efecto de animación intercambiando frames. Pero antes, lo dicho: definir sprites creados con genres.
  • La puñeta siguiente es porque nos va a interesar conocer el número de tiles de los sprites en el "spritesheet" (el bitmap con los frames de nuestro sprite) con que genres genera los sprites, y genres, de por sí no nos lo indica. Si no lo has visto antes, abre la carpeta data y échale un ojo a sonic.bmp, ése es el "spritesheet" que estamos usando en este tuto. ¡Ah! y recuerda que si vas a usar el tuyo propio: 16 colores y dimensiones múltiplo de ocho.
u16 nbTiles = (sonic.height>>3) * (sonic.width>>3);
  • Si la notación binaria aún no es lo tuyo, no pasa nada. Lo que hemos hecho es tomar el valor en binario del alto (height) del sprite y ventilarle los tres últimos dígitos, lo cual equivale a dividir por ocho de una manera más rápida que haciéndolo al uso. Hemos hecho lo propio con el ancho y luego multiplicado ambos valores. Esto es porque genres guarda tanto el ancho como el ato en píxels, no en tiles, y como ya sabemos, un tile tiene un tamaño de 8x8 píxels.
  • Si lo de "sonic" te extraña y no caes en qué momento hemos creado ese dato, fue antes de meternos con la función main. Así:
extern struct genresSprites sonic;
  • Importando a nuestro código los sprites creados con "genres". Justo antes creábamos el struct genresSprites. Vuelve a echarle un vistazo y repasar sus atributos, y si te sobra tiempo y estás de buenas repara en que estamos procediendo igual que en el tuto anterior cuando sólo requeríamos de genres tiles sin más. El atributo "sprites" va a entrar en acción en breve. Estate pendiente.
  • Puñeta anterior realizada, vamos a definir el nuevo sprite usando el método con el que definimos el anterior.
SpriteDef spriteErizo;
  • Esta vez no usaremos una paleta predefinidia sino que cargamos la paleta del spritesheet. Lo hacemos sobre PAL1. Fácil, ¿no?
VDP_setPalette(PAL1, sonic.pal);
  • Asignamos los que serán los valores iniciales que tome nuestro tercer sprite:
spriteErizo.posx = 128; < br/> spriteErizo.posy = 128; < br/> spriteErizo.size = sonic.size>>8; < br/> spriteErizo.tile_attr = TILE_ATTR_FULL(PAL1,1,1,1,5); < br/> 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. */
  • Y tercer sprite definido:
VDP_setSpriteP(2, &spriteErizo);
  • Dijimos por ahí atrás que íbamos a animar un sprite frame a frame. Nuestra variable "frame" será la que indique cuál frame ha de ser mostrada.
u8 frame = 0;
  • ¡Ah!, ábrete la carpeta "data" y déjate a la vista el spritesheet. En el archivo "resource.rc" tenemos indicado que cada frame va a ser de 24x32 píxels con lo que cada imagen queda encuadrada debidamente en un área de ese tamaño. Y lo realmente importante es que genres nos lo deja a huevo para acceder a cada frame de forma muy cómoda: usando el atributo "sprites" como un array al que especificándole un determinado índice accedemos al frame que se encuentre en dicha posición (comenzando a contar de izquierda a derecha y de arriba a abajo). Por ejemplo, sonic.sprites[6] nos ofrecerá los datos del sexto sprite/frame: el Sonic rojo.
while(1){ /* Bucle infinito al canto */
  • Movemos el sprite "bola2" incrementando su posición en un píxel ambas coordenadas...
spriteBola2.posx++;
spriteBola2.posy++;
  • Al usar de nuevo VDP_setSpriteP() pasándole unos valores modificados al sprite 1 (a la "bola2") lo que hacemos es actualizar sus propiedades:
VDP_setSpriteP(1, &spriteBola2);
  • El sprite de Sonic lo moveremos por pantalla y a la vez que lo vamos a animar cambiando su frame continuamente ...
  • Escribimos en la posición 5 de la VRAM tantos tiles como correspondan a un frame (nbTiles, la puñeta de más atrás), tomándolos a partir del segundo sprite/frame del spritesheet (sonic.sprites[frame + 1]). Venga, que no es tan complicado. Reléelo unas cuantas veces teniendo a la vista el spritesheet si hace falta.
VDP_loadTileData(sonic.sprites[frame + 1], 5, nbTiles, 1);
  • Incrementamos el frame en cada iteración:
frame++;
  • Y como los frames corresponidentes a la animación de correr, que son los que nos interesan, con la siguiente instrucción hacemos que cuando la variable frame vaya a tomar el valor 3 "se resetee" y vuelva a cero:
frame%=3; /* solo tenemos 3 frames, volvemos al primero */
  • Movemos el sprite, modificando su coordenada x.
spriteErizo.posx+=10;
  • Y registramos los cambios sobre el "sprite número 2" que no es otro que nuestro sonic presto a correr de una manera un tanto extraña.
VDP_setSpriteP(2, &spriteErizo);
  • VDP_updateSprites() hace que la consola tome en cuenta los nuevos valores que hayamos podido dar a los sprites. Vamos, que actualiza los sprites...
VDP_updateSprites();
  • Sincronizamos el dibujo con el retrazo horizontal de pantalla... unas cuantas veces, sí. Es un apaño para procurarnos un retardo conveniente y que nuestros sprites no vayan demasiado lanzados cuando se desplacen. Eres libre de comentar e invalidar tantos como te apetezca (conveniente también es dejar siempre uno al menos).
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. */
}
  • Y con esto y un bizcocho ya sabemos dibujar tiles en las 2 capas y sprites.
  • Aquí os dejo el fuente del tutorial, recordad cambiar la ruta de vuestro makefile si no es como la mia.

Anexo 1: Creación de bitmaps y tablas de sprite con Gimp

  • 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.

Anexo 2: Diseño y manipulación de tiles

Tiles

  • Antes de meternos con los sprites y bitmaps, empecemos entendiendo un poco como maneja la Megadrive los tiles:
  • 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.
  • Otros datos interesantes a conocer:
  • 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.
  • Entonces para dibujar un tile en la pantalla:
  • 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);
  • 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.
  • 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);

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.rc

BITMAP moon "data/moon.bmp" 0
 ; La última línea de este archivo debe ser una línea en blanco o un comentario.

main.c

struct 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.

Anexo 3: Gestión de sprites y tablas de sprites

Sprites

  • Antes que nada comentar que existe una diferencia entre los tiles de los planos y los tiles de los sprites:
  • 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).
  • 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.
  • Información básica que debemos saber sobre los sprites:
  • 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).
  • Los pasos para dibujar un sprite en la pantalla:
  • 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:
  1. Puedes definir tantos sprites como necesites (80 máximo) antes de solicitar que se dibujen en pantalla.
  2. 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.
  3. 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".
  4. 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:
  1. Usar una estructura para mantener las propiedades del sprite: un objeto SpriteDef (en el tutorial en inglés aparece _spritedef porque está desactualizado).
  2. 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:
  1. Todos tus sprites deben enlazar a otro.
  2. Que no se forme un enlace cíclico (por ejemplo: 1->2, 2->3, 3->1 ...).
  3. 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:
  1. Cargar cada frame y declarar el sprite desde el primer tile en cada actualización.
  2. Declarar un sprite y cargar cada frame en el primer tile en cada actualizació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.
  • 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.

Enlaces relacionados

Basiegaxorz

Para consultas y debates sobre el contenido de este artículo, visita el Hilo oficial.

  • Compilador de basic para megadrive.
  • Una característica muy buena del Basiegaxorz, es que se puede incluir codigo ASM.
  • Descargar e instalar la versión "Newest and Latest - BasiEgaXorz v1.37 Using the ASMX Assembler"

Introducción

  • Una característica muy buena del Basiegaxorz, es que se puede incluir codigo ASM, por lo que en realidad se convierte en una herramienta muy potente incluso para programadores avanzados, pero claro, asm escapa a este tutorial, si alguien le interesa , contacten conmigo, que ASM es una materia que se me da bien, y hago un tutorial.
  • Asumo que ya al menos han leido algun tutorial de basic basico, por si no es el caso, echenle un vistazo al tutorial de Basic generico. Al menos necesitan saber lo que es un “for” “if” “end if” …etc etc
  • Dicho esto, es recomendable que lean el tutorial que hice de trabajo con tiles, aquí mismo en EOL.

01: Manejo de Tiles y Sprites

  • Si leyeron mi tutorial de Trabajo tiles en megadrive, sabran que tiene 64k de video, muestra 64 colores en pantalla, en 4 paletas de 15 colores cada una, y normalmente se usa la resolución de 320x224 (entre otras) .
  • También ya sabrán (no lo sabes? Es porque no leistes mi post de tiles!!) que un tile es un cuadrado de 8x8 pixeles, y que la megadrive tiene memoria para almacenar 1344 tiles simultaneos (43008 bytes de VRAM), pero los primeros 256 estan reservados para texto, lo que deja 1088 tiles utiles.
  • Si usamos como resolucion 320x224, tendremos que hacer esta cuenta:
  • 1 tile: 8x8=64 pixeles
  • Pantalla: 320*224=71680/64= 1120
  • O sea se necesitan 1120 tiles para llenar la pantalla completa.En teoria no cabria una pantalla completa en la memoria, pero si pasamos de usar texto, podemos utilizar los 256 tiles reservados, lo que nos daría suficiente para llenar una pantalla, y además nos sobrarían 224 tiles para usarlos sabiamente.
  • Como dije, el “tile” es la unidad mas pequeña en la que se puede trabajar.Asi que todo estara generado por tiles.O sea la pantalla de la megadrive, no tiene 320x224 pixeles, si no 40x28 tiles.
  • Vamos como primer intento de programación a poner un sprite, y un simple fondo. Bajen el siguiente pack con todo listo para usar:
  • Lo primero necesitamos un buen programa para editar graficos, aconsejo el Paint Shop pro, ya que tiene un manejo de paletas increíble. En linux, se puede usar con wine.
  • Y lo mas importante, un programa para transformar gráficos en tiles, y ese es el imagenesis.

NOTA: X es la resolución horizontal, Y es la vertical

  • Imagenesis es simple de usar FILE->OPEN abrimos una imagen BMP a 16 colores, en Mode, tenemos dos opciones:
  • En dirección X luego a Y, se usa para fondos por ejemplo
  • En dirección Y luego a X, se usa para Sprites
  • Abrimos la imagen fondo.bmp ,seleccionamos 15 colores,4bpp,1plane,8x8 tile, y le damos a ACTIONS->Quantize now:
  • Fijarse en los datos en la parte de abajo, muy importantes, 11 colors in 320 tiles.
  • Si vamos a a ACTIONS->EXPORT TILE DATA, nos aparecerá una ventana, donde si seleccionamos en Format BASIC, nos mostrara un montón de datos en este formato:
DATALONG $11121112 ‘Tile #0
  • Si se fijan bien, cada tile esta formado por 64 caracteres, que representan los píxeles, y los números son el color de cada píxel (de 0 a 15)
  • En ACTIONS-> Export pallette data, veremos un dato en este formato:
DATAINT $0EA0, $0EA0,…etc
  • Esos son los 16 colores de la paleta del grafico en el que estamos trabajando.
  • Todos los datos de este grafico estan en el archivo 1.bex
  • A continuación, abrimos el basiegaxorz, y cargamos el archivo 1.bex, veriamos esto
  • Noten que al final del archivo, esta cargado el grafico, ya transformado a codigo BASIC
  • Aquí tenemos varios comandos muy utiles
  • loadtiles suelo,320,256: Este comando, lo que hace es cargar los tiles en memoria de video, carga el tile “suelo”, que tiene 320 tiles (recuerden del imagenesis), en la posición 256 (como explique la MD tiene 1344 espacios para tiles)
  • Pallettes suelo_pal,0,0,16 : Aquí cargamos la paleta de colores, en este caso “suelo_pal”. El primer 0 es porque cargamos la paleta como la primera de las 4 (0,1,2,3), el segundo 0 es para indicarle cuantos colores cargamos, lo dejamos en 0 porque los queremos todos, y el 16 final es para indicar cuantos colores tiene nuestra paleta.
  • Drawtilesinc 256,0,20,40,8: Muestra los tiles en pantalla, el comando seria
  • Drawtilesinc 256, X, Y, ancho, alto
  • 256 es la posición de tiles donde empezara a leer, X en 0 porque comenzamos en la parte izquierda, Y es 40 porque nuestra imagen tiene 8 de alto, y 40x8 es el tamaño de nuestra imagen.[/quote]
  • Ahora si le damos TOOLS->Compile, nos generara un archivo basic.bin, en la carpeta del basiegaxorz, que podremos cargar con cualquier emulador de megadrive
  • Ahora, cerramos el archivo, 1.bex, y abrimos el 2.bex, aquí ya carge el sprite “sonic.bmp”, que anteriormente transforme en imagenesis
  • Veremos nuevos comandos utiles:
  • sonic=addsprite(4,3): Aquí cargamos el sprite del sonic, donde le indicamos que tiene 4 tiles de alto, por 3 de ancho.
  • propsprite sonic,576,1: Aquí cargamos el sprite en memoria de video, donde 576 es el numero de tile de comienzo (256+320 del fondo), y el 1 es la paleta de colores a usar.
  • x=160: Le asignamos un valor a x para el movesprite
  • movesprite sonic,x,303: Finalmente movemos el sprite al sitio de pantalla que mas nos guste, en este caso a las coordenadas X=32, Y=175 (ACLARACION: tanto X como Y comienzan en 128 no en 0)
  • Cerramos el 2.bex, y abrimos el 3.bex, aquí hay varios comandos basicos de acceso al joystick, y un loop para que el programa este siempre leyendo el pad.
  • Do / loop: Todo lo que este dentro del Do/Loop se repetira por el infinito, es un bucle
  • J=joypad(): Asignamos el pad a una variable J para que sea mas facil de leer
  • Aquí básicamente, le decimos que si se presiona el botón 2 del pad, mover el sprite x lugares en negativo. Como el comando esta dentro de un bucle, x—hace que mientras se mantenga presionado el botón 2, se reste 1 a x:
if j.2 then
movesprite sonic, x,303
x--
end if
  • El resultado final, es el sonic de Master System en nuestra megadrive.

02: Archivos binarios

  • En un proyecto pequeño, tener los graficos trasformados en formato BASIC, no es molestia. Pero a medida que nuestro proyecto crece, nos damos cuenta que un archivo de codigo, con 80 o 90 mil lineas, como que no es muy saludable.
  • Aquí se aprecia, el codigo y el tile grafico, si se ve bien, es lo mismo uno que otro.
  • Asi que para evitar este problema, se cargan los datos en formato binario. Aqui un paso a paso para exportar a binario en el Imagenesis:
  1. Cargamos la imagen (bmp a 16colores)
  2. Actions -> Quantize Now
  3. Actions -> Export tile data
  4. En FORMAT, cambiamos el valor por BINARY, y presionamos Save Data
  5. Guardamos el archivo resultante en la misma carpeta donde esta el codigo, y le damos una extencion que nos resulte facil de recordar, por ejemplo, del sprite del sonic
  • Para graficos: sonic.img. Para paleta: Sonic.pal
  • Una vez salvado el grafico, en el BasiEgaXorz, solo tenemos que suplantar el codigo de Basic por un comando que carge el archivo binario
Sonic:
DATALONG $00000000'Tile #0
DATALONG $00000000
.... etc


Sonic:
datafile sonic.img,BIN
  • Dejo para descargar un ejemplo practico, del sonic tambien, donde se carga los graficos desde un archivo binario.
  • Mi opinión es que lo mejor es cargar los escenarios como binario, y los sprites como codigo Basic.
  • En este ejemplo, ademas de cargar archivos binarios, puse varios ejemplos de animacion de sprites, para el siguiente tutorial

03: Carga y descarga de Sprites + Animación

  • Como habrán visto en la descarga “Ejemplo carga binarios”, aquí el Sonic, ya camina con una pequeña animación, y también esta animado cuando se acerca al borde.
  • En este ejemplo, 5.bex, solo use comandos simples que ya habíamos visto en el tutorial numero 1.
  • El codigo de la intro:
dedo=addsprite(4,3)
loadtiles introtile,240,1
loadtiles dedo,12,241
pallettes intro_pal,0,0,16
pallettes dedo_pal,1,0,16
Drawtilesinc 1,10,8,20,12
propsprite dedo,241,1

while joypad()=0:
sleep 10
movesprite dedo,295,205
sleep 10
movesprite dedo,0,0
wend
waitpadup 0
FreeAllSprites
sleep 10
  • Para el intro, use una imagen de sonic que se carga con loadtiles intro tile,240,1, y se muestra con Drawtilesinc 1,10,8,20,12.
  • El dedo es un sprite de 4x3 pixeles, que se carga dentro de un bucle while. Básicamente este bucle lo que dice es:
  • while joypad()=0: > mientras no se presione un boton
  • sleep 10 > espero un tiempo
  • movesprite dedo,295,205 > muestro el sprite del dedo
  • sleep 10 > espero un tiempo
  • movesprite dedo,0,0 > y lo oculto llevándolo fuera de pantalla
  • wend > cierro el bucle
  • FreeAllSprites > descargo todos los sprites de pantalla antes de cargar el juego
  • Veamos la animacion al caminar de sonic:
If j.2 Then
x=x-2
If FrameCount < 10 Then
pallettes sonic_r_pal,1,0,16
propsprite sonic_l1,1197,1
movesprite sonic_l1,x,y
ElseIf FrameCount < 20 Then
propsprite sonic_l2,1209,1
movesprite sonic_l2,x,y
ElseIf FrameCount < 30 Then
propsprite sonic_l3,1221,1
movesprite sonic_l3,x,y
Else
FrameCount = -1
End If
FrameCount++
  • En la animación de caminar, del ejempo de arriba, se crea una variable Framecount que va de 0 a 30, y cada 10, carga un nuevo frame del sonic, y lo mueve 2 pixeles en la direccion que se presione en el joystick, un codigo muy simple.

04: Mapas de Tiles

  • Bueno, como muchos de ustedes se darian cuenta, de la forma que estamos trabajando actualmente, poco mas de una pantalla se puede hacer, ya que se nos acaba la memoria de video de la megadrive.
  • Como sabrán, la vram de la megadrive puede almacenar 1343 tiles, y una pantalla de 320x224 nos ocupa 1120 tiles, dejando solo 223 para los sprites, y eso sin usar texto claro.
  • La solucion a este problema, son los mapas. Y que son? Pues en cada pantalla, existen multitud de tiles repetidos. Pues lo que se hace es hacer un mapa de la pantalla, donde se especifica en que lugar esta cada tile, y si es repetido o no.
  • De esta forma con pocos tiles, se puede generar una pantalla completa. Es muy recomendable leer a este punto si hay dudas, mi post de trabajo con tiles.
  • Para este ejemplo, hice una pantalla con 68 tiles. (saque los tiles de uno de mis juegos favoritos de NES, el Shaterhand)
  • Para generar el mapa, se hace en el imagenesis, abrimos el bmp con el fondo a optimizar:
  • MODE-> 15color,4bpp,OPTIMIZED
  • Luego ACTION-> Quantize Now
  • Y ahora veremos una nueva opcion, ACTION -> EXPORT TILE MAP
  • Podemos salvar el mapa tanto en binario como en codigo Basic, es lo mismo.
  • Esta vez lo hare todo en codigo Basic asi es mas facil de ver.
  • El codigo es muy simple, y esta bien explicado creo:
Dim Map(39,27) as Integer 'Creamos un mapa de 40x28 tiles

pallettes fondo_pal,0,0,16 'Cargamos la paleta
loadtiles fondo,67,1 'Y cargamos los 67 tiles


reload fondo_map 'Cargamos el mapa


For Y=0 to 27 'la imagen tiene 40x28 tiles, asi que cargamos todos los tiles, del Y=0 al 27
For X=0 to 39 'al X=0 a 39
readint Map(X,Y) 'Y leemos el mapa
Next X
Next Y


For Y=0 to 27
For X=0 to 39
DrawTile Map(X,Y)+pallette(0)+1,X,Y 'Y dibujamos una pantalla completa, leyendo el mapa,

Next X 'usando la paleta 0, y comenzando del tile 1
Next Y 'Siempre en direccion primero X luego Y
  • En este codigo lo que se hace es crear un mapa del tamaño de la pantalla, o sea 40x28 tiles.
  • Luego, se carga el mapa generado por el imagenesis, y se lee desde X=0 a X=39 y de Y=0 a Y=27
  • Una vez leido, se vuelca en la vram, usando el mapa como guia, usando el comando DrawTile
  • Así que en realidad lo que vemos es la pantalla de arriba, pero la realidad son los tiles de abajo

05: Scroll

  • En el tutorial anterior, vimos los mapas, algo muy util para poder cargar una pantalla sin ocupar toda la VRAM de la Megadrive.
  • Esta tecnica es la base de este tutorial, ya que sin ella, no podriamos cargar mas que una pantalla a la vez.
  • Lo primero es entender las limitaciones de hardware. El tamaño maximo de pantalla que permite gestionar la Megadrive es de 512x512 pixeles o 64x64 tiles.
  • La megadrive permite dos planos de scroll (A,B) y un tercer plano fijo, (Window) que no permite scroll.
  • Sabiendo esto, ya podemos ir al codigo. Aqui dejo dos ejemplos. En el primero se crean dos planos de scroll, uno fijo, y el otro movil.
  • Primer ejemplo: Aqui cargo un plano de scroll fijo, con una imagen en primer plano, scroll A,tomada prestada del LB2, y un plano B de scroll movil, que es un escenario completo tambien del LB2
  • Lo nuevo del codigo son los siguientes comandos
  • settextplane <-- Define el plano de scroll donde se dibujaran los textos
  • setgfxplane <-- Define el plano de scroll para los comandos que dibujen tiles (DrawTile, DrawTiles)
  • Este comando permite tres opciones, SCROLL_A/SCROLL_B/WINDOW. Que define en que planos se dibujaran los tiles: setscrollplane <-- Define el plano de scroll actual. Puede ser el A,B,o Window
  • Por ultimo el comando que hace el scroll en si mismo, los comandos basicos son
  • scroll permite UP/DOWN/LEFT/RIGHT (arriba,abajo,izquierda,derecha) y la cantidad de pixeles del scroll.
  • scroll left,1 <---- mueve el plano seleccionado de scroll, 1 pixel a la izquierda
  • scroll down,4 <---- mueve el plano seleccionado de scroll, 4 pixel hacia abajo

06: Audio

  • Bueno, ya tenemos mas o menos definida la parte grafica, scroll, sprites, tiles... pero nos falta la sonora.
  • Vamos a ver como usar el YMH2612, para reproducir un WAV.
  • Aquí un ejemplo, que carga un archivo "Segapcm.bin" de un tamaño de 27000 bytes, y lo reproduce.
  • El codigo
'Gracias Mairtrus por el codigo y las explicaciones
Const #PCMLongitude = 27000 'Definimos el largo en bytes del archivo 26,3 KB = 27.000 bytes
do
ink 2
locate 1,1
Print "Ejemplo de sonido PCM"
Print " El archivo sega.pcm es un WAV sin los primeros 52bytes"
Print ""
Print ""
print " presionar A para reproducir"
SonidoSega:
if JoyPad(0).6 then
enable INTERRUPTVBLANK
on VBLANK gosub PlaySega 'llamamos a la funcion PlaySega
end if
loop
PlaySEGA:
while peek(&hA04000).7 ' espera hasta que se desocupe
wend
poke &hA04000,&h2B ' en el registro $2B, si el bit más alto es 0, el*
while peek(&hA04000).7 ' espera hasta que se desocupe
wend
poke &hA04001,&h80 '*canal 6 es un FM más, caso contrario es para DAC
while peek(&hA04000).7 ' espera hasta que se desocupe
wend
poke &hA04002,&hB6 ' el registro $B4 es para poner en el parlante izquierdo, derecho o ambos (Pannig en ingles, Balance en español). Que sea $B6 es porque, al ser el canal FM 6 el canal 2 de la segunda parte, $B4+2=$B6
while peek(&hA04000).7 'wait for busy
wend
poke &hA04003,&hC0 ' Al poner los 2 bits más altos, pone el canal izquierdo y derecho simultaneamente, o sea lo hace stereo (pone 80 o 40 y vas a ver se escucha por un solo parlante)
while peek(&hA04000).7 ' espera hasta que se desocupe
wend
poke &hA04000,&h2A ' esta es la direccion donde se escribe el dato de 8bits que conforma el sample PCM
reload PCMData
for i=1 to #PCMLongitude
while peek(&hA04000).7 ' espera hasta que se desocupe
wend
read Nota
poke &hA04001, Nota ' Y aqui escribe el dato que carga desde la muestra
halt
next i
gosub SonidoSega
PCMData:
DATAFILE segapcm.bin,BIN
  • Basicamente el archivo segapcm.bin, es un archivo wav de 8bits, 14khz, sin los primeros 52bytes, editado en un editor hexadecimal.
  • Si quieren reproducir archivos wav sin modificar, solo hay que cambiar una linea
reload PCMData
  • Por esta
reload PCMData,52
  • De esta forma, se carga el archivo desde el byte 52.
  • Deberan experimentar diferentes tiempos de espera para diferentes calidades de wav (8khz,11khz,22khz..etc).

07: Ejemplo de juego + Colisiones

  • Muy buenas, en este tutorial, posteare el codigo de un juego muy basico, el Pong.
  • Este codigo, aunque no este totalmente finalizado, es un buen ejemplo de las siguientes cosas
  1. Un juego "terminado", hasta ahora solo teniamos ejemplos sueltos
  2. Colisiones
  • En el zip, va el codigo + binario.
  • Voy a explicar un poco lo de las colisiones, ya que es un tema que no he tocado hasta ahora
IF (X =< 142) then
IF Y=> SPRITEPOSY(PLAYER1)-8 and Y=<SPRITEPOSY(PLAYER1)+24 then
SENTIDOX--
end if
ENDIF
  • Si se fijan en esa porcion de codigo, basicamente lo que sucede aqui es:

NOTA: Le llamo paleta a nuestro sprite

  • IF (X =< 142) then - Este codigo detecta que la posicion X de la pelota es igual o inferior a 142, o sea, que esta a la misma altura que la paleta. Solo con este codigo, podriamos hacer "colision" lo que pasa es que la pelota chocaria con todo la posicion X, o sea, como si fuera un muro, y lo que queremos es que detecte, la posicion de la paleta para poder saber, si lo hicimos bien, o por el contrario fallamos
  • IF Y=> SPRITEPOSY(PLAYER1)-8 and Y=<SPRITEPOSY(PLAYER1)+24. Como la paleta mide 32pixeles de alto (4 tiles) una vez que la bola esta en la posicion X correcta, comprobaremos la posicion Y de la paleta. Para eso, simplemente comprobamos la posicion inicial de la paleta, y le sumamos los 32 pixeles, y sabemos la final. O sea, que si en ese momento el punto Y mas alto de la paleta es 100, sabemos que el Y mas bajo es 68
  • O sea, que entre X 142 y Y 68 a 100 tenemos colisión.

Enlaces relacionados