Sega Genesis Programming Part 25: Onscreen Keyboard

 

This is not a tutorial

This might be difficult to follow because it builds on many previous articles and skips a lot of details covered in them. Over 24 articles and 3 game demos I've built a decent amount of reusable code. This is going to lean heavily on all of it.

Today, well over several days, I'm going to see if I can write an onscreen keyboard for the Sega Genesis.

A physical keyboard is possible. See segaretro.org. I can already imagine how to program for it. It takes a couple cycles to read all the buttons on a 6 button controller. A keyboard would work the same way. The number of cycles could be reduced if it used both controller ports.

Whatever. I'm trying to make an onscreen keyboard. Why would I want to do that?

The last bullet is the real answer. I have no useful plans for this.

Like all my ideas, this started all grandiose. Here's the first keyboard layout I thought of:

Full keyboard layout

Then I decided that wasn't grandiose enough and thought about doing a full keyboard. You know, in case I wanted to write a word processor:

Full keyboard with numpad layout

Time to scale this idea back to something I can manage. This is what I'm going to actually try:

Basic keyboard

This layout can then grow to something like this:

Basic keyboard with commands

So in a text adventure, frequently used commands could be a single button press instead of slowly typing on a virtual keyboard.

Or all that extra space could hold command icons:

Basic keyboard with icons

I'm a really terrible artist so first I need to spend a couple bucks on a tileset. I went with the Keyboard UI asset pack by ElvGames. It is on sale for 89 cents at the exact moment I'm writing this sentence. If you choose to extend this demo then please buy a license from the original content creator too. Everything I post here is free but I am 100% behind paying artists who sell their work.

Let's look at one of the keys (obviously scaled to a larger size):

Example W key

It's a 16x16 graphic in its original form. We need to decompose that into 4 8x8 tiles:

Example W key tiles

If we want to save on tile space, which we usually do, we can see that the bottom two tiles are the same for every key. The top right tile will have a few variants that are reusable. The top left tile will usually be unique.

This gets more complicated when considering all 4 frames of animation included in the tileset:

Example W key tiles with all animations

I'm gonna go ahead and tackle that problem later. The first iteration of this demo will only draw the keys. I have to force myself to again start simple and work up to something more advanced.

There are 4 palettes at our disposal for this demo. We don't need many colors but let's use all 4 anyway:

OK, so starting with the lower section of keys, we have:

The lower part of the keyboard

Oh, I also created a pointer and decided to go with a Windows 95 theme. The pointer color may change.

Now it's time to build a set of tiles for the top half of each key. The top half is of course also two 8x8 tiles. There will be duplicate tiles in this set.

Luckily I already wrote code find duplicate tiles and match colors to a palette. After running those, the resulting tiles look like:

Unique keyboard tiles

Maybe I'll mess around with space, enter, and back to reduce the tileset size a little more. It's not important at the moment.

After adding the tops it looks like:

The full keyboard

The easy part is over, we now have to detect a click event. Since I'm building this on top of the existing Retail Clerk library, there is already default behavior for handling the A button press and finding what the sprite is looking at:

The default click event

The Retail Clerk code is based on an environment where there are at most 10 objects and 4 NPCs to interact with. In the types of demos I've been interested in that is enough. Right now there are 45 potential objects to interact with, potentially more in the future. Since that list is currently stored in RAM, that might be a problem. Hold on, is it? The Genesis has 64 KB of RAM... the base Retail Clerk library uses about 10% of that so bumping up the object list from 10 to 50 isn't a big deal. On the other hand it changes the code to load scenes a lot. I think it's less work for the interact code to hit a different lookup table.

Sorry, none of this makes sense to anyone but me.

See, in the Retail Clerk games (which includes Speedrun Tower) everything is based around the scene object:

Retail Clerk scene object

The list of objects is fixed length unlike Tiles, Scenery, and Text. This was a design mistake I need to correct. In Retail Clerk '90 I already regretted making Exits fixed length but didn't correct it. I will eventually regret making NPCs fixed length. I have a demo in mind that if I start will need to fix NPCs first.

This is all very complicated though. These lists are fixed length because the current scene is stored in RAM. Dynamic RAM management on the Sega Genesis sounds awful. It's possible. Better developers than me can handle this. The whole reason why save state editing on old PC & consoles is so easy comes from everything in RAM being at a fixed address.

So the simplest thing for now is creating a lookup table in the ROM that looks an awful lot like the scene object list. Going back a bit, I'm storing object lists in a format like:


