diff options
| -rw-r--r-- | .gitignore | 1 | ||||
| -rw-r--r-- | Makefile | 5 | ||||
| -rw-r--r-- | README.md | 24 | ||||
| -rw-r--r-- | basics/sprite.s | 17 | ||||
| -rw-r--r-- | docs/blink.gif | bin | 0 -> 57479 bytes | |||
| -rw-r--r-- | docs/roulette.gif | bin | 0 -> 1330387 bytes | |||
| -rw-r--r-- | fx/README.md | 2 | ||||
| -rw-r--r-- | fx/blink.s | 11 | ||||
| -rw-r--r-- | scroll/README.md | 21 | ||||
| -rw-r--r-- | scroll/roulette.s | 521 | ||||
| -rw-r--r-- | shared/diskun.s | 6 | ||||
| -rw-r--r-- | shared/ppu.s | 11 |
12 files changed, 580 insertions, 39 deletions
@@ -1 +1,2 @@ out/ +TODO @@ -54,9 +54,12 @@ space: .PHONY: scroll scroll: - $(E) " CC scroll" + $(E) " CC scroll/level" $(Q) $(CC65) $(CCOPTS) scroll/level.s -C config/nrom.cfg -o out/scroll/level.nes + $(E) " CC scroll/roulette" + $(Q) $(CC65) $(CCOPTS) scroll/roulette.s -C config/mmc3.cfg -o out/scroll/roulette.nes + .PHONY: fx fx: $(E) " CC fx/blink" @@ -26,19 +26,17 @@ watchers or a full debugger. The examples are distributed like this: -- `basics`: simple examples which cover basic stuff for NES development. These - examples are self-contained and supposed to be read by absolute newcomers. The - description for each example is covered by an initial comment on each file, - and you can also find more info in the [basics/README.md](./basics/README.md) - file. -- `space`: example in which you can move a spaceship with subpixel movement and - shoot bullets. Consider this an evolution from the `basics/sprite.s` and - `basics/input.s` examples. That is, we are no longer just showing a sprite, - but we make it move and perform an action like shooting bullets. -- `scroll`: different scrolling tactics. Read the - [scroll/README.md](./scroll/README.md) file for more info. -- `fx`: simple graphical effects that can be pulled off. Read the - [fx/README.md](./fx/README.md) file for more info. +- [basics](./basics/README.md): simple examples which cover basic stuff for NES + development. These examples are self-contained and supposed to be read by + absolute newcomers. The description for each example is covered by an initial + comment on each file. +- [space](./space/README.md): example in which you can move a spaceship with + subpixel movement and shoot bullets. Consider this an evolution from the + `basics/sprite.s` and `basics/input.s` examples. That is, we are no longer + just showing a sprite, but we make it move and perform an action like shooting + bullets. +- [scroll](./scroll/README.md): different scrolling tactics. +- [fx](./fx/README.md): simple graphical effects that can be pulled off. ## Other projects diff --git a/basics/sprite.s b/basics/sprite.s index 1fa55ad..1892ce5 100644 --- a/basics/sprite.s +++ b/basics/sprite.s @@ -386,17 +386,8 @@ palettes: .byte $0F, $00, $00, $00 .endproc -;; WRITE_PPU_DATA is a macro that will write into PPUADDR the given address and -;; into PPUDATA the given byte value. -.macro WRITE_PPU_DATA address, value - bit $2002 ; PPUSTATUS - lda #.HIBYTE(address) - sta $2006 ; PPUADDR - lda #.LOBYTE(address) - sta $2006 ; PPUADDR - lda #value - sta $2007 ; PPUDATA -.endmacro +;; Include the WRITE_PPU_DATA macro. More on this below. +.include "../shared/ppu.s" ;; init_nametable loads the relevant data to the nametable that is then going to ;; be used in order to build up the background. @@ -468,8 +459,8 @@ palettes: ;; Now we have to do the same for the rest of the elements. I have gone ;; through the same process of fetching the offset from a canvas I drew on ;; NEXXT and here's the result. Instead of repeating te same code over and - ;; over but with slightly different values, I am using this macro I created - ;; to do the same. + ;; over but with slightly different values, I am using the WRITE_PPU_DATA + ;; from `shared/ppu.s` to do the same. WRITE_PPU_DATA $20B9, $04 WRITE_PPU_DATA $21CE, $04 WRITE_PPU_DATA $21BA, $04 diff --git a/docs/blink.gif b/docs/blink.gif Binary files differnew file mode 100644 index 0000000..a54d560 --- /dev/null +++ b/docs/blink.gif diff --git a/docs/roulette.gif b/docs/roulette.gif Binary files differnew file mode 100644 index 0000000..805f9f0 --- /dev/null +++ b/docs/roulette.gif diff --git a/fx/README.md b/fx/README.md index e7194dc..f42f323 100644 --- a/fx/README.md +++ b/fx/README.md @@ -10,3 +10,5 @@ the data. A very simple example is provided in [blink.s](./blink.s) where a character blinks periodically. + + @@ -8,8 +8,9 @@ ;; `basics/unrom`, which also perform bank switching albeit with simpler ;; hardware. ;; -;; This examples basically makes use of the bank switching capabilities of the -;; MMC3 chip, so the handling of IRQs is left to other examples. +;; This example basically makes use of the bank switching capabilities of the +;; MMC3 chip, so the handling of IRQs is left to other examples (e.g. see +;; `scroll/roulette.s`). ;; ;; On the context of the MMC3 chip, the CPU ROM space is divided into 4 regions ;; of 8KB each. Two of these regions are swappable, the rest are fixed. This is @@ -154,9 +155,9 @@ reset: sta MMC3::MIRRORING ;; Interrupts are a whole topic of their own on the MMC3 chip. Here we just - ;; disable them and leave it for other examples. Again, this is tied to a - ;; register mapped to a region in memory (even addresses on $E000-$FFFE in - ;; this case). + ;; disable them and leave it for other examples (e.g. see + ;; `scroll/roulette.s`). Again, this is tied to a register mapped to a + ;; region in memory (even addresses on $E000-$FFFE in this case). sta MMC3::IRQ_DISABLE ;; PRG RAM can be protected from writes, but this is a feature that many diff --git a/scroll/README.md b/scroll/README.md index d24117d..be33f87 100644 --- a/scroll/README.md +++ b/scroll/README.md @@ -15,7 +15,20 @@ information), while allowing the rest of the screen to scroll as expected. In the end, it's the same example as `level.s` (same level to scroll), but the top part does not move and shows a "THIS DOES NOT MOVE" message. -Last but not least, the `mmc3.s` example shows how to configure the MMC3 chip -(e.g. Super Mario Bros. 3, Kirby's Adventure) to have better control on which -parts of the screen to scroll or not. To showcase this the example implements a -roulette mini-game (similar to Super Mario Bros. 3 but simpler). +## Scrolling in different ways in the same frame + +Some chips like the MMC3 give programmers a lot of flexibility when it comes to +mid-frame customization. That is, chips like the MMC3 give an interface in which +programmers can ask the chip to submit an IRQ on a given exact scanline. One +main usage of this was to allow a top section of the screen to scroll, while +leaving a small section at the bottom not to scroll. This way, games were no +longer required to have a status bar at the top and they could have it at the +bottom. But these chips allow for a lot of flexibility, so programmers can get +playful with it. One simple example is the roulette mini-game from Super Mario +Bros. 3. In here the game asks for two scanline IRQs and then the scroll +direction is changed on each given IRQ. This way, the background is split in +three sections that move in different directions/speed. Something similar (but +more simple) has been reproduced in [roulette.s](./roulette.s), giving the +following result: + + 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 diff --git a/shared/diskun.s b/shared/diskun.s index d8bab90..8349748 100644 --- a/shared/diskun.s +++ b/shared/diskun.s @@ -26,9 +26,9 @@ ;; Background .byte DEFAULT_COLOR, $36, $17, $0F - .byte DEFAULT_COLOR, $00, $00, $00 - .byte DEFAULT_COLOR, $00, $00, $00 - .byte DEFAULT_COLOR, $00, $00, $00 + .byte DEFAULT_COLOR, $28, $0F, $30 + .byte DEFAULT_COLOR, $20, $0F, $30 + .byte DEFAULT_COLOR, $2F, $20, $30 ;; Foreground .byte DEFAULT_COLOR, $28, $0F, $30 diff --git a/shared/ppu.s b/shared/ppu.s new file mode 100644 index 0000000..42acb06 --- /dev/null +++ b/shared/ppu.s @@ -0,0 +1,11 @@ +;; WRITE_PPU_DATA is a macro that will write into PPUADDR the given address and +;; into PPUDATA the given byte value. +.macro WRITE_PPU_DATA address, value + bit $2002 + lda #.HIBYTE(address) + sta $2006 + lda #.LOBYTE(address) + sta $2006 + lda #value + sta $2007 +.endmacro |
