.segment "CODE" ;; Assuming that the 'x' register indexes a bullet on its pool, increment the ;; register as many times as to point to the next one. Bound checking is not ;; performed, it's up to the caller to implement that. .macro NEXT_BULLET_INDEX_X inx inx inx .endmacro ;; 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 = 10 ;; The capacity of the bullets pool in bytes. BULLETS_POOL_CAPACITY_BYTES = BULLETS_POOL_CAPACITY * 3 ;; 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_pool_base = $A0 ; asan:reserve BULLETS_POOL_CAPACITY_BYTES ;; The current amount of bullets on screen. zp_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 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 = $E2 zp_current_bullet_x = $E3 ;; 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_timer = $35 BULLET_TIMER_VALUE = HZ / 15 ;; Velocity at which bullets move. .ifdef PAL BULLET_VELOCITY = 7 .else BULLET_VELOCITY = 6 .endif ;; Maximum moves that a bullet can do. The tile also transitions depending ;; on the moves done so far. BULLET_MAX_MOVES = BULLET_VELOCITY * 5 BULLET_FIRST_TRANSITION = BULLET_VELOCITY * 2 BULLET_LAST_TRANSITION = BULLET_VELOCITY * 4 ;; Initialize the pool of bullets. .proc init lda #0 sta Bullets::zp_timer sta Bullets::zp_pool_size sta Bullets::zp_last_allocated_index sta Bullets::zp_current_bullet_y sta Bullets::zp_current_bullet_x ;; Initializing the pool is a matter of setting to $FF the state byte ;; for each bullet object. ldx #0 ldy #BULLETS_POOL_CAPACITY lda #$FF @pool_init_loop: sta Bullets::zp_pool_base, x NEXT_BULLET_INDEX_X dey 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 Bullets::zp_pool_size cmp #BULLETS_POOL_CAPACITY beq @move_bullets ;; Can the B button be pressed? If not go to `@move_bullets` directly. lda Bullets::zp_timer beq @check_bullets_pressed dec Bullets::zp_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 Bullets::zp_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 Bullets::zp_last_allocated_index @find_free_bullet_bucket: lda Bullets::zp_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. NEXT_BULLET_INDEX_X 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 Bullets::zp_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 Bullets::zp_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 Bullets::zp_pool_base, x ;; Save the index so it can be used in future bullet creation. Also be ;; careful to wrap around. inx cpx #BULLETS_POOL_CAPACITY_BYTES bne @set_last_allocated ldx #0 @set_last_allocated: stx Bullets::zp_last_allocated_index ;; Increase the number of bullets on screen. inc Bullets::zp_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 Bullets::zp_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 Bullets::zp_pool_base, x cmp #$FF bne @move_active_bullet ;; No, go for the next one. NEXT_BULLET_INDEX_X 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 Bullets::zp_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 Bullets::zp_pool_size bne @decrease_y jmp @end @decrease_y: dey bne @next_iteration jmp @end @next_iteration: ;; We still have active bullets to move, go to the next iteration. NEXT_BULLET_INDEX_X bne @move_loop @do_move_active_bullet: ;; Increase the number of moves that this bullet has done. stx Globals::zp_idx inc Bullets::zp_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 Bullets::zp_pool_base + 1, x sta Bullets::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 Bullets::zp_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 Bullets::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 Bullets::zp_pool_base, x ;; Decrement the number of bullets active. dec Bullets::zp_pool_size beq @end dey beq @end ;; And go for the next iteration. inx inx inx jmp @move_loop ;; Enemy collision for this bullet. @check_enemy_collision: ldx #0 sta Enemies::zp_pool_index @enemy_collision_loop: ;; Does it collide? jsr Enemies::collides beq @next_enemy_collision ;; Yes! Kill the enemy and break the loop. jsr Enemies::bite_the_dust jmp @save_bullet_move @next_enemy_collision: ;; Advance by the size of each pool item. lda Enemies::zp_pool_index clc adc #Enemies::SIZEOF_POOL_ITEM ;; Are we at the end of the enemies' pool? cmp #Enemies::ENEMIES_POOL_CAPACITY_BYTES beq @save_bullet_move ;; Nope! Save the index and fetch the next enemy. sta Enemies::zp_pool_index jmp @enemy_collision_loop @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 Bullets::zp_current_bullet_x sta Bullets::zp_pool_base, x inx dey beq @end jmp @move_loop @end: rts .endproc .endscope