;---------------------------------------------------------------------------
; objects
;---------------------------------------------------------------------------
dc.w OBJ_LIST_LENGTH-1 ; object count
;---------------------------------------------------------------------------
; word0=Object ID (0-65535)
; word1[0-8]=x0 (0-511)
; word1[9-15]=width (0-127)
; word2[0-8]=y0 (0-512)
; word2[9-15]=height (0-127)
;---------------------------------------------------------------------------
dc.w OBJ_SCENE_BASEMENT_CAFE_RACK
;    %wwwwwwwxxxxxxxxx
dc.w %1100000010011010
;    %hhhhhhhyyyyyyyyy
dc.w %0011000010010000
dc.w OBJ_SCENE_BASEMENT_CRATE
;    %wwwwwwwxxxxxxxxx
dc.w %0010000001101100
;    %hhhhhhhyyyyyyyyy
dc.w %0001100001101100
[and so on for the rest of the objects in the scene]

That example is from Retail Clerk '90 not that it matters.

The first step in this is creating a constant for each key:


OBJ_KEY_1=$3001
OBJ_KEY_2=$3002
[... and so on ...]
OBJ_KEY_PERIOD=$302C
OBJ_KEY_ENTER=$302D

Then we're going to need two lookup tables. The first table maps keys to (x,y) coordinates on the screen:


TableKeyMap:
;---------------------------------------------------------------------------
; Data format:
; word0=Object ID (0-65535)
; word1[0-8]=x0 (0-511)
; word1[9-15]=width (0-127)
; word2[0-8]=y0 (0-512)
; word2[9-15]=height (0-127)
;---------------------------------------------------------------------------
TableKeyMapRow0:
    dc.w $0009 ; object count - 1
    ; 1 at (8,136) on screen, (136,264) virtual
    dc.w OBJ_KEY_1
    ;    %wwwwwwwxxxxxxxxx
    dc.w %0001110010001000
    ;    %hhhhhhhyyyyyyyyy
    dc.w %0001110100001000
[... and so on until the last row ...]
TableKeyMapRow4:
    dc.w    $0005 ; object count - 1
    ; ! at (8,200) on screen, (136,328) virtual
    dc.w OBJ_KEY_EXCLAIM
    ;    %wwwwwwwxxxxxxxxx
    dc.w %0001110010001000
    ;    %hhhhhhhyyyyyyyyy
    dc.w %0001110100001000
[... and so on until the end of the table row ...]
    ; enter at (120,200) on screen, (248,328) virtual
    dc.w OBJ_KEY_ENTER
    ;    %wwwwwwwxxxxxxxxx
    dc.w 0101101011111000
    ;    %hhhhhhhyyyyyyyyy
    dc.w %0001110101001000

The second lookup table maps a key value back to the draw instructions in the scene:


TableKeyPattern:
;OBJ_KEY_1=$3001
    dc.w $0002 ; 3 patterns
    dc.l SceneKeyboardKey1
;OBJ_KEY_2=$3002
    dc.w $0002 ; 3 patterns
    dc.l SceneKeyboardKey2
[... and so on...]    
;OBJ_KEY_ENTER=$302D
    dc.w $0003 ; 4 patterns
    dc.l SceneKeyboardKeyENTER

Sorry for glossing over how scenes work. They are described here although they have evolved since I wrote that article.

Now during vblank we need some code that detects whether a key is being hovered over. But first, some memory address constants:


;-------------------------------------------------------------------------------
; keyboard handling
;-------------------------------------------------------------------------------
MEM_HOVER_KEY_CURRENT=$FFFF054E ; which key the pointer is over
MEM_HOVER_KEY_PREVIOUS=$FFFF0550 ; which key the pointer was last over
MEM_LAST_POINTER_X=$FFFF0552 ; previous pointer position
MEM_LAST_POINTER_Y=$FFFF0554 ; previous pointer position
MEM_REDRAW_KEY_PALETTE_SHIFT=$FFFF0556 ; used for hover color change

Just a couple more new constants:


;-------------------------------------------------------------------------------
; keyboard rows
;-------------------------------------------------------------------------------
KEYBOARD_ROW0=$0108 ; top of row 0 (136 on screen 264 virtual)
KEYBOARD_ROW1=$0118 ; top of row 1 (152 on screen 280 virtual)
KEYBOARD_ROW2=$0128 ; top of row 2 (168 on screen 296 virtual)
KEYBOARD_ROW3=$0138 ; top of row 3 (184 on screen 312 virtual)
KEYBOARD_ROW4=$0148 ; top of row 4 (200 on screen 328 virtual)
KEYBOARD_MAX_X=$0126 ; right edge of keyboard (166 on screen 294 virtual)

