Jump to content

Game Engine Building #6: Background Set Up, CHR_RAM, and Room Indexing


Recommended Posts


This write up is pretty straight forward.  It basically merges our Sprite Collision program with our Background Compression program.  There are some changes here and there, but that's basically it.  I've added some extra stuff to give this part some content though.  It should be helpful to you. 


Well, the first thing that we need to do is merge the programs.  This is pretty much a cut/paste job that you can walk yourself through.  Just start at the top and work your way down.  I'll leave it to the user to figure this out as it really is simple.


First, you'll notice that the Sprite Collisions uses CHR_RAM and the Background Compression uses the standard CHR banks.  We'll we are going to use the CHR_RAM technique because it is much more interesting in my opinion.  First thing that we need to do is make sure that the .ineschr value is "$00" because we don't have any CHR banks.  All of our CHR data is stored in the program, not in seperate CHR banks.

Next, we need to add this to our RESET routine:


  LDX #$00
  JSR LoadCompleteBank              ;load the sprite data

  LDX #$02
  JSR LoadCompleteBank              ;load the background data


In the Sprite Collisions we were only using the sprite table, so we hardcoded the addresses.  Here we have 2 banks to mess with so we'll need some sort of pointer set up to reference the graphics we want to load.  So, we set up a address table as follows:


graphicspointers:                 ;addresses of the CHR Data
  .word Sprite_Data,CHR_Data


What does this reference?  Well, now we get into how to include the graphics files into your program without actually having to enter all the numbers in.  Thankfully, we can use the tools and CHR files that we've already made through our friend Tile Molestor.  face-icon-small-smile.gif  Now, you don't have to include entire banks, you can do partial banks, or even one tile using this method, but we'll get into that later.  Here, because we're lazy, we are just going to keep including the entire CHR data banks.  So, we need to do as follows in the various banks of our program data:



  .bank 2
  .org $C000


  .db $10,$00                             ;background address in the PPU

  .incbin "CHR_Files/mario1.chr"


  .bank 3
  .org $E000


  .db $00,$00                             ;sprite address in the PPU

  .incbin "CHR_Files/SpriteMovement.chr"  ;include the sprite graphics data


  .org $FFFA     ;first of the three vectors starts here
  .dw NMI        ;when an NMI happens (once per frame if enabled) the
                   ;processor will jump to the label NMI:
  .dw RESET      ;when the processor first turns on or is reset, it will jump
                   ;to the label RESET:
  .dw 0          ;external interrupt IRQ is not used in this tutorial



Before we used the .include command.  .include is for additional code that we want the assembler to incorperate into our program.  .incbin is "include binary file".  Basically, just include data. 

You'll also note that there are 2 numbers before we say .incbin but after the address labels we used in our table above.  This is the address that we want the CHR data loader to start writing data to.  We have to tell it where to put the data or it will just put it wherever the hell it wants to.  You can use whatever you want, but here we are putting our sprite data at PPU address #$0000 and background data at #$1000.  Hence the labels.

Well, what do we do with it?  This is pretty much exactly the same as writing tiles to get background as you would do when loading a room.  Just remember, you don't have to turn off NMI to write to the CHR space, but you MUST either do it during NMI or with the background off.  Otherwise your program will frac out. 

Next, we need to introduce a new pointer.  This is the pointer to the current tile data to be loaded.  We will just use:


tile_loader_ptr  .rs 2   ;pointer to the tile data for our CHR_RAM loading routine


Next, we need to write a simple program to simply load the data to $2007.  Note that I haven't tried it, but in this form, it looks like without simple modification, this technique will only work if you start your CHR data at the beginning of a .bank XXXX.


LoadCompleteBank:                 ;load in the graphics

  LDA graphicspointers,x          ;specify the address using the first two entries before the .incbin commands
  STA tile_loader_ptr             ;and the value we load into "X" before calling the LoadCompleteBank routine.
  LDA graphicspointers+1,x
  STA tile_loader_ptr+1

  LDY #$00                        ;start with zero
  LDA $2002                       ;read PPU status to reset the high/low latch
  LDA [tile_loader_ptr],y
  STA $2006                       ;write the high byte
  INC tile_loader_ptr             ;go to the next entry
  LDA [tile_loader_ptr],y
  STA $2006                       ;write the low byte
  INC tile_loader_ptr             ;next table entry 
  LDX #$00
  LDY #$00
  LDA [tile_loader_ptr],y         ;load the value
  STA $2007                       ;store it to the PPU at the address we previously specified
  INY                             ;next Y value
  CPY #$00                       
  BNE .LoadBank                   ;if Y flips over back to zero, we need to change our pointer values
  INC tile_loader_ptr+1           ;next chunk of ROM loaded
  INX                             ;incriment our outer loop counter
  CPX #$10                        ;if X=$10, we are done, if not, jump back into the loop
  BNE .LoadBank



