Esta pagina se ve mejor con JavaScript habilitado

GameBoy Dev08: FuckingAwesome Keyboard

 ·  🎃 kr0m

Es recomendable la lectura de los artículos anteriores para comprender mejor el actual:


En este artículo diseñaremos un teclado que le permitirá al usuario introducir su nombre, este puede resultar muy útil para guardar records o datos asociados a los usuarios.

El artículo es bastante extenso así que lo dividiré en varias partes:


Generación de los tiles

Primero debemos generar los tiles de las teclas del teclado y el tile que actuará como cursor de movimiento:

Para mayor comodidad dejo los ficheros disponibles aquí: keyboarddata.gbr cursordata.gbr


Generación de los maps

Ahora generamos el fondo en base a los tiles de los carácteres del teclado:

Para mayor comodidad dejo el fichero disponible aquí: keyboardmap.gbm


Generación de la estructura cursor

La estructura cursor contendrá:

  • Las coordenadas x,y del cursor en pixels, estas nos servirán para limitar el movimiento del cursor por la pantalla.
  • Las coordenadas fila,columna dentro del layout del teclado, mediante dichas coordenadas nos será muy fácil saber que carácter a escrito el usuario.
vi cursor.c
#include <gb/gb.h>

typedef struct Cursor
{
    UINT8 x;
    UINT8 y;
    UINT8 row;
    UINT8 col;
};

Explicación del código

El código es bastante extenso así que es mejor ir por partes.

Incluimos los ficheros que contienen los tiles del background y el mapeo a las posiciones para cada posición de la pantalla.

// Keyboard tiles data and tiles screen mapping
#include "keyboarddata.c"
#include "keyboardmap.c"

El tile que utilizará el sprite del cursor:

// Cursor tiles data
#include "cursordata.c"

La estructura cursor que contiene las coordenadas x,y del cursor y las coordenadas fila,columna del cursor.

// Code cursor C structure for cursor location info
#include "cursor.c"

Definimos el array que contendrá el mapeo de los tiles que componen el nombre del jugador, este será de un máximo de 18 chars ya que es la longitud máxima de la pantalla de la GameBoy.

// GameBoy screen of 18 tiles width so max name length is 18
// Only one line allowed for player name
unsigned char playernamemap[18];

Creamos una vriable de tipo cursor llamada Cursor.

struct Cursor cursor;

Definimos los límites del layout del teclado, debemos tener en cuenta que la pantalla de la GameBoy no empieza en las coordenadas 0,0 si no en las 8,16.
Además la pantalla tiene 20 tiles por fila es decir 20 carácteres, para poder tener todos los números en una sola fila necesitamos 21 tiles ya que la disposición ideal es:

espacio+0+espacio+1+espacio+2+espacio+3+espacio+4+espacio+5+espacio+6+espacio+7+espacio+8+espacio+9+espacio

El truco que emplearemos será disponer los números del siguiente modo:

0+espacio+1+espacio+2+espacio+3+espacio+4+espacio+5+espacio+6+espacio+7+espacio+8+espacio+9+espacio

Y mover 4 pixels, el equivalente a medio tile todo el fondo, de este modo quedará:

1/2espacio+0+espacio+1+espacio+2+espacio+3+espacio+4+espacio+5+espacio+6+espacio+7+espacio+8+espacio+9+1/2espacio

La manera mas sencilla de conocer los límites del teclado es mirarlo en el tile editor:

Con el tile editor hemos conseguido las posiciones de los tiles, recordemos que cada tile es de 8x8, por lo tanto para pasar las coordenadas a pixels debemos multiplicar por 8.

Podemos ver el cálculo en los comentarios del código, este multiplica x8 y tiene en cuenta el desplazamiento de la pantall mas el shifteo que hemos utilizado para que quepan 21 tiles por línea.

// Allowed cursor positions:
// All positions are moved 8,16 pixels
// And X is shifted 4 pixels
// Best way to get border positions is viewing it in tile designer

// xmin: 0*8+8+4 = 12
// ymin: (8*8)+16 = 80
// xmax: (18*8)+8+4 = 144+12 = 156
// ymax: (14*8)+16 = 112+16 = 128

