From 149020464517ff3d021eef5ccd50d3939ad463e6 Mon Sep 17 00:00:00 2001 From: Miquel Sabaté Solà Date: Tue, 24 Oct 2023 17:40:19 +0200 Subject: Lay out a proper structure for the project MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Let's divide things by topic instead of a broad "examples" directory. This will allow for simple files to co-exist together with full-blown projects. Signed-off-by: Miquel Sabaté Solà --- space/Makefile | 15 ++ space/assets/background.nam | Bin 0 -> 1024 bytes space/assets/space.chr | Bin 0 -> 8192 bytes space/include/apu.s | 6 + space/include/joypad.s | 63 ++++++++ space/include/oam.s | 13 ++ space/include/ppu.s | 27 ++++ space/src/space.s | 147 ++++++++++++++++++ space/src/states/game.s | 21 +++ space/src/states/player.s | 356 ++++++++++++++++++++++++++++++++++++++++++++ space/src/vectors/irq.s | 6 + space/src/vectors/nmi.s | 14 ++ space/src/vectors/reset.s | 76 ++++++++++ 13 files changed, 744 insertions(+) create mode 100644 space/Makefile create mode 100644 space/assets/background.nam create mode 100644 space/assets/space.chr create mode 100644 space/include/apu.s create mode 100644 space/include/joypad.s create mode 100644 space/include/oam.s create mode 100644 space/include/ppu.s create mode 100644 space/src/space.s create mode 100644 space/src/states/game.s create mode 100644 space/src/states/player.s create mode 100644 space/src/vectors/irq.s create mode 100644 space/src/vectors/nmi.s create mode 100644 space/src/vectors/reset.s (limited to 'space') diff --git a/space/Makefile b/space/Makefile new file mode 100644 index 0000000..e38607c --- /dev/null +++ b/space/Makefile @@ -0,0 +1,15 @@ +CC65 ?= cl65 +CCOPTS ?= --verbose --target nes + +.PHONY: all +all: clean build + +.PHONY: clean +clean: + @rm -f space.nes + +.PHONY: build +build: space.nes + +%.nes: src/%.s + $(CC65) $(CCOPTS) $< -o $@ diff --git a/space/assets/background.nam b/space/assets/background.nam new file mode 100644 index 0000000..ea228ff Binary files /dev/null and b/space/assets/background.nam differ diff --git a/space/assets/space.chr b/space/assets/space.chr new file mode 100644 index 0000000..ee1f47a Binary files /dev/null and b/space/assets/space.chr differ diff --git a/space/include/apu.s b/space/include/apu.s new file mode 100644 index 0000000..32ccff2 --- /dev/null +++ b/space/include/apu.s @@ -0,0 +1,6 @@ +.segment "CODE" + +.scope APU + DMC = $4010 + FRAME_COUNTER = $4017 +.endscope diff --git a/space/include/joypad.s b/space/include/joypad.s new file mode 100644 index 0000000..8711c28 --- /dev/null +++ b/space/include/joypad.s @@ -0,0 +1,63 @@ +.segment "CODE" + +;;; +;; Joypad controller code. The following memory addresses are reserved: $21-$23. +;; +;; Memory address $21 is used for internal purposes, whereas $22 and $23 contain +;; the bitmask of the buttons that are pressed from each controller. +;;; + +.scope Joypad + ;; Button masks. + BUTTON_A = 1 << 7 + BUTTON_B = 1 << 6 + BUTTON_SELECT = 1 << 5 + BUTTON_START = 1 << 4 + BUTTON_UP = 1 << 3 + BUTTON_DOWN = 1 << 2 + BUTTON_LEFT = 1 << 1 + BUTTON_RIGHT = 1 << 0 + + ;; Port addresses for controllers. + JOYPAD1 = $4016 + JOYPAD2 = $4017 + + ;; We keep all the information from controller from mainly two variables: + ;; m_buttons1 and m_buttons2; containing respectively the buttons pressed + ;; for this frame for both controllers. The m_inv_buttons ($21) is an + ;; internal variable and should not be used for anything outside of this + ;; usage. + m_inv_buttons = $21 + m_buttons1 = $22 + m_buttons2 = $23 + + ;; READ_CONTROLLER reads the input from the controller mapped into the given + ;; port, and saves the state into the given `buttons` address. + ;; Implementation taken from NESHacker's example of smb3-like movement. + .macro READ_CONTROLLER port, buttons + lda m_inv_buttons + tay + lda #1 + sta port + sta m_inv_buttons + lsr + sta port + : + lda port + lsr + rol m_inv_buttons + bcc :- + tya + eor m_inv_buttons + and m_inv_buttons + sta buttons + .endmacro + + ;; read sets the values for m_buttons1 and m_buttons2 as read from both + ;; controllers. + .proc read + READ_CONTROLLER JOYPAD1, m_buttons1 + READ_CONTROLLER JOYPAD2, m_buttons2 + rts + .endproc +.endscope diff --git a/space/include/oam.s b/space/include/oam.s new file mode 100644 index 0000000..ecd1b7d --- /dev/null +++ b/space/include/oam.s @@ -0,0 +1,13 @@ +.segment "CODE" + +.scope OAM + ADDR = $2003 + DMA = $4014 +.endscope + +.macro OAM_WRITE_SPRITES + lda #$00 + sta OAM::ADDR + lda #$02 + sta OAM::DMA +.endmacro diff --git a/space/include/ppu.s b/space/include/ppu.s new file mode 100644 index 0000000..23fd978 --- /dev/null +++ b/space/include/ppu.s @@ -0,0 +1,27 @@ +.segment "CODE" + +.scope PPU + CONTROL = $2000 + MASK = $2001 + STATUS = $2002 + SCROLL = $2005 + ADDRESS = $2006 + DATA = $2007 +.endscope + +.macro PPU_ADDR address + lda #.HIBYTE(address) + sta PPU::ADDRESS + lda #.LOBYTE(address) + sta PPU::ADDRESS +.endmacro + +;; WRITE_PPU_DATA is a macro that will write into PPU::ADDRESS the given address +;; and into PPU::DATA the given byte value. +.macro WRITE_PPU_DATA address, value + bit PPU::STATUS + + PPU_ADDR address + lda #value + sta PPU::DATA +.endmacro 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 -- cgit v1.2.3