Jump to content
IGNORED

Storing uncompressed level data.


Recommended Posts

Hello fellow quarantees. Like I'm sure some of you are doing, I've been using this odd downtime to further my NESDev/6502 ASM skills. I've gotten to a point where I can render and move sprites around, render backgrounds, and scroll both vertically and horizontally (not at the same time) to loop between 2 backgrounds. I understand how to store tile and attribute data for individual backgrounds as the starting from the top and going left to right until the bottom format makes sense to me. I'm having trouble wrapping my head around storing full levels or at the very least more than 2 BGs at a time as a singular entity.

 

I tried the "Nerdy Nights" lesson where it scrolls across part of the first level of Mario in a loop. It comes with 2 .bin files that contains the level and one for the attributes. Unlike my standard background asm files, it's just raw hex data so I opened it in a hex editor but the layout of the tiles didn't quite make sense to me. It seemed to no longer use that Top Left, line by line to the Bottom Right format. Can someone explain to me just how to store a Raw Uncompressed set of backgrounds? I realize this isn't ideal and will absolutely change to a metatile based approach and eventually other compression formats, but I just can't seem to understand those without knowing how to lay it all out without compression. I'm one of those people who likes to really know a concept inside and out and benefits greatly from that in the long run, so any help here is greatly appreciated. My goal is to get this down so that I can eventually make my own level editing tools to speed up the process.

Link to comment
Share on other sites

It may actually be easier for you to convert to metatiles before handling scrolling. That way you can create a routine that works with what you want instead of writing something, throwing it out, and writing it again. Writing a routine that draws a single metatile to the screen on each frame as you move forward is actually a pretty intuitive way to get started with scrolling as the timing works out pretty nicely.

Each metatile is 16 pixels wide. The NES can display 16 metatiles wide and 15 metatiles high. That means if you scroll by one pixel, drawing at least one 2x2 metatile per frame will be fast enough to keep up with your movement. For vertical scrolling, draw the metatiles in rows. For horizontal, draw them in columns and skip every 16th frame. If you want to move faster, draw at least as many metatiles as you are advancing in pixels. That'll give you a basic scrolling engine to start out with. Just make sure to update the attributes as you go.

If you're set on doing it with uncompressed levels, you can do it using the same logic, drawing rows or columns of tiles as you move. You'd need to draw 4 tiles for every pixel that you move forward, making sure to update attributes of course. Metatiles are just really convenient to work with when it comes to the attribute tables because they line up perfectly.

  • Thanks 1
Link to comment
Share on other sites

7 hours ago, toma said:

It may actually be easier for you to convert to metatiles before handling scrolling. That way you can create a routine that works with what you want instead of writing something, throwing it out, and writing it again. Writing a routine that draws a single metatile to the screen on each frame as you move forward is actually a pretty intuitive way to get started with scrolling as the timing works out pretty nicely.

Each metatile is 16 pixels wide. The NES can display 16 metatiles wide and 15 metatiles high. That means if you scroll by one pixel, drawing at least one 2x2 metatile per frame will be fast enough to keep up with your movement. For vertical scrolling, draw the metatiles in rows. For horizontal, draw them in columns and skip every 16th frame. If you want to move faster, draw at least as many metatiles as you are advancing in pixels. That'll give you a basic scrolling engine to start out with. Just make sure to update the attributes as you go.

If you're set on doing it with uncompressed levels, you can do it using the same logic, drawing rows or columns of tiles as you move. You'd need to draw 4 tiles for every pixel that you move forward, making sure to update attributes of course. Metatiles are just really convenient to work with when it comes to the attribute tables because they line up perfectly.

I get what you are saying about creating a routine I'm actually going to keep, but I'm intentionally making a throwaway. For me, this is purely an academic exercise to help me truly understand what's going on. I'll post some code and then try and be more specific with my hang up.

So right now, I have 2 bg files that I .incsrc into the main file that contain the name and attribute table data for each one. One looks like this:

 

