diff options
Diffstat (limited to 'scroll/include/player.s')
| -rw-r--r-- | scroll/include/player.s | 303 |
1 files changed, 269 insertions, 34 deletions
diff --git a/scroll/include/player.s b/scroll/include/player.s index 95d710d..239cec6 100644 --- a/scroll/include/player.s +++ b/scroll/include/player.s @@ -1,87 +1,322 @@ +;; Set the player to its initial coordinates. +.macro RESET_PLAYER_POSITION + lda #40 + sta Player::zp_screen_x + sta Player::zp_screen_y +.endmacro + ;; This is the player code that gets re-used on all scrolling examples. This ;; will show a meta-sprite made up of four sprites that make up diskun. You can ;; move this player with the arrows, and the movement will be pretty basic (1 -;; pixel each time). Furthermore, the sprite handles collision with background -;; elements (TODO), which is shared across all the scrolling examples. +;; pixel at a time). Furthermore, the sprite handles collision with background +;; elements, which is shared across all the scrolling examples. ;; -;; The sprite is stored in OAM memory $200-$20F, and the $30 and $31 memory -;; addresses are used for keeping up with the screen coordinates. +;; The sprite is stored in OAM memory $204-$213 (hence sprite0 is left out so +;; other examples can use it), and the $30 and $31 memory addresses are used for +;; keeping up with the screen coordinates. .scope Player - m_screen_x = $30 - m_screen_y = $31 + ;; The minimum value that the player is allowed to have on the Y axis. + MIN_SCREEN_Y = 8 + + ;; The maximum value of X that is allowed before we start updating the + ;; scroll value. + MAX_SCREEN_X = 144 + + ;; The height for the player when it comes to detecting the edge of the + ;; screen or background collisions. + PLAYER_HEIGHT = 25 + + ;; The width for the player when it comes to detecting the edge of the + ;; screen or background collisions. + PLAYER_WIDTH = 16 + + ;; Indeces for the collisions table as defined on `check_bg_collision_x` so + ;; the setting of the `x` register is less magical. + TOP_COLLISION_INDEX = (0 << 2) + RIGHT_COLLISION_INDEX = (1 << 2) + DOWN_COLLISION_INDEX = (2 << 2) + LEFT_COLLISION_INDEX = (3 << 2) + ;; Coordinates on the X axis. + ;; NOTE: shadowed in collision.s. If you change it here, change it there as + ;; well. + zp_screen_x = $30 + + ;; Coordinates on the Y axis. + ;; NOTE: shadowed in collision.s. If you change it here, change it there as + ;; well. + zp_screen_y = $31 + + ;; Initialize the sprite for the player. .proc init - lda #40 - sta m_screen_x - sta m_screen_y + RESET_PLAYER_POSITION lda #$01 - sta $201 - lda #$02 sta $205 - lda #$11 + lda #$02 sta $209 - lda #$12 + lda #$11 sta $20D + lda #$12 + sta $211 lda #0 - sta $202 sta $206 sta $20A sta $20E + sta $212 rts .endproc + ;; The update for the player is simply about checking for the d-pad. Note + ;; that if pressing right, then the scroll value might also move, which + ;; might mean to load the next background column until the end of the + ;; screen. .proc update - jsr update_coordinates - jsr update_sprites - rts - .endproc - - .proc update_coordinates + ;; Is the player requesting to go up? lda #Joypad::BUTTON_UP and Joypad::m_buttons1 beq @check_down - dec m_screen_y + + ;; If we are already at the top disregard this button press and check + ;; for the left button. + lda #MIN_SCREEN_Y + cmp zp_screen_y + beq @check_left + + ;; If, for whatever reason, we are below the minimum value, then reset + ;; the value to this minimum Y value and jump to check the left button. + bcc @update_y_up + sta zp_screen_y + jmp @check_left + + @update_y_up: + ;; Try to go up and check for a collision with the background. + dec zp_screen_y + dec zp_screen_y + ldx #TOP_COLLISION_INDEX + jsr check_bg_collision_x + beq @check_left + + ;; A collision was detected. Then get back to the old value of Y and + ;; move on. + inc zp_screen_y + inc zp_screen_y jmp @check_left + @check_down: + ;; Is the player requesting to go down? lda #Joypad::BUTTON_DOWN and Joypad::m_buttons1 beq @check_left - inc m_screen_y + + ;; We have to move down unless we are already at the very bottom. + lda zp_screen_y + cmp #(240 - PLAYER_HEIGHT) + bcc @update_y_down + lda #(240 - PLAYER_HEIGHT) + sta zp_screen_y + jmp @check_left + @update_y_down: + ;; Try to go down as requested, but check for a collision with the + ;; background. + inc zp_screen_y + inc zp_screen_y + ldx #DOWN_COLLISION_INDEX + jsr check_bg_collision_x + beq @check_left + + ;; There was a collision with the background, restore back the value on + ;; Y and move on. + dec zp_screen_y + dec zp_screen_y + @check_left: + ;; Is the player requesting to go left? lda #Joypad::BUTTON_LEFT and Joypad::m_buttons1 beq @check_right - dec m_screen_x + + ;; We have to move left unless we are already at the leftmost edge. + lda zp_screen_x + bne :+ + rts + : + ;; We are not at the leftmost edge, try to go left while also checking + ;; for a background collision. + dec zp_screen_x + dec zp_screen_x + ldx #LEFT_COLLISION_INDEX + jsr check_bg_collision_x + bne :+ rts + : + ;; There was a collision, restore back the value on X and quit. + inc zp_screen_x + inc zp_screen_x + rts + @check_right: + ;; Last check! Is the player requesting to go right? lda #Joypad::BUTTON_RIGHT and Joypad::m_buttons1 + bne @check_level_end + rts + + @check_level_end: + ;; Are we at the last screen? If so consume the rest of the missing + ;; scroll. Whenever that is done (i.e. there are no more screens and the + ;; scroll sits at a zero value), then we can no longer load more columns + ;; or update the scroll: stick to updating the X position. + lda #%00001000 + and Globals::zp_flags + beq @check_max_screen + lda Background::zp_scroll + beq @update_x + + @check_max_screen: + ;; Is the X position already at the MAX_SCREEN_X? If so, then we don't + ;; move the character: we move the scroll instead. + lda zp_screen_x + cmp #Player::MAX_SCREEN_X + bcc @update_x + + ;; Moving will then be a matter of increasing the scroll value. + inc Background::zp_scroll + inc Background::zp_scroll + ldx #RIGHT_COLLISION_INDEX + jsr check_bg_collision_x + bne @collision_on_scroll + + ;; If there are no collisions in the scrolling scenario, then we might + ;; need to load the next background column in advance. Whether that is + ;; possible or something that we want to do is handled automatically by + ;; the `Background::load_next_background` function. Hence, call this + ;; function and quit. + JAL Background::load_next_background + + @collision_on_scroll: + ;; Collision was given. Hence, restore the value for the scroll and + ;; quit. + dec Background::zp_scroll + dec Background::zp_scroll + rts + + @update_x: + ;; Do not overwrap the screen. + lda zp_screen_x + cmp #(256 - PLAYER_WIDTH) beq @end - inc m_screen_x - ;; TODO: move scroll + + ;; We are not about to overwrap the screen, let's update the value on + ;; the X axis and check whether a collision would happen. + inc zp_screen_x + inc zp_screen_x + ldx #RIGHT_COLLISION_INDEX + jsr check_bg_collision_x + beq @end + + ;; There was a collision, roll back the value for the X axis. + dec zp_screen_x + dec zp_screen_x + @end: rts .endproc - .proc update_sprites - lda m_screen_x - sta $203 - sta $20B - clc - adc #8 + ;; Check collision between the player and any background element. Note that + ;; collision is not tested on all edges for the player, but you have to + ;; provide a "collision index", which refers to which edges to test. See the + ;; "*_COLLISION_INDEX" constants above and the `edges` data included inside + ;; of this function. This collision index has to be provided on the `x` + ;; register. + ;; + ;; Returns a non-zero value on the `a` register if a collision was detected, + ;; zero otherwise. + .proc check_bg_collision_x + ;; We have to check for a collision on both the left and the right + ;; edges, which should follow the same code. Unrolling it is simply + ;; faster and more clear than setting up a loop. + .repeat 2, I + ;; Save the offset on the Y coordinate. + lda edges, x + sta Globals::zp_arg0 + + ;; Save the offset on the X coordinate. + inx + lda edges, x + sta Globals::zp_arg1 + + ;; Save the edge index in preparation for the following calls. + .if I = 0 + inx + stx Globals::zp_idx + .endif + + ;; Transform screen coordinates into metatile ones and fetch the + ;; collision bitmap for it with its mask. + jsr Collision::screen_to_mt_coordinates + jsr Collision::get_background_collision_y + + ;; Restore back the edge index. + .if I = 0 + ldx Globals::zp_idx + .endif + + ;; Checking for a collision is as simple as performing an AND with the + ;; bitmap at the `a` register and the byte that can be addressed with + ;; the given pointer. If the result is non-zero, then we found a match. + and (Globals::zp_arg0), y + .if I = 0 + ;; If I = 1 we are falling through as expected anyways, so this + ;; instruction is not needed. + bne @collision + .endif + .endrepeat + + ;; The last instruction before ending up here will already have the `a` + ;; register lined up to the proper return value. If a collision was + ;; found (in either left/right case), `bne @collision` is good, and + ;; hence `a` has an expected non-zero value. The same is true when not + ;; in a collision. + @collision: + rts + + edges: + ;; Up: top-left (y, x), top-right (y, x) + .byte $02, $02, $02, $0F + + ;; Right: top-right (y, x), bottom-right (y, x) + .byte $02, $0F, $0F, $0F + + ;; Down: bottom-left (y, x), bottom-right (y, x) + .byte $0F, $02, $0F, $0F + + ;; Left: top-left (y, x), bottom-left (y, x) + .byte $02, $01, $0F, $01 + .endproc + + ;; Update the memory on the PPU with the current values for the screen + ;; coordinates of the player. + ;; + ;; NOTE: only call this function on NMI Code. + .proc update_sprite + lda zp_screen_x sta $207 sta $20F + clc + adc #8 + sta $20B + sta $213 - lda m_screen_y - sta $200 + lda zp_screen_y sta $204 + sta $208 clc adc #8 - sta $208 sta $20C + sta $210 rts .endproc |
