Sega Genesis Programming Part 6: Vertical Scrolling


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.

Collision map

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:

Moving down

And when moving up the counter scrolls back into place:

Moving up

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.

Tileset for shelves

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.

Palette map for 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.

Store shelves

Next up let's add a frame around the store with some transparent windows in front. Here's the tileset for that:

Tileset for frame

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.

Store front

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.

Store counter

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