// xdelete: (16*8)+8+4 = 128+12 = 140
// ydelete: (16*8)+16 = 128+16 = 144
// xdone: (18*8)+8+4 = 144+12 = 156
// ydone: (16*8)+16 = 128+16 = 144

const UINT8 mincursorx = 12;
const UINT8 mincursory = 80;
const UINT8 maxcursorx = 156;
const UINT8 maxcursory = 128;

const UINT8 xdelete = 140;
const UINT8 ydelete = 144;
const UINT8 xdone = 156;
const UINT8 ydone = 144;

Definimos algunas variables de control, como el carácter del nomber que estamos editando, si se ha presionado una tecla o si el usuario ya ha introducido su nombre.

UINT8 nameindex;
UBYTE keydown = 0, playerhasname = 0;

Ahora pasemos a las funciones, la primera de ellas es isWithinKeyboard, esta se encarga de responder true si las coordenadas pasadas como argumentos se encuentran dentro del layout del teclado, para ello utiliza las constantes definidas anteriormente:

UBYTE isWithinKeyboard(UINT8 x, UINT8 y){
    // Cursor is inside main keyboard rectangle layout
    if (x >= mincursorx && x <= maxcursorx && y >= mincursory && y <= maxcursory){
        return 1;
    }
    // Check special locations at bottom of keyboard, DELETE and ENTER keys
    if (x==xdelete && y==ydelete || x==xdone && y==ydone){
        return 1;
    }
    // Any other position
    return 0;
}

La función addtoplayername calcula el tile de la VRAM correspondiente a las coordenadas fila/columna, para ello tan solo debemos tener en cuenta que las filas son de de 10 carácteres siendo el primer carácter un espacio.
Como en cada fila hay 10 carácteres, estas se dividen en:

Cerocenas: A-J, Decenas: K-T, Veintenas: U-, Treintenas: 0-9

Para obtenet la posición del carácter debemos aplicaar la siguiente fórmula:

Row*10 + Col + 1

También comprueba que el nombre no exceda los 18 carácteres, añade la posición del tile asociado al carácter al array playernamemap e incrementa el índice nameindex.

void addtoplayername(struct Cursor* cursor){
    // Working with row/col we can get charsetindex of current char
    // Its simple, we calculate char position knowing that each row has 10 chars
    // So 0-Row: A-J, 10-Row: K-T, 20-Row: U-:, 30-Row: 0-9
    // To get char number: Row*10 + Col + 1 (we add +1 because first char in tiledata is whitespace)
    UINT8 charsetindex = cursor->row * 10 + cursor->col + 1;

    // Max name length reached
    if (nameindex == 18){
        return;
    }

    // Update playernamemap[current_position] with new charsetindex
    playernamemap[nameindex] = charsetindex;
    nameindex++;
}

La siguiente función elimina carácteres del array de tiles que componen el nombre del jugador, cada vez que se añade un carácter al nombre la variable nameindex que actúa como índice es incrementada, por lo tanto si queremos eliminar el último carácter debemos retroceder una posición antes de proceder.

void removefromplayername(){
    if (nameindex > 0){
        // We dont want to delete current name char position
        // We want to delete last char, so we go back one position before deleting char
        nameindex--;
        playernamemap[nameindex] = 0;
    }
}

La función drawplayername simplemente pinta en las coordenadas 1,4 los tiles indicados por las posiciones de playernamemap, sabiendo que las dimensiones del nombre son 18x1.

void drawplayername(){
    // Print in 1,4 position, playernamemap content knowing that it is 18x1 length
    set_bkg_tiles(1, 4, 18, 1, playernamemap);
}

Mediante la función updateplayername controlaremos si se ha presionado la tecla de borrado, la de done o cualquier otra tecla.

  • Delete: borra el último carácter del nombre y repinta el nombre en pantalla.
  • Done: Comprueba que el nombre no esté vacío, si no lo está define playerhasname=1 parra que el flujo del programa salga del bucle de inputplayername.
  • Cualquier otra tecla: Añade el carácter al nombre y repinta el nombre en pantalla.
