Screen scrolling logic

Are you new to 6502, NES, or even programming in general? Post any of your questions here. Remember - the only dumb question is the question that remains unasked.

Moderator: Moderators

Post Reply
User avatar
lerenarzp
Posts: 4
Joined: Fri Feb 02, 2018 4:13 am
Location: Ukraine

Screen scrolling logic

Post by lerenarzp »

Hello, guys!

First of all, thank you for the stuff you are doing. It's really amazing to see such a big and interesting community.

I've decided to test my skills and try building an NES game (jRPG). I'm accustomed to high-level programming languages, like C++, so ASM 6502 is quite new for me. I've decided to use ASM6 at the moment. With the help of your handful guides, I was able to build an NROM-256 image with 32KB PRG, 8KB CHR and vertical mirorring. Eventually, I will switch to the cart with PRG-RAM, I assume (but this doesn't matter at the moment, as I'm still learning).

At the moment, I'm failing to understand the concept how to change contents of nametables and "scroll" the world map and will appreciate any help. I've read a lot of articles/posts, but still failing to undertsand the concept.

So, PPU is drawing tiles based on the content nametables ($2000 and $2400 in my case) and scroll position.
Let's assume that I've loaded the "world map" at $2000. Red rectangle corresponds to the area that is "seen" on the screen (screenshot). Then:
  • A. If player decides to go the the right, I should:
    • 1. Update the left-most column of $2400 with new data.
    • 2. Scroll by 16 pixels to the right.
  • B. If player decides to go to the left, I should:
    • 1. Update the right-most column of $2400 with new data.
    • 2. Flip nametables $2400 and $2000 by changing last 2 bits of $2000 (or it will result in this).
    • 3. Scroll by 16 pixels to the left.
  • C. If player decides to go down, I should:
    • 1. Update the top-most row of $2000 with new data.
    • 2. Scroll the screen 16 pixels to the bottom.
  • D. If player decides to go up, I should:
    • 1. Update the bottom-most row of $2000 with new data.
    • 2. Scroll the screen 16 pixels to the top (it is done by decrementing Y scroll, so I should take care of the the 0->FF change, it should be 0->EF to avoid glitches).
The updates should be done in a vBlank during NMI, because it's the only time when PPU is not "busy". That is why it is critical to "buffer" the change, reserve space at $0100 address area of NES to hold the "shadow" copy of the row/column that we are changing and then to do the writes via $2007 during NMI using the data of that "buffer". "Buffer" should be updated outside of NMI.

Am I understanding the concept correctly or missing something really vital? Should I consider an option to switch to a CHR-RAM based cart right away? Thank you in advance for wasting some time and helping me out and sorry for my Eglish.
lazerbeat
Posts: 64
Joined: Tue Jul 09, 2013 7:13 am

Re: Screen scrolling logic

Post by lazerbeat »

I found the advanced nerdy nights #3 tutorial super helpful for understanding scrolling

http://nintendoage.com/forum/messagevie ... eadid=7155
User avatar
Nioreh
Posts: 115
Joined: Sun Jan 22, 2012 11:46 am
Location: Stockholm, Sweden

Re: Screen scrolling logic

Post by Nioreh »

That all sounds correct to me. I have just done the same in my engine, expect a lot of "off-by-one" mistakes and head scratching :) (Unless you are a programming god)
User avatar
lerenarzp
Posts: 4
Joined: Fri Feb 02, 2018 4:13 am
Location: Ukraine

Re: Screen scrolling logic

Post by lerenarzp »

Thank you for your answers. It's time to write some code :lol:
User avatar
tokumaru
Posts: 12427
Joined: Sat Feb 12, 2005 9:43 pm
Location: Rio de Janeiro - Brazil

Re: Screen scrolling logic

Post by tokumaru »

The basic idea sounds correct, you just need to generalize this logic so you don't end up hardcoding a bunch of NT addresses and $2000 writes.

Thinking of the name tables as a 512x480-pixel area helps, even if you only actually have 2 name tables, because even though the mirroring controls which name tables are unique, as far as the PPU is concerned, it always sees the background as a 512x480-pixel thing. This will make your code more compatible across different mirroring modes (i.e. it should still be mostly playable under wrong mirroring settings, only with glitchy edges), and will make it easier for you to change the mirroring mode if you ever decide to.

Another important aspect of treating the background this way is that the ranges 0 to 511 and 0 to 479 can't be represented with 8 bits, so your scroll coordinates need to be 9 bits long. The advantage is that the values you write to $2005 and $2000 are no longer disconnected, they're different parts of the same value: the lower 8 bits of the scroll values go to $2005 as usual, while the 9th bits of both scroll coordinates are combined to form the lower 2 bits of $2000. That's automatic name table selection, without you having to explicitly handle name table switching. The correct name table will be automatically selected every frame based on the scroll coordinates. Of course you still have to manually skip the 240-255 range in the low byte of the vertical scroll, since each name table is only 240 pixels tall.

Another tip I have is that you don't directly manipulate the scroll values, but instead create a camera object, with its own coordinates, that follows the player using whatever logic you see fit, and have the scroll coordinates derived from it. This will allow you to manipulate the scroll independently for special effects if needed (by temporarily detaching the scroll from the camera), for things like earthquakes or background bosses.

