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
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
|
.segment "CODE"
;; Functions and variables that keep up with the player's sprite. That is,
;; movement, heading, animation, collision with the environment, etc.
.scope Player
;;;
;; *Movement* is done with subpixel precision. Hence, we need to handle
;; things via fixed-point arithemetic. In particular, the `zp_screen_*`
;; variables refer to pure screen coordinates, and they are the only uint8_t
;; variables. As for the rest:
;;
;; - Velocity (actual and target) are 4.4 fixed-point signed values. Hence,
;; the high nibble represents the signed integer part, and the low nibble
;; the fractional one.
;; - Each Position is a 16-bit signed fixed-point value in little-endian
;; format like so: |llll/ffff| - |0000/hhhh|. That is, the low byte is
;; split with the fractional part and the low nibble of the integer part,
;; and the high byte only contains the high nibble of the signed integer
;; part (and the high nibble of that high byte is discarded).
;;
;; This has the following properties:
;;
;; - Adding the velocity to a position is just a matter of performing an
;; `adc` between the low byte of the position and the velocity.
;; - Translating positions into screen coordinates it's a matter of simply
;; rolling the low nibble of the high byte into the low byte.
;; The height of the player, which is 24 pixels long as it's made up of 3
;; sprites.
PLAYER_HEIGHT = $18
;; The initial position is the ground minus the height of the sprite (as the
;; Y accounts for the top left pixel). For the high byte we only want to
;; high nibble put into its low nibble, and for the low byte we shift the
;; low nibble into a high one and leave subpixels to 0.
INIT_Y_POSITION_LO = ((Background::GROUND_Y_COORD - PLAYER_HEIGHT) & $0F) << 4
INIT_Y_POSITION_HI = (Background::GROUND_Y_COORD - PLAYER_HEIGHT) >> 4
INIT_Y_VELOCITY = $08
;; The initial position on the X axis is more or less at the center.
INIT_X_POSITION_LO = $00
INIT_X_POSITION_HI = $07
THROTTLE = $D8
BLAST_OFF = $F8
GRAVITY = $28
zp_screen_y = $40
zp_position_y = $41 ; NOTE: 16-bit.
zp_target_velocity_y = $43 ; TODO: needed?
zp_velocity_y = $44
zp_screen_x = $45
zp_position_x = $46 ; NOTE: 16-bit.
zp_target_velocity_x = $48 ; TODO: needed?
zp_velocity_x = $49
;; Initialize the player's sprite. Note that for the sprite to look
;; correctly on screen you still need to call `Player::update` afterwards.
.proc init
;; Reset velocity
lda #$00
sta zp_target_velocity_y
sta zp_velocity_y
sta zp_target_velocity_x
sta zp_velocity_x
;; Set position, and the screen coordinates will be updated upong
;; calling `update`, which on initialization will happen right after.
lda #INIT_Y_POSITION_LO
sta zp_position_y
lda #INIT_Y_POSITION_HI
sta zp_position_y + 1
lda #INIT_X_POSITION_LO
sta zp_position_x
lda #INIT_X_POSITION_HI
sta zp_position_x + 1
;;;
;; TODO: this should really just go away
lda #$00
sta $201
lda #$00
sta $202
lda #$01
sta $205
lda #$00
sta $206
lda #$10
sta $209
lda #$00
sta $20A
lda #$11
sta $20D
lda #$00
sta $20E
lda #$20
sta $211
lda #$00
sta $212
lda #$21
sta $215
lda #$00
sta $216
rts
.endproc
.proc update
jsr update_vertical_position
;; TODO: horizontal
;; At this point all positions are clear, transform them into screen
;; coordinates, eject out from boundaries and platforms, and update the
;; sprite with the new state.
jsr position_to_screen
jsr background_check
JAL update_sprite
.endproc
;; Updates the `zp_velocity_y` and the `zp_position_y` depending on whether
;; the player is throttling or gravity should just apply.
.proc update_vertical_position
;; Check if the player is asking to throttle, otherwise apply gravity.
lda #(Joypad::BUTTON_UP | Joypad::BUTTON_A)
and Joypad::zp_buttons1
beq @set_gravity
lda zp_velocity_y
beq @blast_off
lda #THROTTLE
bne @compute_vertical
@set_gravity:
lda #GRAVITY
@compute_vertical:
sta Globals::zp_tmp0
;; Check the difference between the given target velocity and what we
;; have now. If it equals to zero, then we change nothing in regards to
;; the velocity.
lda zp_velocity_y
sec
sbc Globals::zp_tmp0
beq @apply_velocity
;; Increase or decrease depending on what we have now.
;; TODO: inc/dec might not quite cut it in NTSC vs PAL
bmi @down
dec zp_velocity_y
jmp @apply_velocity
@down:
inc zp_velocity_y
jmp @apply_velocity
@blast_off:
lda #BLAST_OFF
sta zp_velocity_y
jmp @going_up
@apply_velocity:
lda zp_velocity_y
bmi @going_up
;; The velocity is positive, so it's just a 16-bit addition.
clc
adc zp_position_y
sta zp_position_y
lda #0
adc zp_position_y + 1
sta zp_position_y + 1
rts
@going_up:
;; Negative velocity, we need to go up. This is probably not optimal,
;; but we just invert the number and subtract with that.
lda #0
sec
sbc zp_velocity_y
sta Globals::zp_tmp0
lda zp_position_y
sec
sbc Globals::zp_tmp0
sta zp_position_y
lda zp_position_y + 1
sbc #0
sta zp_position_y + 1
rts
.endproc
;; Convert the positions with subpixel precision into mere screen
;; coordinates. That is, update the values on `zp_screen_{x,y}` given the
;; current values of `zp_position_{x,y}`.
.proc position_to_screen
;; We save the high byte into a temporary value, and we load the low
;; byte into the accumulator.
lda zp_position_y + 1
sta Globals::zp_tmp0
lda zp_position_y
;; And now it's a matter of rotating the high byte into the low one to
;; match a full byte.
lsr Globals::zp_tmp0
ror
lsr Globals::zp_tmp0
ror
lsr Globals::zp_tmp0
ror
lsr Globals::zp_tmp0
ror
;; Ecce Y coordinates.
sta zp_screen_y
;; And the same for the X coordinates.
lda zp_position_x + 1
sta Globals::zp_tmp0
lda zp_position_x
;; And rolling...
lsr Globals::zp_tmp0
ror
lsr Globals::zp_tmp0
ror
lsr Globals::zp_tmp0
ror
lsr Globals::zp_tmp0
ror
;; Ecce X coordinates.
sta zp_screen_x
rts
.endproc
;; Check on whether the player is out of bounds in any way and provide an
;; ejection logic for each situation.
.proc background_check
;; If we are going down, the player's height should be added to the
;; coordinate, as we are checking for the bottom.
lda zp_screen_y
ldx zp_velocity_y
bmi :+
clc
adc #PLAYER_HEIGHT
:
;; And convert raw screen coordinates into a tile coordinate.
lsr
lsr
lsr
sta Globals::zp_tmp0
;; We do the same for the X axis.
lda zp_screen_x
tay
lsr
lsr
lsr
sta Globals::zp_tmp1
tya
clc
adc #16
lsr
lsr
lsr
sta Globals::zp_tmp2
;; Let's first check if there's any match on the vertical axis.
ldx #0
@vertical_check:
lda Background::platforms, x
;; End of the list, no matches: begone!
cmp #$FF
beq @end
;; Prepare for either horizontal check (which require one 'inx') or the
;; next iteration (which require three 'inx').
inx
;; The first byte is the vertical tile coordinate. If that doesn't
;; match, go for the next one.
cmp Globals::zp_tmp0
beq @horizontal_check
inx
inx
jmp @vertical_check
@horizontal_check:
;; Save up this value just in case we are actually grounded.
sta Globals::zp_tmp3
;; Check that the right corner of the player is to the right of the left
;; edge of the platform.
lda Background::platforms, x
cmp Globals::zp_tmp2
bcs @end
;; And now check that the left corner of the player is to the left of
;; the right edge of the platform.
inx
lda Background::platforms, x
cmp Globals::zp_tmp1
bcc @end
;; Hey, we have a collision! Are we grounded or fighting with a ceiling?
ldy zp_velocity_y
bmi @ceiling
;; Translate the stored Y tile index into coordinates and account for
;; the player's height. That's the final screen position.
lda Globals::zp_tmp3
asl
asl
asl
sec
sbc #PLAYER_HEIGHT
sta zp_screen_y
;; Clearing out the subpixel value does the job.
lda #$F0
and zp_position_y
sta zp_position_y
;; And reset the velocity on the Y axis as we are grounded.
lda #0
sta zp_velocity_y
rts
@ceiling:
;; TODO
@end:
rts
.endproc
.proc update_sprite
;; TODO:
;; - Update heading
;; - Update motion state
;; - Update tiles
jsr update_sprite_coordinates
rts
.endproc
;; Update the coordinate for the six sprites that make up the player.
.proc update_sprite_coordinates
;; Y axis.
lda zp_screen_y
sta $0200
sta $0204
clc
adc #8
sta $0208
sta $020C
clc
adc #8
sta $0210
sta $0214
;; X axis.
lda zp_screen_x
sta $0203
sta $020B
sta $0213
clc
adc #8
sta $0207
sta $020F
sta $0217
rts
.endproc
.endscope
|