Game project help and progress thread

A place where you can keep others updated about your NES-related projects through screenshots, videos or information in general.

Moderator: Moderators

User avatar
tokumaru
Posts: 12427
Joined: Sat Feb 12, 2005 9:43 pm
Location: Rio de Janeiro - Brazil

Re: Game project help and progress thread

Post by tokumaru »

Tsutarja wrote:What would be a good way of decompressing this? I probably need to have some kind of pointers in RAM that keeps track of things like current stage, current screen, etc. so the game knows where to look for the metatiles.
I'd do it on the fly... breaking down the coordinates (as I'll explain below) you can tell which screen to load from the level map. Then, using another part of the coordinate, you can tell which column of metatiles in that screen you have to read from. Then you can use the final part of the coordinate to know how many pixels into the metatile that coordinate is, and you can use this information for collision tests and such.

Accessing a level, be it for scrolling/rendering or for collision detection, is about using coordinates and interpreting the "fields" in those coordinates in order to know where to read your data from. You don't need specific variables for "current screen" and such, that information can be deduced from the coordinates you're using to access the map.
I will probably update VRAM (when scrolling) so that I update 2 metatiles every frame the VRAM needs to be updated. So, it takes 4 frames to update one vertical row of metatiles. The player movement speed is around the same speed than what castlevania has, so I guess the updating speed is not too slow.
If it takes you 4 frame to update a 32-pixel wide column, that's equivalent to 8 pixels per frame, which should be enough for most games. Even Sonic, without speed shoes, runs slower than 8 pixels per frame, so you should be safe. Just be sure to have an area with valid graphics wider than the screen, so you don't scroll into incomplete columns. For example, here's a 2-screen section of the map, with the camera hovering over it:

Code: Select all

                  CAMERA/SCREEN
        |-------------------------------|
+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+
| U | P | V | V | V | V | V | V | V | V | P | U |   |   |   |   |
+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+
| U | P | V | V | V | V | V | V | V | V | P | U |   |   |   |   |
+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+
| U | P | V | V | V | V | V | V | V | V | P | U |   |   |   |   |
+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+
| U | P | V | V | V | V | V | V | V | V | P | U |   |   |   |   |
+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+
| U | P | V | V | V | V | V | V | V | V | P | U |   |   |   |   |
+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+
| U | P | V | V | V | V | V | V | V | V | P | U |   |   |   |   |
+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+
| U | P | V | V | V | V | V | V | V | V | P | U |   |   |   |   |
+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+
| U | P | V | V | V | V | V | V | V | V | P | U |   |   |   |   |
+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+
The metatiles marked "V" are visible, and once the screen starts scrolling, left or right, the metatiles marked "P" would become partially visible, so they should be properly loaded before that happens. And when it does happen, the metatiles marked "U" should start being updated, so that they're ready when the camera reaches it.

