aboutsummaryrefslogtreecommitdiff
path: root/scroll/sprite0.s
diff options
context:
space:
mode:
authorMiquel Sabaté Solà <mikisabate@gmail.com>2025-03-11 16:39:44 +0100
committerMiquel Sabaté Solà <mikisabate@gmail.com>2025-03-11 16:39:44 +0100
commitf05ce5d5ff7d650a439b363993f8b38b817d3efe (patch)
tree6d324e019650b1936d84e3c0935192e1fb7cbb72 /scroll/sprite0.s
parent4795fcad304087a9308e024a25233873c43501be (diff)
downloadcode.nes-f05ce5d5ff7d650a439b363993f8b38b817d3efe.tar.gz
code.nes-f05ce5d5ff7d650a439b363993f8b38b817d3efe.zip
scroll: Provide an example with sprite 0 collision
Signed-off-by: Miquel Sabaté Solà <mikisabate@gmail.com>
Diffstat (limited to 'scroll/sprite0.s')
-rw-r--r--scroll/sprite0.s261
1 files changed, 260 insertions, 1 deletions
diff --git a/scroll/sprite0.s b/scroll/sprite0.s
index 215a491..4bd940d 100644
--- a/scroll/sprite0.s
+++ b/scroll/sprite0.s
@@ -1 +1,260 @@
-;; TODO: Through sprite0 hit
+;;;
+;; Show like `level.s` but at the very top of the screen we have a "This is a
+;; message" being shown. This message is part of the background but it does not
+;; scroll like the rest of the screen, but it stays at the same coordinates all
+;; the time. This is done through sprite 0 collision detection, which is a
+;; technique is quite often for early games on the NES/Famicom library (e.g.
+;; Super Mario Bros.).
+;;
+;; The code is really similar to what we had in `level.s`, so I have removed all
+;; comments from sections that are identical to those of `level.s`. That is, you
+;; can just read the comments to get a "diff" between this one and `level.s`.
+
+.segment "HEADER"
+ .byte 'N', 'E', 'S', $1A
+
+ .byte $02
+ .byte $01
+
+ .byte $01
+ .byte $00
+
+.segment "VECTORS"
+ .addr nmi, reset, irq
+
+.segment "CODE"
+
+;; We include the engine as in `level.s`, but we tweak the row offset so we have
+;; at least one empty row in order to fit the HUD from this example.
+BACKGROUND_ROW_OFFSET = 1
+.include "include/all.s"
+
+;; Clear out the first two rows of tiles from the nametable identified by the
+;; `x` register.
+.proc clear_row_x
+ bit PPU::STATUS
+
+ stx PPU::ADDRESS
+ lda #$00
+ sta PPU::ADDRESS
+
+ ldx #$40
+@loop:
+ sta PPU::DATA
+ dex
+ bne @loop
+
+ rts
+.endproc
+
+;; Show our awesome HUD, which simply shows a background message with "This is a
+;; message". Note that the first sprite on OAM will be initialized also here,
+;; which if you look into the CHR file you will realize it's merely two dots put
+;; together. The trick is to put this simple sprite right into the final "E" of
+;; "message", so it's hidden there. Whenever the PPU detects the collision (i.e.
+;; "sprite 0 collision"), we will be able to react accordingly.
+;;
+;; Note also that because of the limitations from the engine in `include` on not
+;; being able to have multiple palettes, it really stands out. This is actually
+;; useful on this example, but in a real game you'd want to dedicate a palette
+;; which matches the same color from the background one. That is, you'd really
+;; want to hide it.
+.proc show_hud
+ ;; Clear the first two rows of tiles for both nametables as this will be
+ ;; where we will place the HUD.
+ ldx #$20
+ jsr clear_row_x
+ ldx #$24
+ jsr clear_row_x
+
+ ;; Set the background for the HUD. Note that we need it in both nametables,
+ ;; as the engine will actually flip the nametable being used on the
+ ;; PPU::CONTROL register.
+
+ ;; This
+ WRITE_PPU_DATA $2028, $23
+ WRITE_PPU_DATA $2029, $17
+ WRITE_PPU_DATA $202A, $18
+ WRITE_PPU_DATA $202B, $22
+ WRITE_PPU_DATA $2428, $23
+ WRITE_PPU_DATA $2429, $17
+ WRITE_PPU_DATA $242A, $18
+ WRITE_PPU_DATA $242B, $22
+
+ ;; is
+ WRITE_PPU_DATA $202D, $18
+ WRITE_PPU_DATA $202E, $22
+ WRITE_PPU_DATA $242D, $18
+ WRITE_PPU_DATA $242E, $22
+
+ ;; a
+ WRITE_PPU_DATA $2030, $10
+ WRITE_PPU_DATA $2430, $10
+
+ ;; message
+ WRITE_PPU_DATA $2032, $1C
+ WRITE_PPU_DATA $2033, $14
+ WRITE_PPU_DATA $2034, $22
+ WRITE_PPU_DATA $2035, $22
+ WRITE_PPU_DATA $2036, $10
+ WRITE_PPU_DATA $2037, $16
+ WRITE_PPU_DATA $2038, $14
+ WRITE_PPU_DATA $2432, $1C
+ WRITE_PPU_DATA $2433, $14
+ WRITE_PPU_DATA $2434, $22
+ WRITE_PPU_DATA $2435, $22
+ WRITE_PPU_DATA $2436, $10
+ WRITE_PPU_DATA $2437, $16
+ WRITE_PPU_DATA $2438, $14
+
+ ;; Initialize sprite 0, which won't change across the run.
+ lda #$06
+ sta $200
+ lda #$03
+ sta $201
+ lda #$00
+ sta $202
+ lda #$C0
+ sta $203
+
+ rts
+.endproc
+
+;; From level.s the only thing changed is the call to `show_hud`.
+.proc main
+ lda #$00
+ sta PPU::zp_mask
+ sta PPU::MASK
+
+ jsr Palettes::init
+ jsr Metatile::init
+ jsr Driver::init
+ jsr Background::init
+ jsr Player::init
+
+ ;; Show HUD.
+ jsr show_hud
+
+ cli
+
+ lda #%10001000
+ sta PPU::zp_control
+ sta PPU::CONTROL
+
+@main_game_loop:
+ jsr joypad_read
+ jsr Player::update
+ jsr Driver::update
+
+ lda #%10000000
+ ora Globals::zp_flags
+ sta Globals::zp_flags
+
+@wait_for_render:
+ bit Globals::zp_flags
+ bmi @wait_for_render
+
+ jmp @main_game_loop
+.endproc
+
+;; It's mostly as in `level.s`, but: 1. the scroll register is always set; 2. we
+;; have to handle sprite 0 collision.
+.proc nmi
+ bit Globals::zp_flags
+ bpl @next
+
+ pha
+ txa
+ pha
+ tya
+ pha
+
+ jsr Player::update_sprite
+
+ OAM_WRITE_SPRITES
+
+ FLUSH_PENDING_VRAM_BUFFER
+
+ ;; Should we update PPU registers? If not, then we can go down to the next
+ ;; section. Otherwise we need to update the PPU registers that have been
+ ;; buffered. Note that in stark difference with `level.s` the scroll
+ ;; register is not included inside of the code below. That's because it has
+ ;; to be set unconditionally or the whole sprite 0 detection trick would
+ ;; flicker whenever the player stops scrolling the screen.
+ bit Globals::zp_flags
+ bvc @scroll
+
+ lda #%10111111
+ and Globals::zp_flags
+ sta Globals::zp_flags
+
+ bit PPU::STATUS
+
+ lda PPU::zp_control
+ sta PPU::CONTROL
+ lda PPU::zp_mask
+ sta PPU::MASK
+
+@scroll:
+ ;; Is the level actually loaded? If not then it's pointless to mess with the
+ ;; scroll register or waiting for anything: skip all of this.
+ lda Globals::zp_flags
+ and #%00100000
+ beq @unset_render_flag
+
+ ;; First of all, reset the scroll register to zero for the HUD.
+ bit PPU::STATUS
+ lda #$00
+ sta PPU::SCROLL
+ lda #$00
+ sta PPU::SCROLL
+
+ ;; To be safe, wait until the sprite 0 bit is unset. This will happen
+ ;; whenever the PPU starts rendering the screen. In other words, we wait
+ ;; until VBlank has been consumed because this whole trick has to happen
+ ;; mid-frame rendering.
+@wait_sprite0_unset:
+ bit PPU::STATUS
+ bvs @wait_sprite0_unset
+
+ ;; And now everything will be rendered as usual, with scroll = 0. So wait
+ ;; until the PPU detects a collision between a background element and sprite
+ ;; 0. This will happen at the last point of the "E" in "MESSAGE", where our
+ ;; sprite 0 has been "hidden". Whenever that happens, the PPU will set the
+ ;; proper bit on the PPU::STATUS register.
+@wait_sprite0_set:
+ bit PPU::STATUS
+ bvc @wait_sprite0_set
+
+ ;; NOTE: after this some games like Super Mario Bros. set up a small delay,
+ ;; but through testing both on emulators (FCEUX and Mesen) and on real
+ ;; hardware, I haven't seen any problems without this delay.
+
+ ;; Update the scroll register again to its real value.
+ lda Background::zp_scroll
+ sta PPU::SCROLL
+ lda #$00
+ sta PPU::SCROLL
+
+ ;; And continue as in `level.s`.
+
+@unset_render_flag:
+ lda #%01111111
+ and Globals::zp_flags
+ sta Globals::zp_flags
+
+ pla
+ tay
+ pla
+ tax
+ pla
+@next:
+ rti
+.endproc
+
+.proc irq
+ rti
+.endproc
+
+.segment "CHARS"
+ .incbin "../assets/diskun.chr"