Today I
worked on a menu program. Because I'm lazy, I used Basiegaxorz, a BASIC compiler
for the Genny.
I decided it would be simple enough - three banners (one
for each game). They would be greyscale unless selected. Up and down to select,
and start to start.
First I had to decide sizes. I decided to work with
the 320x224 mode. Vertically dividing 224 by 3 gives 74.67 pixels. But 74.6
isn't a multiple of 8, and we have a tile-based display. So to make life easier,
I rolled that down to the nearest multiple of 8, which is 72 pixels. (72 x 3 =
216 pixels, so there's actually one unused tile row. That's fine.)
Width
was arbitrary, but I decided that I wanted to horizontally stagger the images,
so I picked 256 pixels wide as a rough value. That gives me images that are 32x9
tiles in size - 228 tiles total for each one. No problem!
I started with
the box covers for TF2, TF3, and Lightening Force (I originally was going to use
the European TF4, but I decided I liked the US version better for the tiny
changes it has, and I didn't want to deal with region changes). A few minutes on
each in Photoshop to overlay the titles on the image in appropriate locations,
and I had the images I wanted.
To convert to Genesis format, I used
"Retro Graphics Toolkit" from here:
https://github.com/ComputerNerd/Retro-Graphics-ToolkitIt
took a little time to figure it out, but it does produce pretty decent results
with minimal work, and has convenient output modes for working with BEX
(including a clipboard output, which I used.)
First thing to do upon
opening it is File->Tilemaps->Import Image to Tilemap. This loads the
image file into the tilemap area for conversion. It asks whether you want to
append or overwrite tiles, for simplicity I always overwrote and treated each
image independently.
On the Plane Mapping/Block Editor tab, you can view
the image - here I have selected Tilemap Actions->Toggle Truecolor Viewing,
just to show you the image. I suggest that you leave it OFF so it's easier to
tell what step you're at. (The image will be black at this point
instead).
Next, we need to create a palette. The Genesis has a 64-color active
palette, split into 4 16-color palettes for the sake of tiles. I decided to give
each image a 16-color palette, and then I could just grey out individual
palettes for the effect. But, this tool will happily use all the colors, while
color 0 in each palette is transparent. To make life easier, I selected color 0
on the Palette Editor tab, made sure it was black (my desired background color),
and then selected 'locked'. This tells the tool that images are allowed to USE
the color (and in this case they are), but they are not allowed to CHANGE that
color.
Next it was simply a matter of Palette Actions->Generate
Optimal Palette with X amount of colors. (long menu name). The tool asks how
many colors you want - well, I have 16 in the desired palette and one is locked,
so I asked for 15. It then asks if you would like all tiles on the tilemap to be
set to row 0 ('row 0' also means 'palette 0', or 'the first 16 colors'). So,
yes, that's the only one we're using. Next, a color reduction algorithm (I am
happy with the default) and the color space (I prefer YUV). Finally, a palette
will appear! (Note the 'locked' radio button set for color 0).
Now we just have to dither the image down to this palette. Select
Tilemap Actions->Dither Tilemap as Image. It asks whether to Dither Entire
Image at once, or Dither each Palette Row Separately. Well, we only have one
palette row anyway, so do it all at once. Switch over to the Plane Mapping/block
editor, and you should see the image, dithered. (If you don't like it, you can
play with the palette settings).
Next, I opened up the basic editor. We need to export three sets of data
from the converter -- the palette, the tilemap, and the tile bitmaps
themselves.
So File->Palettes->Save Palette. First entry to save is
0, last entry is 15 (0-15 is 16 colors). It then asks for the type to save, drop
down and select 'BEX' (which is some abbreviation for the BASIC compiler). It
can also export Binary, C, and ASM. Finally, It asks Clipboard or File - I
wanted clipboard. Then, I just hit paste in the BEX editor.
Repeat the
process for the tilemap: File->Tilemaps->Save tilemap and if nes
attributes (another long name, and we're not doing NES. But never mind.) Select
type as BEX, output to clipboard, and then you are asked if you'd like a
compression algorithm. For me, this project, I left it uncompressed. Then over
to BEX and paste the data into the editor.
Finally for the tiles
themselves, File->Tiles->Save Tiles. Select type BEX, clipboard, and
uncompressed. Then paste into the editor (this will have the most
data.)
BEX export always uses the same label names, and since we're
planning more than one, I went ahead and renamed the labels, too, prefixing with
"tf3" in this case.
I repeated the process with the other two images, until all the data was
converted and in place. Now I needed to draw the screen. There's just one small
issue to overcome -- all the images are saved as though they use palette row 0.
So it is necessary, as we load the tiles to video memory, to update them for
where we /really/ want them to be.
Originally, I had thought I'd use a
staggered layout, as I mentioned, and put text on the bottom row. But that
actually looked rather disorganized and cramped, so, I went for an aligned
layout with no text. With that decided, my palette choices became:
- row
0 - black (was originally for text)
- row 1 - TF2 palette
- row 2 - TF3
palette
- row 3 - TF4 palette
So first it was necessary to set up the
system. I decided to put everything on plane B for simplicity (the only reason
to set text plane was so the 'cls' function would work and save me some trouble
;)
):
OPTION TITLE,"Thunder Force Multicart"
OPTION NOLOADFONT
BGCOLOR 0,0
setgfxplane SCROLL_B
settextplane SCROLL_B
valt
palettes palDat,0,0,16
palettes tf2palDat,1,0,16
palettes tf3palDat,2,0,16
palettes tf4palDat,3,0,16
loadtiles tf2tileDat,288,128
loadtiles tf3tileDat,288,416
loadtiles tf4tileDat,288,704
Here
we set the cart title, skip loading the font, set the background color to row 0,
color 0 (black!), and set the working planes. "valt" is supposed to wait for
vertical blank, this prevents 'snow' on the screen caused by updating video
memory as the beam is being drawn. (Or is it only CRAM? Anyway...)
The
"palettes" function loads each palette to color RAM - palDat is set to all zeros
(and doesn't really need to be loaded now that there's no text), the others load
the TF palettes, into each palette row. Finally, the three calls to loadtiles
loads the bitmap data into memory - each image is 288 tiles and loads as the
listed character offset. TF2 loads at 128 because, again, originally I was
leaving room for a font.