One good way to go about this would be to update two metatiles every time the camera crosses an 8-pixel boundary (look for changes in bit 3 of the camera's coordinate - whenever it changes from 1 to 0, or vice-versa, an 8-pixel boundary was crossed), that way you wouldn't occupy the majority of you VRAM bandwidth with metatiles for 4 consecutive frames (unless the player was running at 8 pixels per frame), giving other updates more opportunities to run (such as pattern table updates, if you ever do that).

I'd use the camera position not only to decide WHEN to load new metatiles, but also to find out WHICH metatiles to update. If you break down the camera position, the bits have the following meanings:

Code: Select all

SSSSSSSS MMMPPPPP
SSSSSSSS: Index of the screen within the stage;
MMM: Index of the column of metatiles within the screen;
PPPPP: Index of the column of pixels within the metatile;
Like I said before, you can detect changes in bit 3 to know when to draw a new pair of metatiles. Now consider that the camera is moving right, and this value is increasing: Since metatiles are 32 pixels wide, the camera will go through 4 8-pixel boundaries before crossing a metatile boundary. This means you can consider bits 3 and 4 of the camera's coordinate as the index of the pair of metatiles to update. For example:

Code: Select all

	lda CameraX+0 ;compare the current position...
	eor OldCameraX+0 ;...against the old position
	and #%00001000 ;keep only the bit of interest
	beq NoMetatileUpdate ;skip if it didn't change
	lda CameraX+0 ;get the position again
	and #%00011000 ;find out how close the camera is to the next metatile boundary
	lsr
	lsr
	lsr
	;OMITTED: USE THIS VALUE TO KNOW WHICH OF THE 4 PAIRS OF METATILES IN A COLUMN TO UPDATE;
	;OMITTED: DECODE THE METATILES AND FILL THE UPDATE BUFFER;
NoMetatileUpdate:
For now I can probably pretty safely say that I can allocate $0700 - $07FF for map RAM (overkill?). I'm assuming that I need to decompress the data to the map RAM and then push it to the "buffer stack".
Unless you want to compress screens or stages even further in the ROM using something like RLE, I don't think you need any RAM at all for maps. This format is friendly enough to be read straight from the ROM.
User avatar
tokumaru
Posts: 12427
Joined: Sat Feb 12, 2005 9:43 pm
Location: Rio de Janeiro - Brazil

Re: Game project help and progress thread

Post by tokumaru »

Just noticed something in your proposed level format:

Code: Select all

.db Metatile00
You can't do it exactly like this because Metatile00 is not a byte, but a 16-bit address.

You could use .dw instead, but if you have 256 metatiles or less that would be a waste of space. So one possible solution is to use a list of metatile addresses that you can look up using their indices:

Code: Select all

MetatileAddressLo:
	.db <Metatile00, <Metatile01, (...)

MetatileAddressHi:
	.db >Metatile00, >Metatile01, (...)
Then you could read the top right tile of metatile 25 like this:

Code: Select all

	ldy #25
	lda MetatileAddressLo, y
	sta Pointer+0
	lda MetatileAddressHi, y
	sta Pointer+1
	ldy #03 ;the top right tile is the 4th byte in the metatile data
	lda (Pointer), y
Another option is to make sure that the size of the data is always a power of 2 (i.e. each screen is always 64 bytes/metatiles) and properly aligned in memory, so that you can quickly calculate the address based on the index, without needing a table:

Code: Select all

	lda #$00
	sta Pointer+0
	lda #17 ;screen 17
	lsr ;multiply by 64
	rol Pointer+0
	lsr
	rol Pointer+0
	adc #>Screen00 ;add the base address
	sta Pointer+1
Making sure that the size of the data is a power of 2 might not be easy in the case of metatiles, since you plan to add collision and palette info to the 16 bytes you already have. Padding that to 32 bytes would be a monumental waste of space, so you might want to go with the table approach, at lest for the metatiles, and go with the calculated address for screens.
User avatar
tokumaru
Posts: 12427
Joined: Sat Feb 12, 2005 9:43 pm
Location: Rio de Janeiro - Brazil

Re: Game project help and progress thread

Post by tokumaru »

Hey, I was just checking a few Capcom games (Mega Man, Duck Tales and The Little Mermaid, specifically) and they all use 32x32-pixel metatiles, like you're planning to, and they appear to update the background similarly to how we were discussing. Take a look at these games in FCEUX's name table viewer for some inspiration.

These games update only 1 metatile each frame, but every 4 scrolled pixels, not 8, so the final result is the same (with the disadvantage that the maximum scroll speed is reduced to 4 pixels per frame - but none of these games are particularly fast anyway). If you divide 32 (the width of a metatile) by 4 (the distance between boundaries that trigger updates), the result is 8, meaning that there will be 8 updates in the space of 32 scrolled pixels, and there are 8 metatiles per column, so there's a direct relationship between which of the 8 4-pixel boundaries is crossed and which of the 8 metatiles of a column is updated, like I suggested. This is evidenced by columns being drawn from top to bottom when scrolling right, and bottom to top when scrolling left.
User avatar
Tsutarja
Posts: 123
Joined: Sun Oct 12, 2014 11:06 am
Location: Finland

Re: Game project help and progress thread

Post by Tsutarja »

Right now I'm trying to get the game to load the background for the first stage's first screen. I was thinking loading the data to the buffer 120 bytes (one vertical metatile row per frame) at a time (of course when the screen is faded to black) and then load the attributes (+64 bytes) at the end, which means that the update takes 9 frames, which doesn't seem like too long time for a transition. Or should I disable rendering and load the whole background as fast as possible? Which one do you think would be easier (speed doesn't really matter here as long as it doesn't take too long, since there are still other stuff that needs to be set up during the same transition)? I still want the subroutine to be usable elsewhere. Not just for the one update.

Here is the way I have packed the data (the thing you noted earlier should be fixed now):

Code: Select all

Stg1Screen1Lo:
 .db LOW(MT0F),LOW(MT0F),LOW(MT0F),LOW(MT0F),LOW(MT0F),LOW(MT0F),LOW(MT0F),LOW(MT0F)
 .db LOW(MT0F),LOW(MT0F),LOW(MT0F),LOW(MT0F),LOW(MT0F),LOW(MT0F),LOW(MT0F),LOW(MT0F)
 .db LOW(MT0F),LOW(MT0F),LOW(MT0F),LOW(MT0F),LOW(MT0F),LOW(MT0F),LOW(MT0F),LOW(MT0F)
 .db LOW(MT0F),LOW(MT0A),LOW(MT0D),LOW(MT0F),LOW(MT0F),LOW(MT0F),LOW(MT0F),LOW(MT0F)
 .db LOW(MT0F),LOW(MT08),LOW(MT0C),LOW(MT0F),LOW(MT13),LOW(MT11),LOW(MT0F),LOW(MT12)
 .db LOW(MT13),LOW(MT07),LOW(MT10),LOW(MT05),LOW(MT04),LOW(MT04),LOW(MT04),LOW(MT04)
 .db LOW(MT04),LOW(MT04),LOW(MT04),LOW(MT06),LOW(MT00),LOW(MT00),LOW(MT00),LOW(MT00)
 .db LOW(MT00),LOW(MT00),LOW(MT00),LOW(MT00),LOW(MT00),LOW(MT00),LOW(MT00),LOW(MT00)

Stg1Screen1Hi:
 .db HIGH(MT0F),HIGH(MT0F),HIGH(MT0F),HIGH(MT0F),HIGH(MT0F),HIGH(MT0F),HIGH(MT0F),HIGH(MT0F)
 .db HIGH(MT0F),HIGH(MT0F),HIGH(MT0F),HIGH(MT0F),HIGH(MT0F),HIGH(MT0F),HIGH(MT0F),HIGH(MT0F)
 .db HIGH(MT0F),HIGH(MT0F),HIGH(MT0F),HIGH(MT0F),HIGH(MT0F),HIGH(MT0F),HIGH(MT0F),HIGH(MT0F)
 .db HIGH(MT0F),HIGH(MT0A),HIGH(MT0D),HIGH(MT0F),HIGH(MT0F),HIGH(MT0F),HIGH(MT0F),HIGH(MT0F)
 .db HIGH(MT0F),HIGH(MT08),HIGH(MT0C),HIGH(MT0F),HIGH(MT13),HIGH(MT11),HIGH(MT0F),HIGH(MT12)
 .db HIGH(MT13),HIGH(MT07),HIGH(MT10),HIGH(MT05),HIGH(MT04),HIGH(MT04),HIGH(MT04),HIGH(MT04)
 .db HIGH(MT04),HIGH(MT04),HIGH(MT04),HIGH(MT06),HIGH(MT00),HIGH(MT00),HIGH(MT00),HIGH(MT00)
 .db HIGH(MT00),HIGH(MT00),HIGH(MT00),HIGH(MT00),HIGH(MT00),HIGH(MT00),HIGH(MT00),HIGH(MT00)
The metatiles are as an attachment to save space:
MetatileData.asm
(2.34 KiB) Downloaded 156 times
UP SIDE DOWN A B A B B A B A Hidari migi
L R L R STOP & DASH & UP & TALK Ijou nashi
User avatar
tokumaru
Posts: 12427
Joined: Sat Feb 12, 2005 9:43 pm
Location: Rio de Janeiro - Brazil

Re: Game project help and progress thread

Post by tokumaru »

So you decided to go with pointers instead of indexing? That's fine, it's simpler to decode, just keep in mind that this will consume more ROM, so pay attention to how much space you can spare for maps.

I think it's a waste to write code to decode the first screen. Since you'll have to write the code that renders columns for the purpose of scrolling, you can simply write a loop that calls this very same code, in order to draw all the columns of the first screen.

By now you know that I'm strongly against redundancy, and having 2 different routines to render the map would be very redundant, wasting ROM and making the code more difficult to maintain (e.g. if you change anything about how the level is encoded, you'll have to modify 2 routines instead of 1).

