;;;
;; The same as in `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:
READ_JOYPAD1
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"