;;; ;; 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 @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