aboutsummaryrefslogtreecommitdiff
path: root/scroll/include/player.s
diff options
context:
space:
mode:
Diffstat (limited to 'scroll/include/player.s')
-rw-r--r--scroll/include/player.s303
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