Simple.  Note again that this is for an entire bank of CHR information.  We'll see later how to change this for smaller data sets.


Now that we've got all our program ready for input, we need to work out a way of switching rooms.  Before we were using the D-PAD to switch rooms.  Now the D-PAD controls character movement and the rest of the buttons are pretty much spoken for.  So....??  Well, how about making the PC sprite switch rooms when you hit the edge of a screen?  Let's do it. 

First, we need to set some arbatrary constants that will trigger our room switching.  These can be whatever you want.  Here, we are going to use:


top_exit = $0F        ;room switching locations
bottom_exit = $CF
left_exit = $0F
right_exit = $E1


Next, we need to define some sort of map for our rooms.  We only have Four Rooms like Tim Roth, so our map is pretty simple.  This can be expanded to up to 256 rooms without too much trouble.  Here is our map layout:

0 Room0  Room2
1 Room1  Room3

Note that Y values increase as you go down and X values increase as you go to the right.  Now that we have this idea in our heads, we need to define variables to track our current location in the grid. 


Y_coord  .rs 1                ;Y map coordinate
X_coord  .rs 1                ;X map coordinate


Again, Y is your vertical location in the map and X is your horizontal location in the map.  Make sure you understand this or you'll screw it up and get lost.  I speak from experience.  Now, we pull out our fancy pointer table that specifies the location of the room data that we had before.


backgroundpointer:                     ;room data for our room_index to reference to
  .word Room1,Room2,Room3,Room4


Now, remember the value "room_index" that we used in the background switching routine?  Well, not it's not set in our switching commands, so we need to find it using our Y_coord and X_coord values.  Let's write the following:


  LDA Y_coord                            ;load the coordinates we set in our movement routines
  LDX X_coord

  CPX #$00                               ;if we are in the first column, the Y coord is the room index
  BEQ .done

.loop:  ;for each X, add 2 to the index  ;note that this would change depending on your map configuration
  CLC                                    ;there are only 2 values that Y can take on, so we use 2. 
  ADC #$02                               ;if there were values 0-5 that Y could be, we'd use 6, etc.
  CPX #$00
  BNE .loop


  ASL A                                  ;indexing to a table of words...careful here!!!  See **.

  STA room_index                         ;store it in the variable and we're done

**If you have more than 128 rooms and you double it, you'll over run and flip back past zero.  It is a simple problem to get around, just keep it in mind. 


This is a very simple routine, but it took me a while (and a lot of help from metalslime) to get it working properly.  Study it and make sure you understand what we're doing.  Add more rooms and modify it to fit.  It's powerful, but can be a tricky bastard. 

The rest of the background loading is exactly the same as the Compression write up.

Now we need to set up our program to call the routine when we want to switch rooms.  Again, nothing too complex.  First, note that all four directions are pretty much the same, so we'll just go over one to save time.  Modifying our "UpMovement" routine, we write:


  LDA #$00                        ;load the appropriate direction into the direction flag
  STA enemy_direction

  LDA sprite_RAM                  ;move the character
  SBC #enemy_speed
  STA sprite_RAM

  INC Enemy_Animation             ;incriment the animation counter


  LDA sprite_RAM                  ;check to see if we are at the top of the screen
  CMP #top_exit
  BCS .done                       ;if not, we are done.  If so, we need to switch rooms

  DEC Y_coord                     ;DEC the Y coordinate of the world map

  LDA #$BF                        ;move our character so that it looks like he is walking across the screen
  STA sprite_RAM                  ;this is another random value, set whatever you like

  JSR update_enemy_sprites        ;update the sprite meta tile
  JSR LoadbgBackground            ;using the new Y_coord, update the background to the new room





Simple like NGD.  Hmmm...what else??  Oh, yes.  Remember that this routine is set up to switch rooms any time you impact an edge of the screen.  There is currently nothing stoping you from entering a room that doesn't exist.  Until we fix this little issue, just notice where the background is set up to have solid walls...and try not to walk through them. 


Well, given my status as a noob, I screwed up the placement of the call for the showCPUUsageBar command.  We need to put it at the end of our Forever loop to show the end of the program.  You can put in as many bars as you like, but I'm mostly interested in how long the total thing takes to run.  You'll notice that it starts to show up here.  Our program is getting some teabag.gif to it.  face-icon-small-smile.gif


Now that we have the tools in place to mess with CHR_RAM, we can add some interesting effects.  Most of the CHR_RAM effects can be done using MMC3 CHR bank switching faster and easier than this, but I'm too lazy to figure out MMC3 and it won't work on the powerpak anyway.  The quickest example that comes to mind are the moving ? marks in Mario 3.  Open the ROM and look at the PPU viewer.  You'll see the bottom part of the background data change instantly every frame.  This is the bank switching that I'm talking about. 

