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
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
|
;; Set the player to its initial coordinates.
.macro RESET_PLAYER_POSITION
lda #40
sta Player::zp_screen_x
sta Player::zp_screen_y
.endmacro
;; This is the player code that gets re-used on all scrolling examples. This
;; will show a meta-sprite made up of four sprites that make up diskun. You can
;; move this player with the arrows, and the movement will be pretty basic (1
;; pixel at a time). Furthermore, the sprite handles collision with background
;; elements, which is shared across all the scrolling examples.
;;
;; The sprite is stored in OAM memory $204-$213 (hence sprite0 is left out so
;; other examples can use it), and the $30 and $31 memory addresses are used for
;; keeping up with the screen coordinates.
.scope Player
;; The minimum value that the player is allowed to have on the Y axis.
MIN_SCREEN_Y = 8
;; The maximum value of X that is allowed before we start updating the
;; scroll value.
MAX_SCREEN_X = 144
;; The height for the player when it comes to detecting the edge of the
;; screen or background collisions.
PLAYER_HEIGHT = 25
;; The width for the player when it comes to detecting the edge of the
;; screen or background collisions.
PLAYER_WIDTH = 16
;; Indeces for the collisions table as defined on `check_bg_collision_x` so
;; the setting of the `x` register is less magical.
TOP_COLLISION_INDEX = (0 << 2)
RIGHT_COLLISION_INDEX = (1 << 2)
DOWN_COLLISION_INDEX = (2 << 2)
LEFT_COLLISION_INDEX = (3 << 2)
;; Coordinates on the X axis.
;; NOTE: shadowed in collision.s. If you change it here, change it there as
;; well.
zp_screen_x = $30
;; Coordinates on the Y axis.
;; NOTE: shadowed in collision.s. If you change it here, change it there as
;; well.
zp_screen_y = $31
;; Initialize the sprite for the player.
.proc init
RESET_PLAYER_POSITION
lda #$01
sta $205
lda #$02
sta $209
lda #$11
sta $20D
lda #$12
sta $211
lda #0
sta $206
sta $20A
sta $20E
sta $212
rts
.endproc
;; The update for the player is simply about checking for the d-pad. Note
;; that if pressing right, then the scroll value might also move, which
;; might mean to load the next background column until the end of the
;; screen.
.proc update
;; Is the player requesting to go up?
lda #Joypad::BUTTON_UP
and Joypad::zp_buttons1
beq @check_down
;; If we are already at the top disregard this button press and check
;; for the left button.
lda #MIN_SCREEN_Y
cmp zp_screen_y
beq @check_left
;; If, for whatever reason, we are below the minimum value, then reset
;; the value to this minimum Y value and jump to check the left button.
bcc @update_y_up
sta zp_screen_y
jmp @check_left
@update_y_up:
;; Try to go up and check for a collision with the background.
dec zp_screen_y
dec zp_screen_y
ldx #TOP_COLLISION_INDEX
jsr check_bg_collision_x
beq @check_left
;; A collision was detected. Then get back to the old value of Y and
;; move on.
inc zp_screen_y
inc zp_screen_y
jmp @check_left
@check_down:
;; Is the player requesting to go down?
lda #Joypad::BUTTON_DOWN
and Joypad::zp_buttons1
beq @check_left
;; We have to move down unless we are already at the very bottom.
lda zp_screen_y
cmp #(240 - PLAYER_HEIGHT)
bcc @update_y_down
lda #(240 - PLAYER_HEIGHT)
sta zp_screen_y
jmp @check_left
@update_y_down:
;; Try to go down as requested, but check for a collision with the
;; background.
inc zp_screen_y
inc zp_screen_y
ldx #DOWN_COLLISION_INDEX
jsr check_bg_collision_x
beq @check_left
;; There was a collision with the background, restore back the value on
;; Y and move on.
dec zp_screen_y
dec zp_screen_y
@check_left:
;; Is the player requesting to go left?
lda #Joypad::BUTTON_LEFT
and Joypad::zp_buttons1
beq @check_right
;; We have to move left unless we are already at the leftmost edge.
lda zp_screen_x
bne :+
rts
:
;; We are not at the leftmost edge, try to go left while also checking
;; for a background collision.
dec zp_screen_x
dec zp_screen_x
ldx #LEFT_COLLISION_INDEX
jsr check_bg_collision_x
bne :+
rts
:
;; There was a collision, restore back the value on X and quit.
inc zp_screen_x
inc zp_screen_x
rts
@check_right:
;; Last check! Is the player requesting to go right?
lda #Joypad::BUTTON_RIGHT
and Joypad::zp_buttons1
bne @check_level_end
rts
@check_level_end:
;; Are we at the last screen? If so consume the rest of the missing
;; scroll. Whenever that is done (i.e. there are no more screens and the
;; scroll sits at a zero value), then we can no longer load more columns
;; or update the scroll: stick to updating the X position.
lda #%00001000
and Globals::zp_flags
beq @check_max_screen
lda Background::zp_scroll
beq @update_x
@check_max_screen:
;; Is the X position already at the MAX_SCREEN_X? If so, then we don't
;; move the character: we move the scroll instead.
lda zp_screen_x
cmp #Player::MAX_SCREEN_X
bcc @update_x
;; Moving will then be a matter of increasing the scroll value.
inc Background::zp_scroll
inc Background::zp_scroll
ldx #RIGHT_COLLISION_INDEX
jsr check_bg_collision_x
bne @collision_on_scroll
;; If there are no collisions in the scrolling scenario, then we might
;; need to load the next background column in advance. Whether that is
;; possible or something that we want to do is handled automatically by
;; the `Background::load_next_background` function. Hence, call this
;; function and quit.
JAL Background::load_next_background
@collision_on_scroll:
;; Collision was given. Hence, restore the value for the scroll and
;; quit.
dec Background::zp_scroll
dec Background::zp_scroll
rts
@update_x:
;; Do not overwrap the screen.
lda zp_screen_x
cmp #(256 - PLAYER_WIDTH)
beq @end
;; We are not about to overwrap the screen, let's update the value on
;; the X axis and check whether a collision would happen.
inc zp_screen_x
inc zp_screen_x
ldx #RIGHT_COLLISION_INDEX
jsr check_bg_collision_x
beq @end
;; There was a collision, roll back the value for the X axis.
dec zp_screen_x
dec zp_screen_x
@end:
rts
.endproc
;; Check collision between the player and any background element. Note that
;; collision is not tested on all edges for the player, but you have to
;; provide a "collision index", which refers to which edges to test. See the
;; "*_COLLISION_INDEX" constants above and the `edges` data included inside
;; of this function. This collision index has to be provided on the `x`
;; register.
;;
;; Returns a non-zero value on the `a` register if a collision was detected,
;; zero otherwise.
.proc check_bg_collision_x
;; We have to check for a collision on both the left and the right
;; edges, which should follow the same code. Unrolling it is simply
;; faster and more clear than setting up a loop.
.repeat 2, I
;; Save the offset on the Y coordinate.
lda edges, x
sta Globals::zp_arg0
;; Save the offset on the X coordinate.
inx
lda edges, x
sta Globals::zp_arg1
;; Save the edge index in preparation for the following calls.
.if I = 0
inx
stx Globals::zp_idx
.endif
;; Transform screen coordinates into metatile ones and fetch the
;; collision bitmap for it with its mask.
jsr Collision::screen_to_mt_coordinates
jsr Collision::get_background_collision_y
;; Restore back the edge index.
.if I = 0
ldx Globals::zp_idx
.endif
;; Checking for a collision is as simple as performing an AND with the
;; bitmap at the `a` register and the byte that can be addressed with
;; the given pointer. If the result is non-zero, then we found a match.
and (Globals::zp_arg0), y
.if I = 0
;; If I = 1 we are falling through as expected anyways, so this
;; instruction is not needed.
bne @collision
.endif
.endrepeat
;; The last instruction before ending up here will already have the `a`
;; register lined up to the proper return value. If a collision was
;; found (in either left/right case), `bne @collision` is good, and
;; hence `a` has an expected non-zero value. The same is true when not
;; in a collision.
@collision:
rts
edges:
;; Up: top-left (y, x), top-right (y, x)
.byte $02, $02, $02, $0F
;; Right: top-right (y, x), bottom-right (y, x)
.byte $02, $0F, $0F, $0F
;; Down: bottom-left (y, x), bottom-right (y, x)
.byte $0F, $02, $0F, $0F
;; Left: top-left (y, x), bottom-left (y, x)
.byte $02, $01, $0F, $01
.endproc
;; Update the memory on the PPU with the current values for the screen
;; coordinates of the player.
;;
;; NOTE: only call this function on NMI Code.
.proc update_sprite
lda zp_screen_x
sta $207
sta $20F
clc
adc #8
sta $20B
sta $213
lda zp_screen_y
sta $204
sta $208
clc
adc #8
sta $20C
sta $210
rts
.endproc
.endscope
|