Progress Report
When I set off on this crazy Genesis programming idea I decided to be realistic about the timeline. I figured if I could sneak in about 1 feature a month that would be acceptable. In the past I had dumb goals like "create an entire RPG roughly the size of Phantasy Star II, III, and IV combined before I turn 30", which I set for myself around the time I turned 29. 10 years later I had a similar unrealistic goal only then it was for something roughly the size of Final Fantasy I-VIII combined before I turned 40. Now it's "let's see what I can get working and if it turns into a game that's great otherwise at least I'm doing something mentally stimulating". At my current pace I'm a little behind the one feature a month goal which isn't awesome but I'm OK with that. The nice thing about working on a Sega Genesis game in 2016 is that I don't have to rush to make Xmas season of 1994. I can't imagine the pressure on teams working on 16-bit games then. The Saturn and PlayStation were on their way so you knew if you missed Xmas 1994 your game was dead. If I miss Xmas 2016, whatever.
My latest very small addition is vertical scrolling. As far as I can tell, vertical scrolling and horizontal scrolling don't work exactly the same. Although it's more likely I just don't completely understand it all yet.
Well, that wasn't my only accomplishment, I also wrote another small tool. One unexpected consequence of this idea is spending time creating utilities to generate code. A couple articles ago I created a tile & palette editor in C#, now I need something to make collision map data creation easier.
In the last article I tried storing collision data for a map in pairs of longs each representing 32 tiles, combined for one row (512px). This worked pretty well so I 'm going to build off it.
Instead of working with 0s and 1s I decided to write a program that could read a bitmap and generate the collision data based on it, where a white pixel = open space and any other color = blocked space. So now my collision data looks like a screen from Adventure on Atari 2600 which is a bonus. Here's the 64x64 (512x512 collision points) map for my work-in-progress demo, obviously scaled-up.
I know many programmer-types who think mixing languages on a project is the worst thing ever. I was one of them at some point in my career. Now I've come around to "use the best tool for the job". When I want to write client UIs I almost always use C# and WinForms, when I want to write a quick command-line tool I almost always use Java. I understand Python is probably a better choice for the latter but right now I can write Java a lot faster. Writing UIs in Java though should be considered a violation of the Geneva Convention. OK, OK, JavaFX is really easy to write UIs with, fine. The only problems are that it only runs on Windows (so long "write once, run anywhere") and I'm 99% sure Oracle is going to scrap it soon.
Anyway, here's the Java code that converts a bitmap to collision data:
//code to read bmp pixels is from: https://stackoverflow.com/questions/17015340/how-to-read-a-bmp-file-identify-which-pixels-are-black-in-java
//everything else is original
public class JBMP2ASM{
private final static String newLine=System.lineSeparator();
//arg[0]=source file (bitmap)
//arg[1]=output file (text)
//if the width of the image isn't a multiple of 32 then bad times
public static void main(String[] args){
FileWriter writer=null;
try{
if(args.length!=2){throw(new Exception("Expecting two arguments: sourcefile outputfile"));}
String sourceFilePath=args[0];
String outputFilePath=args[1];
File sourceFile=new File(sourceFilePath);
BufferedImage image = ImageIO.read(sourceFile);
int width=image.getWidth();
if(width%32!=0){throw(new Exception("Image width must be a multiple of 32"));}
int height=image.getHeight();
writer=new FileWriter(outputFilePath);
for(int y=0;y<height;y++){
long longValue=0;
int power=0;
for(int x=0;x<width;x++){
int color=image.getRGB(x,y);
if (color!=Color.WHITE.getRGB()) {
longValue+=Math.pow(2,power);
}
if(power==31){
StringBuffer hexValue=new StringBuffer(Long.toHexString(longValue).toUpperCase());
int pad=8-hexValue.length();
for(int i=0;i<pad;i++){hexValue.insert(0,'0');}
hexValue.insert(0,"\tdc.l\t$");
writer.write(hexValue.toString());
writer.write(newLine);
longValue=0;
power=0;
}else{
power++;
}
}
}
System.out.println("Successfully wrote "+outputFilePath);
}catch(Exception x){
x.printStackTrace()
}finally{
try{if(writer!=null){writer.flush(); writer.close();}}catch(Exception x){ }
}
}
}
With that out of the way, let's get back to some beautiful assembly code.
Vertical Scrolling
Assuming I have this right, and that's not exactly a safe assumption, updating the vertical scroll position is just a matter of incrementing a value. The most excruciatingly fun part of this was figuring out how to scroll both the A & B planes at the same time. The Genesis allows planes to be scrolled independently, 25 years ago you might have engaged in nerdy debates with your friends about which systems had "parallax scrolling". When writing to the vertical scroll RAM (VSRAM) a long value is passed. The upper word contains the vertical scroll value for the B planes and the lower word contains the vertical scroll value for the A planes. In this demo I'm scrolling both planes at the same time so the same value needs to be passed in both the high and low word. I burned quite a few hours trying to figure out why only the A plane was scrolling which I feel kinda dumb about now.
As usual, I'm creating a sub-routine and couple constants to handle scrolling:
MEM_MAP_POSITION_X=$00FF0020 ; x position of the map
MEM_MAP_POSITION_Y=$00FF0022 ; y position of the map
MEM_FLAG_MAP_POSITION_CHANGED=$00FF0024 ; >0 if map position changed
VDP_VRAM_WRITE_HSCROLL=$7C000002
VDP_VRAM_WRITE_VSCROLL=$40000010
[...]
SetMapScroll:
; set vertical scroll
; -------------------------------------------------------------
; when writing to VSRAM the upper word is the scroll B value
; and the lower word is the scroll A value
; in this demo they are scrolling at the same time so the
; MEM_MAP_POSITION_Y value needs to set for both words
; -------------------------------------------------------------
move.w MEM_MAP_POSITION_Y,d0 ; copy Y map position to d0 lower word
swap d0 ; move Y map position to upper word
move.w MEM_MAP_POSITION_Y,d0 ; copy Y map position to d0 lower word
; the Y map position is now in both words of d0
move.l #VDP_VRAM_WRITE_VSCROLL,VDP_CONTROL ; setup write to vscroll
move.l d0,VDP_DATA ; copy y position to vscroll
move.w #$0000,(MEM_FLAG_MAP_POSITION_CHANGED) ; reset flag to false
EndSetMapScroll:
rts
Yeah, I'm updating the horizontal scroll here too but it's never changed anywhere in the rest of the code. The idea here is there's a flag that's set when the map position has changed which will trigger a call to SetMapScroll. Calling it too frequently definitely slows the entire game down (trust me). That means the main loop is modified a little. The new code is bolded:
MainGameLoop:
bsr.w WaitVSync ; wait for vsync to complete
add.w #$0001,(MEM_DEBUG_MAINLOOP_COUNTER) ; debug code
bsr.w MovePlayer ; move the player sprite
; other game logic would go here
cmpi.w #$0000,(MEM_FLAG_MAP_POSITION_CHANGED) ; test for scrolling
beq.w MainGameLoopEnd ; not scrolling, exit
bsr.w SetMapScroll ; else scroll the map
MainGameLoopEnd:
bra.s MainGameLoop ; return to start of game loop
Since I'm in the mood to create constants, here are a few more that may or may not be used later on:
; screen & plane sizes
DISPLAY_PIXELS_X=$0140 ; width of physical display
DISPLAY_PIXELS_Y=$00E0 ; height of physical display
SPRITE_PLANE_PIXELS_X=$0200 ; width of sprite virtual plane
SPRITE_PLANE_PIXELS_Y=$0200 ; height of sprite virtual plane
SPRITE_PLANE_OFFSET_TOP=$0080 ; sprite plane top to display top edge
SPRITE_PLANE_OFFSET_LEFT=$0080 ; sprite plane left to display left edge
SPRITE_PLANE_OFFSET_BOTTOM=$00A0 ; sprite plane bottom to display bottom edge
SPRITE_PLANE_OFFSET_RIGHT=$0040 ; sprite plane right to display right edge
VDP_PLANE_PIXELS_X=$0200 ; width of VDP plane
VDP_PLANE_PIXELS_Y=$0100 ; height of VDP plane
Now we just need a couple more constants that will be used to compute when to scroll. The idea here is that when the sprite is X pixels away from the edge then we need to scroll. These are the values that will be used:
SPRITE_PLAYER_HEIGHT=$0020 ; how many pixels tall the player sprite is
SPRITE_PLAYER_WIDTH=$0010 ; how many pixels wide the player sprite is
PLAYER_SCROLL_BOUNDARY=$0040 ; base value
PLAYER_SCROLL_BOUNDARY_TOP=PLAYER_SCROLL_BOUNDARY
PLAYER_SCROLL_BOUNDARY_BOTTOM=PLAYER_SCROLL_BOUNDARY+SPRITE_PLAYER_HEIGHT
PLAYER_SCROLL_BOUNDARY_LEFT=PLAYER_SCROLL_BOUNDARY
PLAYER_SCROLL_BOUNDARY_RIGHT=PLAYER_SCROLL_BOUNDARY+SPRITE_PLAYER_WIDTH
Now it's time to add a little code to the MovePlayer method that checks if the player sprite is approaching the top or bottom edge of the display and, if so, sets off the chain of events to scroll vertically. The new code is again bolded:
[...]
TestUpHeld:
move.b (MEM_CONTROL_HELD),d7 ; copy button held value to d7 for andi
andi.w #BUTTON_UP_PRESSED,d7 ; test if the up button is held
beq.s TestDownHeld ; branch if not
move.w #DIRECTION_UP,(MEM_PLAYER_SPRITE_DIRECTION) ; set direction
; test if map should scroll because the player is approaching the boundary
; top boundary is [current map y-position]
move.w (MEM_MAP_POSITION_Y),d7 ; move map y-position to d7
cmpi.w #$0000,d7 ; are we already scrolled to the top?
ble.w MovePlayerSprite ; if so stop testing for scroll
; at this point d7 contains the y value of the top boundary
add.w #SPRITE_PLANE_OFFSET_TOP,d7 ; adjust for sprite plane offset
sub.w (MEM_PLAYER_SPRITE_Y),d7 ; subtract sprite y
cmpi.w #PLAYER_SCROLL_BOUNDARY_TOP,d7 ; is sprite within scroll area?
; bcc used here because previous subtraction can lead to negative number
bcc.w MovePlayerSprite ; if not stop testing for scroll
sub.w #$0001,(MEM_MAP_POSITION_Y) ; decrement map y-position
move.w #$0001,(MEM_FLAG_MAP_POSITION_CHANGED) ; flag to reset the scroll
bra.w MovePlayerSprite ; move the player sprite
TestDownHeld:
move.b (MEM_CONTROL_HELD),d7 ; copy button held value to d7 for andi
andi.w #BUTTON_DOWN_PRESSED,d7 ; test if the down button is held
beq.s TestLeftHeld ; branch if not
move.w #DIRECTION_DOWN,(MEM_PLAYER_SPRITE_DIRECTION) ; set direction
; test if map should scroll because the player is approaching the boundary
; bottom boundary is [current map y-position]+[screen height]
move.w (MEM_MAP_POSITION_Y),d7 ; move map y-position to d7
add.w #DISPLAY_PIXELS_Y,d7 ; add the screen height
cmpi.w #VDP_PLANE_PIXELS_Y,d7 ; are we already scrolled to the bottom?
bge.w MovePlayerSprite ; if so stop testing for scroll
; at this point d7 contains the y value of the bottom boundary
add.w #SPRITE_PLANE_OFFSET_TOP,d7 ; adjust for sprite plane offset
sub.w (MEM_PLAYER_SPRITE_Y),d7 ; subtract sprite y
cmpi.w #PLAYER_SCROLL_BOUNDARY_BOTTOM,d7 ; is sprite within scroll area?
bge.w MovePlayerSprite ; if not stop testing for scroll
add.w #$0001,(MEM_MAP_POSITION_Y) ; increment map y-position
move.w #$0001,(MEM_FLAG_MAP_POSITION_CHANGED) ; flag to reset the scroll
bra.s MovePlayerSprite ; move the player sprite
TestLeftHeld:
[...]
This could probably be trimmed down a little bit. As noted in previous articles, I'm not too concerned about optimizing now except when there's an obvious performance issue.
Now when moving down the counter scrolls:
And when moving up the counter scrolls back into place:
Updating Collision Detection
The collision detection algorithm I patched together in the last article is based on the sprite position against a map but didn't account for scrolling (for obvious reasons). There are barely any changes needed though, again they are bolded:
TestSpriteCollision:
; a6 = SPRITE_ID
; a6 + 2 = SPRITE_X
; a6 + 4 = SPRITE_Y
; a6 + 6 = SPRITE_PATTERN_INDEX
; a6 + 8 = SPRITE_DIRECTION
; a6 + A = SPRITE_FRAME
; a6 + C = SPRITE_STEP_COUNTER
movea.l a6,a4 ; store address in a4 because it is manipulated
adda.l #$4,a4 ; move to a4+4 -> SPRITE_Y
move.w (a4),d6 ; copy the sprite's y-position to d6
add.w #$11,d6 ; sprites are 32px tall, test collision against lower half
add.w (MEM_MAP_POSITION_Y),d6 ; adjust for scroll
adda.l #$4,a4 ; move to a4+8 -> SPRITE_DIRECTION
move.w (a4),d7 ; store direction in a7
cmpi.w #DIRECTION_UP,d7 ; test if sprite is moving up
bne.s TestDownCollision ; branch if not
sub.w #$08,d6 ; sprite is moving up, test tile 1 up from sprite
TestDownCollision:
cmpi.w #DIRECTION_DOWN,d7 ; test if sprite is moving down
bne.s TestSpriteCollisionRoundToRow ; branch if not
add.w #$08,d6 ; sprite is moving down, test tile 1 down from sprite
TestSpriteCollisionRoundToRow:
andi.b #%11111000,d6 ; clear bits 0-2 to round to nearest power of 8
suba.l #$6,a4 ; move back to a4+6 -> SPRITE_X
cmpi.w #DIRECTION_RIGHT,d7 ; test if sprite is moving right
bne.s TestLeftCollision ; branch if not
move.w (a4),d7 ; d7 is no longer needed, copy sprite x to it
add.w (MEM_MAP_POSITION_X),d7 ; adjust for scroll
add.w #$08,d7 ; sprite is moving right, test tile 1 right from sprite
bra.s TestCollisionColumn ; sprite can't move both left & right
TestLeftCollision:
cmpi.w #DIRECTION_LEFT,d7 ; test if sprite is moving left
bne.s NoHCollision ; branch if not
move.w (a4),d7 ; d7 is no longer needed, copy sprite x to it
add.w (MEM_MAP_POSITION_X),d7 ; adjust for scroll
sub.w #$08,d7 ; sprite is moving left, test tile 1 left from sprite
bra.s TestCollisionColumn ; skip default copy of (a4) to d7
NoHCollision:
move.w (a4),d7 ; left & right flows store sprite x in d7
TestCollisionColumn:
cmpi.w #$0100,d7 ; is sprite on the left or right side of the screen?
blt.s TestMapCollision ; left side, go directly to collision test
add.w #$0004,d6 ; on the right side, use 2nd lword for the row
TestMapCollision:
move.w d6,MEM_COLLISION_TEST ; copy d6 to collision test location
lea MapStoreCollision,a3 ; move address of map data to a3
adda.w (MEM_COLLISION_TEST),a3 ; move to row & col
move.l (a3),MEM_COLLISION_MAP_ROW ; copy row data to memory
move.w d7,d6 ; copy the sprite's x-position to d6
and.w #$00FF,d6 ; remove all bits over 255
divu.w #$08,d6 ; divide by 8 to get index in map data
; clear remainder from high word
; credit to https://www.easy68k.com/paulrsm/doc/trick68k.htm for this trick
swap d6 ; swap upper and lower words
clr.w d6 ; clear the upper word
swap d6 ; swap back
move.w #$0000,(MEM_COLLISION_RESULT) ; clear result
move.l MEM_COLLISION_MAP_ROW,d7 ; move map data to d7
btst.l d6,d7 ; test for collision
beq.w ExitTestSpriteCollision ; no collision, exit
move.w #$FFFF,(MEM_COLLISION_RESULT) ; collision is true, set result
ExitTestSpriteCollision:
rts
That was pretty painless all around, so why did it take me almost 3 months to publish this? Simple, I decided the store looked a little dull. After several demos we have the same bland scenery, it was time to spruce things up...
New Scenery
If the setting is going to be a store in a 1989 mall I suppose it needs some merchandise and a storefront.
To keep things simple I'm only using 3 layers. Scroll B Low will be used for tiled background scenery, Scroll A Low will be used for solid ground-level scenery, and Scroll A High will be used for the store structure and other scenery the sprite can walk under.
Let's start with creating some merchandise for the store shelves. Since I'm not very artistic I created a simple 3-tile pattern that looks like a generic game box.
The neat trick I'm trying is making this pattern a little darker with each row to create a shadow-type effect that gives the illusion of depth or something like that. I don't really know the right artistic terms to use. I accomplished that by switching palettes after each row.
Since I'm filling up the palettes I also created a spreadsheet to track what I'm using each entry for. I can still add 10 colors for the next iteration of this demo.
You can also infer that I have some idea of what the first two NPCs will look like from this. Anyway, on to the code...
DrawLowPlaneScenery:
; draw shelves with merchandise
move.w #$0065,d0 ; store base tile ID in d0
move.l #VDP_VRAM_WRITE_B,d4 ; initial address offset
; start drawing the counter at row 1, column 0 = 128
; 128 = 0080 = 0000 0000 1000 0000
add.l #$00800000,d4 ; initial address offset
move.l d4,(VDP_CONTROL) ; initial drawing location
move.w #$0002,d1 ; 3 rows
DrawLowPlaneSceneryRowLoop:
move.w #$0014,d2 ; 64 columns per row / 3 tiles in pattern
DrawLowPlaneSceneryColLoop:
move.w d0,(VDP_DATA) ; copy the pattern to VPD
add.w #$0001,d0 ; move to the 2nd tile in the pattern
move.w d0,(VDP_DATA) ; copy the pattern to VPD
add.w #$0001,d0 ; move to the 3rd tile in the pattern
move.w d0,(VDP_DATA) ; copy the pattern to VPD
sub.w #$0002,d0 ; move back to the first tile in the pattern
dbra d2,DrawLowPlaneSceneryColLoop ; loop to next tile
add.w #$2000,d0 ; increment the palette
dbra d1,DrawLowPlaneSceneryRowLoop ; loop to next tile
So now we have some shelves that don't look half-bad.
Next up let's add a frame around the store with some transparent windows in front. Here's the tileset for that:
The code to create it draws out the border first then the front. The front part is done through the DrawTileset subroutine created previously, the border is done brute-force.
DrawFrame:
move.w #$8068,d0 ; store base tile ID in d0
move.l #VDP_VRAM_WRITE_A,d4 ; initial address offset
move.l d4,(VDP_CONTROL) ; initial drawing location
move.w d0,(VDP_DATA) ; copy the first tile to VPD
; draw the top row
move.w #$8069,d0 ; store tile ID in d0
move.w #$0025,d2 ; 38 columns to draw
DrawFrameTopLoop:
move.w d0,(VDP_DATA) ; copy tile to VPD
dbra d2,DrawFrameTopLoop ; loop to next tile
move.w #$806A,d0 ; store tile ID in d0
move.w d0,(VDP_DATA) ; copy last tile to VPD
; setup for drawing the side
move.w #$806B,d0 ; store tile ID of side in d0
move.w #$001A,d2 ; 27 rows to draw
move.l #VDP_VRAM_WRITE_A,d4 ; initial address offset
add.l #$00800000,d4 ; increment drawing location to move to next row
DrawFrameSideLoop:
move.l d4,(VDP_CONTROL) ; update drawing location
move.w d0,(VDP_DATA) ; copy tile to VPD
add.l #$004E0000,d4 ; column 78 (78x4=312px)
move.l d4,(VDP_CONTROL) ; update drawing location
move.w d0,(VDP_DATA) ; copy tile to VPD
add.l #$00320000,d4 ; increment drawing location
dbra d2,DrawFrameSideLoop ; loop to next tile
DrawStoreFront:
; setup call to DrawTileset
lea PatternStoreFrontStart,a1 ; store the high starting address in a2
; tile 0 for the counter pattern should be at index 6Ch
move.w #$806C,d0 ; store base tile ID in d0
move.w #$0003,d1 ; 4 rows in the pattern
move.w #$0027,d2 ; 40 columns in the pattern
move.l #VDP_VRAM_WRITE_A,d3 ; initial address offset
; draw the high plane at row 28 (3584), column 0 (0px) = 3584 = E00
move.l #VDP_VRAM_WRITE_A,d3 ; initial address offset
add.l #$0E000000,d3 ; initial address offset
bsr.w DrawTileset ; branch to DrawTileset subroutine
This store front doesn't look half-bad either, some window signs would be nice though.
The counter has also been redesigned a little but the tileset didn't change all that much. I made it a good deal larger now that our store has a more space. There are still separate sections to add the high and low tiles.
DrawCounter:
; setup call to DrawTileset for low tiles
lea PatternCounterLowStart,a1 ; store the low starting address in a1
; tile 0 for the counter pattern should be at index 81h, palette 01, low
; 0010 0000 1000 0001 = 2081
move.w #$2081,d0 ; store base tile ID in d0
move.w #$0003,d1 ; 4 rows in the pattern
move.w #$000D,d2 ; 15 columns in the pattern
move.l #VDP_VRAM_WRITE_A,d3 ; initial address offset
; start drawing at row 10, column 28 (112px) = 1280+28 = 51C
add.l #$051C0000,d3 ; initial address offset
bsr.w DrawTileset ; branch to DrawTileset subroutine
; setup call to DrawTileset for high tiles
lea PatternCounterHighStart,a1 ; store the high starting address in a1
; tile 0 for the counter pattern should be at index 91h, palette 01, high
move.w #$A091,d0 ; store base tile ID in d0
move.w #$0006,d1 ; 7 rows in the pattern
move.w #$000D,d2 ; 14 columns in the pattern
move.l #VDP_VRAM_WRITE_A,d3 ; initial address offset
; start drawing at row 3, column 28 (112px) = 394+28 = 19C
add.l #$019C0000,d3 ; initial address offset
bsr.w DrawTileset ; branch to DrawTileset subroutine
Hmmm... maybe this is too large now, or maybe it will be fine whenever I add a cash register to it.
The store looks better but is still kinda bland, I should add some racks or displays in the store too. Maybe I'll save that for next time.
It seems more likely though that my next iteration will include creating some kind of data structure to store all the layers of room rather than brute-force creating them like I'm doing now. There are tons of small things to fix too like some clipping and timing issues.
I also want to redesign the sprite since it looks way too much like a Phantasy Star II sprite.
So what will I tackle next time? One of these ideas or something completely different? Check back in some arbitrary time period to find out...
Download
Download the latest source code on GitHub
Related