diff options
Diffstat (limited to 'space/src')
| -rw-r--r-- | space/src/space.s | 147 | ||||
| -rw-r--r-- | space/src/states/game.s | 21 | ||||
| -rw-r--r-- | space/src/states/player.s | 356 | ||||
| -rw-r--r-- | space/src/vectors/irq.s | 6 | ||||
| -rw-r--r-- | space/src/vectors/nmi.s | 14 | ||||
| -rw-r--r-- | space/src/vectors/reset.s | 76 |
6 files changed, 620 insertions, 0 deletions
diff --git a/space/src/space.s b/space/src/space.s new file mode 100644 index 0000000..eec893c --- /dev/null +++ b/space/src/space.s @@ -0,0 +1,147 @@ +;;; +;;; TODO: reduce the scope of this to just: +;;; - Movement through subpixels. +;;; - Shooting (no collision or anything) +;;; -> link to jetpac.nes for more stuff +;;; +;; This is similar to the sprite.s example, but it expands on it greatly into a +;; full game by: +;; - Having a moving background. +;; - The ship can be moved: +;; - The movement is done through subpixels for a smoother experience. +;; - The ship's sprites are updated accordingly: resting, acceleration, full +;; speed. +;; - Random asteroids will appear from time to time and they can collide with +;; the ship: +;; - A collision decreases the live status from the ship (cracks will appear +;; to the sprite). +;; - When the live status reaches 0 -> game over. +;; - The ship can shoot and destroy asteroids. +;; - There is a score. +;;; + +.segment "HEADER" + .byte 'N', 'E', 'S', $1A + + ;; 2x PRG-ROM; 1x CHR-ROM + .byte $02 + .byte $01 + + ;; Horizontal mirroring, no special mapper. + .byte $00 + .byte $00 + +.segment "VECTORS" + .addr nmi, reset, irq + +.segment "CHARS" + .incbin "../assets/space.chr" + +.segment "STARTUP" + +.segment "CODE" + +.include "../include/apu.s" +.include "../include/oam.s" +.include "../include/ppu.s" +.include "../include/joypad.s" + +.include "states/game.s" +.include "states/player.s" +.include "vectors/reset.s" +.include "vectors/nmi.s" +.include "vectors/irq.s" + +;;; +;; This is our main subroutine, the reset procedure will call at the very end of +;; initializing the hardware. +;;; +.proc main + ;; Before starting the game loop proper we initialize all our assets: load + ;; the palettes, nametables and sprites for this game. + jsr init_palettes + jsr init_nametable + jsr Player::init + + ;; Reset scroll. + bit PPU::STATUS + lda #$00 + sta PPU::SCROLL + sta PPU::SCROLL + + cli + + ;; 7: allow NMI; 5: sprite size is 8x16; 4: background pattern table starts + ;; at $1000. + lda #%10110000 + sta PPU::CONTROL + + ;; 4: show sprites; 3: show background; 2: show sprites in leftmost pixels + ;; on the screen; 1: same as 2 but for background. + lda #%00011110 + sta PPU::MASK + +@main_game_loop: + jsr Joypad::read + jsr Player::Movement::update + jsr Player::Sprite::update + + ;; This is a hand-shake between the code on `main` and the code on the + ;; `nmi`. See Game::flags for more. + SET_RENDER_FLAG +@wait_for_render: + bit Game::flags + bmi @wait_for_render + + ;; Rendering is done, we can perform another iteration of the loop! + jmp @main_game_loop +.endproc + +;; init_palettes copies all the palettes for our game into the proper PPU +;; address. +.proc init_palettes + PPU_ADDR $3F00 + + ldx #0 +@load_palettes_loop: + lda palettes, x + sta PPU::DATA + inx + cpx #$20 + bne @load_palettes_loop + rts +palettes: + ;; Background + .byte $0F, $12, $22, $32 + .byte $0F, $00, $28, $30 + .byte $0F, $28, $16, $2D + .byte $0F, $28, $16, $2D + + ;; Foreground + .byte $0F, $00, $05, $30 + .byte $0F, $00, $00, $00 + .byte $0F, $00, $00, $00 + .byte $0F, $00, $00, $00 +.endproc + +;; init_nametable loads the relevant data to the nametable that is then going to +;; be used in order to build up the background. +.proc init_nametable + bit PPU::STATUS + + ;; Big stars. + WRITE_PPU_DATA $20C8, $02 + WRITE_PPU_DATA $227A, $02 + + ;; Small stars. + WRITE_PPU_DATA $20B9, $04 + WRITE_PPU_DATA $21CE, $04 + WRITE_PPU_DATA $21BA, $04 + WRITE_PPU_DATA $22B8, $04 + WRITE_PPU_DATA $22E7, $04 + + ;; Select palette 1 for one of the small stars, giving it a red-ish look. + WRITE_PPU_DATA $23CE, %00000001 + + rts +.endproc diff --git a/space/src/states/game.s b/space/src/states/game.s new file mode 100644 index 0000000..ca8a49b --- /dev/null +++ b/space/src/states/game.s @@ -0,0 +1,21 @@ +.scope Game + ;; Contains relevant flags for the execution of the game: + ;; - 7: set to 1 whenever the game logic is over and we can start + ;; rendering; set to 0 when rendering is done. + ;; - 6-0: unused. + flags = $20 +.endscope + +;; SET_RENDER_FLAG sets the render bit on Game::flags to 1. +.macro SET_RENDER_FLAG + lda #%10000000 + ora Game::flags + sta Game::flags +.endmacro + +;; UNSET_RENDER_FLAG sets the render bit on Game::flags to 0. +.macro UNSET_RENDER_FLAG + lda #%01111111 + and Game::flags + sta Game::flags +.endmacro diff --git a/space/src/states/player.s b/space/src/states/player.s new file mode 100644 index 0000000..a43f61e --- /dev/null +++ b/space/src/states/player.s @@ -0,0 +1,356 @@ +;;; +;; Player state: movement, animation, etc. The following memory addresses are +;; reserved for the player: +;; -> $30-$3F: internal data. +;; -> $0200-$0207: OAM data. +;;; +.scope Player + ;; TODO: change names of pos_x and signed_x et al + m_pos_x = $30 + m_pos_y = $31 + m_velocity_x = $32 + m_velocity_y = $33 + m_target_velocity_x = $34 + m_target_velocity_y = $35 + m_signed_x = $36 ; NOTE ! + m_signed_y = $38 ; NOTE ! + + ;; Initializes the player by initializing its internal data and loading some + ;; values of the sprite itself. + .proc init + ;; Initialize position + subpixel. + lda #$B0 + sta m_signed_y + lda #$00 + sta m_signed_y + 1 + lda #$7A + sta m_signed_x + lda #$F0 + sta m_signed_x + 1 + + ;; Initialize velocity. + lda #0 + sta m_velocity_x + sta m_velocity_y + sta m_target_velocity_x + sta m_target_velocity_y + + ;; The player itself is built with two identical sprites placed side by + ;; side, where the second one is flipped horizontally. Thus, the player + ;; takes up the first two slots on OAM data ($0200-$0207). Here we only + ;; need to select the sprite and the attributes, since the position will + ;; be updated on each game loop. Hence, here we select the sprite + ;; located at #0 on the pattern table, and then we set for the second + ;; one the horizontal flip bit for the attributes. + lda #0 + sta $0201 ; First sprite select. + sta $0205 ; Second sprite select. + lda #%00000000 + sta $0202 ; First sprite attributes. + lda #%01000000 + sta $0206 ; Second sprite attributes. + + rts + .endproc + + ;; Contains all the subroutines that have to deal with computing the + ;; movement of the sprite depending on the previous state, the buttons being + ;; pressed, the current position, etc. + .scope Movement + .proc update + jsr set_target_velocity + jsr accelerate + jsr apply_velocity + jsr position_to_coordinates + rts + .endproc + + ;; Set the target velocity for the X and Y axis given the current button + ;; presses. + .proc set_target_velocity + ;; The target velocity depends on whether B was pressed or not. + ;; Depending on that we will set the x index to point to one element + ;; of the velocity tables below or to another. + ldx #0 + lda #Joypad::BUTTON_B + and Joypad::m_buttons1 + beq @target_check_right + inx + @target_check_right: + ;; The algorithm from here on is pretty straight-forward. Check the + ;; right button. If it was not pressed jump to the left check. If it + ;; was pressed load the target velocity on the x-axis from the given + ;; table and jump into the arrow-up check. + lda #Joypad::BUTTON_RIGHT + and Joypad::m_buttons1 + beq @target_check_left + lda positive_velocity, x + sta m_target_velocity_x + jmp @target_check_up + + @target_check_left: + ;; Similar to before: if it was not pressed, then set the target + ;; velocity to 0, otherwise set the proper value and jump to the up + ;; check. + lda #Joypad::BUTTON_LEFT + and Joypad::m_buttons1 + beq @target_no_x + lda negative_velocity, x + sta m_target_velocity_x + jmp @target_check_up + + @target_no_x: + ;; None of the buttons on the X-axis were pressed. Set the target + ;; velocity to 0. + lda #0 + sta m_target_velocity_x + ;; NOTE: walkthrough + + @target_check_up: + ;; Same as before but we return early if it was pressed, otherwise + ;; we go into the arrow-down check. + lda #Joypad::BUTTON_UP + and Joypad::m_buttons1 + beq @target_check_down + lda negative_velocity, x + sta m_target_velocity_y + rts + + @target_check_down: + ;; If down was not pressed, go to the "no_y" case, otherwise return + ;; early after setting the proper Y target velocity. + lda #Joypad::BUTTON_DOWN + and Joypad::m_buttons1 + beq @target_no_y + lda positive_velocity, x + sta m_target_velocity_y + rts + + @target_no_y: + ;; None of the buttons on the Y-axis were pressed. Set the target + ;; velocity to 0. + lda #0 + sta m_target_velocity_y + rts + + ;; TODO + positive_velocity: + ;; $18: 0001 | 1000 + ;; $28: 0010 | 1000 + .byte $18, $28 + negative_velocity: + ;; $E8: 1110 | 1000 + ;; $D8: 1101 | 1000 + .byte $E8, $D8 + .endproc + + ;; TODO: give it a closer look + .proc accelerate + lda m_velocity_x + sec + sbc m_target_velocity_x + bne @accelerate_x_check_greater + jmp @accelerate_y + @accelerate_x_check_greater: + bmi @accelerate_x_check_lesser + dec m_velocity_x + jmp @accelerate_y + @accelerate_x_check_lesser: + inc m_velocity_x + + @accelerate_y: + lda m_velocity_y + sec + sbc m_target_velocity_y + bne @accelerate_y_check_greater + rts + @accelerate_y_check_greater: + bmi @accelerate_y_check_lesser + dec m_velocity_y + rts + @accelerate_y_check_lesser: + inc m_velocity_y + rts + .endproc + + ;; TODO: give it a closer look + .proc apply_velocity + lda m_velocity_x + bmi @apply_negative_velocity_x + + clc + adc m_signed_x + sta m_signed_x + lda #0 ;NOTE: adding possible carry! + adc m_signed_x + 1 + sta m_signed_x + 1 + jmp @apply_velocity_y + + @apply_negative_velocity_x: + lda #0 + sec + sbc m_velocity_x + sta $00 + lda m_signed_x + sec + sbc $00 + sta m_signed_x + lda m_signed_x + 1 + sbc #0 + sta m_signed_x + 1 + ;; NOTE: walkthrough + + @apply_velocity_y: + lda m_velocity_y + bmi @apply_negative_velocity_y + + clc + adc m_signed_y + sta m_signed_y + lda #0 + adc m_signed_y + 1 + sta m_signed_y + 1 + rts + + @apply_negative_velocity_y: + lda #0 + sec + sbc m_velocity_y + sta $00 + lda m_signed_y + sec + sbc $00 + sta m_signed_y + lda m_signed_y + 1 + sbc #0 + sta m_signed_y + 1 + rts + .endproc + + .proc position_to_coordinates + jsr position_to_coordinates_x + jsr position_to_coordinates_y + + rts + .endproc + + .proc position_to_coordinates_x + ;; Convert the fixed point position coordinate into screen coordinates + lda m_signed_x + sta $00 + lda m_signed_x + 1 + sta $01 + lsr $01 + ror $00 + lsr $01 + ror $00 + lsr $01 + ror $00 + lsr $01 + ror $00 + ; Assume that everything is fine and save the sprite position + lda $00 + sta m_pos_x + + lda m_velocity_x + bmi @position_from_negative_velocity + + lda $01 + bne @bound_upper_x + lda $00 + cmp #239 + bcs @bound_upper_x + rts + @bound_upper_x: + lda #$EF + sta m_pos_x + lda #$0E + sta m_signed_x + 1 + lda #$F0 + sta m_signed_x + lda #0 + sta m_velocity_x + rts + @position_from_negative_velocity: + lda m_signed_x + 1 + bmi @bound_lower_x + rts + @bound_lower_x: + lda #0 + sta m_signed_x + sta m_signed_x + 1 + sta m_pos_x + sta m_velocity_x + rts + .endproc + + .proc position_to_coordinates_y + ;; Convert the fixed point position coordinate into screen coordinates + lda m_signed_y + sta $00 + lda m_signed_y + 1 + sta $01 + lsr $01 + ror $00 + lsr $01 + ror $00 + lsr $01 + ror $00 + lsr $01 + ror $00 + ; Assume that everything is fine and save the sprite position + lda $00 + sta m_pos_y + + lda m_velocity_y + bmi @position_from_negative_velocity_y + + lda $01 + bne @bound_upper_y + lda $00 + cmp #239 + bcs @bound_upper_y + rts + @bound_upper_y: + lda #$EF + sta m_pos_y + lda #$0E + sta m_signed_y + 1 + lda #$F0 + sta m_signed_y + lda #0 + sta m_velocity_y + rts + @position_from_negative_velocity_y: + lda m_signed_y + 1 + bmi @bound_lower_y + rts + @bound_lower_y: + lda #0 + sta m_signed_y + sta m_signed_y + 1 + sta m_pos_y + sta m_velocity_y + rts + .endproc + .endscope + + ;; Functions related to the rendering and manipulation of the sprite itself. + .scope Sprite + ;; Update the sprite on OAM memory according to what we have in the + ;; internal data stored in $30-$3F. + .proc update + lda m_pos_y + sta $200 + sta $204 + + lda m_pos_x + sta $203 + clc + adc #8 + sta $207 + + rts + .endproc + .endscope +.endscope diff --git a/space/src/vectors/irq.s b/space/src/vectors/irq.s new file mode 100644 index 0000000..2be3a94 --- /dev/null +++ b/space/src/vectors/irq.s @@ -0,0 +1,6 @@ +.segment "CODE" + +;; Interrupt Requests handler. +irq: + ;; Nothing to do for us here :) + rti diff --git a/space/src/vectors/nmi.s b/space/src/vectors/nmi.s new file mode 100644 index 0000000..4bc3dde --- /dev/null +++ b/space/src/vectors/nmi.s @@ -0,0 +1,14 @@ +;; Non-Maskable Interrupts handler. +nmi: + ;; As mentioned on the `main` subroutine, rendering will be skipped until + ;; the proper flag is set. + bit $20 + bpl @next + + ;; We can start rendering stuff. + OAM_WRITE_SPRITES + + ;; And unset the render flag so the `main` code is unblocked. + UNSET_RENDER_FLAG +@next: + rti diff --git a/space/src/vectors/reset.s b/space/src/vectors/reset.s new file mode 100644 index 0000000..afc9fa5 --- /dev/null +++ b/space/src/vectors/reset.s @@ -0,0 +1,76 @@ +.segment "CODE" + +reset: + ;; Ignore IRQs and disable decimal mode. + sei + cld + + ;; Disable APU frame IRQ. + ldx #$40 + stx APU::FRAME_COUNTER + + ;; Set up the stack register with the proper value. + ldx #$ff + txs + + ;; And now disable, in this order, NMI, rendering and DMC IRQs. Note that + ;; `x` was set to $ff, so increasing it by one results in a zero, which is + ;; the value then stored in the aforementioned memory locations. + inx + stx PPU::CONTROL + stx PPU::MASK + stx APU::DMC + + ;;; + ;; NOTE: If you are using a mapper which needs some special configuration, + ;; now it would be a good time set it up. I am not using a special mapper, + ;; so there's nothing from me to do here. + ;;; + + ;; First of the two VBLANK waits. +@vblankwait1: + bit PPU::STATUS + bpl @vblankwait1 + + ;; Clear RAM memory. Notice that we are not clearing $200-$2ff, this is done + ;; later. + ldx #0 + lda #0 +@ram_reset_loop: + sta $000, x + sta $100, x + sta $300, x + sta $400, x + sta $500, x + sta $600, x + sta $700, x + inx + bne @ram_reset_loop + + ;; Reset sprites by moving them off-screen. + lda #$ef +@sprite_reset_loop: + sta $200, x + inx + bne @sprite_reset_loop + + ;; Write "sprites" into OAM. + OAM_WRITE_SPRITES + + ;; Last VBLANK wait. +@vblankwait2: + bit PPU::STATUS + bpl @vblankwait2 + + ;; Reset palettes. + PPU_ADDR $3F00 + + lda #$0F + ldx #$20 +@palettes_reset_loop: + sta PPU::DATA + dex + bne @palettes_reset_loop + + ;; Jump into the main subroutine. + jmp main |