The technique that we are using doesn't happen instantly like bank switching does, so we have to be careful not to run out of NMI.  You can load something like 8-9 tiles during each NMI if you code it efficiently enough.  I've only been able to get 6 to work (on NTSC, PAL works a lot better for this stuff)  and I need time for other background updates, so I stick to 3 or 4 tiles per frame.  In this example, we are going to use 4 tiles/frame and attempt to duplicate the scrolling ? marks of Mario 3 with Mario 1 graphics.  face-icon-small-happy.gif  Awesomeness.

The first step is simple.  We need to create CHR data for each frame of the animation that we want to use.  Tile Molestor to the rescue!!  Simply create a new file using the methods discussed in a previous write up and you'll be on your way.  Simply cut and paste the 4 tiles that make up the ? mark block out of our existing CHR file and paste it into your new one.  Save it.  Then use the handy button that Tile Molestor that shifts all the pixels in the area that you highlight one pixel right/left/up/down.  This is a handy button set, up to this write up, I've been entering every frame manually.  face-icon-small-tongue.gif  Then save the next frame as a new file, and so on till you get every frame saved in its own file.  I guess that I should point out that you could technically store all of this in one file and just specify the appropriate starting address, but I like it this way so blah.

After we've created all of our files for each frame, we need to include them in our program with some sort of word table to pull addresses from.  Simply:


  .word Question1,Question2,Question3,Question4,Question5,Question6,Question7,Question8
  .word Question9,Question10,Question11,Question12,Question13,Question14,Question15,Question16

Question1:                                      ;this is all the data for the various ? block frames
  .incbin "CHR_Files/Question1.chr"

  .incbin "CHR_Files/Question2.chr"

  .incbin "CHR_Files/Question3.chr"

  .incbin "CHR_Files/Question4.chr"

  .incbin "CHR_Files/Question5.chr"

  .incbin "CHR_Files/Question6.chr"

  .incbin "CHR_Files/Question7.chr"

  .incbin "CHR_Files/Question8.chr"

  .incbin "CHR_Files/Question9.chr"

  .incbin "CHR_Files/Question10.chr"

  .incbin "CHR_Files/Question11.chr"

  .incbin "CHR_Files/Question12.chr"

  .incbin "CHR_Files/Question13.chr"

  .incbin "CHR_Files/Question14.chr"

  .incbin "CHR_Files/Question15.chr"

  .incbin "CHR_Files/Question16.chr"


Note here that if you have more than one place to write the various tiles to, you'll need to include some sort of "write to" address table.  We're not going to cover this here as it's fairly easy to modify.

Now that we have all of our files lined up like so, we need some sort of thingie to input them to the background.  Now we need to come up with the starting PPU address to write the tiles to.  Hmmm?  Break out the pencil and paper and do some math?  Nope.  Lazy people unite!!  If you open your ROM and open the PPU, there is a simple trick to finding PPU addresses in CHR space.  Remember that we are using $0000 as sprites and $1000 as background.  If you put your mouse over the tile where you want to write to, it will display the tile number at the bottom.  This is all you need:

-Writing to tile $56 in the sprite space?  The starting address is $0560.
-Writing to tile $F4 in the background space?  The starting address is $1F40.
-Writing to tile $56 in the background space?  The starting address is $1560.

See the pattern??

Turns out that we want to start writing our ? mark data to tile $53, so we start with PPU address $1530.

Now we have everything set up and all we need to do is write a quick code snippet and we're done.  Technically you could use the same code for the complete bank here, but we are going to do this because I like it.  Remember:  NMI time is special.  There's not much of it and more than likely, this CHR_RAM stuff is not the only thing happening during that time.  So, we need to get everything set and ready to go so all NMI has to do is send data to the PPU.  MetalSlime does this in his tutorials by buffering writes.  I'm going to do this in a little different way, but it's basically the same principal.  At the end of the main program, we will set up all the pointers and everything for out next frame of animation.  Then when NMI hits, we simply write the address to $2006 and dump the data into $2007.  Let's set up our pointers.  To do that we use the following routine:


PartialBankSetUp:                  ;load the address that we want to write to.  See the tip in the write up.
  LDA #$15                        
  STA tile_loader_addy
  LDA #$30
  STA tile_loader_addy+1

  LDA #$40                         ;load the number of entries.  #$10 for each tile to load.
  STA tile_loader_stop

  LDA tile_loader_counter          ;load the pointer to the information
  LDA TileUpdates,X
  STA tile_loader_ptr
  LDA TileUpdates+1,X
  STA tile_loader_ptr+1

  INC tile_loader_counter          ;handle the counters, so that the next frame loads next time
  INC tile_loader_counter+1        ;This variable is used when you are loading a different number of tiles
  INC tile_loader_counter+1        ;to different addresses.  Not really needed here.
  INC tile_loader_counter+1

  LDA tile_loader_counter          ;reset if we get to the last frame.
  CMP #$10
  BNE .done
  LDA #$00
  STA tile_loader_counter
  STA tile_loader_counter+1