With
that everything is ready to go, but there is nothing onscreen - we need to load
the tilemaps. The BEX compiler came with examples, one of which included a very
nice assembly subroutine to draw a block of tiles, called DrawTiles16. One very
nice feature of this subroutine is the ability to add an offset to the tiles.
This solves our needs of both character offset (such as 128), AND palette row
offset. In a 16-bit tilemap entry, bits 13 and 14 specify the palette to use.
Since all the images were saved for palette zero, we can just add the offset we
need. So these lines are added to display the screen:
DrawTiles16 lblptr&(tf2mapDat),0,0,32,9,&h2080 ' palette 1, char 128
DrawTiles16 lblptr&(tf3mapDat),7,9,32,9,&h41a0 ' palette 2, char 416
DrawTiles16 lblptr&(tf4mapDat),0,18,32,9,&h62c0 ' palette 3, char 704
(and
of course the DrawTiles16 sub is included in the code too).
I wasn't super fond of the staggering, as I noted, but at least the
image was up successfully! Now I needed a way to make the palette
greyscale.
One way is to just precalculate the palette and load it as
needed, but I wasn't up to that. Instead, I decided I could do it in code. I
decided to create an assembly function (although BASIC may have been quick
enough for just 16 colors!) - mostly for the practice. To keep the code simple,
rather than doing a true greyscale, I decided to just copy the green channel to
red and blue.
Genesis colors are rather simply defined in 16 bits as
0000bbb0 ggg0 rrr0. Note that although only 9 bits are used for color, there is
padding to easily support 12 bit color. It's almost too bad we never saw that
enhancement!
The following subroutine is used to load a palette, but
changes each color to grey before storing it in the VDP:
' GreyPalette lblptr&(<palDat>), <pal num>, <pal idx>, cnt
Declare Asm Sub GreyPalette(d5.l, d0.w, d1.w, d2.w)
; not really grey, we just copy the green channel to all three guns
move.w #$2700,sr ; disable interrupts
movea.l d5,a0 ; make address pointer for data
move.l #$c0000000,d4 ; CRAM address in VDP
lsl.w #$5,d0 ; make palette number into offset
lsl.w #$1,d1 ; make index into offset
add.w d1,d0 ; combine offsets
clr.l d1 ; prepare long word
move.w d0,d1 ; just to be sure the high word is cleared
lsl.l #8,d1 ; VDP addresses are mostly in the high word (CCAAAAAA AAAAAAAA 00000000 CCCC00AA)
lsl.l #8,d1 ; the lowest two address bits in the dword are the most significant A15,A14
add.l d1,d4 ; add offset
move.l d4,$c00004 ; set VDP address into CRAM
@1:
move.w (a0)+,d7 ; palette entry is 0000BBB0 GGG0RRR0
and.w #$00f0,d7
move.w d7,d3
lsl.w #4,d3
or.w d3,d7 ; copy to blue
lsr.w #4,d7 ; shift to red
or.w d3,d7 ; load blue again
move.w d7,$c00000 ; store in VDP data address (auto increments, hopefully)
dbf d2,@1 ; loop around as necessary
move.w #$2000,sr ; restore interrupts
End Sub
With
that coded, I changed the palettes calls to instead call "GreyPalette" like
so:
GreyPalette lblptr&(tf2palDat),1,0,16
GreyPalette lblptr&(tf3palDat),2,0,16
GreyPalette lblptr&(tf4palDat),3,0,16
(I
admit I was forgetting if there was another way to safely clear just the high
word of a 32-bit register, but this works.)
Finally, since I didn't like
the offsets, I centered all the images by changing the 'x' coordinates in the
"DrawTiles16" calls:
DrawTiles16 lblptr&(tf2mapDat),4,0,32,9,&h2080 ' palette 1, char 128
DrawTiles16 lblptr&(tf3mapDat),4,9,32,9,&h41a0 ' palette 2, char 416
DrawTiles16 lblptr&(tf4mapDat),4,18,32,9,&h62c0 ' palette 3, char 704
Success! Now joypad input, and highlighting the images. This code is
mostly done -- to make an image colored, just call 'palettes' and load its
palette into the right place. To make it grey, call GreyPalette instead. Because
it was easier to hard code the values, and I needed it in several places, I
created a variable 'opt' to track which was selected, and created two
subroutines, greyit and lightit. Each simply does the appropriate thing based on
the value of 'opt'.
' warning: select case without a case else will crash on invalid numbers
greyit:
valt
select case opt
case 0
GreyPalette lblptr&(tf2palDat),1,0,16
exit select
case 1
GreyPalette lblptr&(tf3palDat),2,0,16
exit select
case 2
GreyPalette lblptr&(tf4palDat),3,0,16
exit select
end select
return
lightit:
valt
select case opt
case 0
palettes tf2palDat,1,0,16
exit select
case 1
palettes tf3palDat,2,0,16
exit select
case 2
palettes tf4palDat,3,0,16
exit select
end select
return
Now
I needed three variables - one for the option, one for the joystick, and one for
the last joystick value (used to prevent automatic and very fast repeating). I
prefer explicit, so at the top of the program:
dim joy as integer
dim old as integer
dim opt as integer
Then,
after the DrawTiles16 calls, we can init and run our main loop:
old = 0
opt = 0
gosub lightit
while (1)
joy=joypad(0)
old = joy XOR old ' mask new bits
old = joy and old ' gives us just new presses
if old AND &h001 then
' up
if opt > 0 then
gosub greyit
opt=opt-1
gosub lightit
endif
endif
if old AND &h002 then
' down
if opt < 2 then
gosub greyit
opt=opt+1
gosub lightit
endif
endif
if old AND &h080 then
' start
endif
old=joy
wend
The
first 'gosub lightit' makes sure the default option is lit before we start,
since we don't update the screen unless we move. The three lines around the
joypad are a clever trick I saw in Thunder Force 3 -- the end result of those
three instructions is that 'joy' has the current set of inputs, and 'old' has
ONLY the /new/ inputs. Very handy for a menu like this, and easier than simply
waiting for the user to release the button (like I used to). Note the "old=joy"
at the bottom of the loop - without that it doesn't work!

The
"if old AND &h001 then" checks whether 'up' in set in 'old' -- the bit
values come from the BEX documentation. If up WAS just pressed, and opt is
greater than 0 (can't go up at the top), then we first gosub 'greyit' which will
grey the current selection, then subtract 1 from opt, then call 'lightit' so the
new selection is lit. We do essentially the same thing in the opposite direction
for down.
Finally, there's a selection for 'start', but we aren't that
far yet.
And it works! So far! Next entry delves into bank switching, because I
should have been in bed four hours ago.... ;)