;;; ;; Allow the player to scroll a level which spans more than two screens wide. ;; The heavy lifting is pulled by the engine contained in `include`, which even ;; if it has some big limitations, it's good enough for showing how this can be ;; achieved on the NES/Famicom. Read the comments along this file, but you will ;; have to dig deeper into `include` to better grasp how any of this works. ;; ;; As a final touch, you can press "Select" to switch between different levels, ;; even if I was lazy enough to only provide a second level. .segment "HEADER" .byte 'N', 'E', 'S', $1A .byte $02 .byte $01 .byte $01 ; Vertical mirroring (important since we are using horizontal scrolling!) .byte $00 .segment "VECTORS" .addr nmi, reset, irq .segment "CODE" .include "include/all.s" .proc main ;; The PPU mask is zero'ed out so no background nor sprites will be shown at ;; first. This will be the task of the `load_level_x` function which is ;; called during background initialization. lda #$00 sta PPU::zp_mask sta PPU::MASK ;; Initialize engine. jsr Palettes::init jsr Metatile::init jsr Driver::init jsr Background::init jsr Player::init ;; Accept back interrupts. cli ;; 7: allow NMI; 5-4: background pattern table at $0000, sprites at $1000. ;; ;; NOTE: we store this same value both into the PPU register and into this ;; special `zp_control` variable. This variable will shadow the contents for ;; the PPU control register. Any operation that requires changing the value ;; for this register, from now on, we will simply touch this variable ;; instead. This is because from now on touching this register is only safe ;; inside of Vblank (a.k.a. NMI code). Our `nmi` handler has special code to ;; check whether there was an update on this register's value and update ;; things accordingly. lda #%10001000 sta PPU::zp_control sta PPU::CONTROL @main_game_loop: READ_JOYPAD1 jsr Player::update jsr Driver::update ;; Set the `render` flag, meaning that the code logic is over and we can ;; safely go into `nmi` code. lda #%10000000 ora Globals::zp_flags sta Globals::zp_flags @wait_for_render: bit Globals::zp_flags bmi @wait_for_render ;; Rendering is done, we can perform another iteration of the loop! jmp @main_game_loop .endproc ;; The basics are laid out in `basics/sprite.s`. Read the comments below for ;; more info. .proc nmi ;; Skip this entirely if code is not finished. This is a safeguard, but ;; whenever it happens it means that we are stalling at least for a frame. bit Globals::zp_flags bpl @next ;; Save registers pha txa pha tya pha ;; Update the sprite of the player. For this simple game, it just means to ;; update its position. jsr Player::update_sprite ;; Render stuff. OAM_WRITE_SPRITES ;; Flush any pending background updates. This macro actually hides the most ;; expensive part executed during VBlank. Check the document on `buffer.s` ;; for more information. FLUSH_PENDING_VRAM_BUFFER ;; Should we update PPU registers? If not, then we can go down to the next ;; section. Otherwise we need to update the PPU registers that have been ;; buffered and set the scroll. bit Globals::zp_flags bvc @after_ppu ;; Zero out the "ppu" flag. lda #%10111111 and Globals::zp_flags sta Globals::zp_flags ;; Reset the PPU address latch. bit PPU::STATUS ;; Update the PPU control/mask registers with their buffered values. lda PPU::zp_control sta PPU::CONTROL lda PPU::zp_mask sta PPU::MASK ;; Update scroll. lda Background::zp_scroll sta PPU::SCROLL lda #$00 sta PPU::SCROLL @after_ppu: ;; And unset the `render` flag so the `main` code is unblocked. lda #%01111111 and Globals::zp_flags sta Globals::zp_flags ;; Restore registers. pla tay pla tax pla @next: rti .endproc ;; Unused. .proc irq rti .endproc .segment "CHARS" .incbin "../assets/diskun.chr"