Es recomendable la lectura de los artÃculos anteriores para comprender mejor el actual:
En este artÃculo aprenderemos a utilizar la Window de GameBoy, los problemas que pueden surgir y como solventarlos de forma eficiente. Se trata de un artÃculo bastante extenso y tiene contenido técnico avanzado, es por ello por lo que se divide en tres partes:
Introduccion
Como ya comentamos en el artÃculo anterior, la pantalla de la GameBoy está compuesta de 3 capas:
- Background: Fondo de los niveles del juego.
- Sprites: Elementos móviles como personajes o enemigos.
- Window: Parte donde se muestra información como la puntuación, vidas y demás elementos informativos.
En este artÃculo nos centraremos en la capa Window que utilizaremos a modo de HUD, esta está bastante limitada, para empezar no soporta transparencia, su forma es fija y rectangular y solo podemos controlar la posición del pixel izquierdo superior de esta.
PodrÃamos simular el window mediante sprites pero disminuirÃa el número de sprites disponibles para el resto de la pantalla ya que estamos utilizando algunos para la HUD.
Primera aproximación
El primer paso será cargar las fuentes que trae GBDK, de este modo seremos capaces de escribir texto en el Window. GBDK nos proporciona 5 fuentes distintas , tendremos que elegir una de ellas.
#include <gbdk/font.h> --> Incluimos el fichero de fuente
font_init(); --> Inicializamos las fuentes
font_t min_font; --> Creamos una variable de tipo font_t
min_font = font_load(font_min); --> Cargamos la fuente a emplear
font_set(min_font); --> Seteamos la fuente a emplear
set_win_tiles(1,0,4,1,WindowMap); --> Ponemos el Window en las coordenadas (1,0), el texto tiene una lognitud de 4x1(kr0m) y debe leerse el mapa de tiles del array WindowMap
move_win(40,40); --> Posición en pixels donde debe situarse la esquina superior izquierda de la Window
Primero cargamos las fuentes sin utilizarlas para nada, pero anotamos las posiciones de los tiles correspondientes a las letras que vamos a utilizar:
#include <gb/gb.h>
#include <gbdk/font.h>
void main(){
font_init();
font_t min_font;
min_font = font_load(font_min);
font_set(min_font);
while(1){
wait_vbl_done();
}
}
Compilamos el código y lo cargamos en bgb, podemos ver en el VRAM viewer el contenido de las fuentes:
Si nos posicionamos sobre alguna letra podremos ver la posición en VRAM donde se encuentra, por ejemplo:
k: 0x15
r: 0x1C
0: 0x01
m: 0x17
Sabiendo las posiciones vamos a generar manualmente un WindowMap indicando dichos tiles:
/*
WINDOW.C
Hand crafted window map, tile position looked in bgb VRAM viewer
*/
unsigned char WindowMap[] =
{
0x15,0x1C,0x01,0x17
};
/* End of WINDOW.C */
Ahora ya podemos utilizar las fuentes:
#include <gb/gb.h>
#include <gbdk/font.h>
#include "WindowMap.c"
void main(){
font_init();
font_t min_font;
min_font = font_load(font_min);
font_set(min_font);
set_win_tiles(1,0,4,1,WindowMap);
move_win(40,40);
SHOW_BKG;
SHOW_WIN;
DISPLAY_ON;
while(1){
wait_vbl_done();
}
}
Compilamos el código y lo cargamos en bgb:
Si decidimos añadir un fondo como hicimos en el artÃculo anterior, veremos que ocurre algo extraño:
#include <gb/gb.h>
#include <gbdk/font.h>
#include "BackgroundTiles.c"
#include "BackgroundMap.c"
#include "WindowMap.c"
void main(){
font_init();
font_t min_font;
min_font = font_load(font_min);
font_set(min_font);
set_bkg_data(0,3,BackgroundTiles);
set_bkg_tiles(0,0,32,18,BackgroundMap);
set_win_tiles(0,0,5,1,WindowMap);
move_win(40,40);
SHOW_BKG;
SHOW_WIN;
DISPLAY_ON;
while(1){
scroll_bkg(1,0);
wait_vbl_done();
}
}
Que está pasando?, porqué se comporta de este modo?, os estaréis preguntando, primero no aparece el fondo porque el Window está solapando el Background y segundo en el Window ya no aparecen los carácteres correctos, porque set_bkg_data carga BackgroundTiles en las primeras posiciones de la VRAM, machacando lo que habÃa cargado previamente font_load.
Primero abordaremos el problema de solapamiento de tiles ya que es mas sencillo de resolver de los dos, para solventarlo cargaremos los tiles del fondo a partir de la posición 0x25 -> 37 que es donde acaban los tiles de la fuente:
Y tendremos que regenerar el BackgroundMap teniendo en cuenta ese “tile offset”.
#include <gb/gb.h>
#include <gbdk/font.h>
#include "BackgroundTiles.c"
#include "BackgroundMap.c"
#include "WindowMap.c"
void main(){
font_init();
font_t min_font;
min_font = font_load(font_min);
font_set(min_font);
set_bkg_data(37,3,BackgroundTiles); // Hemos retocado el offset: 37
set_bkg_tiles(0,0,32,18,BackgroundMap);
set_win_tiles(0,0,5,1,WindowMap);
move_win(40,40);
SHOW_BKG;
SHOW_WIN;
DISPLAY_ON;
while(1){
scroll_bkg(1,0);
wait_vbl_done();
}
}
Regeneramos el BackgroundMap, tan solo debemos exportarlo como siempre pero indicando un offset:
Compilamos el código y lo cargamos en bgb:
Vemos que ya no se solapan tiles aunque el Window sigue interfiriendo con el Background:
NOTA: Las fuentes no pueden cargarse en otras posiciones de la VRAM por lo tanto siempre debemos adaptar el resto de elementos.
Interrupciones VBLANK/LCD STATUS
Para solventar el problema de interferencia entre las capas Window y Background tendremos que profundizar un poco mas en el funcionamiento interno de la GameBoy, mas especÃficamente el sistema de interrupciones y la PPU(Pixel Processor Unit).
Algunos puntos importantes son:
- El VBLANK es el tiempo que tarda la PPU de pasar del último pixel de la pantalla LCD al primero para empezar a mostar la siguiente imagen, cada vez que se pasa del último pixel al primero se genera una interrupción en la GameBoy, controlando esta interrupción podemos saber si nos encontramos dibujando la primera lÃnea de la pantalla.
- En la GameBoy existe un registro llamado LY que indica la lÃnea de la pantalla que se está dibujando en ese momento.
- La GameBoy permite configurar cuando se producirá una interrupción LCD-STAT mediante un registro llamado STAT.
La idea es habilitar el Window mientras dibujamos las 8 primeras filas del LCD que equivalen a la primera fila de tiles de la pantalla y que conforman nuestro Window, detectar que se ha dibujado el Window y deshabilitarlo, dibujar el resto de pantalla y cuando llegue al final, detectarlo para habilitar de nuevo el Window y volver a repetir el proceso. De este modo podremos mostrar el Window y el Background sin que interfieran entre ellos.
NOTA: Deshabilitar el Window no es un problema porque una vez la PPU ha pintado unos pixels determinados estos ya se quedan en ese estado hasta la próxima pasada.
Para detectar que hemos pintado las 8 primeras lÃneas de pixels utilizaremos el registro LY que contiene el valor de la lÃnea que se está pintando en ese momento y la interrupción LCD-STAT cuyo comportamiento se configura mediante el registro STAT:
Bit 6 - LYC=LY STAT Interrupt source (1=Enable) (Read/Write)
Bit 5 - Mode 2 OAM STAT Interrupt source (1=Enable) (Read/Write)
Bit 4 - Mode 1 VBlank STAT Interrupt source (1=Enable) (Read/Write)
Bit 3 - Mode 0 HBlank STAT Interrupt source (1=Enable) (Read/Write)
Bit 2 - LYC=LY Flag (0=Different, 1=Equal) (Read Only)
Bit 1-0 - Mode Flag (Mode 0-3, see below) (Read Only)
0: HBlank
1: VBlank
2: Searching OAM
3: Transferring Data to LCD Controller
Configuraremos el registro a 01000000 -> 0x40 ya que tan solo deseamos habilitar el bit número 6, de este modo cuando el registro LYC == LY se producirá la interrupción LCD-STAT.
STAT_REG = 0x40;
NOTA: En realidad el registro solo nos deja configurar los bits 4,5,6 ya que el resto son de solo lectura.
En nuestro caso queremos que la interrupción salte justo cuando haya terminado de pintar la octava lÃnea que es cuando habrá terminado de pintar la fila de tiles del Window, asignamos el valor de 0x08 al registro LYC.
LYC_REG = 0x08;
Hasta ahora tenemos dos interrupciones, una que salta cuando se llega a la octava lÃnea de la pantalla y otra cuando se llega a la última y se vuelve al principio, debemos añadir nuestro propio código a las rutinas de servicio existentes, GBDK nos permite hacer esto mediante:
add_LCD(FUNCTION_NAME); -> Añadimos nuestra función a la rutina de servicio asociada al LCD-STAT
add_VBL(FUNCTION_NAME); -> Añadimos nuestra función a la rutina de servicio asociada al VBLANK
Algunas consideraciones a tener en cuenta cuando trabajamos con interrupciones son:
- Para añadir código a las ISRs primero debemos deshabilitar las interrupciones.
- Las ISRs propias deben ser lo mas pequeñas posibles para que su ejecución sea rápida y la CPU tenga ciclos de reloj para atender el código principal.
Creo que la explicación ha quedado bastante clara, pero dejo algunos enlaces de recomendada lectura a continuación:
https://gbdk-2020.github.io/gbdk-2020/docs/api/docs_using_gbdk.html
https://gbdev.io/pandocs/Interrupt_Sources.html
https://gbdev.io/pandocs/STAT.html#ff41—stat-lcd-status-rw
https://es.wikipedia.org/wiki/VBLANK
El código final quedarÃa del siguiente modo.
#include <gb/gb.h>
#include <gbdk/font.h>
#include "BackgroundTiles.c"
#include "BackgroundMap.c"
#include "WindowMap.c"
// Interrupt triggered when PPU reaches line 8 -> hide window
void interruptLCD()
{
// When window is hidden it doesnt interfere with the other PPU operations
HIDE_WIN;
}
// VBlank interrupt generated when PPU reaches last pixel, so enable window again
void interruptVBL() {
SHOW_WIN;
}
void main(){
font_init();
font_t min_font;
min_font = font_load(font_min);
font_set(min_font);
set_bkg_data(37, 3, BackgroundTiles);
set_bkg_tiles(0, 0, 32, 18, BackgroundMap);
// coordinates x,y
// Tiles dimension 4 col X 1 row : kr0m
// Tile mapping: WindowMap
set_win_tiles(1,0,4,1,WindowMap);
// 0,0 position is conflictive, we move 1 pixel to right
move_win(1,0);
// Enable LCD interrupt when LC = LYC
// LC: PPU line counter
// LYC: CPU register that we control to triger LCD-STAT interrupt
// 01000000 -Hex->0x40 we have enabled bit 6 of LCD STATUS REGISTER, so LCD interrupt will be trigerred when LYC == LY
STAT_REG = 0x40;
// LCD interrupt will be triggered when PPU reaches 8 LCD line
LYC_REG = 0x07;
// Needed to be able to manipulate ISR routines
disable_interrupts();
// Add extra function to LCD ISR, in that way when LC == LYC, LCD interrupt will be triggered and interruptLCD code executed
add_LCD(interruptLCD);
// Add extra function to VBL ISR, in that way when PPU reaches last LCD pixel, interrupt will be triggered and interruptVBL code executed
add_VBL(interruptVBL);
// Reenable interrupts
enable_interrupts();
// Define which interrupts has to be looking for: VBL and LCD
set_interrupts(VBL_IFLAG | LCD_IFLAG);
// Enable all screen layers
SHOW_BKG;
SHOW_WIN;
DISPLAY_ON;
while(1){
scroll_bkg(1,0);
wait_vbl_done();
}
}
Compilamos el código y lo cargamos en bgb: