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:
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:
Time to scale this idea back to something I can manage. This is what I'm going to actually try:
This layout can then grow to something like this:
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:
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):
It's a 16x16 graphic in its original form. We need to decompose that into 4 8x8 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:
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:
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:
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 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 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:
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:
And how it looks after reverting the previous hover key and choosing a new one:
That's neat, but we really need to show the text the user is typing. Let's reserve a little space:
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:
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:
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 source code & build tools
Latest version of my Sega Genesis template code & build tools
Thanks for stopping by.
Related