aboutsummaryrefslogtreecommitdiff
;;;
;; Divide the screen in three rows and make them move in different
;; directions/speed. This is achieved thanks to the MMC3 chip (check
;; `fx/blink.s` for further information on this chip). In particular, we are
;; using the scanline IRQ mechanism provided by this chip to react to different
;; parts of the screen being rendered:
;;
;;   1. The top of the screen moves fast on one direction.
;;   2. The center of the screen moves fast on the opposite direction.
;;   3. The bottom of the screen moves on the same direction as 1. but slower.
;;
;; This is achieved by setting an IRQ that hits on points 2. and 3., and then we
;; manipulate the PPU scroll register mid frame. That is, all you see are just
;; background elements being scrolled in different ways.
;;
;; This trick was used, for example, on Super Mario Bros. 3 for the roulette
;; mini-game. That being said, usually games used this capability to handle
;; scroll on the top part of the screen, and then resetting the scroll on the
;; lower part, so they could show a status section (again, as Super Mario Bros.
;; 3 does inside of a level, and in mmc3.s here).

;; Include helpful definitions.
.include "../shared/mmc3.s"

;; Variables used on this example.
.scope Vars
    zp_top_scroll    = $00
    zp_center_scroll = $01
    zp_bottom_scroll = $02
    zp_is_bottom     = $04         ; 0 -> scroll center; 1 -> scroll bottom
.endscope

.segment "HEADER"
    .byte 'N', 'E', 'S', $1A
    .byte $10
    .byte $10
    .byte $42, $08
    .res 8, 0

.segment "VECTORS"
    .addr nmi, reset, irq

;;; NOTE: lots of banks, all of them empty since we don't need them :)

.segment "PRG0_00"
.byte $FF
.segment "PRG0_01"
.byte $FF
.segment "PRG0_02"
.byte $FF
.segment "PRG0_03"
.byte $FF
.segment "PRG0_04"
.byte $FF
.segment "PRG0_05"
.byte $FF
.segment "PRG0_06"
.byte $FF
.segment "PRG0_07"
.byte $FF
.segment "PRG0_08"
.byte $FF
.segment "PRG0_09"
.byte $FF
.segment "PRG0_0A"
.byte $FF
.segment "PRG0_0B"
.byte $FF
.segment "PRG0_0C"
.byte $FF
.segment "PRG0_0D"
.byte $FF
.segment "PRG0_0E"
.byte $FF
.segment "PRG1_00"
.byte $FF
.segment "PRG1_01"
.byte $FF
.segment "PRG1_02"
.byte $FF
.segment "PRG1_03"
.byte $FF
.segment "PRG1_04"
.byte $FF
.segment "PRG1_05"
.byte $FF
.segment "PRG1_06"
.byte $FF
.segment "PRG1_07"
.byte $FF
.segment "PRG1_08"
.byte $FF
.segment "PRG1_09"
.byte $FF
.segment "PRG1_0A"
.byte $FF
.segment "PRG1_0B"
.byte $FF
.segment "PRG1_0C"
.byte $FF
.segment "PRG1_0D"
.byte $FF
.segment "PRG1_0E"
.byte $FF

;;; NOTE: the first fixed PRG bank will simply contain utilities for moving the
;;; player around.
.segment "FIXED"
.include "../shared/diskun.s"
.include "../shared/clear.s"

;;; NOTE: the main bulk of this example. Comments only for the parts which are
;;; specific to this example.
.segment "TAIL"
.include "../shared/ppu.s"