void updateplayername(struct Cursor* cursor){
    // check if cursor at delete or done
    if (cursor->row == 4 && cursor->col==8){
        // delete
        removefromplayername();
        drawplayername();
    }
    else if (cursor->row == 4 && cursor->col==9){
        // player finished
        // If player name is empty, force to write name
        if (nameindex == 0){
            return;
        }
        playerhasname = 1;
    }
    else{
        addtoplayername(cursor);
        drawplayername();
    }
}

La función inputplayername es la que se encarga de la mayoría del trabajo, esta empieza pintando el fondo, hace el scroll de 4 pixels del fondo para que todo quede desplazado medio tile a la derecha, carga los tiles del sprite del cursors y carga la imagen en dicho sprite, asigna las coordenadas iniciales del cursor y mueve el sprite a dichas coordenadas, inicializa el valor de la fila,columna del carácter actual, muestra el fondo, muestra los sprites y enciende la pantalla.

Luego entra en un bucle del que no se sale hasta que playerhasname tenga algún valor, mientras no lo tenga estará pendiente del movimiento del cursor asegurándose siempre que se mantenga dentro del área del teclado, conforme se vaya desplazando el cursor se irán actualizando las coordenadas de este, si se presiona A se llama a la función updateplayername.

Además hay una variable de control keydown con la que controlaremos si se ha presionado una tecla, en el momento en que se levante el dedo la variable se reinicializará, esto es útil para que el desplazamiento por el teclado no sea errático.

void inputplayername(){
    // By default VRAM positions not defined by keyboardmap are filled with first VRAM tile
    // So we have to put blank char in first keyboarddata tile, if we dont do that we will get A char in 4pixel shift
    set_bkg_data(0, 45, keyboarddata);
    set_bkg_tiles(0, 0, 20, 18, keyboardmap);

    // To have all numbers(0-9) in one unique row we need 10*(char+space) tiles by keyboard row
    // 10*(char+space): 20 tiles, but first char doesnt have space before char
    // So we hack the background shifting it 4pixels: 1/2 tile to right
    // In that way we get 1/2 tile before first char and 1/2 tile after last one
    scroll_bkg(-4,0);

    // Load cursordata in sprite number 0 knowing that its length is one sprite 
    set_sprite_data(0, 1, cursordata);
    // Show cursordata[0] in sprite number 0
    set_sprite_tile(0, 0);

    // Set initial cursor position
    // Char A is located in: 00,08 tile position, in pixels -> 0,64
    // But remember that screen coordenates is moved 8,16 pixels so 0,64 -> 8,80
    // And we have shifted background 4 pixels to right so 8,80 -> 12,80
    cursor.x = 12;
    cursor.y = 80;
    move_sprite(0, cursor.x, cursor.y);

    // We keep track of current col and row inside keyboard layout(not screen)
    // That way we can know which letter is selected
    cursor.col = 0;
    cursor.row = 0;
 
    SHOW_BKG;
    SHOW_SPRITES;
    DISPLAY_ON;

    while(playerhasname == 0){
        // If key was pressed we wait to be released and reassign keydown to value 0
        // In that way movement is not erratic
        if (keydown){
            waitpadup();
            keydown = 0;
        }

        switch(joypad()){
            // We check in all movements if future cursor position will be inside keyboard area
            case J_UP:
                if (isWithinKeyboard(cursor.x, cursor.y - 16)){
                    cursor.y -= 16;
                    // Scroll sprite 0, o positions in X axis and -16 in Y axis
                    scroll_sprite(0,0,-16);
                    // Activate keydown variable
                    keydown = 1;
                    // Adjust keyboard matrix position to know current keyboard char
                    cursor.row--;
                }
                break;
            case J_DOWN: 
                if (isWithinKeyboard(cursor.x, cursor.y + 16)){            
                    cursor.y += 16;
                    scroll_sprite(0,0,16);
                    keydown = 1;
                    cursor.row++;
                }
                break;  
            case J_LEFT: 
                if (isWithinKeyboard(cursor.x - 16, cursor.y)){
                    cursor.x -= 16;
                    scroll_sprite(0,-16,0);
                    keydown = 1;
                    cursor.col--;
                }
                break; 
            case J_RIGHT: 
                if (isWithinKeyboard(cursor.x + 16, cursor.y)){            
                    cursor.x += 16;
                    scroll_sprite(0,16,0);
                    keydown = 1;
                    cursor.col++;
                }
                break;
            case J_A:
                // Each time we press A button we update player name
                updateplayername(&cursor);
                keydown = 1;                
                break;
        }
    }
}

