diff options
| author | Miquel Sabaté Solà <msabate@suse.com> | 2023-11-12 09:39:05 +0100 |
|---|---|---|
| committer | Miquel Sabaté Solà <mikisabate@gmail.com> | 2025-02-04 21:02:11 +0100 |
| commit | db9cb2118e7b2575ed3b0f535e50dd85c24df25a (patch) | |
| tree | 8bfe1a7e48bd85e2e9ab0c650c248a7bed1955b7 | |
| parent | cb854722b9890c171f7d0600812a7e9a3a2a25c9 (diff) | |
| download | code.nes-db9cb2118e7b2575ed3b0f535e50dd85c24df25a.tar.gz code.nes-db9cb2118e7b2575ed3b0f535e50dd85c24df25a.zip | |
space: Simplify the code and its goals
Originally this was supposed to be a sort of mini game, but that would
have probably made it too complex for newcomers. Thus, let's just keep
it to subpixel movement and shooting bullets, which is already a big
step from the `basics/sprite.s` example.
Signed-off-by: Miquel Sabaté Solà <msabate@suse.com>
| -rw-r--r-- | README.md | 4 | ||||
| -rw-r--r-- | basics/sprite.s | 13 | ||||
| -rw-r--r-- | space/assets/space.chr | bin | 8192 -> 8192 bytes | |||
| -rw-r--r-- | space/src/space.s | 30 | ||||
| -rw-r--r-- | space/src/states/bullets.s | 181 | ||||
| -rw-r--r-- | space/src/states/player.s | 188 | ||||
| -rw-r--r-- | space/src/vectors/nmi.s | 24 | ||||
| -rw-r--r-- | space/src/vectors/reset.s | 47 |
8 files changed, 335 insertions, 152 deletions
@@ -20,8 +20,8 @@ NES memory. A safe bet is to go with either [Mesen](https://github.com/SourMesen/Mesen2/), which provide tools like RAM watchers or a full debugger. -Notice that I will use some terms without actually introducing them. That is, I -expect you to go over the [NES Dev +Notice also that I will use some terms without actually introducing them. That +is, I expect you to go over the [NES Dev wiki](https://www.nesdev.org/wiki/Nesdev_Wiki) for glossary or for full documentation on the stuff being shown here. For example, if I am writing an example code using the UNROM or the MMC1 chips, I assume that you will go over diff --git a/basics/sprite.s b/basics/sprite.s index 09dd8e5..fc024f7 100644 --- a/basics/sprite.s +++ b/basics/sprite.s @@ -185,10 +185,11 @@ reset: inx bne @ram_reset_loop ; if x overflows back to #00, then we are done. - ;; And now another thing we can do is to reset the sprites. In NES programs - ;; sprites live in a special range of memory addresses from the RAM called - ;; OAM (Object Attribute Memory). This range is located in $0200-02ff (the - ;; range we did not initialize when cleaning up RAM). + ;; And now another thing we can do is to reset the sprites. We will reserve + ;; space for sprites in $0200-02ff (the range we did not initialize when + ;; cleaning up RAM). With this in mind we will perform a DMA process right + ;; after this block of code that will take this memory range and bulk it + ;; into PPU OAM space. ;; ;; "Resetting sprites" is just a matter of giving them a value which will ;; not bother us in the future. One way to do this is to set each value to @@ -209,8 +210,8 @@ reset: ;; the byte given at the OAMADDR, thus XX = #$00; and N = 2 (see `lda ;; #$02`). Therefore, we are telling the PPU to start the DMA process from ;; $200. The PPU will assume that the following 256 bytes of memory are the - ;; ones to be copied, resulting in a DMA copy of $200-$2ff, right where the - ;; sprite data is located in memory. + ;; ones to be copied, resulting in a DMA copy of $200-$2ff, right where we + ;; stored the sprite data in advance. lda #$00 sta $2003 ; OAMADDR lda #$02 diff --git a/space/assets/space.chr b/space/assets/space.chr Binary files differindex ee1f47a..d430508 100644 --- a/space/assets/space.chr +++ b/space/assets/space.chr diff --git a/space/src/space.s b/space/src/space.s index eec893c..f2c5dbe 100644 --- a/space/src/space.s +++ b/space/src/space.s @@ -1,23 +1,14 @@ ;;; -;;; 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. +;; This is similar to the basics/sprite.s example, but it expands a bit on it +;; by: ;; - 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. +;; - The ship can shoot +;; +;; The subpixel movement is largely based on: +;; https://github.com/NesHacker/PlatformerMovement. ;;; .segment "HEADER" @@ -48,6 +39,7 @@ .include "states/game.s" .include "states/player.s" +.include "states/bullets.s" .include "vectors/reset.s" .include "vectors/nmi.s" .include "vectors/irq.s" @@ -62,12 +54,7 @@ jsr init_palettes jsr init_nametable jsr Player::init - - ;; Reset scroll. - bit PPU::STATUS - lda #$00 - sta PPU::SCROLL - sta PPU::SCROLL + jsr Bullets::init cli @@ -85,6 +72,7 @@ jsr Joypad::read jsr Player::Movement::update jsr Player::Sprite::update + jsr Bullets::update ;; This is a hand-shake between the code on `main` and the code on the ;; `nmi`. See Game::flags for more. diff --git a/space/src/states/bullets.s b/space/src/states/bullets.s new file mode 100644 index 0000000..af74ce7 --- /dev/null +++ b/space/src/states/bullets.s @@ -0,0 +1,181 @@ +;;; +;; Implements the pool of bullets available when pressing B. The main code +;; should just want to call `init` for initializing the memory space, and then +;; `update` on each main game loop iteration. The following addresses are +;; reserved: +;; -> $40-$41: internal data. +;; -> $0208-$0213: OAM data (this is a pool of three sprites). +;; +;; NOTE: the code in here touch the addresses on OAM directly, instead of +;; keeping them in some internal addresses on zero page. This is usually not a +;; good idea, and more so if we were to do collision checking and stuff like +;; that. That being said, because of its simplicity, it's not that big of a +;; deal. +.scope Bullets + ;; The number of bullets shown on screen for the current frame. + m_bullets_screen = $40 + + ;; Frame counter. See `FRAMES` below. + m_frames = $41 + + ;; How many frames have to pass to allow the user to shoot another bullet + ;; after the previous one. + FRAMES = 15 + + ;; Initializes the pool of bullets. + .proc init + ;; Initializing variables. + + lda #0 + sta m_bullets_screen + + lda #FRAMES + sta m_frames + + ;; Set X and Y positions off-screen for the three available slots. + + lda #$ff + sta $208 + sta $20C + sta $210 + sta $20B + sta $20F + sta $213 + + ;; All three sprites are instances of the same tile. + + lda #4 + sta $209 + sta $20D + sta $210 + + ;; Nothing special for these sprites, set attributes to 0. + + lda #0 + sta $20A + sta $20E + sta $212 + + rts + .endproc + + ;; Update bullets on screen, check if a new one has to be shown, etc. + .proc update + ;; If the frame counter has the same value as our allowed one, we can go + ;; into the `bullets_pressed` subroutine, otherwise we will skip it + ;; altogether. + lda m_frames + cmp #FRAMES + beq @check + inc m_frames + jmp @position + @check: + jsr bullets_pressed + @position: + jsr update_positions + + rts + .endproc + + ;; Shows a new bullet right in front of the ship if the player requested it + ;; and it is possible. + .proc bullets_pressed + ;; If we reached the maximum of bullets on screen, return early. + lda m_bullets_screen + cmp #3 + bne :+ + rts + : + ;; If the B button is not pressed, return early. + lda #Joypad::BUTTON_B + and Joypad::m_buttons1 + bne :+ + rts + : + ;; The following code will loop through the three bullet sprites and + ;; find one that is free (off-screen). If one could be found, it will + ;; assign it to the ship's position. + ldx #0 + @loop: + ;; Is the Y position on this sprite $FF? If so then it's free and we can + ;; jump into @set so we re-initialize the position for this bullet. + lda $208, x + cmp #$FF + beq @set + + ;; Oops, that was not the case. At this point, is this the last bullet? + ;; If so, we can go to the @end, otherwise we increase the index and try + ;; again. + cpx #8 + beq @end + inx + inx + inx + inx + jmp @loop + @set: + ;; At the current index we have a bullet to initialize. Hence, give it + ;; the Y value from the player and the X one (+4 so it's at the center + ;; of the ship on the X axis). + lda Player::m_screen_y + sta $208, x + lda Player::m_screen_x + clc + adc #4 + inx + inx + inx + sta $208, x + + ;; Reset the `m_frames` so to disallow too many bullets being shot at + ;; once, and increate the `m_bullets_screen` variable. + lda #0 + sta m_frames + inc m_bullets_screen + @end: + rts + .endproc + + ;; Updates the positions from the bullets being shown on screen. If one + ;; bullet goes off screen, then it's properly freed so it can be picked up + ;; by another B press. + .proc update_positions + ldx #0 + @loop: + ;; Do nothing if the bullet is free. + lda $208, x + cmp #$FF + beq @next + + ;; The indexed bullet is shown on screen, and we should make it move + ;; upward by 10 units. That being said, if its Y position is lesser or + ;; equal than 10, we can directly free it now. Otherwise, just update + ;; the Y value by performing this subtraction. + cmp #10 + bcc @free + beq @free + sec + sbc #10 + jmp @save + @free: + ;; This bullet should be freed, decrease `m_bullets_screen` and set `a` + ;; to an off-screen value. + dec m_bullets_screen + lda #$FF + @save: + ;; Either way you reach this, in `a` we have the Y value to be stored. + sta $208, x + @next: + ;; If we are already at the last bullet, jump to the @end. Otherwise + ;; increase the index and loop again. + cpx #8 + beq @end + inx + inx + inx + inx + jmp @loop + @end: + rts + .endproc +.endscope diff --git a/space/src/states/player.s b/space/src/states/player.s index a43f61e..29c1585 100644 --- a/space/src/states/player.s +++ b/space/src/states/player.s @@ -5,28 +5,48 @@ ;; -> $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 + ;; Unsigned screen coordinates on the X axis. + m_screen_x = $30 + + ;; Unsigned screen coordinates on the Y axis. + m_screen_y = $31 + + ;; The actual velocity on the X coordinates. This is a signed fixed point + ;; 4.4 (high nibble: pixels; low: subpixels). + m_velocity_x = $32 + + ;; The actual velocity on the Y coordinates. This is a signed fixed point + ;; 4.4 (high nibble: pixels; low: subpixels). + m_velocity_y = $33 + + ;; The target velocity on the X coordinates. This is a signed fixed point + ;; 4.4 (high nibble: pixels; low: subpixels). m_target_velocity_x = $34 + + ;; The target velocity on the Y coordinates. This is a signed fixed point + ;; 4.4 (high nibble: pixels; low: subpixels). m_target_velocity_y = $35 - m_signed_x = $36 ; NOTE ! - m_signed_y = $38 ; NOTE ! + + ;; Computed position on the X coordinates at the subpixel level. This is a + ;; signed fixed point 12.4. NOTE: two bytes! + m_position_x = $36 + + ;; Computed position on the X coordinates at the subpixel level. This is a + ;; signed fixed point 12.4. NOTE: two bytes! + m_position_y = $38 ;; 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 + sta m_position_y lda #$00 - sta m_signed_y + 1 + sta m_position_y + 1 lda #$7A - sta m_signed_x + sta m_position_x lda #$F0 - sta m_signed_x + 1 + sta m_position_x + 1 ;; Initialize velocity. lda #0 @@ -35,21 +55,6 @@ 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 @@ -87,7 +92,6 @@ 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 @@ -98,14 +102,11 @@ 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. @@ -115,7 +116,6 @@ 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. @@ -125,26 +125,21 @@ 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 + ;; Increase the current velocity on each axis so to match the target + ;; velocity on each case. Note that the velocity is simply increased by + ;; one. A more detailed code could be more nuanced than this. .proc accelerate lda m_velocity_x sec @@ -173,72 +168,70 @@ rts .endproc - ;; TODO: give it a closer look + ;; Apply the currently computed velocity to the position at subpixel + ;; level. .proc apply_velocity lda m_velocity_x bmi @apply_negative_velocity_x clc - adc m_signed_x - sta m_signed_x + adc m_position_x + sta m_position_x lda #0 ;NOTE: adding possible carry! - adc m_signed_x + 1 - sta m_signed_x + 1 + adc m_position_x + 1 + sta m_position_x + 1 jmp @apply_velocity_y - @apply_negative_velocity_x: lda #0 sec sbc m_velocity_x sta $00 - lda m_signed_x + lda m_position_x sec sbc $00 - sta m_signed_x - lda m_signed_x + 1 + sta m_position_x + lda m_position_x + 1 sbc #0 - sta m_signed_x + 1 - ;; NOTE: walkthrough - + sta m_position_x + 1 @apply_velocity_y: lda m_velocity_y bmi @apply_negative_velocity_y clc - adc m_signed_y - sta m_signed_y + adc m_position_y + sta m_position_y lda #0 - adc m_signed_y + 1 - sta m_signed_y + 1 + adc m_position_y + 1 + sta m_position_y + 1 rts - @apply_negative_velocity_y: lda #0 sec sbc m_velocity_y sta $00 - lda m_signed_y + lda m_position_y sec sbc $00 - sta m_signed_y - lda m_signed_y + 1 + sta m_position_y + lda m_position_y + 1 sbc #0 - sta m_signed_y + 1 + sta m_position_y + 1 rts .endproc + ;; Translate the position at subpixel level to actual screen coordinates. .proc position_to_coordinates jsr position_to_coordinates_x jsr position_to_coordinates_y - rts .endproc + ;; Translate the X position at subpixel level to actual screen coordinates. .proc position_to_coordinates_x ;; Convert the fixed point position coordinate into screen coordinates - lda m_signed_x + lda m_position_x sta $00 - lda m_signed_x + 1 + lda m_position_x + 1 sta $01 lsr $01 ror $00 @@ -250,7 +243,7 @@ ror $00 ; Assume that everything is fine and save the sprite position lda $00 - sta m_pos_x + sta m_screen_x lda m_velocity_x bmi @position_from_negative_velocity @@ -263,32 +256,33 @@ rts @bound_upper_x: lda #$EF - sta m_pos_x + sta m_screen_x lda #$0E - sta m_signed_x + 1 + sta m_position_x + 1 lda #$F0 - sta m_signed_x + sta m_position_x lda #0 sta m_velocity_x rts @position_from_negative_velocity: - lda m_signed_x + 1 + lda m_position_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_position_x + sta m_position_x + 1 + sta m_screen_x sta m_velocity_x rts .endproc + ;; Translate the Y position at subpixel level to actual screen coordinates. .proc position_to_coordinates_y ;; Convert the fixed point position coordinate into screen coordinates - lda m_signed_y + lda m_position_y sta $00 - lda m_signed_y + 1 + lda m_position_y + 1 sta $01 lsr $01 ror $00 @@ -300,7 +294,7 @@ ror $00 ; Assume that everything is fine and save the sprite position lda $00 - sta m_pos_y + sta m_screen_y lda m_velocity_y bmi @position_from_negative_velocity_y @@ -313,23 +307,23 @@ rts @bound_upper_y: lda #$EF - sta m_pos_y + sta m_screen_y lda #$0E - sta m_signed_y + 1 + sta m_position_y + 1 lda #$F0 - sta m_signed_y + sta m_position_y lda #0 sta m_velocity_y rts @position_from_negative_velocity_y: - lda m_signed_y + 1 + lda m_position_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_position_y + sta m_position_y + 1 + sta m_screen_y sta m_velocity_y rts .endproc @@ -340,16 +334,44 @@ ;; 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 + ;; Update Y position. + lda m_screen_y sta $200 sta $204 - lda m_pos_x + ;; Update X position. + lda m_screen_x sta $203 clc adc #8 sta $207 + ;; If we have a target velocity, then we will show some fire, + ;; otherwise we keep the basic ship. + lda m_target_velocity_x + bne @fire + lda m_target_velocity_y + bne @fire + lda #0 + jmp @sprite_set + @fire: + lda #2 + @sprite_set: + ;; 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 as indexed by the value set on the `a` register + ;; on the pattern table, and then we set for the second one the + ;; horizontal flip bit for the attributes. + 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 .endscope diff --git a/space/src/vectors/nmi.s b/space/src/vectors/nmi.s index 4bc3dde..e049239 100644 --- a/space/src/vectors/nmi.s +++ b/space/src/vectors/nmi.s @@ -1,14 +1,32 @@ -;; Non-Maskable Interrupts handler. +;; See `basics/sprite.s` for more info. I'm not doing anything fancier here. nmi: - ;; As mentioned on the `main` subroutine, rendering will be skipped until - ;; the proper flag is set. bit $20 bpl @next + ;; Save registers. + pha + txa + pha + tya + pha + ;; We can start rendering stuff. OAM_WRITE_SPRITES + ;; Reset scroll. + bit $2002 ; PPUSTATUS + lda #$00 + sta $2005 ; PPUSCROLL + sta $2005 ; PPUSCROLL + ;; And unset the render flag so the `main` code is unblocked. UNSET_RENDER_FLAG + + ;; Restore registers. + pla + tay + pla + tax + pla @next: rti diff --git a/space/src/vectors/reset.s b/space/src/vectors/reset.s index afc9fa5..1216146 100644 --- a/space/src/vectors/reset.s +++ b/space/src/vectors/reset.s @@ -1,39 +1,25 @@ .segment "CODE" +;; Check `basics/sprite.s` for a deeper look on the logic below. I have only +;; added code after configuration/reset is done. reset: - ;; Ignore IRQs and disable decimal mode. sei cld - - ;; Disable APU frame IRQ. ldx #$40 - stx APU::FRAME_COUNTER + stx $4017 - ;; 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. - ;;; + stx $2000 + stx $2001 + stx $4010 - ;; First of the two VBLANK waits. @vblankwait1: - bit PPU::STATUS + bit $2002 bpl @vblankwait1 - ;; Clear RAM memory. Notice that we are not clearing $200-$2ff, this is done - ;; later. ldx #0 lda #0 @ram_reset_loop: @@ -47,30 +33,17 @@ reset: 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 + bit $2002 bpl @vblankwait2 - ;; Reset palettes. - PPU_ADDR $3F00 - - lda #$0F - ldx #$20 -@palettes_reset_loop: - sta PPU::DATA - dex - bne @palettes_reset_loop + ;;; + ;; NOTE: configuration/reset is done, the code below is our actual program :D - ;; Jump into the main subroutine. jmp main |
