Esta pagina se ve mejor con JavaScript habilitado

GameBoy Dev04: Window

 ·  🎃 kr0m

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:

vi 04a.c

#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:

~/GBDEV/gbdk/bin/lcc 04a.c -o 04a.gb

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:

vi WindowMap.c

/*

 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:

vi 04b.c

#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:

~/GBDEV/gbdk/bin/lcc 04a.c -o 04a.gb

Si decidimos añadir un fondo como hicimos en el artículo anterior, veremos que ocurre algo extraño:

vi 04c.c

#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”.

vi 04d.c

#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:

~/GBDEV/gbdk/bin/lcc 04a.c -o 04a.gb

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.

vi 04e.c

#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:

~/GBDEV/gbdk/bin/lcc 04e.c -o 04e.gb

Si te ha gustado el artículo puedes invitarme a un RedBull aquí