.proc reset
    sei
    cld

    ;; NOTE: as explained on the `basics/sprite.s` example, this is done to
    ;; disable the APU frame IRQ. This is usually done without giving it a
    ;; second thought, but it's specially relevant on the MMC3 chip because
    ;; disabling this allows the `irq` handler to be able to assume that the
    ;; only kind of IRQ available is a scanline one.
    ldx #$40
    stx $4017

    ldx #$FF
    txs

    inx
    stx $2000
    stx $2001
    stx $4010

    ;;;
    ;; NOTE: Setup MMC3. Nothing different from `fx/blink.s`. Take that example
    ;; as a reference on how to configure the MMC3 chip and bank switching on
    ;; it.

    lda #$00
    sta MMC3::MIRRORING
    sta MMC3::IRQ_DISABLE

    lda #$80
    sta MMC3::RAM_PROTECT

    BANK_REGISTER_SET 0, 0
    BANK_REGISTER_SET 1, 2
    BANK_REGISTER_SET 2, 4
    BANK_REGISTER_SET 3, 5
    BANK_REGISTER_SET 4, 6
    BANK_REGISTER_SET 5, 7
    BANK_REGISTER_SET 6, 0
    BANK_REGISTER_SET 7, 1

    bit $2002
@vblankwait1:
    bit $2002
    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

    lda #$00
    sta $2003
    lda #$02
    sta $4014

@vblankwait2:
    bit $2002
    bpl @vblankwait2

    lda #$3F
    sta $2006
    lda #$00
    sta $2006

    lda #$0F
    ldx #$20
@palettes_reset_loop:
    sta $2007
    dex
    bne @palettes_reset_loop
    jmp main
.endproc

;; The main function is used here only for further initialization purposes.
.proc main
    ;; Clear both screens to avoid funky business, as we are not doing anything
    ;; specially clever here.
    CLEAR_SCREENS $20, $24

    ;; Initialize both the palettes and the nametables.
    jsr Diskun::init_palettes
    jsr init_nametables

    ;; NOTE: enable back interrupts so we can set them up later on `nmi` code.
    cli

    lda #%10001000
    sta $2000
    lda #%00011110
    sta $2001

@main_game_loop:
    ;; NOTE: nothing :D

    lda #%10000000
    ora $20
    sta $20
@wait_for_render:
    bit $20
    bmi @wait_for_render

    ;; NOTE: no game logic, everything happens on NMI and IRQ handlers.

    jmp @main_game_loop
.endproc

