aboutsummaryrefslogtreecommitdiff
;;;
;; 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"