aboutsummaryrefslogtreecommitdiff
path: root/basics/flicker.s
blob: 234c8a15c34fd859903687a9375d873d23989d11 (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
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
;;;
;; This is a simple example of sprite cycling, so sprites don't disappear on a
;; scanline overflow. This example is extremely simple, and on a real game you'd
;; take into account lots of things that would make this process less tedious
;; and less cycle consuming.

.segment "HEADER"
    .byte 'N', 'E', 'S', $1A

    .byte $02, $01

    .byte $00
    .byte $00

.segment "VECTORS"
    .addr nmi, reset, irq

.segment "CHARS"
.incbin "../assets/diskun.chr"

.segment "CODE"

.include "../shared/diskun.s"

.proc main
    jsr Diskun::init_palettes
    jsr init_sprites

    cli
    lda #%10001000
    sta $2000                   ; PPUCTRL
    lda #%00011110
    sta $2001                   ; PPUMASK

@main_game_loop:
    ;; NOTE: the logic is pretty simple: read the pad, move the player
    ;; accordingly, and apply the flickering effect.
    jsr joypad_read
    jsr Diskun::update

    ;; NOTE: comment this `jsr` out if you want to see what happens if no
    ;; flickering effect is applied (spoiler alert: the last character will
    ;; suddenly disappear :P).
    ;;
    ;; NOTE: this function is called mindlessly. In a real game you'd try to
    ;; detect scanline overflows, or you will try to be more careful with sprite
    ;; priorities and apply the flickering more carefully. Here it's applied for
    ;; (almost) every sprite on each frame and we roll with it.
    jsr apply_flicker

    lda #%10000000
    ora $20
    sta $20
@wait_for_render:
    bit $20
    bmi @wait_for_render

    jmp @main_game_loop
.endproc


;;;
;; NOTE: this is the actual meat of the example :D
.proc apply_flicker
    ;; We will store a 16-bit pointer to the first sprite that can be flickered.
    ;; In this case, we will point to the first sprite from the first NPC. That
    ;; is, we suppose that, for whatever reason, we don't want to apply the
    ;; flickering effect for the player.
    lda #$10
    sta $40
    lda #$02
    sta $41

    ;;;
    ;; The algorithm is really straight-forward here: we will simply cycle the
    ;; sprites starting from the one being pointed at $40-$41 until the last
    ;; sprite. The main idea is that the OAM will show sprites on a scanline in
    ;; order. Hence, those sprites that are left beyond the eigth on a scanline
    ;; will suddenly disappear. Flickering is a technique by which sprites are
    ;; rotated from OAM priority, and hence they won't disappear always, but
    ;; just for a small amount of time: just enough to see them, creating a
    ;; flickering effect.
    ;;
    ;; For this, we will reserve four bytes (1 sprite) for auxiliary temporary
    ;; values ($42-$45) which will then be used in order to carry values along
    ;; the cycle.

    ;; We initialize the auxiliary bytes with the data from the last sprite to
    ;; be iterated since these are the values to be stored for the first sprite
    ;; now (that is, the last element is now the first one).
    lda $24C
    sta $42
    lda $24D
    sta $43
    lda $24E
    sta $44
    lda $24F
    sta $45

@loop:
    ;; For each of the four bytes from a sprite, pick the value stored on its
    ;; auxiliary byte and save the old value into the auxiliary byte afterwards.
    ;; This way the current byte holds the value from the previous sprite, but
    ;; its value will be carry on into the next sprite.
    .repeat 4, I
        ldy #I
        lda $42, y
        pha
        lda ($40), y
        sta $42, y
        pla
        sta ($40), y
    .endrepeat

    ;; And move the sprite pointer 4 bytes (1 sprite). Note that we only need to
    ;; move the low byte since we know beforehand that everything will be inside
    ;; of the $2xx range.
    lda $40
    clc
    adc #4
    sta $40

    ;; We know that the last byte from the last sprite is held at $24F. Thus, if
    ;; the sprite pointer is already passed this point, we can break the loop.
    ;; Otherwise just carry on.
    cmp #$50
    bne @loop

    rts
.endproc

;;;
;; NOTE: and from here on stuff that is not relevant for sprite flickering.

nmi:
    bit $20
    bpl @next

    pha
    txa
    pha
    tya
    pha

    jsr Diskun::nmi_update

    lda #$00
    sta $2003                   ; OAMADDR
    lda #$02
    sta $4014                   ; OAMDMA

    bit $2002                   ; PPUSTATUS
    lda #$00
    sta $2005                   ; PPUSCROLL
    sta $2005                   ; PPUSCROLL

    lda #%01111111
    and $20
    sta $20

    pla
    tay
    pla
    tax
    pla
@next:
    rti

reset:
    sei
    cld

    ldx #$40
    stx $4017                   ; APU Frame Counter

    ldx #$ff
    txs

    inx
    stx $2000                   ; PPUCTRL
    stx $2001                   ; PPUMASK
    stx $4010                   ; APU DMC

@vblankwait1:
    bit $2002                   ; PPUSTATUS
    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         ; if x overflows back to #00, then we are done.

    lda #$ef
@sprite_reset_loop:
    sta $200, x
    inx
    bne @sprite_reset_loop

    lda #$00
    sta $2003                   ; OAMADDR
    lda #$02
    sta $4014                   ; OAMDMA

@vblankwait2:
    bit $2002                   ; PPUSTATUS
    bpl @vblankwait2

    lda #$3F
    sta $2006                   ; PPUADDR
    lda #$00
    sta $2006                   ; PPUADDR

    lda #$0F
    ldx #$20
@palettes_reset_loop:
    sta $2007                   ; PPUDATA
    dex
    bne @palettes_reset_loop

    jmp main

irq:
    rti

.proc init_sprites
    NUM_SPRITES = 20

    lda #$40
    sta Diskun::m_screen_y
    lda #$46
    sta Diskun::m_screen_x

    ldx #$00
@load_sprites_loop:
    lda initial_sprite_data, x
    sta $0200, x
    inx
    cpx #(4 * NUM_SPRITES)
    bne @load_sprites_loop
    rts
initial_sprite_data:
    ;; $200-$20F
    .byte $40, $01, %00000000, $46
    .byte $40, $01, %01000000, $4E
    .byte $48, $11, %00000000, $46
    .byte $48, $11, %01000000, $4E

    ;; $210-$21F
    .byte $60, $01, %00000000, $68
    .byte $60, $01, %01000000, $70
    .byte $68, $11, %00000000, $68
    .byte $68, $11, %01000000, $70

    ;; $220-$22F
    .byte $60, $01, %00000000, $7A
    .byte $60, $01, %01000000, $82
    .byte $68, $11, %00000000, $7A
    .byte $68, $11, %01000000, $82

    ;; $230-$23F
    .byte $60, $01, %00000000, $8C
    .byte $60, $01, %01000000, $94
    .byte $68, $11, %00000000, $8C
    .byte $68, $11, %01000000, $94

    ;; $240-$24F
    .byte $60, $01, %00000000, $9E
    .byte $60, $01, %01000000, $A6
    .byte $68, $11, %00000000, $9E
    .byte $68, $11, %01000000, $A6
.endproc