Step by step, here's what we are doing here:

1.  We need to specify the address that we want to send the CHR data to.  This is hard coded here because we are ONLY WRITING TO ONE PLACE!!  Normally, you'd set up a table of addresses for the program to write to.  This is the function of the currently useless "tile_loader_counter+1" variable, to pull data from this table. 

2.  Specify the number of tiles that we want to send to the PPU.  Remember from previous lessons that writing one tile is $10, 2 is $20, etc.  Again, you'd have a table with the quantities of tiles to be loaded each frame.  Another function of the "tile_loader_counter+1" variable.

Note:  You can see that tile_loader_counter+1 is INC 3 times per frame.  If you wanted to set this up a little more complicated using this other variable, you'd do this:


  LDX tile_loader_counter+1                  ;load the addresses
  LDA LoaderSpecs,X
  STA tile_loader_addy
  LDA LoaderSpecs+1,X
  STA tile_loader_addy+1

  LDA Specs+2,X               ;load the number of entries
  STA tile_loader_stop


  .db $15,$30,$40,etc for each frame


3.  Using the frame counter, tile_loader_counter, you load the starting address for the next CHR data block into the data pointer using the "TileUpdates" table that we laid out before.  

4.  Set up the frame counter and address/quantity data counter for the next animation frame.

5.  Reset the counters if you are on the last frame of animation.

We should now be set up to load our data into the PPU during the next NMI.  So, we write the shortest, most efficient sub-routine we can make and run it during NMI.  Simply:



  LDA $2002
  LDA tile_loader_addy             ;input the address
  STA $2006
  LDA tile_loader_addy+1
  STA $2006
  LDY #$00
  LDA [tile_loader_ptr],y          ;load the data to the CHR space DURING NMI
  STA $2007
  CPY tile_loader_stop
  BNE .LoadBank



That's it.  Now we should have a scrolling ? mark block everywhere we specify it in our background.


So, now all our room data should look the same.  A meta tile bank specification, a nice rectangle of meta tiles, then a $FF signifying the end of the data string.  Basically:


  .db %11000000
  .db $08,$03,$03,$04,$04,$04,$03,$03,$03,$03,$04,$04,$04,$03,$03,$03
  .db $08,$06,$06,$06,$06,$06,$06,$06,$06,$06,$06,$06,$06,$06,$06,$06
  .db $08,$06,$06,$06,$06,$06,$06,$06,$06,$06,$06,$06,$06,$06,$06,$06
  .db $08,$04,$04,$04,$04,$04,$04,$06,$06,$04,$04,$04,$04,$04,$04,$06
  .db $08,$06,$06,$06,$06,$06,$06,$06,$06,$06,$06,$06,$06,$06,$06,$06
  .db $08,$00,$06,$00,$06,$00,$06,$00,$00,$06,$00,$06,$00,$06,$00,$06
  .db $08,$02,$02,$02,$02,$02,$06,$02,$02,$06,$02,$02,$02,$02,$02,$06
  .db $08,$03,$03,$04,$04,$04,$06,$03,$03,$06,$04,$04,$04,$03,$03,$06
  .db $08,$06,$06,$06,$06,$06,$06,$06,$06,$06,$06,$06,$06,$06,$06,$06
  .db $08,$04,$04,$06,$04,$04,$04,$04,$04,$04,$04,$04,$06,$04,$04,$06
  .db $08,$06,$06,$06,$06,$06,$06,$06,$06,$06,$06,$06,$06,$06,$06,$06
  .db $08,$06,$06,$06,$06,$06,$06,$06,$06,$06,$06,$06,$06,$06,$06,$06
  .db $08,$05,$06,$05,$06,$06,$05,$06,$06,$05,$06,$06,$05,$06,$05,$06
  .db $08,$07,$07,$07,$07,$07,$07,$06,$06,$07,$07,$07,$07,$07,$07,$07
  .db $08,$08,$08,$08,$08,$08,$08,$06,$06,$08,$08,$08,$08,$08,$08,$08
  .db $FF


See how it is nice and even and perfect for looking up data?  Sweet.  And it still takes us from 960 bytes per room to 240, a size reduction of 75% (not counting meta tiles). 

Then we go back and make our room data look like this, then we're set for our next challenge.


Next time we will tackle simple collision detection with the background.  This will basically be the money write up, the place where most people get stuck.  Stay tuned and post comments/questions if you have them.  If you don't understand everything to date, the next stuff will be trouble for you. 

Until next time...teabag.gif.  Thanks for reading.


Edited by MRN
Fix pics.
  • Like 1
Link to comment
Share on other sites

  • Create New...