Finalmente la función main, esta simplemente mantiene la ejecución en el bucle principal controlando en cada iteración si playerhasname es igual a 0, en tal caso llama a inputplayername, cuando se retorna de dicha función se vuelve al estado inicial del programa para volver a ejecutarlo.

void main(){
    while(1){
        if (playerhasname == 0){
            inputplayername();
        }
        
        // Revert to initial state
        scroll_bkg(4,0);
        playerhasname = 0;
        for (nameindex=0; nameindex<=18; nameindex++){
            playernamemap[nameindex] = 0;
        }
        nameindex = 0;

        delay(100);
    }
}

Código

vi 08.c
#include <gb/gb.h>

// Keyboard tiles data and tiles screen mapping
#include "keyboarddata.c"
#include "keyboardmap.c"

// Cursor tiles data
#include "cursordata.c"

// Code cursor C structure for cursor location info
#include "cursor.c"

// GameBoy screen of 18 tiles width so max name length is 18
// Only one line allowed for player name
unsigned char playernamemap[18];

struct Cursor cursor;

// Allowed cursor positions:
// All positions are moved 8,16 pixels
// And X is shifted 4 pixels
// Best way to get border positions is viewing it in tile designer

// xmin: 0*8+8+4 = 12
// ymin: (8*8)+16 = 80
// xmax: (18*8)+8+4 = 144+12 = 156
// ymax: (14*8)+16 = 112+16 = 128

// xdelete: (16*8)+8+4 = 128+12 = 140
// ydelete: (16*8)+16 = 128+16 = 144
// xdone: (18*8)+8+4 = 144+12 = 156
// ydone: (16*8)+16 = 128+16 = 144

const UINT8 mincursorx = 12;
const UINT8 mincursory = 80;
const UINT8 maxcursorx = 156;
const UINT8 maxcursory = 128;

const UINT8 xdelete = 140;
const UINT8 ydelete = 144;
const UINT8 xdone = 156;
const UINT8 ydone = 144;


UINT8 nameindex;
UBYTE keydown = 0, playerhasname = 0;

UBYTE isWithinKeyboard(UINT8 x, UINT8 y){
    // Cursor is inside main keyboard rectangle layout
    if (x >= mincursorx && x <= maxcursorx && y >= mincursory && y <= maxcursory){
        return 1;
    }
    // Check special locations at bottom of keyboard, DELETE and ENTER keys
    if (x==xdelete && y==ydelete || x==xdone && y==ydone){
        return 1;
    }
    // Any other position
    return 0;
}

void addtoplayername(struct Cursor* cursor){
    // Working with row/col we can get charsetindex of current char
    // Its simple, we calculate char position knowing that each row has 10 chars
    // So 0-Row: A-J, 10-Row: K-T, 20-Row: U-:, 30-Row: 0-9
    // To get char number: Row*10 + Col + 1 (we add +1 because first char in tiledata is whitespace)
    UINT8 charsetindex = cursor->row * 10 + cursor->col + 1;

    // Max name length reached
    if (nameindex == 18){
        return;
    }

    // Update playernamemap[current_position] with new charsetindex
    playernamemap[nameindex] = charsetindex;
    nameindex++;
}

void removefromplayername(){
    if (nameindex > 0){
        // We dont want to delete current name char position
        // We want to delete last char, so we go back one position before deleting char
        nameindex--;
        playernamemap[nameindex] = 0;
    }
}

void drawplayername(){
    // Print in 1,4 position, playernamemap content knowing that it is 18x1 length
    set_bkg_tiles(1, 4, 18, 1, playernamemap);
}

