aboutsummaryrefslogtreecommitdiff
path: root/scroll/sprite0.s
blob: a1944e62165533d344052a7c7c337553c6713b78 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
;;;
;; 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:
    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"