-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathgrid-collision.asm
451 lines (382 loc) · 24.7 KB
/
grid-collision.asm
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
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
; Grid Collision Example for the Nintendo Game Boy
; by Dave VanEe 2022
; Tested with RGBDS 0.6.0
; License: CC0 (https://creativecommons.org/publicdomain/zero/1.0/)
; This example builds on the following examples:
; - background-tilemap (except using a manually constructed tilemap)
; - vblank/oamdma/sprite
; - joypad
; The main additions in this example are moving a player in response to input (ProcessInput), and checking the tilemap
; entries to determine if attempted moves are into valid spaces based on tile ID (GetTileID). Note that instead of using an
; automatically generated tilemap as in the background-filemap example, the tilemap is pre-constructed with tiles organized
; such that walkable tiles start at 0 and are sequential to simplify the collision check.
include "../hardware.inc" ; Include hardware definitions so we can use nice names for things
;============================================================================================================================
; Game Constants
;============================================================================================================================
def MAX_WALKABLE_TILE_ID equ 8 ; All tiles from 0 to this tile ID will be considered walkable for the purposes of collision
def OBJ_Y_OFFSET equ -9 ; Since we're using two objects to draw the player larger than 8x8, but we're still moving
def OBJ_X_OFFSET equ -4 ; on an 8x8 grid, we offset things slightly to center the player on the current tile
rsreset ; Reset the _RS counter to 0 for a new set of defines
def FACE_LEFT rb 1 ; Define FACE_LEFT as 0
def FACE_RIGHT rb 1 ; Define FACE_RIGHT as 1
def FACE_UP rb 1 ; Define FACE_UP as 2
def FACE_DOWN rb 1 ; Define FACE_DOWN as 3
;============================================================================================================================
; Game State Variables
;============================================================================================================================
SECTION "Game State Variables", WRAM0
wPlayer:
.y ds 1 ; Player's Y coordinate (in grid space)
.x ds 1 ; Player's X coordinate (in grid space)
.facing ds 1 ; Player's facing direction (0=left, 1=right, 2=up, 3=down)
;============================================================================================================================
; Interrupts
;============================================================================================================================
; The VBlank vector is where execution is passed when the VBlank interrupt fires
SECTION "VBlank Vector", ROM0[$40]
; We only have 8 bytes here, so push all the registers to the stack and jump to the rest of the handler
; Note: Since the VBlank handler used here only affects A and F, we don't have to push/pop BC, DE, and HL,
; but it's done here for demonstration purposes.
VBlank:
push af ; Push AF to the stack
ld a, HIGH(wShadowOAM) ; Load the high byte of our Shadow OAM buffer into A
jp VBlankHandler ; Jump to the rest of the handler
; The rest of the handler is contained in ROM0 to ensure it's always accessible without banking
SECTION "VBlank Handler", ROM0
VBlankHandler:
call hOAMDMA ; Call our OAM DMA routine (in HRAM), quickly copying from wShadowOAM to OAMRAM
pop af ; Pop AF off the stack
reti ; Return and enable interrupts (ret + ei)
;============================================================================================================================
; Initialization
;============================================================================================================================
; Define a section that starts at the point the bootrom execution ends
SECTION "Start", ROM0[$0100]
jp EntryPoint ; Jump past the header space to our actual code
ds $150-@, 0 ; Allocate space for RGBFIX to insert our ROM header by allocating
; the number of bytes from our current location (@) to the end of the
; header ($150)
EntryPoint:
di ; Disable interrupts as we won't be using them
ld sp, $e000 ; Set the stack pointer to the end of WRAM
; Turn off the LCD when it's safe to do so (during VBlank)
.waitVBlank
ldh a, [rLY] ; Read the LY register to check the current scanline
cp SCRN_Y ; Compare the current scanline to the first scanline of VBlank
jr c, .waitVBlank ; Loop as long as the carry flag is set
xor a ; Once we exit the loop we're safely in VBlank
ldh [rLCDC], a ; Disable the LCD (must be done during VBlank to protect the LCD)
; Copy the OAMDMA routine to HRAM, since during DMA we're limited on which
; memory the CPU can access (but HRAM is safe)
ld hl, OAMDMA ; Load the source address of our routine into HL
ld b, OAMDMA.end - OAMDMA ; Load the length of the OAMDMA routine into B
ld c, LOW(hOAMDMA) ; Load the low byte of the destination into C
.oamdmaCopyLoop
ld a, [hli] ; Load a byte from the address HL points to into the register A, increment HL
ldh [c], a ; Load the byte in the A register to the address in HRAM with the low byte stored in C
inc c ; Increment the low byte of the HRAM pointer in C
dec b ; Decrement the loop counter in B
jr nz, .oamdmaCopyLoop ; If B isn't zero, continue looping
; Copy our sprite and background tiles to VRAM
ld hl, SpriteTileData ; Load the source address of our tiles into HL
ld de, _VRAM ; Load the destination address in VRAM into DE
ld bc, SpriteTileData.end - SpriteTileData ; Load the number of bytes to copy into BC
call MemCopy ; Call our general-purpose memory copy routine
ld hl, BackgroundTileData ; Load the source address of our tiles into HL
ld de, _VRAM+$1000 ; Load the destination address in VRAM into DE
ld bc, BackgroundTileData.end - BackgroundTileData ; Load the number of bytes to copy into BC
call MemCopy ; Call our general-purpose memory copy routine
; Copy our 20x18 tilemap to VRAM
ld de, TilemapData ; Load the source address of our tilemap into DE
ld hl, _SCRN0 ; Point HL to the first byte of the tilemap ($9800)
ld b, SCRN_Y_B ; Load the height of the screen in tiles into B (18 tiles)
.tilemapLoop
ld c, SCRN_X_B ; Load the width of the screen in tiles into C (20 tiles)
.rowLoop
ld a, [de] ; Load a byte from the address DE points to into the A register
ld [hli], a ; Load the byte in the A register to the address HL points to and increment HL
inc de ; Increment the source pointer in DE
dec c ; Decrement the loop counter in C (tiles per row)
jr nz, .rowLoop ; If C isn't zero, continue copying bytes for this row
push de ; Push the contents of the register pair DE to the stack
ld de, SCRN_VX_B - SCRN_X_B ; Load the number of tiles remaining in the row into DE
add hl, de ; Add the remaining row length to HL, advancing the destination pointer to the next row
pop de ; Recover the former contents of the the register pair DE
dec b ; Decrement the loop counter in B (total rows)
jr nz, .tilemapLoop ; If B isn't zero, continue copying rows
; Setup palettes and scrolling
ld a, %11100100 ; Define a 4-shade palette from darkest (11) to lightest (00)
ldh [rBGP], a ; Set the background palette
ld a, %11010000 ; Define a 4-shade palette which omits the 10 value to increase player contrast
ldh [rOBP0], a ; Set an object palette
xor a ; Set A to zero
ldh [rSCX], a ; Set the background scroll registers to show the top-left
ldh [rSCY], a ; corner of the background in the top-left corner of the screen
ldh [hCurrentKeys], a ; Zero our current keys just to be safe (A is already zero from earlier)
; Initialize shadow OAM to zero
ld hl, wShadowOAM ; Point HL to the start of shadow OAM
ld b, wShadowOAM.end - wShadowOAM ; Load the size of shadow OAM into B (it's less than 256 so we can use a single byte)
.clearOAM
ld [hli], a ; Zero this OAM byte
dec b ; Decrement the loop counter in B (bytes of OAM)
jr nz, .clearOAM ; If B isn't zero, continue zeroing bytes
; Perform OAM DMA once to ensure OAM doesn't contain garbage
ld a, HIGH(wShadowOAM) ; Load the high byte of our Shadow OAM buffer into A
call hOAMDMA ; Call our OAM DMA routine (in HRAM), quickly copying from wShadowOAM to OAMRAM
; Setup the world state
ld hl, wPlayer ; Point HL to the start of the player's state in WRAM
ld a, 4 ; Load the starting Y coordinate into A
ld [hli], a ; Set the starting wPlayer.y value in WRAM
ld a, 2 ; Load the starting X coordinate into A
ld [hli], a ; Set the starting wPlayer.x value in WRAM
ld a, FACE_DOWN ; Load the starting facing direction into A
ld [hli], a ; Set the starting wPlayer.facing value in WRAM
; Setup the VBlank interrupt
ld a, IEF_VBLANK ; Load the flag to enable the VBlank interrupt into A
ldh [rIE], a ; Load the prepared flag into the interrupt enable register
xor a ; Set A to zero
ldh [rIF], a ; Clear any lingering flags from the interrupt flag register to avoid false interrupts
ei ; enable interrupts!
; Combine flag constants defined in hardware.inc into a single value with logical ORs and load it into A
ld a, LCDCF_ON | LCDCF_BG8800 | LCDCF_BGON | LCDCF_OBJ16 | LCDCF_OBJON | LCDCF_WINOFF
ldh [rLCDC], a ; Enable and configure the LCD to show the background and objects
;============================================================================================================================
; Main Loop
;============================================================================================================================
LoopForever:
halt ; Halt the CPU, waiting until an interrupt fires (this will sync our loop with VBlank)
call UpdateJoypad ; Poll the joypad and store the state in HRAM
call ProcessInput ; Update the game state in response to user input
call PopulateShadowOAM ; Update the sprite locations for the next frame
jr LoopForever ; Loop forever
;============================================================================================================================
; Main Routines
;============================================================================================================================
SECTION "Main Routines", ROMX
; Process the user's inputs and update the game state accordingly
ProcessInput:
ldh a, [hNewKeys] ; Load the newly pressed keys byte into A
bit PADB_LEFT, a ; Check the state of the LEFT bit in A
ld bc, $00ff ; Preload B/C with dy/dx for left movement (0, -1)
ld d, FACE_LEFT ; Preload D with the facing value for LEFT
jr nz, .attemptMove ; If the bit was set, jump to attempt movement in that direction
bit PADB_RIGHT, a ; Check the state of the RIGHT bit in A
ld bc, $0001 ; Preload B/C with dy/dx for left movement (0, +1)
ld d, FACE_RIGHT ; Preload D with the facing value for RIGHT
jr nz, .attemptMove ; If the bit was set, jump to attempt movement in that direction
bit PADB_UP, a ; Check the state of the UP bit in A
ld bc, $ff00 ; Preload B/C with dy/dx for left movement (-1, 0)
ld d, FACE_UP ; Preload D with the facing value for UP
jr nz, .attemptMove ; If the bit was set, jump to attempt movement in that direction
bit PADB_DOWN, a ; Check the state of the DOWN bit in A
ld bc, $0100 ; Preload B/C with dy/dx for left movement (+1, 0)
ld d, FACE_DOWN ; Preload D with the facing value for DOWN
jr nz, .attemptMove ; If the bit was set, jump to attempt movement in that direction
ret ; No inputs to handle, return to main loop
; Attempt a move in a direction defined by the contents of BC and D
; @param: B Delta Y to apply to current player position
; @param: C Delta X to apply to current player position
; @param: D New facing direction value to apply
.attemptMove
ld a, d ; Move new facing direction from D to A
ld [wPlayer.facing], a ; Store new facing direction regardless of move success
; Calculate the destination coordinates by applying the deltas
ld a, [wPlayer.y] ; Load the current player Y coordinate into A
add b ; Add the dY value from B to get the new Y coordinate
ld b, a ; Store the new Y coordinate back in B
ld a, [wPlayer.x] ; Load the current player X coordinate into A
add c ; Add the dX value from C to get the new X coordinate
ld c, a ; Store the new Y coordinate back in C
; Check if the attempted move is valid
call GetTileID ; Call a routine to get the tile ID at the B=y, C=x coordinates
cp MAX_WALKABLE_TILE_ID ; Compare the tile ID from TilemapData to the maximum walkable tile ID
ret nc ; If the tile ID is greater than the maximum walkable tile ID, return
; Store the new coordinates
ld a, b ; Load the new Y coordinate into A
ld [wPlayer.y], a ; Store the new Y coordinate in memory
ld a, c ; Load the new X coordinate into A
ld [wPlayer.x], a ; Store the new X coordinate in memory
ret
; Return the tile ID in TilemapData at provided coordinates
; @param B: Y coordinate in tilemap
; @param C: X coordinate in tilemap
; @return A: Tile ID at coordinates given
GetTileID:
push bc ; Store the input coordinates on the stack
ld hl, TilemapData ; Load the start address of the TilemapData into HL
ld a, b ; Load the Y coordinate into A
or a ; Check if the Y coordinate is zero
jr z, .yZero ; If zero, skip the Y seeking code
ld de, SCRN_X_B ; Load the number of tiles per row of TilemapData into DE
.yLoop
add hl, de ; Add the number of tiles per row to the pointer in HL
dec b ; Decrease the loop counter in B
jr nz, .yLoop ; Loop until we've offset to the correct row
.yZero
ld a, c ; Load the X coordinate into A
; Add the X coordinate offset to HL (this is a common way to add A to a 16-bit register)
add l ; Add the X coordinate to the low byte of the pointer in HL
ld l, a ; Store the new low byte of the pointer in L
adc h ; Add H plus the carry flag to the contents of A
sub l ; Subtract the contents of L from A
ld h, a ; Store the new high byte of the pointer in H
ld a, [hl] ; Read the value of TilemapData at the coordinates of interest into A
pop bc ; Recover the original input coordinates from the stack
ret
; Populate ShadowOAM with sprites based on the game state
PopulateShadowOAM:
ld hl, wShadowOAM ; Point HL at the beginning of wShadowOAM
; First sprite
ld a, [wPlayer.y] ; Load the player's Y coordinate into A
add a ; To convert the Y grid coordinate into screen coordinates we have to multiply
add a ; by 8, which can be done quickly by adding A to itself 3 times
add a ; ...
add $10+OBJ_Y_OFFSET ; Add the sprite offset ($10), plus the centering offset
ld [hli], a ; Store the sprite's Y coordinate in shadow OAM
ld b, a ; Cache the Y coordinate in B for use by the second sprite
ld a, [wPlayer.x] ; Load the player's X coordinate into A
add a ; Multiply the X coordinate by 8 the same as we did for Y above
add a ; ...
add a ; ...
add $08+OBJ_X_OFFSET ; Add the sprite offset ($08), plus the centering offset
ld [hli], a ; Store the sprite's X coordinate in shadow OAM
add $08 ; Add 8 to the X coordinate for the second sprite
ld c, a ; Cache the X coordinate in C for use by the second sprite
ld a, [wPlayer.facing] ; Load the player's facing direction into A
add a ; The player tiles have been stored in VRAM such that the facing direction multiplied
add a ; by 4 will yield the tile ID for the first sprite, so multiply by 4 using adds
ld [hli], a ; Store the sprite's tile ID in shadow OAM
add 2 ; Add 2 to the tile ID for the second sprite
ld d, a ; Cache the tile ID in D for use by the second sprite
xor a ; Set A to zero
ld [hli], a ; Store the sprite's attributes in shadow OAM
; Second sprite
ld a, b ; Load the prepared Y coordinate from B to A
ld [hli], a ; Store the sprite's Y coordinate in shadow OAM
ld a, c ; Load the prepared X coordinate from C to A
ld [hli], a ; Store the sprite's X coordinate in shadow OAM
ld a, d ; Load the prepared tile ID from D to A
ld [hli], a ; Store the sprite's tile ID in shadow OAM
xor a ; Set A to zero
ld [hli], a ; Store the sprite's attributes in shadow OAM
; Zero the remaning shadow OAM entries
; Note: Since we're only using 2/40 sprites, we could just loop 38 times, but the following approach will scale better if
; additional sprites are added. This will also clear previously used entires in cases where the number of sprites used
; each frame varies (which isn't the case here).
ld b, a ; Load zero (from the prior use) into B, since A will be used to check loop completion
.clearOAM
ld [hl], b ; Set the Y coordinate of this OAM entry to zero to hide it
inc l ; Advance 4 bytes to the next OAM entry
inc l ; ...
inc l ; ...
inc l ; ...
ld a, l ; Load the low byte of the shadow OAM pointer into A
cp LOW(wShadowOAM.end) ; Compare the low byte to the end of wShadowoAM
jr nz, .clearOAM ; Loop until we've hidden every unused sprite
ret
;============================================================================================================================
; Utility Routines
;============================================================================================================================
SECTION "MemCopy Routine", ROM0
; Since we're copying data few times, we'll define a reusable memory copy routine
; Copy BC bytes of data from HL to DE
; @param HL: Source address to copy from
; @param DE: Destination address to copy to
; @param BC: Number of bytes to copy
MemCopy:
ld a, [hli] ; Load a byte from the address HL points to into the register A, increment HL
ld [de], a ; Load the byte in the A register to the address DE points to
inc de ; Increment the destination pointer in DE
dec bc ; Decrement the loop counter in BC
ld a, b ; Load the value in B into A
or c ; Logical OR the value in A (from B) with C
jr nz, MemCopy ; If B and C are both zero, OR B will be zero, otherwise keep looping
ret ; Return back to where the routine was called from
;============================================================================================================================
; Joypad Handling
;============================================================================================================================
SECTION "Joypad Variables", HRAM
; Reserve space in HRAM to track the joypad state
hCurrentKeys: ds 1
hNewKeys: ds 1
SECTION "Joypad Routine", ROM0
; Update the newly pressed keys (hNewKeys) and the held keys (hCurrentKeys) in memory
; Note: This routine is written to be easier to understand, not to be optimized for speed or size
UpdateJoypad:
; Poll half the controller
ld a, P1F_GET_BTN ; Load a flag into A to select reading the buttons
ldh [rP1], a ; Write the flag to P1 to select which buttons to read
ldh a, [rP1] ; Perform a few dummy reads to allow the inputs to stabilize
ldh a, [rP1] ; ...
ldh a, [rP1] ; ...
ldh a, [rP1] ; ...
ldh a, [rP1] ; ...
ldh a, [rP1] ; The final read of the register contains the key state we'll use
or $f0 ; Set the upper 4 bits, and leave the action button states in the lower 4 bits
ld b, a ; Store the state of the action buttons in B
ld a, P1F_GET_DPAD ; Load a flag into A to select reading the dpad
ldh [rP1], a ; Write the flag to P1 to select which buttons to read
call .knownRet ; Call a known `ret` instruction to give the inputs to stabilize
ldh a, [rP1] ; Perform a few dummy reads to allow the inputs to stabilize
ldh a, [rP1] ; ...
ldh a, [rP1] ; ...
ldh a, [rP1] ; ...
ldh a, [rP1] ; ...
ldh a, [rP1] ; The final read of the register contains the key state we'll use
or $f0 ; Set the upper 4 bits, and leave the dpad state in the lower 4 bits
swap a ; Swap the high/low nibbles, putting the dpad state in the high nibble
xor b ; A now contains the pressed action buttons and dpad directions
ld b, a ; Move the key states to B
ld a, P1F_GET_NONE ; Load a flag into A to read nothing
ldh [rP1], a ; Write the flag to P1 to disable button reading
ldh a, [hCurrentKeys] ; Load the previous button+dpad state from HRAM
xor b ; A now contains the keys that changed state
and b ; A now contains keys that were just pressed
ldh [hNewKeys], a ; Store the newly pressed keys in HRAM
ld a, b ; Move the current key state back to A
ldh [hCurrentKeys], a ; Store the current key state in HRAM
.knownRet
ret
;============================================================================================================================
; OAM Handling
;============================================================================================================================
SECTION "Shadow OAM", WRAM0, ALIGN[8]
; Reserve page-aligned space for a Shadow OAM buffer, to which we can safely write OAM data at any time,
; and then use our OAM DMA routine to copy it quickly to OAMRAM when desired. OAM DMA can only operate
; on a block of data that starts at a page boundary, which is why we use ALIGN[8].
wShadowOAM:
ds OAM_COUNT * 4
.end
SECTION "OAM DMA Routine", ROMX
; Initiate OAM DMA and then wait until the operation is complete, then return
; @param A High byte of the source data to DMA to OAM
OAMDMA:
ldh [rDMA], a
ld a, OAM_COUNT
.waitLoop
dec a
jr nz, .waitLoop
ret
.end
SECTION "OAM DMA", HRAM
; Reserve space in HRAM for the OAMDMA routine, equal in length to the routine
hOAMDMA:
ds OAMDMA.end - OAMDMA
;============================================================================================================================
; Tile/Tilemap Data
;============================================================================================================================
SECTION "Tile/Tilemap Data", ROMX
; Obj tiles based on "Micro Character Bases" by Kacper Woźniak (https://thkaspar.itch.io/micro-character-bases)
; Licensed under CC BY 4.0 (https://creativecommons.org/licenses/by/4.0/)
; Skeleton tiles adjusted to 3-shade, and additional facing directions created based on the original art
SpriteTileData:
incbin "grid-collision-obj-ztiles.2bpp"
.end
; BG tiles based on "Dungeon Package" tileset by nyk-nck (https://nyknck.itch.io/dungeonpack)
; License for original assets not clearly specified, but not CC0. Attribution/link included here for completness.
BackgroundTileData:
incbin "grid-collision-bg-tiles.2bpp" ; Include binary tile data inline using incbin
.end ; The .end label is used to let the assembler calculate the length of the data
TilemapData:
incbin "grid-collision.tilemap" ; Include tilemap built using Tilemap Studio and the grid-collision-bg-tiles tileset