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: http://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

      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 http://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




Tweet