I think you should focus on rendering columns right from the start, even if you haven't implemented any scrolling yet. Make this simple, just give the routine the position of the column to render, and have the routine use that to read data from the map and fill the BG uldate buffers with the data.

Then you can use this routine for both purposes: when drawing the first screen, increment the column position in a loop until the whole screen is done. When scrolling, calculate the position of the column based on the position of the camera and direction of the movement.
User avatar
Tsutarja
Posts: 123
Joined: Sun Oct 12, 2014 11:06 am
Location: Finland

Re: Game project help and progress thread

Post by Tsutarja »

Next thing I should know is that how do I make the update routine to recognize that the background is updated far enough (updated until 'U' area is reached). In the case of updating first screen and in case the updating leaves behind for some reason. If I only use camera's position, the routine probably only updates the 'U' column without checking that the columns before it are updated correctly.

Code: Select all

                  CAMERA/SCREEN
        |-------------------------------|
+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+
| U | P | V | V | V | V | V | V | V | V | P | U |   |   |   |   |
+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+
| U | P | V | V | V | V | V | V | V | V | P | U |   |   |   |   |
+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+
| U | P | V | V | V | V | V | V | V | V | P | U |   |   |   |   |
+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+
| U | P | V | V | V | V | V | V | V | V | P | U |   |   |   |   |
+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+
| U | P | V | V | V | V | V | V | V | V | P | U |   |   |   |   |
+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+
| U | P | V | V | V | V | V | V | V | V | P | U |   |   |   |   |
+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+
| U | P | V | V | V | V | V | V | V | V | P | U |   |   |   |   |
+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+
| U | P | V | V | V | V | V | V | V | V | P | U |   |   |   |   |
+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+---+
I probably need to feed the metatile to the buffer in four parts of four individual tile rows.

Code: Select all

 ; This is just a example I came up with. I don't know if this actually works.

 LDY #$04
Loop:
 LDX bg_update_requests
 INX
 LDA #$04                              ; 4 tiles to update
 STA bg_update_count-1, x
 LDA pre_calculated_address_lo, y      ; pre-calculated from camera position
 STA bg_update_address_lo-1, x
 LDA pre_calculated_address_hi, y      ; pre-calculated from camera position
 STA bg_update_address_hi-1, x
 STX

 ; push four individual tiles to the stack

 DEY
 BNE Loop
UP SIDE DOWN A B A B B A B A Hidari migi
L R L R STOP & DASH & UP & TALK Ijou nashi
User avatar
tokumaru
Posts: 12427
Joined: Sat Feb 12, 2005 9:43 pm
Location: Rio de Janeiro - Brazil

Re: Game project help and progress thread

Post by tokumaru »

Tsutarja wrote:Next thing I should know is that how do I make the update routine to recognize that the background is updated far enough (updated until 'U' area is reached). In the case of updating first screen and in case the updating leaves behind for some reason. If I only use camera's position, the routine probably only updates the 'U' column without checking that the columns before it are updated correctly.
For the first screen you can just use a loop. Start from CameraX - 32 and go all the way up to CameraX + 255 + 32, incrementing 8 and updating 2 metatiles each step. When scrolling, check if the camera moved left or right (CameraX - OldCameraX < 0 = left, CameraX - OldCameraX > 0 = right), and calculate ColumnX based on that: If left, ColumnX = CameraX - 32, if right, ColumnX = CameraX + 255 + 32.

