Monorepo for Aesthetic.Computer aesthetic.computer
4
fork

Configure Feed

Select the types of activity you want to include in your feed.

at main 449 lines 25 kB view raw
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