Another tip: don't hardcode name table updates to game events like the player moving. Watching for changes in the scroll values themselves is a much safer way to do it. Always save the old scroll values before changing them, so you can compare the old values against the new ones. If you're using 16x16-pixel metatiles, then you probably want to detect when the scroll crosses 16-pixel boundaries. This means you have to watch out for changes in bit 4 of the scroll values... e.g: 15 to 18 (00001111 to 00010010) or 30 to 33 (00011110 to 00100001). Notice how bit 4 always changes when a 16-pixel boundary is crossed. A very useful operation for detecting bit changes is XOR (EOR on the 6502), since it results in 0 when the bits are equal and 1 when they're different. So you can easily detect boundary crossing like this:

Code: Select all

  lda OldScrollX+0
  eor ScrollX +0
  and #%00010000
  bne UpdateColumn
One last advice I have is to use the scroll values to calculate all the NT and AT addresses you need for background updates. Name table addresses have the following format:

Code: Select all

ScrollX: Xxxxxx***
ScrollY: Yyyyyy***
Address: 0010YXyy yyyxxxxx
While attribute table addresses have the following format:

Code: Select all

ScrollX: Xxxx*****
ScrollY: Yyyy*****
Address: 0010YX11 11yyyxxx
User avatar
lerenarzp
Posts: 4
Joined: Fri Feb 02, 2018 4:13 am
Location: Ukraine

Re: Screen scrolling logic

Post by lerenarzp »

Thank you for the detailed explanation, tokumaru. It makes a lot of sense now.

My previous concept was to use only ScrollX [0;255], ScrollY [0;240] (1 bytes for each), a counter Scroll16px [0;15] and ScrollFlag value (bit 7 is set, if scrolling required, because we should forcefully scroll 16 pixels somewhere and ignore changes of direction during this time). It worked fine, though wasn't efficient in terms of cycle/byte saving.

Your concept is far more interesting and, well, flexible. However, I have 1 question. I've re-designed the code now and scrolling to the right/down works fine. But that is not the case for left/up and here is why (a part of code):

Code: Select all

some_routine:
...
LDA OldScrollX
EOR ScrollX
AND #%00010000
BNE @ScrDone
LDA OldScrollY
EOR ScrollY
AND #%00010000
BNE @ScrDone
RTS
@ScrDone:
...
Let's assume that OldScrollX is #$30 (or #%00110000) and I'm moving to the right. ScrollX will be incremented, starting from #$30 up to #$40 (or #%01000000) totally fine. But if I'm moving left, the check will branch as soon, as the first decrement of ScrollX will happen from #$30 (or #%00110000) to #$2F (or #%00101111).

And here is the question: I'm failing to understand how to make this check more generic without using a counter (as I did before) or adding some checks (for example, AND-ing ScrollX with #%00001111 and using BEQ for branching), etc.

Am I missing something really obvious in such case? As it really looks like it is so and I'm only wasting your time :(

But I will be really glad for any advice. Thank you in advance!
User avatar
tokumaru
Posts: 12427
Joined: Sat Feb 12, 2005 9:43 pm
Location: Rio de Janeiro - Brazil

Re: Screen scrolling logic

Post by tokumaru »

Well, here's the thing: ideally, when scrolling, you must have more than a screen's worth of valid picture, for when the camera is between tiles. On the NES you don't always have that luxury, so depending on the type of NT mirroring you use you might have visible glitches near the edges of the screen, but let's assume we do have an extra row and an extra column of background data to accommodate scroll values in-between blocks.

This buffer is normally in front of the camera (i.e. right and bottom), so it's perfectly normal that you have to scroll a whole block before new background data has to be drawn. On the trailing edges of the camera though, there's no extra background data, so as soon as the scroll goes below 0, you indeed need to draw more background.

As I see it, what you're describing as a problem is in fact intended behavior. How exactly is it manifesting as a problem in your case? Why is it that scrolling left/up immediately triggering background updates becoming a problem for you?
User avatar
lerenarzp
Posts: 4
Joined: Fri Feb 02, 2018 4:13 am
Location: Ukraine

Re: Screen scrolling logic

Post by lerenarzp »

Thank you for the quick response.

Well, I'm not dealing with the nametable updates at the moment, since I'm checking that scrolling problem at first.

I'm now fully getting the problem in my logic. I've decided to use your code:

Code: Select all

LDA OldScrollX
EOR ScrollX
AND #%00010000
BNE @ScrDone
...to handle both scrolling and drawing. That will not do the trick.

I've attached the test rom that now has only 2 nametables and scrolls the area. By browsing $0002 and $0004 values in RAM, you'll see that:
1. While scrolling right/down, ScrollX ($0002) or ScrollY ($0004) increments totally fine by 16.
2. While scrolling left/up, at first ScrollX or ScrollY will decrement by 1, after that it will decrement by 16 (it is better to use a breakpoint for writes done to $0002 and $0004 to get the idea).

Your code should work totally well to toggle PPU updates and filling the buffer. But to force 16px scrolling to the left/up, it looks like I should have another condition for these directions. Basically, such code:

Code: Select all

LDA ScrollX
AND #%00001111
BEQ @ScrDone
This should totally fix the isssue I'm facing while scrolling to the left/up those 2 nametables.

Indeed, your code is really great and such behavior is intended for it :)

Edit:
It looks like adding a counter for 16px forced scroll is the most elegant solution. I've added that counter and it fixed scrolling problem mentioned above. Now I can freely try using your code for updating nametable row/column. Sorry for wasting your time, tokumaru. Your help was really appreciated. I'm able now to work further.
Attachments
test_fixed.nes
(40.02 KiB) Downloaded 161 times
test.nes
(40.02 KiB) Downloaded 147 times
Post Reply