This page looks best with JavaScript enabled

GameBoy Dev11: FuckingAwesome Keyboard + Save

 ·  🎃 kr0m

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


In the previous article we learned how to use the ROM/RAM memory banks of the cartridge. In this case, we will use this knowledge to save the user’s name. Basically, it is the keyboard from the GameBoy Dev08 article but with this added functionality.

The modifications made consist of declaring the variable that will contain the user’s name as external:

extern unsigned char playernamemap[18];

Create a function that will set a specific background in case a saved username is detected in the cartridge’s RAM:

void initialize_welcome_display(){
    DISPLAY_OFF;
    HIDE_SPRITES;
    HIDE_BKG;

    set_bkg_data(0, 45, keyboarddata);
    set_bkg_tiles(0, 0, 20, 18, welcomemap);

    SHOW_BKG;
    DISPLAY_ON;
}

Auxiliary function to reset the user’s name characters to 0x00. This is necessary because the default values of the cartridge’s RAM variables are 0xFF. When we edit the name, it will contain the assigned characters plus the rest of the default 0xFF values until it reaches 18 chars.

For example, if we are writing KR0M and we are on the second letter, playernamemap will have the following value:

KR"0xFF"*16

The result on the screen is as follows:

To avoid this, we will call the resetcharactername function before starting to edit the user’s name:

void resetcharactername(){
    for(i=0;i<=18;i++){
        playernamemap[i] = 0x00;
    }
}

In this way, the previous edition would be as follows:

KR"0x00"*16

This solution involves a problem, which is that if we start our game for the first time and we reach the player name editing keyboard, playernamemap will contain the value 0x00 in all its characters. If we turn off the console without entering any name, playernamemap will remain with the value 0x00 in all its characters. When we start the console again, these values will be read and an illegal name will be displayed.

Therefore, we must perform a double check to know if the username was saved previously, where we check if the variable has the default value 0xFF or if it has the reset value 0x00:

for(i=0;i<=18;i++){
    // Uninitialized value: 0xFF OR Resetted value: 0x00
    if (playernamemap[i] == 0xFF || playernamemap[i] == 0x00){
        empty_playernamemap = 1;
    }
    else{
        empty_playernamemap = 0;
        break;
    }
}

The content of the memory bank is simply the definition of the playernamemap variable:

vi bank0.c

#include <gb/gb.h>

unsigned char playernamemap[18];

I leave the file welcomemap.c in case you want to test it without having to generate it yourself, the rest of the files are the same as in the article about the keyboard.


The main code would be the following:

vi 11.c

#include <gb/gb.h>

// Keyboard tiles data and tiles screen mapping
#include "keyboarddata.c"
#include "keyboardmap.c"
#include "welcomemap.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];
//extern UINT8 playernamemap[18];
extern 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, i, empty_playernamemap;
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){
        // done
        // If player name is empty, force to write name
        if (nameindex == 0){
            return;
        }
        playerhasname = 1;
    }
    else{
        addtoplayername(cursor);
        drawplayername();
    }
}

void inputplayername(){
    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 initialize_input_display(){
    // 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;
}

void initialize_welcome_display(){
    DISPLAY_OFF;
    HIDE_SPRITES;
    HIDE_BKG;

    set_bkg_data(0, 45, keyboarddata);
    set_bkg_tiles(0, 0, 20, 18, welcomemap);

    SHOW_BKG;
    DISPLAY_ON;
}

void resetcharactername(){
    for(i=0;i<=18;i++){
        playernamemap[i] = 0x00;
    }
}

void main(){
    while(1){
        ENABLE_RAM;
        SWITCH_RAM(0);
        
        // Uninitialized RAM default value(Emulicious debugger SRAM viewer): 0xFF
        // A weird behaviour showing empty username can be possible if we power on GameBoy for first time
        // and power off without inputting an username, in that way our username would be (0x00*18)
        // as result of resetcharactername function.
        // We can check both values simultaneously because none of them is a keyboard allowed char
        // so we dont have to check (0xFF)*18 nor (0x00)*18 username content, only one match with
        // one of the two chars will be considered empty playername.
        for(i=0;i<=18;i++){
            // Uninitialized value: 0xFF OR Resetted value: 0x00
            if (playernamemap[i] == 0xFF || playernamemap[i] == 0x00){
                empty_playernamemap = 1;
            }
            else{
                empty_playernamemap = 0;
                break;
            }
        }

        if(empty_playernamemap == 1){
            // Uninitialized RAM is 0xFF, so while we are editing our name
            // the program will show our username as (our chars + 0xFF*X)
            // where X is the length to fill 18 chars username length
            // So if its a new input, we first reset RAM playernamemap variable to (0x00*18)
            resetcharactername();
            initialize_input_display();
            inputplayername();
        }
        else{
            initialize_welcome_display();
            drawplayername();
        }

        DISABLE_RAM;
        waitpad(J_START);
    }   
}

The Makefile would be the following:

vi make.sh

#!/usr/local/bin/bash

~/GBDEV/gbdk/bin/lcc -Wf--vc -Wf--debug -Wf--nooverlay -Wf--nogcse -Wf--nolabelopt -Wf--noinvariant -Wf--noinduction -Wf--noloopreverse -Wf--no-peep -Wf--no-reg-params -Wf--no-peep-return -Wf--nolospre -Wf--nostdlibcall -Wa-l -Wa-y -Wl-m -Wl-w -Wl-y -Wf-ba0 -c -o bank0.o bank0.c
~/GBDEV/gbdk/bin/lcc -Wf--vc -Wf--debug -Wf--nooverlay -Wf--nogcse -Wf--nolabelopt -Wf--noinvariant -Wf--noinduction -Wf--noloopreverse -Wf--no-peep -Wf--no-reg-params -Wf--no-peep-return -Wf--nolospre -Wf--nostdlibcall -Wa-l -Wa-y -Wl-m -Wl-w -Wl-y -c -o 11.o 11.c
~/GBDEV/gbdk/bin/lcc -Wf--vc -Wf--debug -Wf--nooverlay -Wf--nogcse -Wf--nolabelopt -Wf--noinvariant -Wf--noinduction -Wf--noloopreverse -Wf--no-peep -Wf--no-reg-params -Wf--no-peep-return -Wf--nolospre -Wf--nostdlibcall -Wa-l -Wa-y -Wl-m -Wl-w -Wl-y -Wl-yt0x1B -Wl-ya1 -o 11.gb 11.o bank0.o

And the final result is this:

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