aboutsummaryrefslogtreecommitdiff
path: root/scroll/roulette.s
diff options
context:
space:
mode:
authorMiquel Sabaté Solà <mikisabate@gmail.com>2025-02-04 11:00:22 +0100
committerMiquel Sabaté Solà <mikisabate@gmail.com>2025-02-04 22:00:07 +0100
commit6c4ac7727094259b3b5a25e8c168245983e70b2f (patch)
tree089c185a436f57a58f43b6c0ed3eed53e8b93da5 /scroll/roulette.s
parentfdf44365e56c3741f47cbd421a57f0d074a5492c (diff)
downloadcode.nes-6c4ac7727094259b3b5a25e8c168245983e70b2f.tar.gz
code.nes-6c4ac7727094259b3b5a25e8c168245983e70b2f.zip
scroll: Implement a roulette-like game
Signed-off-by: Miquel Sabaté Solà <mikisabate@gmail.com>
Diffstat (limited to 'scroll/roulette.s')
-rw-r--r--scroll/roulette.s521
1 files changed, 521 insertions, 0 deletions
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