;; NOTE: everything displayed on this example only happens on the background.
;; Moreover, to give a more accurate illusion of the scrolling, the same
;; background elements are repeated on the other nametable. That's why we have
;; to set the data twice on each section: once for each nametable.
.proc init_nametables
    ;; Top left
    WRITE_PPU_DATA $20A6, $01
    WRITE_PPU_DATA $20C6, $11
    WRITE_PPU_DATA $20A7, $02
    WRITE_PPU_DATA $20C7, $12

    WRITE_PPU_DATA $24A6, $01
    WRITE_PPU_DATA $24C6, $11
    WRITE_PPU_DATA $24A7, $02
    WRITE_PPU_DATA $24C7, $12

    ;; Top center
    WRITE_PPU_DATA $20AF, $01
    WRITE_PPU_DATA $20CF, $11
    WRITE_PPU_DATA $23CB, %01000100
    WRITE_PPU_DATA $20B0, $02
    WRITE_PPU_DATA $20D0, $12
    WRITE_PPU_DATA $23CC, %00010001

    WRITE_PPU_DATA $24AF, $01
    WRITE_PPU_DATA $24CF, $11
    WRITE_PPU_DATA $27CB, %01000100
    WRITE_PPU_DATA $24B0, $02
    WRITE_PPU_DATA $24D0, $12
    WRITE_PPU_DATA $27CC, %00010001

    ;; Top right
    WRITE_PPU_DATA $20B8, $01
    WRITE_PPU_DATA $20D8, $11
    WRITE_PPU_DATA $23CE, %00100010
    WRITE_PPU_DATA $20B9, $02
    WRITE_PPU_DATA $20D9, $12

    WRITE_PPU_DATA $24B8, $01
    WRITE_PPU_DATA $24D8, $11
    WRITE_PPU_DATA $27CE, %00100010
    WRITE_PPU_DATA $24B9, $02
    WRITE_PPU_DATA $24D9, $12

    ;; Center left
    WRITE_PPU_DATA $21E6, $01
    WRITE_PPU_DATA $2206, $11
    WRITE_PPU_DATA $21E7, $02
    WRITE_PPU_DATA $2207, $12

    WRITE_PPU_DATA $25E6, $01
    WRITE_PPU_DATA $2606, $11
    WRITE_PPU_DATA $25E7, $02
    WRITE_PPU_DATA $2607, $12

    ;; Center center
    WRITE_PPU_DATA $21EF, $01
    WRITE_PPU_DATA $220F, $11
    WRITE_PPU_DATA $21F0, $02
    WRITE_PPU_DATA $2210, $12
    WRITE_PPU_DATA $23DB, %01000000
    WRITE_PPU_DATA $23E3, %00000100
    WRITE_PPU_DATA $23DC, %00010000
    WRITE_PPU_DATA $23E4, %00000001

    WRITE_PPU_DATA $25EF, $01
    WRITE_PPU_DATA $260F, $11
    WRITE_PPU_DATA $25F0, $02
    WRITE_PPU_DATA $2610, $12
    WRITE_PPU_DATA $27DB, %01000000
    WRITE_PPU_DATA $27E3, %00000100
    WRITE_PPU_DATA $27DC, %00010000
    WRITE_PPU_DATA $27E4, %00000001

    ;; Center right
    WRITE_PPU_DATA $21F8, $01
    WRITE_PPU_DATA $2218, $11
    WRITE_PPU_DATA $21F9, $02
    WRITE_PPU_DATA $2219, $12
    WRITE_PPU_DATA $23DE, %00100000
    WRITE_PPU_DATA $23E6, %00000010

    WRITE_PPU_DATA $25F8, $01
    WRITE_PPU_DATA $2618, $11
    WRITE_PPU_DATA $25F9, $02
    WRITE_PPU_DATA $2619, $12
    WRITE_PPU_DATA $27DE, %00100000
    WRITE_PPU_DATA $27E6, %00000010

    ;; Bottom left
    WRITE_PPU_DATA $2326, $01
    WRITE_PPU_DATA $2327, $02
    WRITE_PPU_DATA $2346, $11
    WRITE_PPU_DATA $2347, $12

    WRITE_PPU_DATA $2726, $01
    WRITE_PPU_DATA $2727, $02
    WRITE_PPU_DATA $2746, $11
    WRITE_PPU_DATA $2747, $12

    ;; Bottom center
    WRITE_PPU_DATA $232F, $01
    WRITE_PPU_DATA $234F, $11
    WRITE_PPU_DATA $2330, $02
    WRITE_PPU_DATA $2350, $12
    WRITE_PPU_DATA $23F3, %01000100
    WRITE_PPU_DATA $23F4, %00010001

    WRITE_PPU_DATA $272F, $01
    WRITE_PPU_DATA $274F, $11
    WRITE_PPU_DATA $2730, $02
    WRITE_PPU_DATA $2750, $12
    WRITE_PPU_DATA $27F3, %01000100
    WRITE_PPU_DATA $27F4, %00010001

    ;; Bottom right
    WRITE_PPU_DATA $2338, $01
    WRITE_PPU_DATA $2358, $11
    WRITE_PPU_DATA $2339, $02
    WRITE_PPU_DATA $2359, $12
    WRITE_PPU_DATA $23F6, %00100010

    WRITE_PPU_DATA $2738, $01
    WRITE_PPU_DATA $2758, $11
    WRITE_PPU_DATA $2739, $02
    WRITE_PPU_DATA $2759, $12
    WRITE_PPU_DATA $27F6, %00100010

    rts
.endproc

