Monorepo for Aesthetic.Computer
aesthetic.computer
1; Grid Collision Example for the Nintendo Game Boy
2; by Dave VanEe 2022
3; Tested with RGBDS 1.0.0
4; License: CC0 (https://creativecommons.org/publicdomain/zero/1.0/)
5
6; This example builds on the following examples:
7; - background-tilemap (except using a manually constructed tilemap)
8; - vblank/oamdma/sprite
9; - joypad
10
11; The main additions in this example are moving a player in response to input (ProcessInput), and checking the tilemap
12; entries to determine if attempted moves are into valid spaces based on tile ID (GetTileID). Note that instead of using an
13; automatically generated tilemap as in the background-filemap example, the tilemap is pre-constructed with tiles organized
14; such that walkable tiles start at 0 and are sequential to simplify the collision check.
15
16include "hardware.inc" ; Include hardware definitions so we can use nice names for things
17
18;============================================================================================================================
19; Game Constants
20;============================================================================================================================
21
22def MAX_WALKABLE_TILE_ID equ 8 ; All tiles from 0 to this tile ID will be considered walkable for the purposes of collision
23
24def OBJ_Y_OFFSET equ -9 ; Since we're using two objects to draw the player larger than 8x8, but we're still moving
25def OBJ_X_OFFSET equ -4 ; on an 8x8 grid, we offset things slightly to center the player on the current tile
26
27rsreset ; Reset the _RS counter to 0 for a new set of defines
28def FACE_LEFT rb 1 ; Define FACE_LEFT as 0
29def FACE_RIGHT rb 1 ; Define FACE_RIGHT as 1
30def FACE_UP rb 1 ; Define FACE_UP as 2
31def FACE_DOWN rb 1 ; Define FACE_DOWN as 3
32
33;============================================================================================================================
34; Game State Variables
35;============================================================================================================================
36
37SECTION "Game State Variables", WRAM0
38
39wPlayer:
40.y ds 1 ; Player's Y coordinate (in grid space)
41.x ds 1 ; Player's X coordinate (in grid space)
42.facing ds 1 ; Player's facing direction (0=left, 1=right, 2=up, 3=down)
43
44;============================================================================================================================
45; Interrupts
46;============================================================================================================================
47
48; The VBlank vector is where execution is passed when the VBlank interrupt fires
49SECTION "VBlank Vector", ROM0[$40]
50; We only have 8 bytes here, so push all the registers to the stack and jump to the rest of the handler
51; Note: Since the VBlank handler used here only affects A and F, we don't have to push/pop BC, DE, and HL,
52; but it's done here for demonstration purposes.
53VBlank:
54 push af ; Push AF to the stack
55 ld a, HIGH(wShadowOAM) ; Load the high byte of our Shadow OAM buffer into A
56 jp VBlankHandler ; Jump to the rest of the handler
57
58; The rest of the handler is contained in ROM0 to ensure it's always accessible without banking
59SECTION "VBlank Handler", ROM0
60VBlankHandler:
61 call hOAMDMA ; Call our OAM DMA routine (in HRAM), quickly copying from wShadowOAM to OAMRAM
62 pop af ; Pop AF off the stack
63 reti ; Return and enable interrupts (ret + ei)
64
65;============================================================================================================================
66; Initialization
67;============================================================================================================================
68
69; Define a section that starts at the point the bootrom execution ends
70SECTION "Start", ROM0[$0100]
71 jp EntryPoint ; Jump past the header space to our actual code
72
73 ds $150-@, 0 ; Allocate space for RGBFIX to insert our ROM header by allocating
74 ; the number of bytes from our current location (@) to the end of the
75 ; header ($150)
76
77EntryPoint:
78 di ; Disable interrupts as we won't be using them
79 ld sp, $e000 ; Set the stack pointer to the end of WRAM
80
81 ; Turn off the LCD when it's safe to do so (during VBlank)
82.waitVBlank
83 ldh a, [rLY] ; Read the LY register to check the current scanline
84 cp SCREEN_HEIGHT_PX ; Compare the current scanline to the first scanline of VBlank
85 jr c, .waitVBlank ; Loop as long as the carry flag is set
86 xor a ; Once we exit the loop we're safely in VBlank
87 ldh [rLCDC], a ; Disable the LCD (must be done during VBlank to protect the LCD)
88
89 ; Copy the OAMDMA routine to HRAM, since during DMA we're limited on which
90 ; memory the CPU can access (but HRAM is safe)
91 ld hl, OAMDMA ; Load the source address of our routine into HL
92 ld b, OAMDMA.end - OAMDMA ; Load the length of the OAMDMA routine into B
93 ld c, LOW(hOAMDMA) ; Load the low byte of the destination into C
94.oamdmaCopyLoop
95 ld a, [hli] ; Load a byte from the address HL points to into the register A, increment HL
96 ldh [c], a ; Load the byte in the A register to the address in HRAM with the low byte stored in C
97 inc c ; Increment the low byte of the HRAM pointer in C
98 dec b ; Decrement the loop counter in B
99 jr nz, .oamdmaCopyLoop ; If B isn't zero, continue looping
100
101 ; Copy our sprite and background tiles to VRAM
102 ld hl, SpriteTileData ; Load the source address of our tiles into HL
103 ld de, STARTOF(VRAM) ; Load the destination address in VRAM into DE
104 ld bc, SpriteTileData.end - SpriteTileData ; Load the number of bytes to copy into BC
105 call MemCopy ; Call our general-purpose memory copy routine
106
107 ld hl, BackgroundTileData ; Load the source address of our tiles into HL
108 ld de, STARTOF(VRAM)+$1000 ; Load the destination address in VRAM into DE
109 ld bc, BackgroundTileData.end - BackgroundTileData ; Load the number of bytes to copy into BC
110 call MemCopy ; Call our general-purpose memory copy routine
111
112 ; Copy our 20x18 tilemap to VRAM
113 ld de, TilemapData ; Load the source address of our tilemap into DE
114 ld hl, TILEMAP0 ; Point HL to the first byte of the tilemap ($9800)
115 ld b, SCREEN_HEIGHT ; Load the height of the screen in tiles into B (18 tiles)
116.tilemapLoop
117 ld c, SCREEN_WIDTH ; Load the width of the screen in tiles into C (20 tiles)
118.rowLoop
119 ld a, [de] ; Load a byte from the address DE points to into the A register
120 ld [hli], a ; Load the byte in the A register to the address HL points to and increment HL
121 inc de ; Increment the source pointer in DE
122 dec c ; Decrement the loop counter in C (tiles per row)
123 jr nz, .rowLoop ; If C isn't zero, continue copying bytes for this row
124 push de ; Push the contents of the register pair DE to the stack
125 ld de, TILEMAP_WIDTH - SCREEN_WIDTH ; Load the number of tiles remaining in the row into DE
126 add hl, de ; Add the remaining row length to HL, advancing the destination pointer to the next row
127 pop de ; Recover the former contents of the the register pair DE
128 dec b ; Decrement the loop counter in B (total rows)
129 jr nz, .tilemapLoop ; If B isn't zero, continue copying rows
130
131 ; Setup palettes and scrolling
132 ld a, %11100100 ; Define a 4-shade palette from darkest (11) to lightest (00)
133 ldh [rBGP], a ; Set the background palette
134 ld a, %11010000 ; Define a 4-shade palette which omits the 10 value to increase player contrast
135 ldh [rOBP0], a ; Set an object palette
136
137 xor a ; Set A to zero
138 ldh [rSCX], a ; Set the background scroll registers to show the top-left
139 ldh [rSCY], a ; corner of the background in the top-left corner of the screen
140
141 ldh [hCurrentKeys], a ; Zero our current keys just to be safe (A is already zero from earlier)
142
143 ; Initialize shadow OAM to zero
144 ld hl, wShadowOAM ; Point HL to the start of shadow OAM
145 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)
146.clearOAM
147 ld [hli], a ; Zero this OAM byte
148 dec b ; Decrement the loop counter in B (bytes of OAM)
149 jr nz, .clearOAM ; If B isn't zero, continue zeroing bytes
150
151 ; Perform OAM DMA once to ensure OAM doesn't contain garbage
152 ld a, HIGH(wShadowOAM) ; Load the high byte of our Shadow OAM buffer into A
153 call hOAMDMA ; Call our OAM DMA routine (in HRAM), quickly copying from wShadowOAM to OAMRAM
154
155 ; Setup the world state
156 ld hl, wPlayer ; Point HL to the start of the player's state in WRAM
157 ld a, 4 ; Load the starting Y coordinate into A
158 ld [hli], a ; Set the starting wPlayer.y value in WRAM
159 ld a, 2 ; Load the starting X coordinate into A
160 ld [hli], a ; Set the starting wPlayer.x value in WRAM
161 ld a, FACE_DOWN ; Load the starting facing direction into A
162 ld [hli], a ; Set the starting wPlayer.facing value in WRAM
163
164 ; Setup the VBlank interrupt
165 ld a, IE_VBLANK ; Load the flag to enable the VBlank interrupt into A
166 ldh [rIE], a ; Load the prepared flag into the interrupt enable register
167 xor a ; Set A to zero
168 ldh [rIF], a ; Clear any lingering flags from the interrupt flag register to avoid false interrupts
169 ei ; enable interrupts!
170
171 ; Combine flag constants defined in hardware.inc into a single value with logical ORs and load it into A
172 ld a, LCDC_ON | LCDC_BLOCK21 | LCDC_BG_ON | LCDC_OBJ_16 | LCDC_OBJ_ON | LCDC_WIN_OFF
173 ldh [rLCDC], a ; Enable and configure the LCD to show the background and objects
174
175;============================================================================================================================
176; Main Loop
177;============================================================================================================================
178
179LoopForever:
180 halt ; Halt the CPU, waiting until an interrupt fires (this will sync our loop with VBlank)
181
182 call UpdateJoypad ; Poll the joypad and store the state in HRAM
183 call ProcessInput ; Update the game state in response to user input
184 call PopulateShadowOAM ; Update the sprite locations for the next frame
185
186 jr LoopForever ; Loop forever
187
188;============================================================================================================================
189; Main Routines
190;============================================================================================================================
191
192SECTION "Main Routines", ROMX
193
194; Process the user's inputs and update the game state accordingly
195ProcessInput:
196 ldh a, [hNewKeys] ; Load the newly pressed keys byte into A
197 bit B_PAD_LEFT, a ; Check the state of the LEFT bit in A
198 ld bc, $00ff ; Preload B/C with dy/dx for left movement (0, -1)
199 ld d, FACE_LEFT ; Preload D with the facing value for LEFT
200 jr nz, .attemptMove ; If the bit was set, jump to attempt movement in that direction
201 bit B_PAD_RIGHT, a ; Check the state of the RIGHT bit in A
202 ld bc, $0001 ; Preload B/C with dy/dx for left movement (0, +1)
203 ld d, FACE_RIGHT ; Preload D with the facing value for RIGHT
204 jr nz, .attemptMove ; If the bit was set, jump to attempt movement in that direction
205 bit B_PAD_UP, a ; Check the state of the UP bit in A
206 ld bc, $ff00 ; Preload B/C with dy/dx for left movement (-1, 0)
207 ld d, FACE_UP ; Preload D with the facing value for UP
208 jr nz, .attemptMove ; If the bit was set, jump to attempt movement in that direction
209 bit B_PAD_DOWN, a ; Check the state of the DOWN bit in A
210 ld bc, $0100 ; Preload B/C with dy/dx for left movement (+1, 0)
211 ld d, FACE_DOWN ; Preload D with the facing value for DOWN
212 jr nz, .attemptMove ; If the bit was set, jump to attempt movement in that direction
213 ret ; No inputs to handle, return to main loop
214
215; Attempt a move in a direction defined by the contents of BC and D
216; @param: B Delta Y to apply to current player position
217; @param: C Delta X to apply to current player position
218; @param: D New facing direction value to apply
219.attemptMove
220 ld a, d ; Move new facing direction from D to A
221 ld [wPlayer.facing], a ; Store new facing direction regardless of move success
222
223 ; Calculate the destination coordinates by applying the deltas
224 ld a, [wPlayer.y] ; Load the current player Y coordinate into A
225 add b ; Add the dY value from B to get the new Y coordinate
226 ld b, a ; Store the new Y coordinate back in B
227 ld a, [wPlayer.x] ; Load the current player X coordinate into A
228 add c ; Add the dX value from C to get the new X coordinate
229 ld c, a ; Store the new Y coordinate back in C
230
231 ; Check if the attempted move is valid
232 call GetTileID ; Call a routine to get the tile ID at the B=y, C=x coordinates
233 cp MAX_WALKABLE_TILE_ID ; Compare the tile ID from TilemapData to the maximum walkable tile ID
234 ret nc ; If the tile ID is greater than the maximum walkable tile ID, return
235
236 ; Store the new coordinates
237 ld a, b ; Load the new Y coordinate into A
238 ld [wPlayer.y], a ; Store the new Y coordinate in memory
239 ld a, c ; Load the new X coordinate into A
240 ld [wPlayer.x], a ; Store the new X coordinate in memory
241 ret
242
243; Return the tile ID in TilemapData at provided coordinates
244; @param B: Y coordinate in tilemap
245; @param C: X coordinate in tilemap
246; @return A: Tile ID at coordinates given
247GetTileID:
248 push bc ; Store the input coordinates on the stack
249 ld hl, TilemapData ; Load the start address of the TilemapData into HL
250 ld a, b ; Load the Y coordinate into A
251 or a ; Check if the Y coordinate is zero
252 jr z, .yZero ; If zero, skip the Y seeking code
253 ld de, SCREEN_WIDTH ; Load the number of tiles per row of TilemapData into DE
254.yLoop
255 add hl, de ; Add the number of tiles per row to the pointer in HL
256 dec b ; Decrease the loop counter in B
257 jr nz, .yLoop ; Loop until we've offset to the correct row
258.yZero
259
260 ld a, c ; Load the X coordinate into A
261
262 ; Add the X coordinate offset to HL (this is a common way to add A to a 16-bit register)
263 add l ; Add the X coordinate to the low byte of the pointer in HL
264 ld l, a ; Store the new low byte of the pointer in L
265 adc h ; Add H plus the carry flag to the contents of A
266 sub l ; Subtract the contents of L from A
267 ld h, a ; Store the new high byte of the pointer in H
268
269 ld a, [hl] ; Read the value of TilemapData at the coordinates of interest into A
270 pop bc ; Recover the original input coordinates from the stack
271 ret
272
273; Populate ShadowOAM with sprites based on the game state
274PopulateShadowOAM:
275 ld hl, wShadowOAM ; Point HL at the beginning of wShadowOAM
276
277 ; First sprite
278 ld a, [wPlayer.y] ; Load the player's Y coordinate into A
279 add a ; To convert the Y grid coordinate into screen coordinates we have to multiply
280 add a ; by 8, which can be done quickly by adding A to itself 3 times
281 add a ; ...
282 add $10+OBJ_Y_OFFSET ; Add the sprite offset ($10), plus the centering offset
283 ld [hli], a ; Store the sprite's Y coordinate in shadow OAM
284 ld b, a ; Cache the Y coordinate in B for use by the second sprite
285 ld a, [wPlayer.x] ; Load the player's X coordinate into A
286 add a ; Multiply the X coordinate by 8 the same as we did for Y above
287 add a ; ...
288 add a ; ...
289 add $08+OBJ_X_OFFSET ; Add the sprite offset ($08), plus the centering offset
290 ld [hli], a ; Store the sprite's X coordinate in shadow OAM
291 add $08 ; Add 8 to the X coordinate for the second sprite
292 ld c, a ; Cache the X coordinate in C for use by the second sprite
293 ld a, [wPlayer.facing] ; Load the player's facing direction into A
294 add a ; The player tiles have been stored in VRAM such that the facing direction multiplied
295 add a ; by 4 will yield the tile ID for the first sprite, so multiply by 4 using adds
296 ld [hli], a ; Store the sprite's tile ID in shadow OAM
297 add 2 ; Add 2 to the tile ID for the second sprite
298 ld d, a ; Cache the tile ID in D for use by the second sprite
299 xor a ; Set A to zero
300 ld [hli], a ; Store the sprite's attributes in shadow OAM
301
302 ; Second sprite
303 ld a, b ; Load the prepared Y coordinate from B to A
304 ld [hli], a ; Store the sprite's Y coordinate in shadow OAM
305 ld a, c ; Load the prepared X coordinate from C to A
306 ld [hli], a ; Store the sprite's X coordinate in shadow OAM
307 ld a, d ; Load the prepared tile ID from D to A
308 ld [hli], a ; Store the sprite's tile ID in shadow OAM
309 xor a ; Set A to zero
310 ld [hli], a ; Store the sprite's attributes in shadow OAM
311
312 ; Zero the remaning shadow OAM entries
313 ; Note: Since we're only using 2/40 sprites, we could just loop 38 times, but the following approach will scale better if
314 ; additional sprites are added. This will also clear previously used entires in cases where the number of sprites used
315 ; each frame varies (which isn't the case here).
316 ld b, a ; Load zero (from the prior use) into B, since A will be used to check loop completion
317.clearOAM
318 ld [hl], b ; Set the Y coordinate of this OAM entry to zero to hide it
319 ld a, l ; Load the low byte of the shadow OAM pointer into A
320 add OBJ_SIZE ; Advance 4 bytes to the next OAM entry
321 ld l, a ; Store the new low byte of the pointer in L
322 cp LOW(wShadowOAM.end) ; Compare the low byte to the end of wShadowoAM
323 jr nz, .clearOAM ; Loop until we've hidden every unused sprite
324
325 ret
326
327;============================================================================================================================
328; Utility Routines
329;============================================================================================================================
330
331SECTION "MemCopy Routine", ROM0
332; Since we're copying data few times, we'll define a reusable memory copy routine
333; Copy BC bytes of data from HL to DE
334; @param HL: Source address to copy from
335; @param DE: Destination address to copy to
336; @param BC: Number of bytes to copy
337MemCopy:
338 ld a, [hli] ; Load a byte from the address HL points to into the register A, increment HL
339 ld [de], a ; Load the byte in the A register to the address DE points to
340 inc de ; Increment the destination pointer in DE
341 dec bc ; Decrement the loop counter in BC
342 ld a, b ; Load the value in B into A
343 or c ; Logical OR the value in A (from B) with C
344 jr nz, MemCopy ; If B and C are both zero, OR B will be zero, otherwise keep looping
345 ret ; Return back to where the routine was called from
346
347;============================================================================================================================
348; Joypad Handling
349;============================================================================================================================
350
351SECTION "Joypad Variables", HRAM
352; Reserve space in HRAM to track the joypad state
353hCurrentKeys: ds 1
354hNewKeys: ds 1
355
356SECTION "Joypad Routine", ROM0
357
358; Update the newly pressed keys (hNewKeys) and the held keys (hCurrentKeys) in memory
359; Note: This routine is written to be easier to understand, not to be optimized for speed or size
360UpdateJoypad:
361 ; Poll half the controller
362 ld a, JOYP_GET_BUTTONS ; Load a flag into A to select reading the buttons
363 ldh [rP1], a ; Write the flag to P1 to select which buttons to read
364 ldh a, [rP1] ; Perform a few dummy reads to allow the inputs to stabilize
365 ldh a, [rP1] ; ...
366 ldh a, [rP1] ; ...
367 ldh a, [rP1] ; ...
368 ldh a, [rP1] ; ...
369 ldh a, [rP1] ; The final read of the register contains the key state we'll use
370 or $f0 ; Set the upper 4 bits, and leave the action button states in the lower 4 bits
371 ld b, a ; Store the state of the action buttons in B
372
373 ld a, JOYP_GET_CTRL_PAD ; Load a flag into A to select reading the dpad
374 ldh [rP1], a ; Write the flag to P1 to select which buttons to read
375 call .knownRet ; Call a known `ret` instruction to give the inputs to stabilize
376 ldh a, [rP1] ; Perform a few dummy reads to allow the inputs to stabilize
377 ldh a, [rP1] ; ...
378 ldh a, [rP1] ; ...
379 ldh a, [rP1] ; ...
380 ldh a, [rP1] ; ...
381 ldh a, [rP1] ; The final read of the register contains the key state we'll use
382 or $f0 ; Set the upper 4 bits, and leave the dpad state in the lower 4 bits
383
384 swap a ; Swap the high/low nibbles, putting the dpad state in the high nibble
385 xor b ; A now contains the pressed action buttons and dpad directions
386 ld b, a ; Move the key states to B
387
388 ld a, JOYP_GET_NONE ; Load a flag into A to read nothing
389 ldh [rP1], a ; Write the flag to P1 to disable button reading
390
391 ldh a, [hCurrentKeys] ; Load the previous button+dpad state from HRAM
392 xor b ; A now contains the keys that changed state
393 and b ; A now contains keys that were just pressed
394 ldh [hNewKeys], a ; Store the newly pressed keys in HRAM
395 ld a, b ; Move the current key state back to A
396 ldh [hCurrentKeys], a ; Store the current key state in HRAM
397.knownRet
398 ret
399
400;============================================================================================================================
401; OAM Handling
402;============================================================================================================================
403
404SECTION "Shadow OAM", WRAM0, ALIGN[8]
405; Reserve page-aligned space for a Shadow OAM buffer, to which we can safely write OAM data at any time,
406; and then use our OAM DMA routine to copy it quickly to OAMRAM when desired. OAM DMA can only operate
407; on a block of data that starts at a page boundary, which is why we use ALIGN[8].
408wShadowOAM:
409 ds OAM_SIZE
410.end
411
412SECTION "OAM DMA Routine", ROMX
413; Initiate OAM DMA and then wait until the operation is complete, then return
414; @param A High byte of the source data to DMA to OAM
415OAMDMA:
416 ldh [rDMA], a
417 ld a, OAM_COUNT
418.waitLoop
419 dec a
420 jr nz, .waitLoop
421 ret
422.end
423
424SECTION "OAM DMA", HRAM
425; Reserve space in HRAM for the OAMDMA routine, equal in length to the routine
426hOAMDMA:
427 ds OAMDMA.end - OAMDMA
428
429;============================================================================================================================
430; Tile/Tilemap Data
431;============================================================================================================================
432
433SECTION "Tile/Tilemap Data", ROMX
434
435; Obj tiles based on "Micro Character Bases" by Kacper Woźniak (https://thkaspar.itch.io/micro-character-bases)
436; Licensed under CC BY 4.0 (https://creativecommons.org/licenses/by/4.0/)
437; Skeleton tiles adjusted to 3-shade, and additional facing directions created based on the original art
438SpriteTileData:
439 incbin "grid-collision-obj-ztiles.2bpp"
440.end
441
442; BG tiles based on "Dungeon Package" tileset by nyk-nck (https://nyknck.itch.io/dungeonpack)
443; License for original assets not clearly specified, but not CC0. Attribution/link included here for completness.
444BackgroundTileData:
445 incbin "grid-collision-bg-tiles.2bpp" ; Include binary tile data inline using incbin
446.end ; The .end label is used to let the assembler calculate the length of the data
447
448TilemapData:
449 incbin "grid-collision.tilemap" ; Include tilemap built using Tilemap Studio and the grid-collision-bg-tiles tileset