In the function that decodes columns, you should check whether ColumnX is out of bounds (if ColumnX >= LevelLength, the column is outside the level - remember that -1 is $FFFF, which would also be considered larger in an unsigned comparison), in which case you can simply return and not do anything.

If you do have to draw metatiles, you have to decode the individual bits of ColumnX to know what to render and where. Here's a breakdown of what the individual bits of ColumnX mean:

Code: Select all

SSSSSSSS CCCPPPPP
SSSSSSSS: screen within the level;
CCC: column of metatiles within the screen;
PPPPP: column of pixels within the metatile;
With this information you can do everything you need, like calculating the destination address for the tiles in VRAM, or locating the correct pointers necessary to decode the compressed data from PRG-ROM. For example, to know the name table where the metatiles must be written to, you can look at the lowest bit of SSSSSSSS (since name tables are 256 pixels wide, just like level screens).

You have to decide whether you're going to update 8 metatiles every 32 scrolled pixels (at the rate of 2 per frame) or 2 metatiles every 8 scrolled pixels. I'd go with the latter, so as to not hog VRAM bandwidth for 4 consecutive frames. It's not such a big difference, but it will affect how you use the bits of ColumnX to calculate the source and destination addresses for the metatiles.

Code: Select all

 LDY #$04
Loop:
 LDX bg_update_requests
 INX
 LDA #$04                              ; 4 tiles to update
 STA bg_update_count-1, x
 LDA pre_calculated_address_lo, y      ; pre-calculated from camera position
 STA bg_update_address_lo-1, x
 LDA pre_calculated_address_hi, y      ; pre-calculated from camera position
 STA bg_update_address_hi-1, x
 STX

 ; push four individual tiles to the stack

 DEY
 BNE Loop
Yeah, pushing the tiles to the buffer could be something like this. You probably won't have the luxury of using Y like this though, since it will most likely be necessary for reading the tile indices from the metatile definitions. So I'd probably calculate the address of the first row of 4 tiles, and add 32 to it each iteration of this loop before pushing it to the update list.

I'd probably do the whole scrolling process like this:

Code: Select all

- skip to the end if no 8-pixel boundary was crossed;
- calculate column position from camera position based on the direction of the movement;
(steps below could be a subroutine, so you can use the same code for the first screen and for scrolling)
- skip to the end if the column is out of bounds;
- calculate the first name table address based the column position;
- calculate the first attribute table address based the column position;
- use the column position to read a screen from the map;
- repeat the following twice:
  - use the column position and iteration count to read a metatile from the screen;
  - push AT update to update list;
  - repeat the following 4 times:
    - push new update to update list;
	- read 4 tiles from the metatile and push to buffer;
	- move one tile down (increment NT address by 32);
  - move one metatile down (increment AT address by 8);
User avatar
Tsutarja
Posts: 123
Joined: Sun Oct 12, 2014 11:06 am
Location: Finland

Re: Game project help and progress thread

Post by Tsutarja »

I made the scrolling update code until the point the buffer needs to be filled. Does this look alright so far?

Code: Select all

 LDA scroll_x
 EOR scroll_x_prev
 AND #%00000100
 BEQ NoMetatileUpdate
 LDA scroll_x
 AND #%00011100
 STA column_inc
 LSR
 LSR
 LSR
 STA row_inc
 LDA screen
 AND #%00000001
 BNE SecondNT

FirstNT:
 LDA #$00
 STA ppu_addr_lo
 LDA #$20
 STA ppu_addr_hi
JMP CheckScrollDir

SecondNT:
 LDA #$00
 STA ppu_addr_lo
 LDA #$24
 STA ppu_addr_hi

CheckScrollDir:
 LDA scroll_x
 CMP scroll_x_prev
 BCC ScrollingLeft

ScrollingRight:
 LDA ppu_addr_lo
 CLC
 ADC column_inc
 BCS ScrollCarry
 JMP FillBuffer

ScrollingLeft:
 LDA ppu_addr_lo
 SEC
 SBC column_inc
 BCC ScrollCarry
 JMP FillBuffer

ScrollCarry:
 LDA ppu_addr_hi
 ADC #$04
 STA ppu_addr_hi

FillBuffer:

( ... )

NoMetatileUpdate:
UP SIDE DOWN A B A B B A B A Hidari migi
L R L R STOP & DASH & UP & TALK Ijou nashi
User avatar
tokumaru
Posts: 12427
Joined: Sat Feb 12, 2005 9:43 pm
Location: Rio de Janeiro - Brazil

Re: Game project help and progress thread

Post by tokumaru »

Tsutarja wrote:

Code: Select all

 LDA scroll_x
 EOR scroll_x_prev
 AND #%00000100
 BEQ NoMetatileUpdate
So you decided to update 1 metatile every 4 scrolled pixels? That's probably simpler, but you can't scroll more than 4 pixels per frame or you might miss some metatiles.

I'm in a bit of a hurry now so I haven't been able to debug the rest of the code in my head, but I did see one thing that doesn't make sense: scroll_x is an 8-bit variable, and you can't possibly make all the decisions you need with that.

An 8-bit value can only track the pixel position within the screen, so you can't possibly select a name table based on that. You also can't detect the direction of the movement using 8-bit coordinates, because when it wraps from 255 to 0 you'll missinterpret that as a movement to the left.