And the code to lookup whether a key is being hovered over:


VBlank:
[... existing vblank code ...]

VBlankFindHoverKey:
    bsr.w FindHoverKey
    move.w (MEM_HOVER_KEY_CURRENT),d6 ; copy current hover key to d6
    cmp.w (MEM_HOVER_KEY_PREVIOUS),d6 ; did the key change?
    beq.s VBlankExit

[... code being added next ...]

VBlankExit:
    rte

;-------------------------------------------------------------------------------
; Find which key the pointer is over
; This is similar to FindActionTarget but with (suspected) optimizations 
;-------------------------------------------------------------------------------
FindHoverKey:
    move.w (MEM_PLAYER_SPRITE_X),d4 ; copy base sprite x
    move.w (MEM_PLAYER_SPRITE_Y),d5 ; move base sprite y
    cmp.w (MEM_LAST_POINTER_X),d4 ; did x change?
    bne.s .pointermoved
    cmp.w (MEM_LAST_POINTER_Y),d5 ; did y change?
    bne.s .pointermoved
    rts ; pointer didn't move, nothing to do
.pointermoved
    move.w (MEM_HOVER_KEY_CURRENT),(MEM_HOVER_KEY_PREVIOUS) ; save the current hover key
    cmpi.w #KEYBOARD_ROW0,d5 ; test if y is outside the keyboard
    blt.w HoverKeyNotFound ; y < top of keyboard
    cmpi.w #KEYBOARD_MAX_X,d4 ; test if x is outside the keyboard
    bgt.w HoverKeyNotFound ; x > right edge of keyboard
    ; test which row the pointer is on
    cmpi.w #KEYBOARD_ROW1,d5 ; test if y is higher than row 1
    blt.w .row0 ; y < top of row1
    cmpi.w #KEYBOARD_ROW2,d5 ; test if y is higher than row 2
    blt.w .row1 ; y < top of row2
    cmpi.w #KEYBOARD_ROW3,d5 ; test if y is higher than row 3
    blt.w .row2 ; y < top of row3
    cmpi.w #KEYBOARD_ROW4,d5 ; test if y is higher than row 4
    blt.w .row3 ; y < top of row2
.row4 ; fall here if all other tests failed
    lea TableKeyMapRow4,a0 ; point a0 to the keymap
    bra.s .firstentry
.row3
    lea TableKeyMapRow3,a0 ; point a0 to the keymap
    bra.s .firstentry
.row2
    lea TableKeyMapRow2,a0 ; point a0 to the keymap
    bra.s .firstentry
.row1
    lea TableKeyMapRow1,a0 ; point a0 to the keymap
    bra.s .firstentry
.row0
    lea TableKeyMapRow0,a0 ; point a0 to the keymap
.firstentry
    move.w (a0)+,d6 ; first entry is list size, d6 is loop control
FindHoverKeyLoop:
    move.w (a0),(MEM_HOVER_KEY_CURRENT) ; copy the current object id in the list
    ; test if sprite x is between left and right edge
    move.w (2,a0),d2 ; copy word 1 (x and width)
    move.w d2,d1 ; use d1 for width
    and.w #%0000000111111111,d2 ; clear bits[9-15]
    cmp.w d2,d4 ; (htx>=x1)
    blt.w FindHoverKeyLoopDbra ; loop if sprite x < object left
    ; need to shift 9 bits right
    lsr.w #$08,d1 ; shift 8
    lsr.w #$01,d1 ; shift 1 more
    add.w d1,d2 ; add width to left edge to get right edge
    cmp.w d2,d4 ; (htx<=x0)
    bgt.w FindHoverKeyLoopDbra ; loop if sprite x > object right
    ; test if sprite y is between top and bottom edge
    move.w (4,a0),d2 ; copy word 2 (y and width)
    move.w d2,d1 ; use d1 for height
    and.w #%0000000111111111,d2 ; clear bits[9-15]
    cmp.w d2,d5 ; (hty>y0)
    blt.w FindHoverKeyLoopDbra ; loop if sprite y < object top
    ; need to shift 9 bits right
    lsr.w #$08,d1 ; shift 8
    lsr.w #$01,d1 ; shift 1 more
    add.w d1,d2 ; add height to top edge to get bottom edge
    cmp.w d2,d5 ; (hty<y1)
    bge.w FindHoverKeyLoopDbra ; loop if sprite y > object bottom
    ; if we made it here then we have a hit
    bra.s ExitFindHoverKey ; exit
