diff options
| author | Miquel Sabaté Solà <msabate@suse.com> | 2023-10-24 17:40:19 +0200 |
|---|---|---|
| committer | Miquel Sabaté Solà <mikisabate@gmail.com> | 2025-02-04 20:58:36 +0100 |
| commit | 149020464517ff3d021eef5ccd50d3939ad463e6 (patch) | |
| tree | 056ec1919fe0c0d515e7a0a414916307b768ef84 | |
| parent | be2414f5b11b182998fca7e87712ff0502c8ee93 (diff) | |
| download | code.nes-149020464517ff3d021eef5ccd50d3939ad463e6.tar.gz code.nes-149020464517ff3d021eef5ccd50d3939ad463e6.zip | |
Lay out a proper structure for the project
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à <msabate@suse.com>
| -rw-r--r-- | Makefile | 18 | ||||
| -rw-r--r-- | README.md | 33 | ||||
| -rw-r--r-- | basics/assets/background.nam (renamed from assets/background.nam) | bin | 1024 -> 1024 bytes | |||
| -rw-r--r-- | basics/assets/basic.chr (renamed from assets/basic.chr) | bin | 8192 -> 8192 bytes | |||
| -rw-r--r-- | basics/input.s (renamed from examples/input.s) | 2 | ||||
| -rw-r--r-- | basics/sprite.s (renamed from examples/sprite.s) | 62 | ||||
| -rw-r--r-- | scroll/README.md | 24 | ||||
| -rw-r--r-- | scroll/level.s | 1 | ||||
| -rw-r--r-- | scroll/mmc3.s | 1 | ||||
| -rw-r--r-- | scroll/sprite0.s | 1 | ||||
| -rw-r--r-- | space/Makefile | 15 | ||||
| -rw-r--r-- | space/assets/background.nam | bin | 0 -> 1024 bytes | |||
| -rw-r--r-- | space/assets/space.chr (renamed from assets/old.chr) | bin | 8192 -> 8192 bytes | |||
| -rw-r--r-- | space/include/apu.s | 6 | ||||
| -rw-r--r-- | space/include/joypad.s | 63 | ||||
| -rw-r--r-- | space/include/oam.s | 13 | ||||
| -rw-r--r-- | space/include/ppu.s | 27 | ||||
| -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 |
23 files changed, 847 insertions, 39 deletions
@@ -1,23 +1,27 @@ CC65 ?= cl65 CCOPTS ?= --verbose --target nes -## -# General targets. - .PHONY: all all: clean deps build .PHONY: clean clean: @rm -rf out - @mkdir out + @mkdir -p out/basics .PHONY: deps deps: @which $(CC65) >/dev/null 2>/dev/null || (echo "ERROR: $(CC65) not found." && false) .PHONY: build -build: out/input.nes out/sprite.nes +build: basics space + +.PHONY: basics +basics: + $(CC65) $(CCOPTS) basics/sprite.s -o out/basics/sprite.nes + $(CC65) $(CCOPTS) basics/input.s -o out/basics/input.nes -out/%.nes: examples/%.s - $(CC65) $(CCOPTS) $< -o $@ +.PHONY: space +space: + @cd space && CC65=$(CC65) CCOPTS="$(CCOPTS)" $(MAKE) + @mv space/space.nes out/ @@ -15,23 +15,32 @@ way, the resulting ROMs will be placed in the `out` directory. After that, it's recommended that you run the ROMs with an emulator with debugging support or at least some form of memory visualization. This is because some examples have nothing to show for other than updating some values on the -NES memory. A safe bet is to go with [fceux](https://fceux.com/web/home.html), -which works on all major platforms and provides tools like RAM watchers or a -full debugger. +NES memory. A safe bet is to go with either +[fceux](https://fceux.com/web/home.html) or +[Mesen](https://github.com/SourMesen/Mesen2/), which provide tools like RAM +watchers or a full debugger. -All that being said, for now I have written the following examples: +The examples are distributed like this: -- `examples/input.s`: it reads the input from the first controller and it - increments a counter stored in memory for each press of the right arrow - button. -- `examples/sprites`: shows a sprite with a background on screen. This is a - detailed explanation on how all this magic can happen, with a very basic - example and all those "magic" numbers explained. +- `basics`: simple examples which cover basic stuff for NES development. These + examples are self-contained and supposed to be read by absolute newcomers. The + description for each example is covered by an initial comment on each file. +- `space`: example in which you can move a spaceship with subpixel movement and + shoot bullets. +- `scroll`: different scrolling tactics. Read the + [scroll/README.md](./scroll/README.md) file for more info. + +Other than that, I have also written complete games. Take a look at +[jetpac.nes](https://github.com/mssola/jetpac.nes), which is simple enough so a +newcomer can read it, but complex enough so it's an actual game. ## License Released under the [GPLv3+](http://www.gnu.org/licenses/gpl-3.0.txt), Copyright (C) 2023-<i>Ω</i> Miquel Sabaté Solà. -There are some files which I have taken from other people (e.g. -`examples/wrapper.s`). Take a look at each file for the specifics. +I have taken lots of ideas from different developers and open source projects. +Most notably, I have taken lots of notes from the [Famicom Party +Book](https://famicom.party/book/), +[NESHacker](https://www.youtube.com/c/neshacker) and, of course, from the +awesome [NES Dev wiki](https://www.nesdev.org/wiki/Nesdev_Wiki). diff --git a/assets/background.nam b/basics/assets/background.nam Binary files differindex ea228ff..ea228ff 100644 --- a/assets/background.nam +++ b/basics/assets/background.nam diff --git a/assets/basic.chr b/basics/assets/basic.chr Binary files differindex ee1f47a..ee1f47a 100644 --- a/assets/basic.chr +++ b/basics/assets/basic.chr diff --git a/examples/input.s b/basics/input.s index 25b17b0..dbb9ee6 100644 --- a/examples/input.s +++ b/basics/input.s @@ -1,4 +1,4 @@ -;; +;;; ;; This example shows how to read from one controller and set it into the $20 ;; memory address. The `Main` subroutine will call the `ReadController` ;; subroutine and then increment the value on $42 if the right arrow was diff --git a/examples/sprite.s b/basics/sprite.s index f429bb1..a7984de 100644 --- a/examples/sprite.s +++ b/basics/sprite.s @@ -1,4 +1,8 @@ ;;; +;; Show a sprite to the screen! This example also contains fully detailed +;; explanations on each section on how an NES game is initialized and stored. + +;;; ;; The iNES is the de facto standard for the distribution of NES binary programs ;; and it's compatible with the format used by NES cartridges themselves (used, ;; even, by the Wii Virtual Console). The layout is composed by segments in @@ -230,8 +234,8 @@ reset: ;; memory? The NES allows developers to store eight palettes (four ;; background, four foreground), and each palette group four colors. This ;; way, whenever we want to draw a sprite or a piece of background, we don't - ;; specify which colors to pick for each pixels, but we rather apply a - ;; palette to a srite or background tile definition. + ;; specify which colors to pick for each pixel, but we rather apply a + ;; palette to a sprite or background tile definition. lda #$3F sta $2006 ; PPUADDR lda #$00 @@ -272,17 +276,6 @@ reset: jsr init_nametable jsr init_sprites - ;; Reset scroll. This could have been done in the reset process, but - ;; scrolling feels like something that should be in control entirely by the - ;; game itself. Any ways, the scroll is handled through the $2005 - ;; (PPUSCROLL) memory address. We first need to write the X coordinate and - ;; then the Y coordinate for the camera. Since we are not planning on doing - ;; anything fancy with scrolling, we just initialize it to a zero position. - bit $2002 ; PPUSTATUS - lda #$00 - sta $2005 ; PPUSCROLL - sta $2005 ; PPUSCROLL - ;; Remember when we disabled rendering and NMI on the reset code? Now it's ;; time to enable them back. Things to note: ;; - PPUCTRL: we enable three bits: @@ -430,11 +423,13 @@ palettes: ;; offset for the first element was $00C8, which added to the base address ;; of $2000 (start of the first nametable), gives us the address $20C8. ;; Therefore, if I want this background element to be rendered in the - ;; position I envisioned on this tool, I need to write $20C8 into PPUADDR. - ;; As for the PPUDATA address, I need to pass #$02 because that's the index - ;; inside of the CHR file of the star in the second pattern table (check the - ;; PPUCTRL setting at the end of the `main` function on why it's the second - ;; pattern table). + ;; position I envisioned on this tool, I need to write $20C8 into PPUADDR + ;; (NOTE: these are a total of two bytes to be loaded, for operations that + ;; only support one byte at a time. Hence, we have to load byte by byte (in + ;; little-endian format) and store them. As for the PPUDATA address, I need + ;; to pass #$02 because that's the index inside of the CHR file of the star + ;; in the second pattern table (check the PPUCTRL setting at the end of the + ;; `main` function on why it's the second pattern table). lda #$20 sta $2006 ; PPUADDR lda #$C8 @@ -540,6 +535,17 @@ nmi: bit $20 bpl @next + ;; An NMI can happen at any time. Hopefully whenever that happens we are + ;; already done with the main code, so replacing the current value of + ;; registers isn't that big of a deal, but it's considered good practice to + ;; not assume that (e.g. a particular frame being too laggy). Because of + ;; this, we backup registers now and we restore them at the end. + pha + txa + pha + tya + pha + ;; We are instructed that we can start rendering stuff. Transfer the sprites ;; via OAM. This is the same we did when we resetted sprites in our `reset` ;; code. @@ -548,10 +554,28 @@ nmi: lda #$02 sta $4014 ; OAMDMA + ;; Reset the scroll. This is needed because we have touched the PPUADDR in + ;; multiple places. Touching the PPUADDR memory address will also toggle the + ;; PPUSCROLL one because they share a register on hardware. Because of this, + ;; we always need to reset the scroll back to the coordinates we want, and + ;; we do it right here, when everything has already been sent and we are + ;; done. + bit $2002 ; PPUSTATUS + lda #$00 + sta $2005 ; PPUSCROLL + sta $2005 ; PPUSCROLL + ;; And unset the render flag so the `main` code is unblocked. lda #%01111111 and $20 sta $20 + + ;; Restore registers. + pla + tay + pla + tax + pla @next: rti @@ -574,4 +598,4 @@ irq: ;; of the scope of this file and my expertise, to be honest. ;;; .segment "CHARS" - .incbin "../assets/basic.chr" + .incbin "assets/basic.chr" diff --git a/scroll/README.md b/scroll/README.md new file mode 100644 index 0000000..553a497 --- /dev/null +++ b/scroll/README.md @@ -0,0 +1,24 @@ +## Scrolling + +Scrolling is a big topic and it's something that evolved with the NES hardware. +This set of examples try to cover it as much as possible while being +approachable as single files. + +First of all, you should take a look at `level.s`, which shows how games can +scroll a level and continuously load/unload the next/previous sections of the +level. + +The second example is `sprite0.s`, which covers the scrolling done by games such +as Super Mario Bros. or Punch-out. That is, we use the "sprite 0 hit" detection +to keep the top level part of the screen from moving (so to show relevant +information), while allowing the rest of the screen to scroll as expected. In +the end, it's the same example as `level.s` (same level to scroll), but the top +part does not move and shows a "THIS DOES NOT MOVE" message. + +Last but not least, the `mmc3.s` example shows how to configure the MMC3 chip +(e.g. Super Mario Bros. 3, Kirby's Adventure) to have better control on which +parts of the screen to scroll or not. To showcase this the example implements a +Pong game by scrolling the paddles instead of directly setting their positions. +This is probably the most stupid way to use this hardware expansion (kind of +like killing a fly with a cannon), but it at least shows a simple way to have +two independent scrolls while having a static middle ground. diff --git a/scroll/level.s b/scroll/level.s new file mode 100644 index 0000000..9480a4a --- /dev/null +++ b/scroll/level.s @@ -0,0 +1 @@ +;; TODO: Scroll everything and show how to load the level diff --git a/scroll/mmc3.s b/scroll/mmc3.s new file mode 100644 index 0000000..3f97a52 --- /dev/null +++ b/scroll/mmc3.s @@ -0,0 +1 @@ +;; TODO: Through MMC3 diff --git a/scroll/sprite0.s b/scroll/sprite0.s new file mode 100644 index 0000000..215a491 --- /dev/null +++ b/scroll/sprite0.s @@ -0,0 +1 @@ +;; TODO: Through sprite0 hit 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 Binary files differnew file mode 100644 index 0000000..ea228ff --- /dev/null +++ b/space/assets/background.nam diff --git a/assets/old.chr b/space/assets/space.chr Binary files differindex 3e565da..ee1f47a 100644 --- a/assets/old.chr +++ b/space/assets/space.chr 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 |