The PPU may treat the scroll as an 8 bit value (or 9, if you count the NT bits in $2000, which you should), but your levels are larger than that, so you absolutely need to keep track of the scroll/camera. Even if you only write 9 bits worth of scroll info to the PPU, the rest is necessary for detecting the direction of the movement and to locate the correct metatiles to draw, since they come from the level and the level is larger than 2 screens.

Another important thing is that the camera/scroll position represents the top left corner of the screen, and when calculating addresses of updates at the right side I don't see you compensating for the width of the screen. When scrolling to the right, updates must be offset from the right edge of the camera, so you have to add 255 to the camera's position in order to find that edge, and then you add a few columns to find the actual update position. As an optimization, you can merge the 2 additions into 1, but it's important that you understand what is hapenning underneath.

There are quite a lot of things you have to do before you can fill the buffer. You are calculating the output addresses, but you haven't located any of the source addresses.

I might be able to write some example code later if you think that's going to help.
User avatar
Tsutarja
Posts: 123
Joined: Sun Oct 12, 2014 11:06 am
Location: Finland

Re: Game project help and progress thread

Post by Tsutarja »

tokumaru wrote: An 8-bit value can only track the pixel position within the screen, so you can't possibly select a name table based on that. You also can't detect the direction of the movement using 8-bit coordinates, because when it wraps from 255 to 0 you'll missinterpret that as a movement to the left.
I'm using the screen variable for that. AND-ing off all other bits than the bit 0 should let me see which nametable I'm on at the moment (to know which nametables' start address to use ($2000 or $2400)).
tokumaru wrote: I might be able to write some example code later if you think that's going to help.
I'm sure that'll help. You probably don't need to make example code out of the whole process, just the stuff that I may have forgotten or done incorrectly.
UP SIDE DOWN A B A B B A B A Hidari migi
L R L R STOP & DASH & UP & TALK Ijou nashi
User avatar
tokumaru
Posts: 12427
Joined: Sat Feb 12, 2005 9:43 pm
Location: Rio de Janeiro - Brazil

Re: Game project help and progress thread

Post by tokumaru »

Tsutarja wrote:You probably don't need to make example code out of the whole process
Oops, too late! Long ass post ahead! I started coding the important parts but ended up doing the whole thing because I wanted to see it everything fit together. It's all untested though, so follow with caution. You don't have to follow everything I did step by step, this is just an example of how I'd do things considering the compression format you've chosen. You might want to do things differently in some places, but it's important that you understand what's going on. I also separated the actual metatile decoding to its own subroutine, so it can be used for rendering the initial screen too.

Code: Select all

	;decide whether to render a metatile
	lda OldCameraX+0
	eor CameraX+0
	and #%00000100
	beq Done

	;detect the direction of the movement
	lda OldCameraX+0
	cmp CameraX+0
	lda OldCameraX+1
	sbc CameraX+1
	bcc MovedRight

MovedLeft:

	;calculate the position of the column to the left of the camera
	;sec (omitted because the carry is always set by this point)
	lda CameraX+0 ;subtract 32 ($0020)
	sbc #$20
	sta ColumnX+0
	lda CameraX+1
	sbc #$00
	sta ColumnX+1
	jmp ColumnReady

MovedRight:

	;calculate the position of the column to the right of the camera
	;clc (omitted because the carry is always clear by this point)
	lda CameraX+0 ;add 255 + 32 = 287 ($011F)
	adc #$1F
	sta ColumnX+0
	lda CameraX+1
	adc #$01
	sta ColumnX+1

ColumnReady:

	;go decode the metatile
	jsr DecodeMetatile

Done:
And here's the subroutine:

Code: Select all

DecodeMetatile:

	;return if the column is out of bounds
	lda ColumnX+1
	cmp LevelLength
	bcc Continue
	rts

Continue:

	;use the screen index to set up a pointer to the screen we'll read from
	asl
	tay
	lda (LevelPointer), y
	sta ScreenPointer+0
	iny
	lda (LevelPointer), y
	sta ScreenPointer+1

	;set up the indices needed to access pre-calculated values
	lda ColumnX+0
	and #%11111100
	lsr
	tay ;index for tables of words
	lsr
	tax ;index for tables of bytes

	;prepare the bit that will be used for name table selection
	lda ColumnX+1
	and #%00000001 ;keep only the bit that selects 1 of the 2 name tables
	asl
	asl
	sta Temp

	;prepare the name table address
	lda NameTableAddresses+0, y
	sta NameTableAddress+0
	lda NameTableAddresses+1, y
	ora Temp
	sta NameTableAddress+1

	;prepare the attribute table address
	lda AttributeTableAddresses, x
	sta AttributeTableAddress+0
	lda #%00100011
	ora Temp
	sta AttributeTableAddress+1

	;get the index of the metatile within the screen
	ldy MetatileIndices, x

	;set up a pointer to the metatile
	lda (ScreenPointer), y
	sta MetatilePointer+0
	iny
	lda (ScreenPointer), y
	sta MetatilePointer+1

	;OMITTED: SWITCH TO BUFFER STACK;

	;OMITTED: SET UP VRAM UPDATE USING THE ATTRIBUTE TABLE ADDRESS;

	;put the attribute byte in the buffer
	ldy #ATTRIBUTE_OFFSET
	lda (MetatilePointer), y
	pha

	;prepare to read the first tile of the metatile
	ldy #$00

BufferRow:

	;OMITTED: SET UP VRAM UPDATE USING THE NAME TABLE ADDRESS;

	;put 4 tiles in the buffer
	lda (MetatilePointer), y
	pha
	iny
	lda (MetatilePointer), y
	pha
	iny
	lda (MetatilePointer), y
	pha
	iny
	lda (MetatilePointer), y
	pha
	iny

	;check if all 16 tiles have already been processed
	cpy #$10
	beq Done

	;move the output position one row down
	clc
	lda NameTableAddress+0
	adc #$20
	sta NameTableAddress+0
	lda NameTableAddress+1
	adc #$00
	sta NameTableAddress+1

	;process another row if we didn't invade the attribute tables (happens with the last metatile)
	lda NameTableAddress+0
	and #%11000000
	cmp #%11000000
	bne BufferRow
	lda NameTableAddress+1
	and #%00000011
	cmp #%00000011
	bne BufferRow

Done:

	;OMITTED: SWITCH TO THE NORMAL STACK;

	;return
	rts
Note that this is reading pointers stored as words, not split into bytes like you originally intended. This means you can define levels like this:

Code: Select all

Level0:
	.dw Screen00, Screen01, Screen00, Screen03, Screen03, Screen01
And screens like this:

Code: Select all

Screen00:
	.dw Metatile00, Metatile00, Metatile00, Metatile00, Metatile00, Metatile00, Metatile00, Metatile00
	.dw Metatile00, Metatile00, Metatile00, Metatile00, Metatile00, Metatile00, Metatile00, Metatile00
	.dw Metatile00, Metatile00, Metatile00, Metatile00, Metatile00, Metatile00, Metatile00, Metatile00
	.dw Metatile00, Metatile00, Metatile00, Metatile00, Metatile03, Metatile03, Metatile00, Metatile03
	.dw Metatile00, Metatile00, Metatile00, Metatile00, Metatile00, Metatile00, Metatile00, Metatile00
	.dw Metatile03, Metatile03, Metatile03, Metatile00, Metatile03, Metatile03, Metatile03, Metatile03
	.dw Metatile02, Metatile02, Metatile02, Metatile00, Metatile02, Metatile02, Metatile02, Metatile02
	.dw Metatile02, Metatile02, Metatile02, Metatile00, Metatile02, Metatile02, Metatile02, Metatile02
Which is much easier to write by hand than splitting the high and low bytes of each pointer. You'd probably go crazy trying to design levels the split way!

Also, I owe you some explanations about how the addresses are set up, since I used tables to solve this part. First I'm gonna tell you how we're finding the position of the metatile that will be updated, and then how all the addresses and indices are calculated from that position.

There are only 8 columns of metatiles per screen, but since updating all 8 metatiles of a column would take much more time than available during VBlank, we'll only update 1 metatile at a time. This means we need 8 updates to complete a column, and since each column is 32 pixels wide, we'll have to update one metatile every 32 / 8 = 4 pixels.

We could use a separate counter to keep track of how many metatiles we've already updated, but we already have this info implied in the column coordinate. You can think of the bits in the column coordinate like this:

Code: Select all

SSSSSSSS MMM444PP
SSSSSSSS: index of the screen within the level (0 to 255);
MMM: index of the metatile column (0 to 7);
444: index of the 4-pixel column within the metatile column (0 to 7);
PP: index of the pixel column within the 4-pixel column (0 to 3);
OK, so we already know the column where the metatile we'll be processing is, but we still need to figure out its row. We have absolutely no use for the pixel index, but the 4-pixel column index can be REPURPOSED as the row of the metatile, since it will count from 0 to 7 during the course of one metatile column, which is exactly what we need. We'll be using this to count rows instead of a separate variable, so we'll treat the coordinate like this:

Code: Select all

SSSSSSSS CCCRRR**
SSSSSSSS: index of the screen within the level (0 to 255);
CCC: column of the metatile within the screen;
RRR: row of the metatile within the screen;
**: not used;
Since we now know the column and the row of the metatile being processed, we can use that information to generate all the pointers and indices necessary to access it, we just need to move the bits around. This could be done with bit shifting and bitwise operations, but that could be slow and difficult to understand. To avoid that, I decided to use tables. To index these tables I used the position of the metatile exactly like it's arranged in the low byte of the column index, but shifted according to the size of the entries of each table (0CCCRRR0 for words, 00CCCRRR for bytes).

To read from the screens, you have to think of the format in which they're stored: a grid of 8x8 words. This means that indices to read screens are in the following format:

Code: Select all

0RRRCCC0
The formula used to find this format is (y * 8 + x) * 2. This is the good old formula used to convert 2D to 1D, and the multiplication by 2 is there because each entry is 2 bytes (a pointer). So there's one table (MetatileIndices) that converts 00CCCRRR into 0RRRCCC0.

Another thing we need to find out is the target address for the metatile in the name tables. Name table addresses are in the following format (this was defined by Nintendo):

Code: Select all

0010YXYY YYYXXXXX
This is a bit more complicated because we have a base address (name tables start at $2000, not $0000), and because the top X and Y bits are separated from the rest and given more relevance, for name table selection. We can ignore that when creating the tables (to keep their size down) and assume that all addresses are in the first name table ($2000), and change the name table bit after reading the address from the table. Anyway, the conversion goes like this:

Code: Select all

0CCCRRR0 (index)
001000RR R00CCC00 (NT address)
The last table we need converts the index into an attribute table address. AT addresses are in the folowing format (again, defined by Nintendo):

Code: Select all

0010YX11 11YYYXXX
Which means that the conversion goes like this:

Code: Select all

0CCCRRR0 (index)
00100011 11RRRCCC (AT address)
Like I said before, you could do these conversions in real time instead of using tables, but I wanted to keep the routine fast, and didn't want to confuse you with all the bit shifting. But you can decide to go that route if you don't want to waste space with these tables. The important thing is that the bits end up where they have to be.

I'll give you all the tables, but there might be errors since I wrote them manually instead of writing a script to generate them. Each table has 64 entries, but one uses words while the other 2 use bytes, for a total of 256 btes worth of tables.

Code: Select all

NameTableAddresses:

	.dw %0010000000000000, %0010000010000000, %0010000100000000, %0010000110000000, %0010001000000000, %0010001010000000, %0010001100000000, %0010001110000000
	.dw %0010000000000100, %0010000010000100, %0010000100000100, %0010000110000100, %0010001000000100, %0010001010000100, %0010001100000100, %0010001110000100
	.dw %0010000000001000, %0010000010001000, %0010000100001000, %0010000110001000, %0010001000001000, %0010001010001000, %0010001100001000, %0010001110001000
	.dw %0010000000001100, %0010000010001100, %0010000100001100, %0010000110001100, %0010001000001100, %0010001010001100, %0010001100001100, %0010001110001100
	.dw %0010000000010000, %0010000010010000, %0010000100010000, %0010000110010000, %0010001000010000, %0010001010010000, %0010001100010000, %0010001110010000
	.dw %0010000000010100, %0010000010010100, %0010000100010100, %0010000110010100, %0010001000010100, %0010001010010100, %0010001100010100, %0010001110010100
	.dw %0010000000011000, %0010000010011000, %0010000100011000, %0010000110011000, %0010001000011000, %0010001010011000, %0010001100011000, %0010001110011000
	.dw %0010000000011100, %0010000010011100, %0010000100011100, %0010000110011100, %0010001000011100, %0010001010011100, %0010001100011100, %0010001110011100

AttributeTableAddresses:

	.db %11000000, %11001000, %11010000, %11011000, %11100000, %11101000, %11110000, %11111000
	.db %11000001, %11001001, %11010001, %11011001, %11100001, %11101001, %11110001, %11111001
	.db %11000010, %11001010, %11010010, %11011010, %11100010, %11101010, %11110010, %11111010
	.db %11000011, %11001011, %11010011, %11011011, %11100011, %11101011, %11110011, %11111011
	.db %11000100, %11001100, %11010100, %11011100, %11100100, %11101100, %11110100, %11111100
	.db %11000101, %11001101, %11010101, %11011101, %11100101, %11101101, %11110101, %11111101
	.db %11000110, %11001110, %11010110, %11011110, %11100110, %11101110, %11110110, %11111110
	.db %11000111, %11001111, %11010111, %11011111, %11100111, %11101111, %11110111, %11111111

MetatileIndices:

	.db %00000000, %00010000, %00100000, %00110000, %01000000, %01010000, %01100000, %01110000
	.db %00000010, %00010010, %00100010, %00110010, %01000010, %01010010, %01100010, %01110010
	.db %00000100, %00010100, %00100100, %00110100, %01000100, %01010100, %01100100, %01110100
	.db %00000110, %00010110, %00100110, %00110110, %01000110, %01010110, %01100110, %01110110
	.db %00001000, %00011000, %00101000, %00111000, %01001000, %01011000, %01101000, %01111000
	.db %00001010, %00011010, %00101010, %00111010, %01001010, %01011010, %01101010, %01111010
	.db %00001100, %00011100, %00101100, %00111100, %01001100, %01011100, %01101100, %01111100
	.db %00001110, %00011110, %00101110, %00111110, %01001110, %01011110, %01101110, %01111110
User avatar
tokumaru
Posts: 12427
Joined: Sat Feb 12, 2005 9:43 pm
Location: Rio de Janeiro - Brazil

Re: Game project help and progress thread

Post by tokumaru »

Tsutarja wrote:I'm using the screen variable for that. AND-ing off all other bits than the bit 0 should let me see which nametable I'm on at the moment (to know which nametables' start address to use ($2000 or $2400)).
Right, but that's not the only situation where you need the screen index. You need it when calculating the scroll direction, when calculating the position of the column you need to render, when generating the target NT and AT addresses, and so on. And the position of the column also needs to be a 16-bit number, since levels are larger than 1 screen.

I really think you should consider implementing a "camera" object, and giving it its own position. The low byte of the camera's position is the scroll value you'll write to $2005, without any modifications, and the lowest bit of the high byte will go to $2000, so there are no added complications. Having a camera entity will allow for advanced scrolling later down the road if you decide to. You can apply physics to the camera, so it moves with a bit of inertia, like real cameras do. You can move it more to the right or to the left of the player depending on which side he's facing. You can easily move the camera around in cutscenes while the player is stopped, to reveal things that are farther away. The camera is a real thing that moves around the stage, like the other game objects do. Pretending it doesn't exist and manipulating the scroll directly is a poor way to treat such an important dynamic entity, IMO.
User avatar
Tsutarja
Posts: 123
Joined: Sun Oct 12, 2014 11:06 am
Location: Finland

Re: Game project help and progress thread

Post by Tsutarja »

Okay, here are some of the things I don't understand:

Here you are comparing OldCameraX against CameraX, but you are not doing anything with the result. Does this set one of the processor flags or something?

Code: Select all

   ;detect the direction of the movement
   lda OldCameraX+0
   cmp CameraX+0
   lda OldCameraX+1
   sbc CameraX+1
   bcc MovedRight
What's the point of having SBC here if you are subtracting #$00? Again, does this set a processor flag?

Code: Select all

   lda CameraX+1
   sbc #$00
   sta ColumnX+1
Are these pointers supposed to be memory addresses or do they point to a table? The way you are writing the variable names and labels makes them sometimes hard to distinguish which one you mean. (I use different style for both, example_variable for variables and ExampleTable for lookup tables and labels)

Code: Select all

 lda (LevelPointer), y

( ... )

 lda (ScreenPointer), y
UP SIDE DOWN A B A B B A B A Hidari migi
L R L R STOP & DASH & UP & TALK Ijou nashi
User avatar
tokumaru
Posts: 12427
Joined: Sat Feb 12, 2005 9:43 pm
Location: Rio de Janeiro - Brazil

Re: Game project help and progress thread

Post by tokumaru »

Nice to see you trying to figure out the code!
Tsutarja wrote:Here you are comparing OldCameraX against CameraX, but you are not doing anything with the result. Does this set one of the processor flags or something?

Code: Select all

   ;detect the direction of the movement
   lda OldCameraX+0
   cmp CameraX+0
   lda OldCameraX+1
   sbc CameraX+1
   bcc MovedRight
I'm using the result (look at the branch instruction), but since the camera position is unsigned I'm using the C flag instead of the N flag to check the result. After a comparison/subtraction, the CPU indicates a borrow by clearing the C flag. So, if there was a borrow when subtracting CameraX from OldCameraX, that means CameraX is larger than OldCameraX, meaning the camera moved right.
What's the point of having SBC here if you are subtracting #$00? Again, does this set a processor flag?

Code: Select all

   lda CameraX+1
   sbc #$00
   sta ColumnX+1
That's how 16-bit math works. This part propagates the borrow onto the next byte. For example, let's subtract $0014 from $0a12. First we do $12 - $14, which is -$02 ($fe). since the result was smaller than 0, there was a borrow. If you don't do anything with that information, you'll end up with $0afe as your result, which is wrong. You have to do the other half of the subtraction, which is $0a - $00 = $0a, which looks pointless at first, but since there was a borrow, an extra unit is subtracted from $0a and you end up with $09, forming $09fe, which is the correct result.

It's very important that you understand this. Since the 6502 is an 8-bit CPU, it's very convenient to work with 8-bit numbers, but a lot of things in a game need numbers larger than that, so you absolutely must understand how 16-bit math works.
Are these pointers supposed to be memory addresses or do they point to a table?
These are ZP variables that point to the current level map, a screen definition and a metatile definition, so you can read the data that composes the level.
User avatar
Tsutarja
Posts: 123
Joined: Sun Oct 12, 2014 11:06 am
Location: Finland

Re: Game project help and progress thread

Post by Tsutarja »

tokumaru wrote: That's how 16-bit math works. This part propagates the borrow onto the next byte. For example, let's subtract $0014 from $0a12. First we do $12 - $14, which is -$02 ($fe). since the result was smaller than 0, there was a borrow. If you don't do anything with that information, you'll end up with $0afe as your result, which is wrong. You have to do the other half of the subtraction, which is $0a - $00 = $0a, which looks pointless at first, but since there was a borrow, an extra unit is subtracted from $0a and you end up with $09, forming $09fe, which is the correct result.
So, when the carry flag is set, SBC #$00 subtracts nothing, but if it's cleared, it will subtract #$01 instead? If this is right, does this work with ADC too?
tokumaru wrote: These are ZP variables that point to the current level map, a screen definition and a metatile definition, so you can read the data that composes the level.
So, they point the locations of the tables in ROM?
LevelPointer points to [1.], ScreenPointer points to [2.] and MetatilePointer points to [3.]

[1.]

Code: Select all

Stage1:
 .dw Stg1Screen1,Stg1Screen2,Stg1Screen3,Stg1Screen4,Stg1Screen5
[2.]

Code: Select all

Stg1Screen1:
 .dw MT0F,MT0F,MT0F,MT0F,MT0F,MT0F,MT0F,MT0F
 .dw MT0F,MT0F,MT0F,MT0F,MT0F,MT0F,MT0F,MT0F
 .dw MT0F,MT0F,MT0F,MT0F,MT0F,MT0F,MT0F,MT0F
 .dw MT0F,MT0A,MT0D,MT0F,MT0F,MT0F,MT0F,MT0F
 .dw MT0F,MT08,MT0C,MT0F,MT13,MT11,MT0F,MT12
 .dw MT13,MT07,MT10,MT05,MT04,MT04,MT04,MT04
 .dw MT04,MT04,MT04,MT06,MT00,MT00,MT00,MT00
 .dw MT00,MT00,MT00,MT00,MT00,MT00,MT00,MT00
[3.]

Code: Select all

MT00:
 .db $29,$2A,$29,$2A
 .db $39,$3A,$39,$3A
 .db $29,$2A,$29,$2A
 .db $39,$3A,$39,$3A
 .db %01010101
UP SIDE DOWN A B A B B A B A Hidari migi
L R L R STOP & DASH & UP & TALK Ijou nashi
Post Reply