Something went wrong. Try again.
Monorepo for Aesthetic.Computer aesthetic.computer
Something went wrong. Try again.
25 kB · 449 lines
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449; Grid Collision Example for the Nintendo Game Boy; by Dave VanEe 2022; Tested with RGBDS 1.0.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 movingdef 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 definesdef FACE_LEFT rb 1 ; Define FACE_LEFT as 0def FACE_RIGHT rb 1 ; Define FACE_RIGHT as 1def FACE_UP rb 1 ; Define FACE_UP as 2def 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 firesSECTION "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 bankingSECTION "VBlank Handler", ROM0VBlankHandler: 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 endsSECTION "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 SCREEN_HEIGHT_PX ; 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, STARTOF(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, STARTOF(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, TILEMAP0 ; Point HL to the first byte of the tilemap ($9800) ld b, SCREEN_HEIGHT ; Load the height of the screen in tiles into B (18 tiles).tilemapLoop ld c, SCREEN_WIDTH ; 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, TILEMAP_WIDTH - SCREEN_WIDTH ; 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, IE_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, LCDC_ON | LCDC_BLOCK21 | LCDC_BG_ON | LCDC_OBJ_16 | LCDC_OBJ_ON | LCDC_WIN_OFF 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 accordinglyProcessInput: ldh a, [hNewKeys] ; Load the newly pressed keys byte into A bit B_PAD_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 B_PAD_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 B_PAD_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 B_PAD_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 givenGetTileID: 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, SCREEN_WIDTH ; 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 statePopulateShadowOAM: 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 ld a, l ; Load the low byte of the shadow OAM pointer into A add OBJ_SIZE ; Advance 4 bytes to the next OAM entry ld l, a ; Store the new low byte of the pointer in L 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 copyMemCopy: 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 statehCurrentKeys: ds 1hNewKeys: 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 sizeUpdateJoypad: ; Poll half the controller ld a, JOYP_GET_BUTTONS ; 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, JOYP_GET_CTRL_PAD ; 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, JOYP_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_SIZE.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 OAMOAMDMA: 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 routinehOAMDMA: 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 artSpriteTileData: 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