background:
  .db $00,$00,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D  ;;row 1
  .db $2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$00,$00  

  .db $00,$01,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D  ;;row 2
  .db $2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$00,$01  

  .db $00,$02,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D  ;;row 3
  .db $2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$00,$02  

  .db $00,$03,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D  ;;row 4
  .db $2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$00,$03  

  .db $00,$04,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D  ;;row 5
  .db $2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$00,$04  

  .db $00,$05,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D  ;;row 6
  .db $2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$00,$05  

  .db $00,$06,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D  ;;row 7
  .db $2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$00,$06  

  .db $00,$07,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D  ;;row 8
  .db $2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$00,$07  

  .db $00,$08,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D  ;;row 9
  .db $2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$00,$08  

  .db $00,$09,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D  ;;row 10
  .db $2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$00,$09  

  .db $00,$0A,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D  ;;row 11
  .db $2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$00,$0A  

  .db $00,$0B,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D  ;;row 12
  .db $2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$00,$0B  

  .db $00,$0C,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D  ;;row 13
  .db $2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$00,$0C  

  .db $00,$0D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D  ;;row 14
  .db $2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$00,$0D  

  .db $00,$0E,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D  ;;row 15
  .db $2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$00,$0E  

  .db $00,$0F,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D  ;;row 16
  .db $2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$00,$0F  

  .db $01,$00,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D  ;;row 17
  .db $2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$01,$00  

  .db $01,$01,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D  ;;row 18
  .db $2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$01,$01  

  .db $01,$02,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D  ;;row 19
  .db $2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$01,$02  

  .db $01,$03,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D  ;;row 20
  .db $2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$01,$03  

  .db $01,$04,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D  ;;row 21
  .db $2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$01,$04  

  .db $01,$05,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D  ;;row 22
  .db $2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$01,$05  

  .db $01,$06,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D  ;;row 23
  .db $2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$01,$06  

  .db $01,$07,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D  ;;row 24
  .db $2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$01,$07  

  .db $01,$08,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D  ;;row 25
  .db $2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$01,$08  

  .db $01,$09,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D  ;;row 26
  .db $2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$01,$09  

  .db $01,$0A,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D  ;;row 27
  .db $2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$01,$0A  

  .db $01,$0B,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D  ;;row 28
  .db $2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$01,$0B  

  .db $01,$0C,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D  ;;row 29
  .db $2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$01,$0C  

  .db $01,$0D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D  ;;row 30
  .db $2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$2D,$01,$0D  

attributes:
  ; BR BL TR TL
  ;     TopLeft                                                                     TopRight
  .db %00010000, %00000000, %00000000, %00000000, %00000000, %00000000, %00000000, %01000000
  .db %00110010, %00000000, %00000000, %00000000, %00000000, %00000000, %00000000, %11001000
  .db %00010000, %00000000, %00000000, %00000000, %00000000, %00000000, %00000000, %01000000
  .db %00110010, %00000000, %00000000, %00000000, %00000000, %00000000, %00000000, %11001000
  .db %00010000, %00000000, %00000000, %00000000, %00000000, %00000000, %00000000, %01000000
  .db %00110010, %00000000, %00000000, %00000000, %00000000, %00000000, %00000000, %11001000
  .db %00010000, %00000000, %00000000, %00000000, %00000000, %00000000, %00000000, %01000000
  .db %00110010, %00000000, %00000000, %00000000, %00000000, %00000000, %00000000, %11001000

So in here I just have raw tile and attribute data that I loop over using the whole 16bit pointerHi/Lo method to fill the nametable. I understand completely how this method of holding background data in a file works. Each 2 line chunk being a row of 32 tiles starting from the top and going left to right per row for 30 rows. I also understand the attribute portion for setting the palettes for the 2x2 tile chunks and how each byte controls the palette selection for a 2x2 set of 2x2 tile chunks with every 2 bits controlling a separate quadrant. The concept of incrementing the counter by 32 combined with a horizontal offset in order to reference this as columns makes sense to me.

The issue I'm having is that I'm currently using a separate file per screen, and loading them separately into their respective nametables using a system of 4 loops (2xNT, 2xAT) prior to enabling NMI and starting the game loop, and then just incrementing the scroll register once per v-blank, checking each time if the scrollX position has rolled over so I can swap the nametable bit in PPU_CTRL and have a seamless loop. This is fine but not ideal for 2 screens, but I just don't get how I would store in a single file the tile map data for multiple screens and their attributes. I know I can do a label per screen but I want to have multiple screens under 1 label and be able to just scroll until I run out of background data and loop back around.

