diff options
| author | Miquel Sabaté Solà <mikisabate@gmail.com> | 2025-03-11 16:40:12 +0100 |
|---|---|---|
| committer | Miquel Sabaté Solà <mikisabate@gmail.com> | 2025-03-11 16:40:12 +0100 |
| commit | 6f7a0d24cfa7c767e9eee8283c65b4480a6c231b (patch) | |
| tree | 52351a9021e4d74b359d06d5fc61f8f7af20265a /scroll | |
| parent | f05ce5d5ff7d650a439b363993f8b38b817d3efe (diff) | |
| download | code.nes-6f7a0d24cfa7c767e9eee8283c65b4480a6c231b.tar.gz code.nes-6f7a0d24cfa7c767e9eee8283c65b4480a6c231b.zip | |
scroll: Provide an even more basic example
That is, just toggle between two fixed nametables.
Signed-off-by: Miquel Sabaté Solà <mikisabate@gmail.com>
Diffstat (limited to 'scroll')
| -rw-r--r-- | scroll/README.md | 23 | ||||
| -rw-r--r-- | scroll/toggle.s | 273 |
2 files changed, 293 insertions, 3 deletions
diff --git a/scroll/README.md b/scroll/README.md index e29186a..45c96e8 100644 --- a/scroll/README.md +++ b/scroll/README.md @@ -8,11 +8,30 @@ try to understand the concept of scrolling in NES/Famicom programming. The very basic concepts of scrolling can be seen in [toggle.s](./toggle.s), which gives you this as a result: -TBD +<div align="center"> + <img src="../docs/toggle.gif" alt="toggle.gif" /> +</div> That is, we only have filled the two nametable available, and we are modifying the [PPU scroll register](https://www.nesdev.org/wiki/PPU_registers#PPUSCROLL) -to move between one or the other. +to move between one or the other. Another important note, easily missed when +programming scrolling on the NES/Famicom for the first time, is that whenever +the PPU scroll "wraps around", you should also update the base nametable address +from the [PPU control +register](https://www.nesdev.org/wiki/PPU_registers#PPUCTRL). That happens in +two situations: + +1. If you are scrolling right and PPU scroll turns into a `$00` value, then it + means that there's nothing else to show from the origin nametable, and that + `$00` on the scroll means it's `$00` in respect to a new nametable. +2. If you are scrolling left and the PPU scroll turns into `$FF` (i.e. the first + step on scrolling left), then the base nametable address has to be updated + because it's `$FF` from the point of view of the nametable from the left. + +It's easy to miss these points, but from a PPU perspective (and hence from the +perspective of a programmer interfacing with the PPU), it really makes sense. +All in all, the scroll register is relative to whatever base nametable is set on +the control register. This looks rather simplistic but some games used this technique. For example, in Dropzone it was used to perform some effects on the title screen. Hence, diff --git a/scroll/toggle.s b/scroll/toggle.s index fe99cca..b9214b4 100644 --- a/scroll/toggle.s +++ b/scroll/toggle.s @@ -1 +1,272 @@ -;; TODO: simply toggle between two namespaces with smooth scrolling. +;;; +;; Toggle between two nametables with smooth scrolling. You can press right/left +;; to move on that direction, or press Select as a toggle. +;; +;; The main take away is that the scroll is the last thing to be updated on +;; `nmi` code, and that the nametable on the PPU control register should be +;; update if the camera changed focus. In other words, if we gradually move into +;; the next nametable and the PPU scroll register wraps around; then the $00 on +;; that register doesn't mean to go over the same screen, but that it's $00 on +;; the other nametable. This is something that can be easily missed when +;; programming scrolling on the NES/Famicom for the first time. +;; +;; Besides initialization and setup functions like `setup_screens`, the most +;; relevant code is in `update` and `nmi`. Read the comments there carefully. + +.segment "HEADER" + .byte 'N', 'E', 'S', $1A + .byte $02, $01 + .byte $01, $00 + +.segment "VECTORS" + .addr nmi, reset, irq + +.segment "CHARS" +.incbin "../assets/diskun.chr" + +.segment "CODE" + +.include "../shared/diskun.s" +.include "./include/apu.s" +.include "./include/ppu.s" +.include "./include/oam.s" +.include "./include/globals.s" +.include "../shared/ppu.s" +.include "../shared/clear.s" + +;; Variables used on this example. +.scope Vars + ;; Whether we are scrolling or not. + zp_scrolling = $A0 + + ;; Right = 1; Left = 0. + zp_direction = $A1 + + ;; Scroll value on the X axis. + zp_scroll = $A2 +.endscope + +;; Setup both nametables to contain the background indications that we want. +;; It's not much, just a square with a number to identify each screen. +.proc setup_screens + CLEAR_SCREENS $20, $24 + + WRITE_PPU_DATA $21AE, $31 + WRITE_PPU_DATA $21AF, $31 + WRITE_PPU_DATA $21CE, $31 + WRITE_PPU_DATA $21CF, $31 + + WRITE_PPU_DATA $25AE, $32 + WRITE_PPU_DATA $25AF, $32 + WRITE_PPU_DATA $25CE, $32 + WRITE_PPU_DATA $25CF, $32 + + rts +.endproc + +.proc update + ;; Are we already scrolling? If so just skip this. + lda Vars::zp_scrolling + bne @end + + ;; Read the joypad. + jsr joypad_read + + ;; Is the player pressing left? If so then the direction is to the left. + lda #Joypad::BUTTON_LEFT + and Joypad::m_buttons1 + beq @check_right + inc Vars::zp_scrolling + lda #0 + sta Vars::zp_direction + rts + +@check_right: + ;; Is the player pressing right? If so then the direction is to the right. + lda #Joypad::BUTTON_RIGHT + and Joypad::m_buttons1 + beq @check_select + inc Vars::zp_scrolling + lda #1 + sta Vars::zp_direction + rts + +@check_select: + ;; Is the player pressing Select? Then the direction depends on the current + ;; nametable. + lda #Joypad::BUTTON_SELECT + and Joypad::m_buttons1 + beq @end + inc Vars::zp_scrolling + ldx #0 + lda PPU::zp_control + and #%00000001 + bne @left + inx +@left: + stx Vars::zp_direction + +@end: + rts +.endproc + +.proc main + lda #0 + sta Globals::zp_flags + sta Vars::zp_scrolling + sta Vars::zp_direction + sta Vars::zp_scroll + + jsr setup_screens + jsr Diskun::init_palettes + + cli + lda #%10001000 + sta PPU::zp_control + sta PPU::CONTROL + lda #%00011110 + sta PPU::MASK + +@main_game_loop: + jsr update + + lda #%10000000 + ora Globals::zp_flags + sta Globals::zp_flags +@wait_for_render: + bit Globals::zp_flags + bmi @wait_for_render + + jmp @main_game_loop +.endproc + +.proc nmi + bit Globals::zp_flags + bpl @next + + pha + txa + pha + tya + pha + + ;; Are we scrolling at all? If not, then just go to `@set_scroll` which will + ;; simply set the scroll to 0 again. + lda Vars::zp_scrolling + beq @set_scroll + + ;; We are scrolling... on which direction? + lda Vars::zp_direction + beq @left + + ;; If we are scrolling right, flip the nametable if we already reached zero. + ;; Otherwise we can set the new scroll value. + inc Vars::zp_scroll + beq @flip_nametable + jmp @set_scroll + +@left: + ;; If we are scrolling left, and it's actually the first step, change the + ;; nametable already. Otherwise set the new scroll value. + dec Vars::zp_scroll + lda #$FF + cmp Vars::zp_scroll + bne @set_scroll + +@flip_nametable: + ;; Reset the PPU address latch. + bit PPU::STATUS + + ;; Flip the current nametable. + lda PPU::zp_control + eor #%00000001 + sta PPU::zp_control + sta PPU::CONTROL + +@set_scroll: + ;; Set whatever was computed as the new scroll. Note that `zp_scrolling` can + ;; be set at the same value, as we only care whenever it reaches 0, when it + ;; should stop scrolling. + lda Vars::zp_scroll + sta Vars::zp_scrolling + sta PPU::SCROLL + lda #$00 + sta PPU::SCROLL + + ;; And the rest as usual. + + lda #%01111111 + and Globals::zp_flags + sta Globals::zp_flags + + pla + tay + pla + tax + pla +@next: + rti +.endproc + +.proc reset + sei + cld + + ldx #$40 + stx APU::FRAME_COUNTER + + ldx #$FF + txs + + inx + stx PPU::CONTROL + stx PPU::MASK + stx APU::DMC + +@vblankwait1: + bit PPU::STATUS + bpl @vblankwait1 + + ldx #0 + lda #0 +@ram_reset_loop: + sta $000, x + sta $100, x + sta $300, x + sta $400, x + sta $500, x + sta $600, x + sta $700, x + inx + bne @ram_reset_loop + + lda #$EF +@sprite_reset_loop: + sta $200, x + inx + bne @sprite_reset_loop + + OAM_WRITE_SPRITES + +@vblankwait2: + bit PPU::STATUS + bpl @vblankwait2 + + lda #$3F + sta PPU::ADDRESS + lda #$00 + sta PPU::ADDRESS + + lda #$0F + ldx #$20 +@palettes_reset_loop: + sta PPU::DATA + dex + bne @palettes_reset_loop + + jmp main +.endproc + +.proc irq + rti +.endproc |
