miércoles, 28 de diciembre de 2016

Games aside #1: Desarrollo para Super Game Boy (paso a paso)

A lo largo de esta entrada vamos a aprender a implementar alguna de las funcionalidades que ofrece el Super Game Boy, concretamente las que aprovecha la recientemente publicada ROM mejorada para Super Game Boy de la demo de Rocket Man. Demo expuesta como producción local en el certamen internacional de videojuegos independientes AzPlay celebrado el pasado noviembre en Bilbao.

Antes de comenzar me gustaría aclarar una serie de puntos:
  1. el programa que se va a elaborar está programado en ensamblador y hace uso de RGBDS para la creación de la ROM;
  2. esta publicación no cubre las bases del desarrollo para Game Boy en ensamblador, pero tampoco requiere un conocimiento avanzado sobre el tema. Puede funcionar como lección adicional a los numerosos tutoriales que existen en la red al respecto (recomendación personal en castellano, http://wiki.ladecadence.net/doku.php?id=tutorial_de_ensamblador);
  3. de acuerdo al segundo punto, se parte de un programa base en el que se trabaja la implementación de funciones adicionales relacionadas con el Super Game Boy.
En esta entrada se va a cubrir el uso de las siguientes características exclusivas del Super Game Boy:
  • despliegue de un marco personalizado;
  • paletas personalizadas para los gráficos del juego.
Dando como resultado del aprendizaje la siguiente ROM de Game Boy.

Visualización de la ROM en una Super Game Boy
 DESCARGA DE LA ROM 

Primero deberíamos hablar sobre qué es el Super Game Boy. Se trata, según el Game Boy Programming Manual (pág 124), de un dispositivo que permite disfrutar del software de Game Boy en una pantalla de televisión. Este software (contenido en el cartucho de Game Boy) puede conectarse al Super Game Boy, el cual opera en una Super Nintendo.

Sobre esta definición cabe resaltar la última parte, el Super Game Boy opera en una Super Nintendo. Este cartucho es una réplica del hardware de la Game Boy capaz de comunicarse con la SNES, nos ayuda a informar a la sobremesa de las operaciones que queremos realizar y a enviarle los datos necesarios, pero es en última instancia su hardware y no el nuestro (el del Super Game Boy) el que se encarga de procesar y aplicar la funcionalidades exclusivas que nos ofrece esta tecnología. Es decir, los gráficos del marco por ejemplo, obedecen las limitaciones de la SNES y no de la Game Boy.

Preparativos

Dicho esto, pasamos a la preparación del equipo donde vayamos a desarrollar. Esta sería una lista de elementos que deberíamos tener listos en nuestro PC con Windows:
  1. editor de texto/código para la edición del programa (recomendación personal, Visual Studio Code y la extensión Z80 Assembly);
  2. RGBDS correctamente configurado como variable de entorno;
  3. Game Boy Tile Designer y Game Boy Map Builder para la edición de gráficos y mapas respectivamente;
  4. a pesar de que el programa de esta publicación está testeado en un Super Game Boy real, se recomienda disponer de la tecnología necesaria para ejecutar la ROM resultante en el hardware final. En su defecto, emuladores como BGB o bsnes pueden servir para probar los desarrollos que se hagan con este dispositivo en mente, pero no garantizan una emulación perfecta.
Pues bien, toca ponerse manos a la obra. Como esta publicación trata sobre el desarrollo para Super Game Boy y no sobre el desarrollo para Game Boy vamos a partir de un proyecto base cuya ROM ofrece el siguiente resultado.

Una pantalla de título estática

Este es el código del fichero principal de este proyecto plantilla.


Durante el desarrollo va a ser importante tener cerca cierta documentación a mano. En mi opinión, estos serían los textos que condensan todo lo que se necesita saber cuando se trata de programar para Super Game Boy:
Cada vez que dé información sobre el Super Game Boy procuraré complementarla con la fuente de dicha información, indicando en qué parte de estos dos documentos se encuentra.

Elaboración de gráficos

Empezaremos diseñando libremente el marco de Super Game Boy que queremos aplicar. Eso sí, teniendo en cuenta las siguientes limitaciones:
  1. la resolución del marco debe cuadrar con la resolución de la SNES, es decir, 256 píxeles de ancho por 224 píxeles de alto;
  2. debemos dejar un espacio en blanco de 160 por 144 píxeles en la parte central, que es donde se mostrará la pantalla de la Game Boy;
  3. la SNES dispone de 8 paletas de 16 colores RGB de 15 bit (SGB Functions > Available SNES Palettes). Las cuatro primeras (0-3) se usan para colorear los propios gráficos de la Game Boy y las cuatro últimas (4-7) para el marco. En el apartado Paletas del marco explico con más detalle esta limitación;
  4. no podemos usar más de 256 tiles de 8 por 8 píxeles a la hora de conformar el marco (los tiles de la SNES son de 8x8 y disponemos de un byte para direccionarlos. Lo bueno es que estos tiles pueden voltearse horizontal y verticalmente.

Paletas del marco

Como hemos comentado tenemos ha nuestra disposición 4 paletas de 16 colores para el marco. Aunque no exactamente, según la documentación oficial tenemos de la 4-6 (Game Boy Programming Manual > pág 162), es decir, 3 paletas. De todas formas también podemos hacer uso de la 7. Eso sí, algunos emuladores no reconocerán esta última paleta correctamente.

Color RGB de 15 bit, ¿qué implica esto? Pues que tenemos 5 bits para indicar el nivel de rojo, 5 para el verde y 5 para el azul. Es decir, una precisión de 0-31 para definir la intensidad de cada uno de los colores. Si tenemos un color RGB de 24 bit (el formato predominante) que queremos usar en alguna de las paletas tendriamos que redondear el valor de 0 a 255 que tenemos por color al equivalente entre 0 y 31.

Hemos dicho 16 colores, hay que matizarlo: ¡el color 0 es compartido (SGB Functions > Color 0 Restriction)! Esto significa que si el color 0 de la última paleta que asignemos es rojo, el color 0 del resto de gráficos pasará a ser el rojo. Y tenemos 16 colores sólo en el caso de que queramos gastar la memoria necesaria para almacenar gráficos 4bpp (4 bits por pixel) en la Game Boy. Si con 2bpp nos basta tenemos entonces cuatro colores por paleta con el color 0 compartido, pero los gráficos nos ocuparán la mitad de memoria.

Hay muchos juegos que aceptan la limitación de cuatro colores por paleta en el marco mostrando el color 0 para ahorrar memoria. Como por ejemplo ocurre con el marco del Pokémon Azul.

Comparando los dos cuadrados rojos vemos que el marco muestra el color 0 compartido con el resto de paletas. Usando además cuatro colores por tile (2bpp)

Nuestro marco

Este es nuestro marco.

Marco de 256 por 224 píxeles

Estos nuestros tiles.

176 tiles (incluyendo el tile nulo) de los 256 disponibles

Y estas nuestras paletas.

Paletas 4-6 usadas para representar los colores de cada uno de los tiles 

Tres paletas de 16 colores (4bpp), el color 0 (primera columna) no lo usamos para no tener que compartirlo con las paletas que se van a usar en los gráficos de Game Boy. Por las características del desarrollo, esta es la distribución de colores que ha quedado en las tres paletas. Bien podría haberse usado una sola, ya que guardan entre ellas bastantes colores en común.

Y ahora, es momento de convertir estas especificaciones en datos que añadir a nuestro programa.

Archivo de tiles

Los datos gráficos de los tiles los crearemos con Game Boy Tile Designer. En nuestro marco alguna de las paletas usa más de 4 colores así que nuestros gráficos deberán ser 4bpp.

En el caso de que los gráficos sean 2bpp sencillamente dibujamos los tiles con el editor teniendo en cuenta que los colores que eligamos para el valor 0, 1, 2, y 3 se corresponderán con los colores 0, 1, 2 y 3 de la paleta que tengamos pensada para ese tile.

Izquierda: tile en Game Boy Tile Designer; centro: paleta especificada para el tile; derecha: visualización del tile en el Super Game Boy

Los gráficos 4bpp de la SNES también podemos crearlos de manera sencilla usando el mismo programa: dibujando dos tiles consecutivos de Game Boy por cada tile de Super. El primero para los bit planes 1 y 2, y el segundo para los número 3 y 4.

Podemos combinar dos píxeles de Game Boy (2 bits) para crear un pixel de SNES (4 bits)

Aunque usemos gráficos 4bpp también debemos tener un archivo .gbr que dedique un sólo tile de Game Boy por tile de SNES. Y lo usaremos al elaborar el mapa del marco con Game Boy Map Builder, de esta forma referenciaremos el número de tile como lo haría la SNES y no la Game Boy.

El resultado de la elaboración de estos tileset en Game Boy Tile Designer es el siguiente: el tileset que almacenará los gráficos a usar en el marco (DESCARGAR EL ARCHIVO) y el tileset cuya única función será contener un índice de los tiles para mapearlos posteriormente (DESCARGAR EL ARCHIVO). Dentro del proyecto, ubicaremos estos dos archivos en res/tiles/.

Y con la siguiente configuración, exportamos el tileset de 4bpp para obtener el archivo de datos necesario para el proyecto. Borramos la declaración de sección y las etiquetas generadas automáticamente y ubicamos el archivo resultante en dev/data/tiles/ como sgb_tileset.z80 (DESCARGAR EL ARCHIVO).

Generamos un archivo .z80 compatible con RGBDS

Archivo de mapa

Ahora, con Game Boy Map Builder, construiremos el marco. Debemos asociarlo al archivo .gbr del tileset con los índices correctos, el que utiliza un sólo tile de Game Boy por tile de Super. Recordemos además que las dimensiones del marco son 256 por 224 píxeles, lo que significa 32  por 28 tiles. Así que una vez abramos el programa nos dirigimos a "File > Map properties..." y nos encargamos de cuadrar dimensiones y tileset.

 El tileset que aquí usemos sólo sirve como referencia visual a la hora de mapear los tiles

Como comentaba anteriormente, los tiles de la SNES se pueden voltear tanto horizontal (X Flip) como verticalmente (Y Flip). Adicionalmente cada uno de los tiles va a estar asociado a una de las paletas que hayamos especificado. Por eso necesitamos definir atributos adicionales para cada tile que coloquemos en nuestro mapa. Podemos hacer esto en "File > Location properties...".

No definimos el volteo vertical (Y Flip) porque no es usado en nuestro marco

Como veremos a continuación el volteo se define con un único bit (1 volteado, 0 no volteado) y la paleta con 2 (rango de 0 a 3 que se correponde respectivamente con las paletas de la 4 a la 7), estos serán los 2 bit más bajos de los 3 que se asignan posteriormente a la paleta. Al tercer bit, el más alto, le daremos siempre el valor de 1. Esto nos da un rango de valores de 4 a 7 real al exportar.

Ahora mapeamos los tiles del marco, y definimos sus location properties a través de los campos disponibles en la fila inferior del editor.

Recordemos que debemos dejar un espacio de tiles nulos en la ubicación de la pantalla de la Game Boy

Como resultado, tenemos un archivo .gbm con el diseño del mapa que conformará el marco (DESCARGAR EL ARCHIVO), lo situamos en res/maps/. Como apunte, aclarar que lo primero que debemos hacer al abrir el mapa en Game Boy Map Builder es referenciar el tileset de los índices a través de "File > Map properties...".

Ahora vamos a generar el archivo de datos relativo al mapa a través de "File > Export to...". Vamos a asegurarnos de que cumplimos con la especificación sobre el formato del mapa (SGB Functions > SGB Command 14h - PCT_TRN). En las opciones de la exportación incluiremos las location properties que hemos definido anteriormente. Esta sería la especificación oficial respecto a nuestra configuración de exportación.

Especificación oficial del formato de los tiles del mapa del marco

En el caso de la propiedad Y Flip, la ignoramos ocupando su lugar con un 0

Una vez exportado, borramos la declaración de sección y las etiquetas del archivo resultante y lo ubicamos en dev/data/maps/ (DESCARGAR EL ARCHIVO). 

Ya tenemos todos los archivos gráficos listos, sólo faltan los datos de las paletas. Para ello vamos a crear ahora el archivo sgb.z80, que ubicaremos en dev/data/. Aquí vamos a ubicar las definiciones de los paquetes que enviaremos a la SNES y en este caso también la macro del RGB de 15 bits del que se servirán los colores de nuestras paletas.


Los tres parámetros de la macro se corresponden respectivamente con el valor de rojo, de verde y de azul que queramos darle a cada color. Pues bien, ahora toca especificar las paletas en base a esta macro.

Debemos colocar las paletas a continuación de los datos del mapa (SGB Functions > SGB Command 14h - PCT_TRN), así que crearemos un archivo en dev/data/maps que se llame sgborder.z80 (DESCARGAR EL ARCHIVO), allí cuadraremos nuestras paletas con la especificación.
 
El mapa debe ser de 32x32 tiles pero las últimas 4 filas no entran en la pantalla y son innecesarias

  
Recordamos la especificación de nuestras paletas

El primer color de cada paleta (color 0) está especificado pero no llega a usarse

Paletas del juego

Por último vamos a añadir a sgb.z80 la declaración de la paleta que usaremos para los gráficos del propio juego. Recordemos que mientras las paletas de la 4-7 se usan para el marco, las de la 0-3 se usan para los gráficos del juego. Los gráficos monocromos de la Game Boy tienen una limitación de 4 colores así que sólo podremos usar 4 colores en esta paleta.

Sólo usaremos una de las cuatro paletas asignables al juego de Game Boy

El color 0 que definamos debería ser el que queramos compartir con el resto de paletas

Pues ya tenemos especificados todos los archivos gráficos necesarios, llegó el momento de añadirlos al banco de memoria que tenemos declarado en main.asm.

 

Programación de la lógica


Definiciones auxiliares

Existen dos formas de enviar información a la Super (Game Boy Programming Manual > pág 127), a través de los registros P14 y P15 y a través la propia VRAM de la Game Boy (transferencia de VRAM). Ambas implican el uso de comandos. Así que empezaremos definiendo en nuestro archivo sgb.z80 las macros de los comandos que vamos a utilizar (tenemos una definición de lo que hace cada comando en Game Boy Programming Manual > pág 132 y SGB Functions > SGB Command Summary):
  1. MLT_REQ, hace una petición del modo multijugador, se usará para detectar si el cartucho se está ejecutando en un Super Game Boy;
  2. CHR_TRN, lo usaremos para copiar los tiles del marco a la RAM de la SNES (con una transferencia de VRAM);
  3. PCT_TRN, lo usaremos para copiar el mapa del marco a la RAM de la SNES (con una transferencia de VRAM);
  4. PAL_SET, asignará la paleta que tenemos definida para los gráficos del juego;
  5. PAL_TRN, copiará la paleta que tenemos definida para los gráficos del juego a la RAM de la SNES (con una transferencia de VRAM);
  6. MASK_EN, congelará la pantalla durante los procesos de comunicación con el Super Game Boy, y la descongelará al terminar;
  7. Por último definiremos ocho paquetes de datos que enviaremos para inicializar la comunicación tal y como se recomienda en la documentación oficial (Game Boy Programming Manual > pág 178).


Y con esto completamos definitivamente el archivo auxiliar sgb.z80 (DESCARGAR EL ARCHIVO).

Como requisito previo a la programación de la lógica debemos escribir en la dirección 0x0146 de nuestra ROM un 0x03, para indicar que soportamos la funcionalidad de Super Game Boy (Game Boy Programming Manual > pág 178). Ya tenemos parametrizada esta dirección en gbhw.inc, así que modificamos el segundo parámetro de la cabecera en main.asm.


Implementación de la comunicación

Vamos a explicar con un ejemplo práctico cómo hemos definido las macros de los paquetes en sgb.z80. Cojamos nuestra macro de de MLT_REQ y comparémosla con la especificación de memoria del comando (SGB Functions > SGB Command 11h - MLT_REQ).

Como vemos el Super Game Boy permite un modo multijugador de hasta 4 jugadores con el Super 5 multi-player adapter de SNES


Vemos como el byte 1 define el número de jugadores solicitados en el modo multijugador del Super Game Boy. Nosotros parametrizamos dicho byte en nuestra macro y especificamos con dos etiquetas las dos variantes que vamos a usar en nuestro programa.

Todos los comandos están conformados por 16 bytes (128 bits), que es el tamaño del paquete de datos que podemos enviar. Por lo que sólo enviamos un paquete por comando. El número de paquetes por transferencia en cualquier caso puede especificarse a través de los últimos tres bits del byte 0 del primer paquete (como vemos en la especificación, byte 0: Command*8, que pasa a ocupar los 5 bits de mayor prioridad, + Length, que ocupa los 3 bits restantes), por lo que podemos enviar de 1 a 7 paquetes por transferencia. Nosotros representaremos la posibilidad de enviar más de un paquete por comando en el código del programa, aunque no se llegue a hacer uso de dicha funcionalidad.

Ahora describiremos el proceso de envío de cada paquete tal y como se explica en la pág 128 del Game Boy Programming Manual. Envío que realizaremos a través de los bits 4 y 5 del registro P1 (0xFF00) de la Game Boy, estos bit se corresponden respectivamente con los puertos de salida P14 y P15:
  1. empezamos la comunicación poniendo a 0 P14 y P15;
  2. enviamos los 128 bits del paquete poniendo a 0 a P14 y a 1 a P15 si quieremos enviar un 0 y a 1 a P14 y a 0 a P15 si queremos enviar un 1;
  3. enviamos un 0 en el bit 129 para cerrar la comunicación (P14 a 0 y P15 a 1).
Hay que tener en cuenta la siguiente lista de reglas:
  1. los valores que escribimos en P14 y P15 deben mantenerse por 5µs, y entre escrituras debemos poner P14 y P15 a 1 durante al menos 15µs. Teniendo en cuenta que un ciclo de reloj de Game Boy ya dura más de 23µs, ignoramos los tiempos, pero anotamos que debemos escribir un 1 en ambos puertos por cada bit enviado;
  2. entre el envío de un paquete y el siguiente debemos esperar un mínimo de 60 msec (unos 4 frames de Game Boy). La rutina "sgbpackettransfer_wait" se encargará de cumplir con esta espera.
Con todo esto presente, esta es nuestra rutina de envío de paquetes, que añadimos a main.asm.


La primera funcionalidad que va a servirse de esta rutina de envío es la de reconocer si nos encontramos o no en un Super Game Boy. Lo haremos de la siguiente manera (que viene a ser la forma recomendada en SGB Functions > Detecting SGB hardware):
  1. leeremos el identificador del registro P1, que además de servir para enviar información a través de los puertos es el registro que nos devuelve la información de los controles de la Game Boy. Por defecto, su lectura nos devuelve el identificador de los controles;
  2. seleccionamos el modo modo dos jugadores de Super Game Boy enviando el paquete de MLT_REQ con dos jugadores como parámetro;
  3. volvemos a leer el identificador del registro P1. Si es el mismo, significa que no se ha seleccionado el modo de dos jugadores y que por lo tanto no estamos en un SGB.
Ubicamos pues el siguiente código en main.asm.


La siguiente rutina se llamará "init_sgb_default", y no hace otra cosa que enviar los paquetes de inicialización recomendados por la doumentación (Game Boy Programming Manual > pág 178). La añadimos también a main.asm.


Los comandos de transferencia CHR_TRN, PCT_TRN, y PAL_TRN usan la transferencia de VRAM para copiar sus datos a la RAM de la SNES. Estos son los pasos que debemos seguir para realizar este tipo de transferencia (tal y como se explica en SGB Functions > SGB VRAM Transfers):
  1. si no queremos que los datos a enviar se visualicen en la pantalla de la Game Boy debemos haber configurado previamente una máscara para el LCD de la Game Boy haciendo uso del comando MASK_EN;
  2. deshabilitamos interrupciones y apagamos el LCD, para evitar que los gráficos que queremos enviar se corrompan;
  3. asignamos los valores por defecto a la paleta de los gráficos del fondo de la Game Boy, que se traduce a escribir el valor 0xE4 en el registro de la paleta que se encuentra en 0xFF47 (0 blanco, 1 gris claro, 2 gris oscuro, 3 negro);
  4. copiamos los 4KB de datos que queremos enviar al fondo visible del LCD de la Game Boy, la dirección a la que se deben copiar comienza en 0x9800;
  5. enviamos el comando de transferencia, ya sea CHR_TRN, PCT_TRN o PAL_TRN. Los datos desplegados en el fondo visible de la Game Boy se enviarán a la SNES.
Antes de mostrar el código de la rutina, un pequeño paréntesis sobre MASK_EN. Habitualmente se usa al comienzo del programa para ocultar la visualización real del LCD de la Game Boy durante la transferencia de datos.

(SGB Functions > SGB Command 17h - MASK_EN)

Existen 4 posibles valores para el byte 1 del comando, nosotros usamos el valor 1 para congelar la imagen de la pantalla y el 0 para descongelarla cuando termina el proceso. Como vemos en las etiquetas que tenemos definidas en sgb.z80.


Y cerrado el paréntesis, está es la rutina de transferencia de datos. La ubicaremos en main.asm.


Como se puede ver, tenemos una rutina adicional que se llama "parsesgbbordertiles". Esta rutina nos permite ahorrar memoria en el caso de que los datos que queramos copiar sean tiles 2bpp, ya que se dedica a rellenar con ceros los bit planes 3 y 4 (que no usamos). En nuestro caso, al tratarse de tiles 4bpp, no hacemos uso de ella.

Y por último, sirviéndonos de las rutinas que hemos elaborado hasta el momento, esta sería la descripción de la rutina principal de inicialización del Super Game Boy:
  1. comprobamos si nos encontramos en un Super Game Boy, si no lo estamos, volvemos y la rutina termina;
  2. congelamos la visualización del LCD de la Game Boy durante todo el proceso;
  3. enviamos los paquetes de inicialización recomendados por la documentación;
  4. copiamos los 256 tiles que usaremos en el marco en la VRAM de la SNES. En dos tandas de 128;
  5. copiamos la información del marco en la VRAM de la SNES;
  6. copiamos la información de las paletas a usar en el propio juego en la RAM de la SNES;
  7. asignamos la paleta previamente copiada como paleta a usar en el juego;
  8. despejamos la VRAM de la Game Boy y descongelamos la visualización del LCD de la Game Boy para finalizar el proceso.
Añadimos este código al fichero principal del programa main.asm (DESCARGAR EL ARCHIVO).


Si ahora ejecutamos el archivo assemble.bat del proyecto tendremos como resultado la ROM con el marco y las paletas personalizadas.



Doy por terminada la explicación de este sencillo prototipo. Naturalmente, quedan por aplicar muchas funcionalidades del Super Game Boy. Pero espero que este artículo sirva como punto de partida a la gente que se ha animado a desarrollar para Game Boy pero que no ha encontrado ningún recurso mínimamente guiado sobre el desarrollo para Super Game Boy.

Todo apunte o crítica de cualquier tipo será bienvenida, y cualquier parte de la entrada es susceptible de ser modificada en caso de ser necesario.

Así que sólo me queda daros ánimos. Ahora os toca implementar ese marco de Super Game Boy que siempre habéis querido diseñar.

No hay comentarios:

Publicar un comentario