So I guess question 1 is what is the multiple screen equivalent of the raw tile/attribute byte storage method I have above? And question 2 would then be about continuously scrolling through, although I feel like if I knew how the data was stored I wouldn't have too much of an issue figuring out the column based bg data method of rendering.

Link to comment
Share on other sites

I wouldn't want to program an uncompressed level. But, if I did, I would have a pointer to each data set. Let's say  X scrolling multiple rooms. Keep track of X as a 16 bit variable. Draw the right side as you scroll by adding 0x100 to the scroll, use the upper byte as the room #, which directs us to the correct data set. Use the lower byte >> 3 as the index to the data set. (32 tiles wide rooms)... and attributes, 8 attribute blocks wide, would be lower byte >> 5 as the index to the attribute table for that room.

But I would prefer metatiles of 16x16, so the data for each room could fit in 240 bytes, which simplifies the math also, but allows for significantly more levels to be stored in ROM.

 

Edited by dougeff
  • Thanks 1
Link to comment
Share on other sites

  • 4 months later...

In some ways, uncompressed levels could be the simplest thing that could possibly work. 
Assuming you base collision properties off tile ID:s, collision becomes very direct. For instance, it is safe to assume that a blank tile at position $00 in the tile space is always a noncollision, so you can skip out testing this majority case with a bne and continue with the rest. then, if you've sorted your tiles according to their collision properties, you can check for ranges of tile ID:s. 

Different attributes can even have overlapping ranges, if you use a method like so:
 

.macro getInRange lo, hi 
    ;expects value in A
    ;returns carry if in range 
    ;a is not preserved
    ; 6 cycles, 5 bytes

    ; other assumptions: 
    ; lo must be higher than 0.
    ; hi must be higher than lo. 
    ; hi must be lower than #$ff
    
    clc 
    adc #$ff - hi
    adc #hi - lo + 1 
.endmacro 



of course, the biggest problem with uncompressed levels are the sheer size - just one full screen = 1kB. A typical homebrew cartridge might hold 512kB since that costs roughly the same as any smaller size, so there is space. But it's not endless. 

Link to comment
Share on other sites

As for NESST, i cannot praise it highly enough. It doesn't roll out a red carpet or welcome mat (reading the readme a couple of times between the first couple of sessions really hepls), but it is very efficient to be able to seamlessly work on a layout and on the tileset interchangably and let one process inform the other and vise versa. 

I used to draw tilesets first, then apply them. Once i learned how to use NESST, it became evident that i don't know what a good tileset will be until i'm able to try it out. So now, i start on layouts and tiles simultanously, or sometimes i even plot out basic monocolour tiles for layout to give me some values to reference when making the tiles. It feels great. 

The key bindings and mouse button actions are quicker than your average pixel editor to work with. It's very smooth once you get the hang of it.

Link to comment
Share on other sites

On 4/10/2020 at 12:12 AM, MachineCode said:

I just don't get how I would store in a single file the tile map data for multiple screens and their attributes.

Think of it like this. Each of the screens you want to interlock are just bytes. Each screen is ONLY a stream of bytes. So let's say you wanna go from left to right. Take your hex editor, and move the boundaries of the hex program so that you see 32 bytes horizontally across the screen for each row. Copy and paste all those bytes, so that you have 32 bytes on each row. After that, open the next nametable in the hex editor. Grab the first 32 bytes, and place them IMMEDIATELY after the first set of 32 bytes from the prior nametable. Now do the same thing for each successive row. That way you have the "nametables" aligned and right next to each other. Leave a whitespace in between each one so you can actually visualize it by looking at the code/bytes. Like this (notice the whitespace):

.byte $00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,  $00,$00,$00,etc. for 32 again...

I did something similar to this in my Rise of Amondus code... separated the tables so that I could visually picture it (this is in metatiles, but the same concept applies):

https://github.com/SlyDogStudios/riseofamondus/blob/master/includes/bankFixed/amondus_levels.asm

Now, accessing this data is different than just storing it, which is what I think you're trying to sort out. When the tables are over 255 bytes in size, you need to use a formula to be able to manipulate the data or read it. I got this formula from Kevtris years ago, and like he did, I'll leave it to you to sort it out. It helped me a ton, and I'm sure it will help you too. I'm gonna try and attach it, but I'm new here, so I dunno if it will work haha

formula.png

Link to comment
Share on other sites

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now
 Share

×
×
  • Create New...