diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/bullets.s | 303 | ||||
| -rw-r--r-- | src/driver.s | 162 | ||||
| -rw-r--r-- | src/jetpac.s | 1 | ||||
| -rw-r--r-- | src/player.s | 3 |
4 files changed, 467 insertions, 2 deletions
diff --git a/src/bullets.s b/src/bullets.s new file mode 100644 index 0000000..ea224b3 --- /dev/null +++ b/src/bullets.s @@ -0,0 +1,303 @@ +.segment "CODE" + +;; Function and variables which deal with the pool of bullets that the +;; `driver.s` will use in order to render and deal with bullets on screen. +.scope Bullets + ;; Maximum amount of bullets allowed on screen at the same time. + BULLETS_POOL_CAPACITY = 20 + + ;; Base address for the pool of bullets used on this game. The pool has + ;; #BULLETS_POOL_CAPACITY bullet objects where each one is 3 bytes long: + ;; 1. State: which can have two formats: + ;; - $FF: the bullet is not active. + ;; - |Dxxx|xxxx|: where D is the direction bit (1: right; 0: left); and + ;; the rest of bits count the number of moves from this + ;; bullet. + ;; 2. Y coordinate. + ;; 3. X coordinate. + zp_bullets_pool_base = $A0 + + ;; The screen coordinates of the bullet being inspected right now. Used when + ;; computing the move of bullets and checking possible collisions with + ;; background/enemies. + zp_current_bullet_y = $A1 + zp_current_bullet_x = $A2 + + ;; The capacity of the bullets pool in bytes. + BULLETS_POOL_CAPACITY_BYTES = BULLETS_POOL_CAPACITY * 3 + + ;; The current amount of bullets on screen. + zp_bullets_pool_size = $E0 + + ;; The index on the pool where the next bullet can start iterating from. + ;; This is a small optimization so not to start from the beginning every + ;; time, as consecutive allocation is a very common case. + zp_last_allocated_index = $E1 + + ;; The amount of time we are not allowing B presses. This is a rather low + ;; value so you can have quite some presses per frame. + zp_bullet_timer = $35 + BULLET_TIMER_VALUE = HZ / 15 + + ;; Maximum moves that a bullet can do. The tile also transitions depending + ;; on the moves done so far. + BULLET_MAX_MOVES = 26 + BULLET_FIRST_TRANSITION = 20 + BULLET_LAST_TRANSITION = 25 + + ;; Velocity at which bullets move. + .ifdef PAL + BULLET_VELOCITY = 7 + .else + BULLET_VELOCITY = 6 + .endif + + ;; Initialize the pool of bullets. + .proc init + lda #0 + sta zp_bullet_timer + sta zp_bullets_pool_size + sta zp_last_allocated_index + sta zp_current_bullet_y + sta zp_current_bullet_x + + ;; Initializing the pool is a matter of setting to $FF the state byte + ;; for each bullet object. + ldx #0 + ldy #0 + lda #$FF + @pool_init_loop: + sta zp_bullets_pool_base, x + inx + inx + inx + + iny + cpy #BULLETS_POOL_CAPACITY + bne @pool_init_loop + + rts + .endproc + + ;; Update the status of the pool by doing mainly three things: + ;; 1. Create a new bullet if the player can and has requested it. + ;; 2. Move all active bullets. + ;; 3. Check background/enemy collisions. + .proc update + ;; Are we already full of bullets on screen? If so go move them. + lda zp_bullets_pool_size + cmp #BULLETS_POOL_CAPACITY + beq @move_bullets + + ;; Can the B button be pressed? If not go to `@move_bullets` directly. + lda zp_bullet_timer + beq @check_bullets_pressed + dec zp_bullet_timer + jmp @move_bullets + + @check_bullets_pressed: + ;; Is the B button pressed? If not go to `@move_bullets` directly. + lda #(Joypad::BUTTON_B) + and Joypad::zp_buttons1 + beq @move_bullets + + ;; The B button was pressed. Reset the bullet timer. + lda #BULLET_TIMER_VALUE + sta zp_bullet_timer + + ;; Let's fetch a free spot for the new bullet. Note that since we have + ;; checked that the pools size is not the same as the capacity, there + ;; *must* be a free spot. If that's not the case and we get into an + ;; infinite loop, then that's a bug we have to fix :) + ldx zp_last_allocated_index + @find_free_bullet_bucket: + lda zp_bullets_pool_base, x + cmp #$FF + beq @initialize_bucket + + @next_free_loop: + ;; Prepare the `x` register for the next iteration. Notice that if we + ;; are over the total size in memory, we have to roll the `x` back to + ;; zero. This is possible because the loop starts at + ;; `zp_last_allocated_index`, which is not necessarily 0. + inx + inx + inx + cpx #BULLETS_POOL_CAPACITY_BYTES + bne @find_free_bullet_bucket + ldx #0 + beq @find_free_bullet_bucket + + @initialize_bucket: + ;; We found a free bucket. Initialize the first byte to 0 since it has + ;; not moved yet. The heading is taken from the player's state. + lda Player::zp_state + asl + and #%10000000 + sta zp_bullets_pool_base, x + + ;; Set the Y coordinate to the player's waist. + inx + lda Player::zp_screen_y + clc + adc #(Player::PLAYER_WAIST - 1) + sta zp_bullets_pool_base, x + + ;; Set the X coordinate to the player while also adjusting to the future + ;; velocity applied on `@move_bullets` which, in turn, depends on the + ;; player's heading stored on `Player::zp_state`. + inx + lda Player::zp_screen_x + bit Player::zp_state + clc + bvc @set_bullet_left + adc #(Player::PLAYER_WIDTH - BULLET_VELOCITY) + jmp @set_bullet_x + @set_bullet_left: + adc #BULLET_VELOCITY + @set_bullet_x: + sta zp_bullets_pool_base, x + + ;; Save the index so it can be used in future bullet creation. + inx + stx zp_last_allocated_index + + ;; Increase the number of bullets on screen. + inc zp_bullets_pool_size + + @move_bullets: + ;; We will have on the 'y' register the amount of bullets on screen + ;; pending to be moved. If there are none, we can return early. + ldy zp_bullets_pool_size + bne @do_move + rts + + @do_move: + ;; There's at least one bullet to be moved. In this case, we will + ;; proceed to move any active bullet and check for collisions. + ;; + ;; The 'x' register will index the pool of bullets. + ldx #0 + + @move_loop: + ;; Is the current bullet active? + lda zp_bullets_pool_base, x + cmp #$FF + bne @move_active_bullet + + ;; No, go for the next one. + inx + inx + inx + jmp @move_loop + + @move_active_bullet: + ;; Store the original value into a temporary variable, and mask out the + ;; direction flag. + sta Globals::zp_tmp1 + and #%01111111 + + ;; Ok, has this bullet moved to its maximum capacity? + cmp #BULLET_MAX_MOVES + bne @do_move_active_bullet + + ;; Yes! Then mark it as over. + lda #$FF + sta zp_bullets_pool_base, x + + ;; Decrease the number of bullets active and go check collisions if we + ;; are done checking for bullets. In this case, if this was the last + ;; bullet active, return early. + dec zp_bullets_pool_size + beq @end + dey + beq @end + + ;; We still have active bullets to move, go to the next iteration. + inx + inx + inx + bne @move_loop + + @do_move_active_bullet: + ;; Increase the number of moves that this bullet has done. + stx Globals::zp_idx + inc zp_bullets_pool_base, x + + ;; Save the position on the Y axis as the value for the current bullet, + ;; then convert it into tile coordinates so it can be used later for + ;; background collision check. + lda zp_bullets_pool_base + 1, x + sta zp_current_bullet_y + lsr + lsr + lsr + sta Globals::zp_arg0 + + ;; Grab the position on the X axis and apply the velocity depending on + ;; the direction, which was stored back on the `Globals::zp_tmp1` + ;; variable. + lda zp_bullets_pool_base + 2, x + bit Globals::zp_tmp1 + bmi @move_right + sec + sbc #BULLET_VELOCITY + jmp @collision_check + @move_right: + clc + adc #BULLET_VELOCITY + + @collision_check: + ;; We now have the future value for the X axis. Store it as the current + ;; value and then convert it into tile coordinates so it can be used for + ;; background collision check. + sta zp_current_bullet_x + lsr + lsr + lsr + sta Globals::zp_arg1 + + ;; The actual check for background collision. + jsr Background::collides + beq @check_enemy_collision + + ;; There was a collision! Disable the bullet. + ldx Globals::zp_idx + lda #$FF + sta zp_bullets_pool_base, x + + ;; Decrement the number of bullets active. + dec zp_bullets_pool_size + beq @end + dey + beq @end + + ;; And go for the next iteration. + inx + inx + inx + jmp @move_loop + + @check_enemy_collision: + ;; TODO + + @save_bullet_move: + ;; Restore back the old value from the 'x' register. + ldx Globals::zp_idx + inx + inx + + ;; Store the new value for the X axis, increment the 'x' register and + ;; decrease the number of active bullets to be moved. If we are already + ;; into no bullets to be moved, then fall through and consider + ;; collisions. + lda zp_current_bullet_x + sta zp_bullets_pool_base, x + inx + dey + bne @move_loop + + @end: + rts + .endproc +.endscope diff --git a/src/driver.s b/src/driver.s index 4c2220f..c428475 100644 --- a/src/driver.s +++ b/src/driver.s @@ -18,7 +18,13 @@ PAUSE_TIMER_VALUE = (HZ / 3) zp_pause_timer = $32 - ;; Switch from the title screen to the main street. Note that this function + ;; Number of sprites available for sprite cycling. + SPRITE_CYCLING_BYTES = (64 - Player::PLAYER_SPRITES_COUNT) * 4 + + ;; TODO + zp_next_bullet_cycle = $33 + + ;; Switch from the title screen to the main screen. Note that this function ;; is to be called with the PPU disabled. If that's not the case, then it ;; will set the proper values to disable it on the next `nmi` call and set ;; the `title over` flag. With that, call again this function so the @@ -80,11 +86,15 @@ @load_player: jsr Player::init + jsr Bullets::init ;; Initialize pause timer. lda #0 sta zp_pause_timer + ;; Initialize variables for sprite cycling. + sta zp_next_bullet_cycle + @game: ;; Check if the player is toggling the `pause` state. lda #(Joypad::BUTTON_START | Joypad::BUTTON_SELECT) @@ -131,8 +141,156 @@ beq @do_update rts + ;; This is the actual meat of the main game, which updates the state of + ;; the player, bullets, enemies, etc. @do_update: - JAL Player::update + jsr Player::update + jsr Bullets::update + JAL sprite_cycling + ;; TODO: fall through? + .endproc + + .proc sprite_cycling + ;; The 'y' register will contain the index on OAM of the sprite to be + ;; allocated. + ldy #(Player::PLAYER_SPRITES_COUNT * 4) + + ;; The 'x' register will index from the different sprite pools. + ldx zp_next_bullet_cycle + lda Bullets::zp_bullets_pool_base, x + + ;; Is this a valid bullet? + cmp #$FF + beq @after_first_bullet + + ;; It is a valid bullet! Set it now. + lda Bullets::zp_bullets_pool_base + 1, x + sta $200, y + iny + + ;; The tile selection depends on how many moves the bullet has done. + lda Bullets::zp_bullets_pool_base, x + and #%01111111 + cmp #Bullets::BULLET_LAST_TRANSITION + bcs @last_bullet_tile + cmp #Bullets::BULLET_FIRST_TRANSITION + bcs @mid_bullet_tile + lda #$0E + bne @set_bullet_tile + @mid_bullet_tile: + lda #$0F + bne @set_bullet_tile + @last_bullet_tile: + lda #$1E + @set_bullet_tile: + sta $200, y + + iny + lda #0 + sta $200, y + iny + lda Bullets::zp_bullets_pool_base + 2, x + sta $200, y + iny + + @after_first_bullet: + ;; Save the index that was considered for the first bullet. + stx Globals::zp_tmp0 + + ;; Increase the index for the bullets cycling. If wrapping is detected, + ;; then it resets this value back to zero. + inx + inx + inx + cpx #Bullets::BULLETS_POOL_CAPACITY_BYTES + bne @set_next_bullets_cycle + ldx #0 + @set_next_bullets_cycle: + stx zp_next_bullet_cycle + + ;; TODO: ensure 1 enemy + iny + iny + iny + iny + + ;; TODO: ensure 1 item + iny + iny + iny + iny + + ;; TODO: rest of bullets + ldx #0 + @rest_o_bullets: + cpx #Bullets::BULLETS_POOL_CAPACITY_BYTES + beq @rest_o_enemies + cpx Globals::zp_tmp0 + bne @do_bullet + inx + inx + inx + cpx #Bullets::BULLETS_POOL_CAPACITY_BYTES + beq @rest_o_enemies + @do_bullet: + lda Bullets::zp_bullets_pool_base, x + cmp #$FF + beq @next_bullet + + lda Bullets::zp_bullets_pool_base + 1, x + sta $200, y + iny + + ;; The tile selection depends on how many moves the bullet has done. + lda Bullets::zp_bullets_pool_base, x + and #%01111111 + cmp #Bullets::BULLET_LAST_TRANSITION + bcs @other_last_bullet_tile + cmp #Bullets::BULLET_FIRST_TRANSITION + bcs @other_mid_bullet_tile + lda #$0E + bne @other_set_bullet_tile + @other_mid_bullet_tile: + lda #$0F + bne @other_set_bullet_tile + @other_last_bullet_tile: + lda #$1E + @other_set_bullet_tile: + sta $200, y + + iny + lda #0 + sta $200, y + iny + lda Bullets::zp_bullets_pool_base + 2, x + sta $200, y + iny + + @next_bullet: + inx + inx + inx + jmp @rest_o_bullets + + @rest_o_enemies: + ;; TODO: rest of enemies + ;; TODO: rest of items + + ;; We are done with all the sprites we wanted to allocat. Now let's + ;; clear out the rest of the slots just in case there was some leftover + ;; from a past sprite. + lda #$EF + @check_cycle_end: + cpy #SPRITE_CYCLING_BYTES + bne @reset_sprite + rts + @reset_sprite: + sta $200, y + iny + iny + iny + iny + jmp @check_cycle_end ;TODO: maybe just bne? .endproc .ifdef PAL diff --git a/src/jetpac.s b/src/jetpac.s index 10f6df3..0c75bf4 100644 --- a/src/jetpac.s +++ b/src/jetpac.s @@ -40,6 +40,7 @@ .include "assets.s" .include "background.s" .include "player.s" +.include "bullets.s" .include "title.s" .include "driver.s" .include "vectors.s" diff --git a/src/player.s b/src/player.s index aac5dd0..c7b65af 100644 --- a/src/player.s +++ b/src/player.s @@ -110,6 +110,9 @@ ;; How many frames are we allowing for each walk animation state? WALK_COUNTER_MAX = (HZ / 10) + ;; Number of sprites from which the player is made of. + PLAYER_SPRITES_COUNT = 6 + ;; Initialize the player's sprite. Note that for the sprite to look ;; correctly on screen you still need to call `Player::update` afterwards. .proc init |