FindHoverKeyLoopDbra:
    adda.w #OBJ_LIST_STRUCT_SIZE,a0 ; move to next object list entry
    dbra    d6,FindHoverKeyLoop ; decrement and loop
HoverKeyNotFound:
    move.w #$FFFF,(MEM_HOVER_KEY_CURRENT)
ExitFindHoverKey:
    ; save pointer position for next time
    move.w (MEM_PLAYER_SPRITE_X),(MEM_LAST_POINTER_X)
    move.w (MEM_PLAYER_SPRITE_Y),(MEM_LAST_POINTER_Y)
    rts ; exit

After this, it's time to add the rest of the code to change the palette when a key is being hovered and revert it after:


VBlank:
[... existing vblank code ...]

VBlankFindHoverKey:
    bsr.w FindHoverKey
    move.w (MEM_HOVER_KEY_CURRENT),d6 ; copy current hover key to d6
    cmp.w (MEM_HOVER_KEY_PREVIOUS),d6 ; did the key change?
    beq.s VBlankExit

VBlankHighlightHoverKey:
    cmpi.w #$FFFF,d6 ; is there a hover key?
    beq.s VBlankRevertPreviousKey ; no hover key to highlight
    cmpi.w #OBJ_KEY_1,d6 ; sanity check
    blt.s VBlankRevertPreviousKey ; no hover key to highlight
    move.w #$2000,(MEM_REDRAW_KEY_PALETTE_SHIFT) ; bump to palette 2
    bsr.w RedrawKey

VBlankRevertPreviousKey:
    move.w (MEM_HOVER_KEY_PREVIOUS),d6 ; copy previous hover key to d6
    cmpi.w #$FFFF,d6 ; is there a previous hover key?
    beq.s VBlankExit ; no previous hover key to revert
    cmpi.w #OBJ_KEY_1,d6 ; sanity check
    blt.s VBlankExit ; no previous hover key to revert
    move.w #$0000,(MEM_REDRAW_KEY_PALETTE_SHIFT) ; keep default palette
    bsr.w RedrawKey

VBlankExit:
    rte
    
RedrawKey:
    sub.w #OBJ_KEY_1,d6 ; key1 is 0 index of the list
    lea TableKeyPattern,a0 ; point to the pattern table
    mulu.w #TABLE_KEY_PATTERN_ENTRY_LENGTH,d6 ; compute table offset
    adda.l d6,a0 ; move to offset
    move.w (a0)+,d7 ; number of scenery items to draw
    movea.l (a0),a6 ; address of the first scenery entry in the set 
RedrawKeyLoop:
    movea.l (a6)+,a0 ; start address of pattern
    move.w (a6)+,d0 ; base pattern
    add.w (MEM_REDRAW_KEY_PALETTE_SHIFT),d0 ; palette change (or not)
    move.w (a6)+,d1 ; repeat
    movea.l (a6)+,a1 ; initial drawing location
    bsr.w DrawTileset ; branch to DrawTileset subroutine
    dbra d7,RedrawKeyLoop ; loop until all data is loaded
    rts    

Here's how it looks when a key is being hovered:

Hovering over a key

And how it looks after reverting the previous hover key and choosing a new one:

Hovering over another key

That's neat, but we really need to show the text the user is typing. Let's reserve a little space:

Text area

This means, you guessed it, another lookup table. This one maps the key that was clicked to a character value:


;*******************************************************************************
; map keys to the actual letter value
;*******************************************************************************
TableKeyValue:
;OBJ_KEY_1,$3001
    dc.b "1"
;OBJ_KEY_2,$3002
    dc.b "2"
[... and so on ...]
;OBJ_KEY_PERIOD,$302C
    dc.b "."
;OBJ_KEY_ENTER,$302D
    align 2

I bet I could simplify this by combining two of the tables. If I go any further with this idea maybe I'll think about it some more.

OK, we need to track the location of where text is being drawn. Two new memory address constants:


MEM_TEXT_ROW=$FFFF0558 ; current row to draw text
MEM_TEXT_COL=$FFFF055C ; current column to draw text

Some initial and maximum values:


