aboutsummaryrefslogtreecommitdiff
path: root/space
diff options
context:
space:
mode:
authorMiquel Sabaté Solà <msabate@suse.com>2023-11-12 09:39:05 +0100
committerMiquel Sabaté Solà <mikisabate@gmail.com>2025-02-04 21:02:11 +0100
commitdb9cb2118e7b2575ed3b0f535e50dd85c24df25a (patch)
tree8bfe1a7e48bd85e2e9ab0c650c248a7bed1955b7 /space
parentcb854722b9890c171f7d0600812a7e9a3a2a25c9 (diff)
downloadcode.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>
Diffstat (limited to 'space')
-rw-r--r--space/assets/space.chrbin8192 -> 8192 bytes
-rw-r--r--space/src/space.s30
-rw-r--r--space/src/states/bullets.s181
-rw-r--r--space/src/states/player.s188
-rw-r--r--space/src/vectors/nmi.s24
-rw-r--r--space/src/vectors/reset.s47
6 files changed, 326 insertions, 144 deletions
diff --git a/space/assets/space.chr b/space/assets/space.chr
index ee1f47a..d430508 100644
--- a/space/assets/space.chr
+++ b/space/assets/space.chr
Binary files differ
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