;;;
;; 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/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.
READ_JOYPAD1
;; Is the player pressing left? If so then the direction is to the left.
lda #Joypad::BUTTON_LEFT
and Joypad::zp_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::zp_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::zp_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
bit PPU::STATUS
@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