Esta pagina se ve mejor con JavaScript habilitado

GameBoy Dev11: FuckingAwesome Keyboard + Save

 ·  🎃 kr0m

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


En el artículo anterior aprendimos a utilizar los bancos de memoria ROM/RAM del cartucho, en este caso utilizaremos dichos conocimientos para guardar el nombre de nuestro usuario, básicamente es el teclado del artículo GameBoy Dev08 pero con esta funcionalidad añadida.

Las modificaciones realizadas consisten en declarar como extern la variable que contendrá el nombre del usuario:

extern unsigned char playernamemap[18];

Crear una función que pondrá un fondo determinado en caso de detectar un nombre de usuario guardado en la RAM del cartucho:

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

Función auxiliar de reseteo a 0x00 de los carácteres del nombre de usuario, esto es necesario porque las variables de la RAM del cartucho por defecto tienen el valor 0xFF, cuando editamos el nombre este contendrá los carácteres asignados mas el resto de valores por defecto 0xFF hasta llegar a 18 chars.

Por ejemplo si estamos escribiendo KR0M y vamos por la segunda letra playernamemap tendrá el siguiente valor:

KR"0xFF"*16

El resultado en pantalla es el siguiente:

Para evitar esto llamaremos a la función resetcharactername antes de empezar a editar el nombre de usuario:

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

De este modo la edición anterior quedaría del siguiente modo:

KR"0x00"*16

Esta solución implica un problema y es que si arrancamos nuestro juego por primera vez cuando llegamos al teclado de edición playernamemap contendrá el valor 0x00 en todos sus carácteres y si apagamos la consola sin introducir ningún nombre playernamemap queda con el valor 0x00 en todos sus carácteres. Cuando volvamos a arrancar la consola se leerán estos valores y se mostrará un nombre ilegal.

Por lo tanto debemos hacer un doble check para saber si el nombre de usuario fué guardado con anterioridad donde comprobamos si la variable tiene el valor por defecto 0xFF o si tiene el valor reseteado 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;
    }
}

El contenido del banco de memoria simplemente es la definición de la variable playernamemap:

vi bank0.c

#include <gb/gb.h>

unsigned char playernamemap[18];

Dejo el fichero welcomemap.c por si queréis hacer pruebas sin tener que generarlo por vosotros mismos.


El código principal sería el siguiente:

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

El fichero de Make sería el siguiente:

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

Y el resultado final este:

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