void updateplayername(struct Cursor* cursor){
    // check if cursor at delete or done
    if (cursor->row == 4 && cursor->col==8){
        // delete
        removefromplayername();
        drawplayername();
    }
    else if (cursor->row == 4 && cursor->col==9){
        // player finished
        // If player name is empty, force to write name
        if (nameindex == 0){
            return;
        }
        playerhasname = 1;
    }
    else{
        addtoplayername(cursor);
        drawplayername();
    }
}

void inputplayername(){
    // By default VRAM positions not defined by keyboardmap are filled with first VRAM tile
    // So we have to put blank char in first keyboarddata tile, if we dont do that we will get A char in 4pixel shift
    set_bkg_data(0, 45, keyboarddata);
    set_bkg_tiles(0, 0, 20, 18, keyboardmap);

    // To have all numbers(0-9) in one unique row we need 10*(char+space) tiles by keyboard row
    // 10*(char+space): 20 tiles, but first char doesnt have space before char
    // So we hack the background shifting it 4pixels: 1/2 tile to right
    // In that way we get 1/2 tile before first char and 1/2 tile after last one
    scroll_bkg(-4,0);

    // Load cursordata in sprite number 0 knowing that its length is one sprite 
    set_sprite_data(0, 1, cursordata);
    // Show cursordata[0] in sprite number 0
    set_sprite_tile(0, 0);

    // Set initial cursor position
    // Char A is located in: 00,08 tile position, in pixels -> 0,64
    // But remember that screen coordenates is moved 8,16 pixels so 0,64 -> 8,80
    // And we have shifted background 4 pixels to right so 8,80 -> 12,80
    cursor.x = 12;
    cursor.y = 80;
    move_sprite(0, cursor.x, cursor.y);

    // We keep track of current col and row inside keyboard layout(not screen)
    // That way we can know which letter is selected
    cursor.col = 0;
    cursor.row = 0;
 
    SHOW_BKG;
    SHOW_SPRITES;
    DISPLAY_ON;

    while(playerhasname == 0){
        // If key was pressed we wait to be released and reassign keydown to value 0
        // In that way movement is not erratic
        if (keydown){
            waitpadup();
            keydown = 0;
        }

        switch(joypad()){
            // We check in all movements if future cursor position will be inside keyboard area
            case J_UP:
                if (isWithinKeyboard(cursor.x, cursor.y - 16)){
                    cursor.y -= 16;
                    // Scroll sprite 0, o positions in X axis and -16 in Y axis
                    scroll_sprite(0,0,-16);
                    // Activate keydown variable
                    keydown = 1;
                    // Adjust keyboard matrix position to know current keyboard char
                    cursor.row--;
                }
                break;
            case J_DOWN: 
                if (isWithinKeyboard(cursor.x, cursor.y + 16)){            
                    cursor.y += 16;
                    scroll_sprite(0,0,16);
                    keydown = 1;
                    cursor.row++;
                }
                break;  
            case J_LEFT: 
                if (isWithinKeyboard(cursor.x - 16, cursor.y)){
                    cursor.x -= 16;
                    scroll_sprite(0,-16,0);
                    keydown = 1;
                    cursor.col--;
                }
                break; 
            case J_RIGHT: 
                if (isWithinKeyboard(cursor.x + 16, cursor.y)){            
                    cursor.x += 16;
                    scroll_sprite(0,16,0);
                    keydown = 1;
                    cursor.col++;
                }
                break;
            case J_A:
                // Each time we press A button we update player name
                updateplayername(&cursor);
                keydown = 1;                
                break;
        }
    }
}

void main(){
    while(1){
        if (playerhasname == 0){
            inputplayername();
        }
        
        // Revert to initial state
        scroll_bkg(4,0);
        playerhasname = 0;
        for (nameindex=0; nameindex<=18; nameindex++){
            playernamemap[nameindex] = 0;
        }
        nameindex = 0;

        delay(100);
    }
}

Compilamos el programa y lo cargamos en el emulador:

~/GBDEV/gbdk/bin/lcc 08.c -o 08.gb

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