aboutsummaryrefslogtreecommitdiff
;;;
;; 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"