From 6c4ac7727094259b3b5a25e8c168245983e70b2f Mon Sep 17 00:00:00 2001 From: Miquel Sabaté Solà Date: Tue, 4 Feb 2025 11:00:22 +0100 Subject: scroll: Implement a roulette-like game MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Miquel Sabaté Solà --- scroll/roulette.s | 521 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 521 insertions(+) create mode 100644 scroll/roulette.s (limited to 'scroll/roulette.s') diff --git a/scroll/roulette.s b/scroll/roulette.s new file mode 100644 index 0000000..ecf2569 --- /dev/null +++ b/scroll/roulette.s @@ -0,0 +1,521 @@ +;;; +;; Divide the screen in three rows and make them move in different +;; directions/speed. This is achieved thanks to the MMC3 chip (check the +;; `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). + +;; Include helpful definitions. +.include "../shared/mmc3.s" + +;; Variables used on this example. +.scope Vars + top_scroll = $00 + center_scroll = $01 + bottom_scroll = $02 + is_bottom = $04 ; 0 -> scroll center; 1 -> scroll bottom +.endscope + +.segment "HEADER" + .byte 'N', 'E', 'S', $1A + .byte $10 ; 16 * 16 PRG-ROM (256KB) + .byte $10 ; 16 * 8 CHR-ROM (128KB) + .byte $42, $08 ; Mapper 4, battery present, iNES 2.0 header + .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" + +;;; NOTE: the main bulk of this example. Comments only for the parts which are +;;; specific to this example. +.segment "TAIL" +.include "../shared/ppu.s" + +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 + +;; The main function is used here only for further initialization purposes. +.proc main + ;; 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 #%10010000 + 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. +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 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::top_scroll + clc + adc #2 + sta Vars::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 + +;;; +;; 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. +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::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::bottom_scroll`. + lda Vars::bottom_scroll + clc + adc #1 + sta Vars::bottom_scroll + ldy #0 + sty Vars::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::center_scroll`. + lda Vars::center_scroll + sec + adc #$FD + sta Vars::center_scroll + ldy #1 + sty Vars::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 + +;;; NOTE: pretty much the same as `fx/blink.s` but we also copy the same data on +;;; the second pattern table as that's the one being used for background +;;; elements. + +.segment "CHARS" +.incbin "../assets/diskun0.chr" +.incbin "../assets/diskun1.chr" +.incbin "../assets/diskun0.chr" +.incbin "../assets/diskun1.chr" + +;; 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 -- cgit v1.2.3