Oh, so you're using horizontal mirroring (name tables arranged vertically) and only scrolling vertically? That makes things easier, but still more complicated than pure horizontal scrolling due to the height of the name tables not being a power of two.
Ok, let's think about this. The screen is 30 tiles tall, but since the scroll can be between tiles, you need to have a clean image that's 31 tiles tall at all times. That's not a problem since you're working with an area that's 60 tiles high (due to the horizontal mirroring).
The first thing you need to do is to detect WHEN to draw a new row of tiles. That would be when the camera crosses a tile boundary (i.e. every 8 scrolled pixels). In order to detect that, you need to backup the scroll position before changing it, and then compare the new value with the old one. You can do this by EOR'ing both values:
Code: Select all
lda ScroolY
eor OldScrollY
and #%11111000
beq SkipUpdate
EOR outputs 1 if the two input bits are different, and we need to know whether the tile row changed when the scroll changed, so we use that. Then we isolate the bits that matter (only the tile row matters - the fine scroll within the tile can change without triggering an update) and they will tell us whether a name table update is needed: if the result is 0, no bits changed, so no update is necessary, otherwise we DO need to draw a new row.
The next step is to determine the SOURCE address of the new tiles in the level map, and the TARGET address for them in the name tables. These can be calculated from the camera's coordinates and from the scroll value, respectively. If your game scrolls, it should have a virtual camera indicating which part of the map is currently visible. This is normally a 16-bit variable, since 8 bits can only count the pixels of a single screen. And you also need a corresponding value to tie the camera's position to the name table position where the image "captured" by the camera will be displayed. This is the scroll value that you write to PPU registers $2005/$2000. Do note that on the NES, the scroll is actually 9 bits long, where the lower 8 bits go to $2005 and the 9th bit goes to $2000 (i.e. the "name table" bits).
Anyway, horizontal scrolling is simpler because the camera's position can be the same variable as the scroll position, because both wrap around at 256. But for vertical scrolling, the camera's position will usually wrap around at 256, as normal, but the scroll position will wrap around at 240, since that's how tall name tables are. Different programmers deal with this differently, some will even weirdly pretend that entire rows of their level maps don't exist, since skipping those rows allows them to perfectly sync the camera with the scroll, but that comes with it's own set of problems (collision detection for example becomes HELL). Others will divide the camera's position by 240 in order to calculate the scroll position on the fly every frame, but divisions are troublesome in 6502 assembly. I personally prefer to maintain two separate variables, one for the camera and one for the scroll, and always update them by the same amounts, so that they're keep in sync but one moves in label map space (where coordinates wrap around at 256) and the other in name table space (where coordinates wrap around at 240).
The camera's position is the one you use to calculate the SOURCE address for the tiles in ROM or RAM. The actual calculation depends on HOW your levels are stored, and what kind of compression they use, but assuming you're using raw uncompressed name table data, the formula would be
Source = CameraY / 8 * 32, since each row is 8 pixels high and 32 tiles wide. CameraX is always 0 (no horizontal scroll whatsoever), so it doesn't affect this calculation at all. Anyway, that's when scrolling UP, and can be optimized to
Source = (CameraY & %1111111111111000) << 2:
Code: Select all
lda CameraY+0
and #%11111000
sta Source+0
lda CameraY+1
asl Source+0
rol a
asl Source+0
rol a
sta Source+1
When scrolling down, you need to use the coordinate that's 240 pixels down, because that's how tall the screen (i.e. the area covered by the camera) is:
Source = ((CameraY + 240) & %11111111 11111000) << 2:
Code: Select all
clc
lda CameraY+0
adc #240
and #%11111000
sta Source+0
lda CameraY+1
adc #$00
asl Source+0
rol a
asl Source+0
rol a
sta Source+1
Now you have to calculate the TARGET address for the tiles in the name tables. Name table addresses have the following layout:
Code: Select all
0010BAYY YYYXXXXX
B: name table Y;
A: name table X;
YYYYY: tile Y;
XXXXX: tile X;
So you simply have to take your ScrollY variable and put the bits in the right places:
Code: Select all
lda ScrollY+0
and #%11111000
sta Target+0
lda ScrollY+1
lsr a
lda #%10
rol a
asl a
asl Target+0
rol a
asl Target+0
rol a
sta Target+1
This is for scrolling up. When scrolling down you have to add 240 again, but since ScrollY wraps at 240, adding 240 consists simply in flipping the NT bit:
Code: Select all
lda ScrollY+0
and #%11111000
sta Target+0
lda ScrollY+1
eor #%1 ;<- this flips the bit
lsr a
lda #%10
rol a
asl a
asl Target+0
rol a
asl Target+0
rol a
sta Target+1
Now that you have both "Source" and "Target" calculated, you can do the data transfer during vblank as usual:
Code: Select all
lda Target+1
sta $2006
lda Target+0
sta $2006
ldx #32
ldy #$00
TransferByte:
lda (Source), y
sta $2007
iny
dex
bne TransferByte
Keep in mind that this is all very simplified, since we're talking about uncompressed data and we're avoiding the subject of attribute tables altogether. Due to how the attribute tables work (each pair of attribute bits affect a 16x16-pixel area), some people like to work in increments of 2 tiles (or even 4, like many Capcom games), instead of 1 tile like we're doing here.