diff options
| author | Miquel Sabaté Solà <mikisabate@gmail.com> | 2025-05-13 14:57:15 +0200 |
|---|---|---|
| committer | Miquel Sabaté Solà <mikisabate@gmail.com> | 2025-05-14 16:25:52 +0200 |
| commit | ae857d302cd6f81c130b216f5dcc16655a09b4ac (patch) | |
| tree | ba377720e962501e052fe622e933dfc5516081e5 /src | |
| parent | ad830a7f81871e15f43d7255f7af5463d9dd0bc5 (diff) | |
| download | jetpac.nes-ae857d302cd6f81c130b216f5dcc16655a09b4ac.tar.gz jetpac.nes-ae857d302cd6f81c130b216f5dcc16655a09b4ac.zip | |
Implement the base for moving bullets
This establishes a way for bullets to move, be displayed as independent
sprites, cycle these sprites, and check for the collision on the
background.
This is still lacking the collision check with enemies and has some
obvious bugs that will be fixed on the next commits.
Signed-off-by: Miquel Sabaté Solà <mikisabate@gmail.com>
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 |
