This page looks best with JavaScript enabled

GameBoy Dev08: FuckingAwesome Keyboard

 ·  🎃 kr0m

It is recommended to read the previous articles to better understand the current one:


In this article, we will design a keyboard that will allow the user to enter their name, which can be very useful for saving records or data associated with users.

The article is quite extensive, so I will divide it into several parts:


Generation of tiles

First, we must generate the tiles of the keyboard keys and the tile that will act as a movement cursor:

For greater convenience, I leave the files available here: keyboarddata.gbr cursordata.gbr


Generation of maps

Now we generate the background based on the tiles of the keyboard characters:

For greater convenience, I leave the file available here: keyboardmap.gbm


Generation of the cursor structure

The cursor structure will contain:

  • The x,y coordinates of the cursor in pixels, which will serve us to limit the movement of the cursor on the screen.
  • The row,column coordinates within the keyboard layout, through these coordinates, it will be very easy for us to know which character the user has written.
vi cursor.c
#include <gb/gb.h>

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

Code Explanation

The code is quite extensive, so it’s better to break it down.

We include the files that contain the background tiles and the mapping to the positions for each screen position.

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

The tile that the cursor sprite will use:

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

The cursor structure that contains the x,y coordinates of the cursor and the row,column coordinates of the cursor.

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

We define the array that will contain the mapping of the tiles that make up the player’s name. This will be a maximum of 18 chars since it is the maximum length of the GameBoy screen.

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

We create a cursor variable of type Cursor.

struct Cursor cursor;

We define the limits of the keyboard layout, taking into account that the GameBoy screen does not start at coordinates 0,0 but at 8,16.
In addition, the screen has 20 tiles per row, which is 20 characters. To have all the numbers in a single row, we need 21 tiles since the ideal arrangement is:

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

The trick we will use is to arrange the numbers as follows:

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

And move 4 pixels, the equivalent of half a tile, across the entire background. This way it will be:

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

The easiest way to know the limits of the keyboard is to look at it in the tile editor:

With the tile editor we have obtained the positions of the tiles, remember that each tile is 8x8, therefore to convert the coordinates to pixels we must multiply by 8.

We can see the calculation in the code comments, which multiplies by x8 and takes into account the screen displacement plus the shifting we have used to fit 21 tiles per line.

// 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;

We define some control variables, such as the character of the name we are editing, whether a key has been pressed or if the user has already entered their name.

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

Now let’s move on to the functions, the first one is isWithinKeyboard, which returns true if the coordinates passed as arguments are within the keyboard layout, for this it uses the constants defined above:

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;
}

The function addtoplayername calculates the VRAM tile corresponding to the row/column coordinates, for this we only need to take into account that the rows are 10 characters long with the first character being a space.
As there are 10 characters in each row, they are divided into:

Zeroes: A-J, Tens: K-T, Twenties: U-, Thirties: 0-9

To obtain the character position we must apply the following formula:

Row*10 + Col + 1

It also checks that the name does not exceed 18 characters, adds the position of the tile associated with the character to the playernamemap array and increments the nameindex index.

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++;
}

The next function removes characters from the array of tiles that make up the player’s name, each time a character is added to the name the nameindex variable that acts as an index is incremented, therefore if we want to remove the last character we must go back one position before proceeding.

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;
    }
}

The drawplayername function simply paints the tiles indicated by the positions of playernamemap at coordinates 1,4, knowing that the dimensions of the name are 18x1.

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

Through the updateplayername function, we will control whether the delete key, the done key, or any other key has been pressed.

  • Delete: deletes the last character of the name and repaints the name on the screen.
  • Done: checks that the name is not empty, if it is not, sets playerhasname=1 so that the program flow exits the inputplayername loop.
  • Any other key: adds the character to the name and repaints the name on the screen.
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();
    }
}

The inputplayername function is responsible for most of the work. It starts by painting the background, scrolls the background 4 pixels so that everything is shifted half a tile to the right, loads the cursor sprite tiles, and loads the image into the sprite. It assigns the initial coordinates of the cursor and moves the sprite to those coordinates, initializes the value of the current character’s row and column, shows the background, shows the sprites, and turns on the screen.

Then it enters a loop that does not exit until playerhasname has some value. While it does not have a value, it will be pending the movement of the cursor, always making sure that it stays within the keyboard area. As the cursor moves, its coordinates will be updated. If A is pressed, the updateplayername function is called.

In addition, there is a control variable keydown that we will use to control whether a key has been pressed. At the moment the finger is lifted, the variable will be reset. This is useful so that the keyboard scrolling is not erratic.

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;
        }
    }
}

Finally, the main function simply keeps the execution in the main loop, controlling in each iteration if playerhasname is equal to 0. In such a case, it calls inputplayername. When returning from that function, it returns to the initial state of the program to execute it again.

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);
    }
}

Code

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);
    }
}

We compile the program and load it into the emulator:

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

If you liked the article, you can treat me to a RedBull here