INIT_TEXT_ROW=$01000000
INIT_TEXT_COL=$00040000
MAX_TEXT_ROW=$07000000
MAX_TEXT_COL=$004A0000

And a bunch of code to type the keys when the A button is pressed:


MainGameLoop:
[... a lot of existing code that isn't really needed for this demo ...]
TestAButtonPressed: ; test if the player pressed the A button
    move.b (MEM_CONTROL_PRESSED),d6 ; copy pressed buttons to d6
    andi.b #BUTTON_A,d6 ; test if the A button was pressed
    beq.w MainGameLoopUpdateSprites ; A button is not pressed - this code is not listed on this page but is in the source code download
    ;-----------------------------------
    ; test the selected key
    ;-----------------------------------
    move.w (MEM_HOVER_KEY_CURRENT),d6
    cmpi.w #$FFFF,d6 ; is there a hover key?
    beq.w MainGameLoop ; no key, loop
    cmpi.w #OBJ_KEY_BACK,d6
    bne.s .testenter
.handleback
    ;-----------------------------------
    ; move to the previous row/column
    ;-----------------------------------
    cmpi.l #INIT_TEXT_COL,(MEM_TEXT_COL)
    ble.s .prevrow
    sub.l #$00020000,(MEM_TEXT_COL) ; move to the previous column
    bra.s .clearchar
.prevrow
    cmpi.l #INIT_TEXT_ROW,(MEM_TEXT_ROW)
    ble.s .clearchar
    sub.l #$00800000,(MEM_TEXT_ROW) ; move to the next row
    move.l #MAX_TEXT_COL,(MEM_TEXT_COL) ; back to last column
.clearchar
    ; clear the character
    move.w #FONT_BASE_TILE_LOW,d6 ; add the base tile
    move.l #VDP_VRAM_WRITE_A,d7
    add.l (MEM_TEXT_ROW),d7
    add.l (MEM_TEXT_COL),d7
    move.l d7,(VDP_CONTROL) ; set VDP address
    move.w  d6,(VDP_DATA) ; copy the character to VPD
    bra.w MainGameLoop ; return to start of game loop
.testenter
    cmpi.w #OBJ_KEY_ENTER,d6
    beq.s .nextrow
.defaultkeyhandle
    ; update d6 to point to the tile ID
    clr d6 ; for later adda.l 
    move.w (MEM_HOVER_KEY_CURRENT),d6 ; copy current hover key to d6
    sub.w #OBJ_KEY_1,d6 ; subtract first item to get an index
    lea TableKeyValue,a6 ; point a6 to the table of key values
    adda.l d6,a6 ; move to offset
    move.b (a6),d6 ; copy value from table
    sub.w #$20,d6 ; subtract 32 to get the character index
    add.w #FONT_BASE_TILE_LOW,d6 ; add the base tile
    move.l #VDP_VRAM_WRITE_A,d7
    add.l (MEM_TEXT_ROW),d7
    add.l (MEM_TEXT_COL),d7
    move.l d7,(VDP_CONTROL) ; set VDP address
    move.w  d6,(VDP_DATA) ; copy the character to VPD
    ;-----------------------------------
    ; move to the next row/column
    ;-----------------------------------
    cmpi.l #MAX_TEXT_COL,(MEM_TEXT_COL)
    bge.s .nextrow
    add.l #$00020000,(MEM_TEXT_COL) ; move to the next column
    bra.w MainGameLoop ; return to start of game loop
.nextrow
    cmpi.l #MAX_TEXT_ROW,(MEM_TEXT_ROW)
    bge.s .donewithtext
    add.l #$00800000,(MEM_TEXT_ROW) ; move to the next row
    move.l #INIT_TEXT_COL,(MEM_TEXT_COL) ; back to first column
.donewithtext
    bra.w MainGameLoop ; return to start of game loop

Here's the first test:

OK the keys all work

Perhaps you noticed I'm not storing the text anywhere. It is trivial to do that, I just didn't. This code only draws the tiles on the screen.

Now let's make it look a little nicer:

Final(?) demo

How about some new music? I want to try out a new sound driver but today is not that day. I'm going to stick with Echo which works off .xm files. I perused The Mod Archive for something simple (under 10 instruments and tracks) with a friendly license and landed on ant512_-_something_sinister.xm. It's not really a "typing song" but this demo will get roughly 1 download so who cares.

And on that note...

Download the demo ROM

Download the source code & build tools

Latest version of my Sega Genesis template code & build tools

Thanks for stopping by.


 


Related