;;;
;; NOTE: for this example the NMI is a bit different than on other examples. It
;; has to do mainly two things:
;;   1. Set up a scanline IRQ so the scroll at the center/bottom is different.
;;   2. Set the scroll for the top region.
.proc nmi
    bit $20
    bpl @next

    pha
    txa
    pha
    tya
    pha

    ;; NOTE: no DMA transfer as usual since there are no sprites involved.

    ;;;
    ;; NOTE: setting up IRQs for scanline counting.

    ;; Disable scanline IRQs and acknowledge any previous one. Technically
    ;; speaking this is not needed because the only times we set up an IRQ we
    ;; know it's going to be acknowledge where it is needed. That being said,
    ;; let's be safe.
    ldx #$00
    stx MMC3::IRQ_DISABLE

    ;; The screen is made up of 240 visible scan lines. Since we are dividing
    ;; the screen by 3: 240 / 3 = 80. Hence, the next IRQ should happen on
    ;; scanline 80, where we would need to update the scroll value through the
    ;; `{center/bottom}_scroll` values instead. Moreover, note that
    ;; `MMC3::IRQ_ENABLE` accepts any value, so the same value as the two other
    ;; registers is just fine.
    lda #80
    sta MMC3::IRQ_LATCH
    sta MMC3::IRQ_RELOAD
    sta MMC3::IRQ_ENABLE

    ;; The IRQ for scanline 80 has been set up. Now proceed with the scroll for
    ;; the top section. The scroll will only happen on the X axis and it's going
    ;; to be a bit fast.
    bit $2002
    lda Vars::zp_top_scroll
    clc
    adc #2
    sta Vars::zp_top_scroll
    sta $2005
    lda #$00
    sta $2005

    ;; NOTE: the rest as usual.

    lda #%01111111
    and $20
    sta $20

    pla
    tay
    pla
    tax
    pla
@next:
    rti
.endproc

;;;
;; NOTE: handle a scanline IRQ.
;;
;; Notice that we cannot at first glance know what kind of IRQ is hitting at the
;; moment, but we have disabled the frame counter on our `reset` code, so on the
;; context of the MMC3 chip the only thing left are scanline IRQs, which we have
;; set up on `nmi` code.
.proc irq
    ;; Save current context.
    pha
    txa
    pha
    tya
    pha

    ;; Disable IRQs and acknowledge the current one.
    ldx #$00
    stx MMC3::IRQ_DISABLE

    ;; What are we trying to scroll, exactly?
    lda Vars::zp_is_bottom
    beq @scroll_right

    ;; We are scrolling the bottom section, which scrolls in the same direction
    ;; as the top one but a bit slower at that. Load the next scroll value on
    ;; the `a` register and `Vars::zp_bottom_scroll`.
    lda Vars::zp_bottom_scroll
    clc
    adc #1
    sta Vars::zp_bottom_scroll
    ldy #0
    sty Vars::zp_is_bottom
    jmp @do_scroll

@scroll_right:
    ;; We are scrolling the center, which works by going on the opposite
    ;; direction as the top and bottom sections. Load the next scroll value on
    ;; the `a` register and `Vars::zp_center_scroll`.
    lda Vars::zp_center_scroll
    sec
    adc #$FD
    sta Vars::zp_center_scroll
    ldy #1
    sty Vars::zp_is_bottom

    ;; We are at the center, but there is still the bottom section to be
    ;; scrolled differently. Hence, set a new scanline IRQ 80 lines ahead of
    ;; where we are now. This is done in pretty much the same way as we did in
    ;; `nmi` code.
    ldx #80
    stx MMC3::IRQ_LATCH
    stx MMC3::IRQ_RELOAD
    stx MMC3::IRQ_ENABLE

@do_scroll:
    ;; Regardless of the path, the `a` register contains the value for the
    ;; scroll on the X axis. Store this value now and leave.
    sta $2005
    lda #$00
    sta $2005

    ;; Restore previous context.
    pla
    tay
    pla
    tax
    pla

    rti
.endproc

;;; NOTE: pretty much the same as `fx/blink.s`.

.segment "CHARS"
.incbin "../assets/diskun0.chr"
.incbin "../assets/diskun1.chr"
.res $1000, $00

;; The 15 other 8KB portions are left empty.
.res $2000, $00
.res $2000, $00
.res $2000, $00
.res $2000, $00
.res $2000, $00
.res $2000, $00
.res $2000, $00
.res $2000, $00
.res $2000, $00
.res $2000, $00
.res $2000, $00
.res $2000, $00
.res $2000, $00
.res $2000, $00
.res $2000, $00