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à --- Makefile | 18 +- README.md | 33 ++- assets/background.nam | Bin 1024 -> 0 bytes assets/basic.chr | Bin 8192 -> 0 bytes assets/old.chr | Bin 8192 -> 0 bytes basics/assets/background.nam | Bin 0 -> 1024 bytes basics/assets/basic.chr | Bin 0 -> 8192 bytes basics/input.s | 170 ++++++++++++ basics/sprite.s | 601 +++++++++++++++++++++++++++++++++++++++++++ examples/input.s | 170 ------------ examples/sprite.s | 577 ----------------------------------------- scroll/README.md | 24 ++ scroll/level.s | 1 + scroll/mmc3.s | 1 + scroll/sprite0.s | 1 + 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 ++++++ 28 files changed, 1574 insertions(+), 766 deletions(-) delete mode 100644 assets/background.nam delete mode 100644 assets/basic.chr delete mode 100644 assets/old.chr create mode 100644 basics/assets/background.nam create mode 100644 basics/assets/basic.chr create mode 100644 basics/input.s create mode 100644 basics/sprite.s delete mode 100644 examples/input.s delete mode 100644 examples/sprite.s create mode 100644 scroll/README.md create mode 100644 scroll/level.s create mode 100644 scroll/mmc3.s create mode 100644 scroll/sprite0.s 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 diff --git a/Makefile b/Makefile index 9500e45..5f1b54e 100644 --- a/Makefile +++ b/Makefile @@ -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/ diff --git a/README.md b/README.md index 7c6bc3b..49eaa04 100644 --- a/README.md +++ b/README.md @@ -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-Ω 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/assets/background.nam deleted file mode 100644 index ea228ff..0000000 Binary files a/assets/background.nam and /dev/null differ diff --git a/assets/basic.chr b/assets/basic.chr deleted file mode 100644 index ee1f47a..0000000 Binary files a/assets/basic.chr and /dev/null differ diff --git a/assets/old.chr b/assets/old.chr deleted file mode 100644 index 3e565da..0000000 Binary files a/assets/old.chr and /dev/null differ diff --git a/basics/assets/background.nam b/basics/assets/background.nam new file mode 100644 index 0000000..ea228ff Binary files /dev/null and b/basics/assets/background.nam differ diff --git a/basics/assets/basic.chr b/basics/assets/basic.chr new file mode 100644 index 0000000..ee1f47a Binary files /dev/null and b/basics/assets/basic.chr differ diff --git a/basics/input.s b/basics/input.s new file mode 100644 index 0000000..dbb9ee6 --- /dev/null +++ b/basics/input.s @@ -0,0 +1,170 @@ +;;; +;; 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 +;; pressed. When running this ROM, watch for the following RAM addresses: +;; +;; - $20: the bitmap of the current status of the controller (notice that since +;; we are constantly polling it and filling it, the value will move constantly). +;; - $21: the previous status of the right arrow. +;; - $42: the counter which is incremented on each press of the right arrow button. +;;; + +;;; +;; You can safely ignore all of this up until the `ReadController` subroutine. +;; This is boilerplate that is explained on the `sprite.s` example. +;;; + +.segment "HEADER" + .byte 'N', 'E', 'S', $1A + .byte $02 + .byte $01 + .byte $00 + .byte $00 + +.segment "VECTORS" + .addr nmi + .addr reset + .addr irq + +.segment "STARTUP" + +.segment "CODE" + +nmi: +irq: + rti + +reset: + sei + cld + ldx #$40 + stx $4017 + + ldx #$ff + txs + + inx + stx $2000 + stx $2001 + stx $4010 + +@vblankwait1: + bit $2002 + bpl @vblankwait1 + + ldx #0 + lda #0 +@ram_reset_loop: + sta $000, x + sta $100, x + sta $200, x + sta $300, x + sta $400, x + sta $500, x + sta $600, x + sta $700, x + inx + bne @ram_reset_loop + +@vblankwait2: + bit $2002 + bpl @vblankwait2 + + jmp main + +.proc ReadController + ;; The status of the eight buttons fits into a single byte. We start the whole + ;; dance by setting the first bit of the position we are storing this info + ;; ($20). This bit will act as a guard in the following code. + lda #1 + sta $20 + + ;; The 4021 chip is the one responsible to bring the input from the controller + ;; into the NES. The console reserves two addresses on the memory for the + ;; controllers: $4016 and $4017 (see + ;; https://www.nesdev.org/wiki/Input_devices). If you write into one of them + ;; first with a #1 and then with a #0, we activate the latch for the + ;; controller, and it will start to send a bit representing the state for each + ;; button upon each read. + ;; + ;; Thus, since we conveniently now have #1 into the 'a' register, we send this + ;; value to the 4021 chip, and we follow it by sending #0. This way we tell + ;; the controller to start to deliver the data. + sta $4016 + lda #0 + sta $4016 + + ;; The status of the buttons will be provided one by one following a specific + ;; order. The algorithm goes as follows: + ;; + ;; 1. Load the bit you get from the 4021 chip into `a`. After performing + ;; this read the controller will send the next one so it's ready for the + ;; next iteration. + ;; 2. Shift the value right so to set the carry flag as its comes (note: + ;; overflowing from the right also sets the carry flag on!). + ;; 3. Rotate one bit left from $20: C <- [$20] <- C. This way, we always get + ;; the result we put on the carry register at the right-most part of the + ;; byte on $20, and we clear the carry flag (the previous left-most bit + ;; moves into the carry register, which is 0 until we reach the one we + ;; planted as a guard). + ;; 4. We jump back into `read_loop` if the carry flag is clear. This is the + ;; case for most of the time until the #1 that we set at the very + ;; beginning as a guard flows into the carry flag as expected from the + ;; `rol` instruction. At this point, we have already read the full byte. +read_loop: + lda $4016 + lsr a + rol $20 + bcc read_loop + + rts +.endproc + +;; The main function will run indefinitely and it will continuously poll from +;; the controller and increment the value on $42 each time the user performs a +;; new press on the right arrow (that is, we want to count new presses on this +;; button, and we don't want to increment this value while the right arrow is +;; being pressed). +.proc main + ;; Initialize the value on $21 (previous state) and on $42 (counter). + lda #0 + sta $42 + sta $21 + +loop: + jsr ReadController + + ;; Was the right arrow being pressed? If that's the case, then jump into the + ;; `pressed` label to compare it with the previous state. + lda #1 + and $20 + bne pressed + + ;; The right arrow was not being pressed. Thus, we need to update the previous + ;; state to #0 before we read the controller again. + lda #0 + sta $21 + jmp loop + +pressed: + ;; Now the right arrow is being pressed, and we have the guarantee that `a = + ;; 1` (because of the `and $20` instruction returning a non-zero result). Now + ;; do the same with the previous state. If it's a non-zero result, then it + ;; means that the previous state was already of pressed. Hence, at this point + ;; we can return into the main loop. If this was not the case, then it's a new + ;; press. + and $21 + bne loop + + ;; It's a new press, set $21 to #1 accordinly and increment the counter on $42. + inc $21 + inc $42 + + ;; There and back again. + jmp loop + + rts +.endproc + +.segment "CHARS" diff --git a/basics/sprite.s b/basics/sprite.s new file mode 100644 index 0000000..a7984de --- /dev/null +++ b/basics/sprite.s @@ -0,0 +1,601 @@ +;;; +;; 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 +;; memory, which are defined with the `.segment` macro and known by the compiler +;; through a linker configuration. You can provide a configuration of your own, +;; but bear in mind that compilers like `cc65` (the one used here, which is the +;; most common) already provide a default configuration for the linker that +;; glues a set of pretty common defined named segments. You can read about this +;; in `cfg/nes.cfg` from inside your cc65 installation. +;;; + +;;; +;; The "HEADER" is the first segment of any iNES binary and it contains basic +;; information about what this "cartridge" requires in order to work. Besides +;; basic identification, it defines some relevant things like PRG and CHR sizes, +;; plus mapping if desired. See https://www.nesdev.org/wiki/NES_2.0#Header for +;; documentation on this, or also: https://www.nesdev.org/neshdr20.txt. +;;; +.segment "HEADER" + ;; The first thing to do is to define the magic "NES\0" string identifier + ;; ($1A is the ASCII that MS-DOS wanted as end of string). Some people write + ;; it in hexadecimal through and through, but cc65 already compiles + ;; characters into their proper hexadecimal values, so there's no need to be + ;; extra cryptic here. + .byte 'N', 'E', 'S', $1A + + ;; The next two bytes define the size of the PRG and CHR ROMs in this order. + ;; More specifically, they define how many 8KB banks are available for PRG + ;; and CHR. Hence, the next two bytes define a 16KB (2x 8KB) of PRG-ROM, and + ;; 8KB of CHR-ROM. + .byte $02 + .byte $01 + + ;; Next we have two bytes for selecting a mapper. This is a huge topic (see + ;; the NesDev wiki for this), but it basically refers to the fact that some + ;; cartridges had specific requirements on how to place their data, or the + ;; amount of it they required, or how they expected their data to be + ;; mirrored in the memory, etc. During the life-time of the NES, and as + ;; developers pushed the boundaries of the NES hardware, more intricate + ;; mappers were used on the hardware of cartridges themselves. Thus, in + ;; these two bytes we are telling the emulator: "hey, act as if this was a + ;; cartridge that used this kind of mapper". + .byte $00 ; Horizontal mirroring (good for vertical scrollers) + .byte $00 ; No mapper nor special-case flags. + + ;; The previous are the mandatory bytes in order to get a "cartridge" going. + ;; After this there are some other bytes you can put into the header, like + ;; if the "cartridge" counts on a battery-backed RAM-mapped section to save + ;; states (e.g. The Legend of Zelda) or rare flags like specifying the + ;; region (NTSC vs PAL), but for now this falls out of my radar :-) + +;;; +;; This is the segment where we tell the processor where to find the code for +;; three important topics: the Non-Maskable Interrupts handler, the Reset +;; handler, and the IRQ handler. If you look at the configuration from `cc65` +;; that was provided on your installation (or if you are pesky enough to create +;; one yourself), you will notice that these vector addresses are placed at the +;; very end of memory ($fffa-ffff). The NES (and emulators) will look at these +;; three last positions in memory to know where to jump for each case. +;;; +.segment "VECTORS" + .addr nmi + .addr reset + .addr irq + +;;; +;; Required by the default linker configuration. Theoretically there should be a +;; semantical difference between this section and "CODE", but as for the linker +;; goes, there is no difference and everything will be put sequentially on the +;; resulting binary. Hence, if you want (and as I do here), you can leave this +;; empty (so to make the default configuration of the linker happy), and put +;; everything into the "CODE" segment. In fact, according to the Famicom Party +;; Book (https://famicom.party/book/04-hardwareoverview/), the "STARTUP" section +;; is only used by C programs compiled down into 6502 assembly, so it might not +;; be even relevant for us (and in fact said book actually removes this segment +;; in its linker configuration down the road). +;;; +.segment "STARTUP" + +.segment "CODE" + +;;; +;; The Reset handler is executed when the whole thing starts (i.e. the user has +;; pressed either the reset or the power on buttons on the NES). Thus, this +;; piece of code is pretty standard and an implementation is even given in the +;; NesDev wiki which is pretty much followed by everyone as I could see. The +;; main idea is to leave the hardware in a known state and then jump into the +;; main game subroutine. +;;; +reset: + ;; We first instruct the NES to disable everything. That is, we don't want + ;; any pesky interrupt to make us jump into the `nmi` section, for example, + ;; before we have configured everything. + + ;; Ignore IRQs and disable decimal mode (the NES 6502 chip, for copyright + ;; issues or legal reasons against MOS that I don't fully know nor care, + ;; does not have decimal mode anyway, but it's considered good practice). + sei + cld + + ;; Disable APU frame IRQ. This is the first instance we see of Memory-Mapped + ;; I/O. This is a core concept in NES programming and, to sum things up, the + ;; memory range $2000-$6000 is reserved to I/O operations, and each address + ;; is reserved to a specific hardware operation. This is because the NES CPU + ;; doesn't directly control the PPU nor other chips. In this case, ranges + ;; $4000-$4017 control the APU (Audio Processing Unit). More precisely, the + ;; $4017 address controls what is called the "Frame counter" from the APU + ;; (https://www.nesdev.org/wiki/APU#Frame_Counter_($4017)). Setting #$40 to + ;; it disables it completely, so we are in a known state. If we were to use + ;; sound, at the end of the reset code we should enable it back. We do *not* + ;; do it here because we don't need it. + ldx #$40 + stx $4017 ; APU Frame Counter + + ;; Set up the stack register with the proper value (the stack will grow in + ;; decreasing order from $01FF -> $0100). + 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. + ;; + ;; The one on $4010 refers again to the APU (as described before), and it + ;; directly controls the DMC. Again, if you wanted sound, you should enable + ;; this back after the whole reset block. + ;; + ;; On the other side, the low addresses of $2000 control the PPU. In + ;; particular, we disable NMIs from the PPU by setting the PPUCTRL address + ;; ($2000) to zero, and we do the same for the PPUMASK ($2001). Don't worry + ;; about them just now, we will go deeper down below. + inx + stx $2000 ; PPUCTRL + stx $2001 ; PPUMASK + stx $4010 ; 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. + ;;; + + ;; At this point, we have to wait for the PPU to stabilize. This is + ;; typically done by checking a flag from the PPUSTATUS address ($2002) and + ;; waiting until the proper value is set by the PPU. Since this wait can + ;; take a while, programmers typically put other initialization code here, + ;; like sprite resetting and such. + ;; + ;; The PPUSTATUS memory address contains general information on the status + ;; of the PPU, and is read-only. Moreover, reading from the PPUSTATUS has a + ;; convenient side-effect: it resets the "address latch" for PPUADDR. We + ;; will see how we can take advantage of this when we use the PPU properly + ;; down the road. + + ;; First of the two waits. +@vblankwait1: + bit $2002 ; PPUSTATUS + bpl @vblankwait1 + + ;; The PPU has at least started, now we have a bunch of cycles for it to + ;; stabilize, which will be properly announced through the PPUSTATUS memory + ;; address. Instead of just waiting for it again, we will take the chance to + ;; initialize more stuff. + + ;; One typical thing to do is to leave the RAM in a known state. That is, we + ;; will set to 0 addresses $0000 - $07FF. Apparently there are some people + ;; who say that doing this is bad because it will hide programming mistakes + ;; (e.g. bad initialization code). So, if you are one of these people, you + ;; can safely remove this loop. Otherwise let's get the RAM clean. That + ;; being said, notice that we are skipping $200-$2ff. This is no mistake as + ;; you will see below. + 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 ; 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). + ;; + ;; "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 + ;; $ef, which will give each "sprite" off-screen Y-coordinates. How any of + ;; this is the case will be shown whenever we deal with loading proper + ;; sprites below. + lda #$ef +@sprite_reset_loop: + sta $200, x + inx + bne @sprite_reset_loop + + ;; And write these resetted "sprites" into the PPU. How does this work? + ;; Well, address $2003 has the OAMADDR. That is, from which position the PPU + ;; should start the DMA process. In our case we set it to the very + ;; beginning. Then, if you write to the OAMDMA memory address ($4014), you + ;; will instruct the PPU to start a DMA process starting at $1XX*N. XX is + ;; 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. + lda #$00 + sta $2003 ; OAMADDR + lda #$02 + sta $4014 ; OAMDMA + + ;; As advertised, the other wait. Once we are out of this loop, we are 100% + ;; guaranteed to have a properly working PPU which is ready to render stuff + ;; into the screen. +@vblankwait2: + bit $2002 ; PPUSTATUS + bpl @vblankwait2 + + ;; And we also reset the palettes. How is this done? Well, the $2006 memory + ;; address is the PPU address (PPUADDR). This address is given byte by byte, + ;; the most significat byte first. Thus, the four lines below store into + ;; $2006 the value $3F00. This is the first address where palettes are + ;; stored. Palettes are the answer from old systems like the NES to: how can + ;; you display this amount of colors on screen without taking too much + ;; 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 pixel, but we rather apply a + ;; palette to a sprite or background tile definition. + lda #$3F + sta $2006 ; PPUADDR + lda #$00 + sta $2006 ; PPUADDR + + ;; After setting the address for the first palette, now we loop #$20 times: + ;; 8 palettes * 4 bytes per palette (1 byte per color) = 32 (#$20 + ;; hexadecimal). At each iteration we will write into $2007 the value #$0F. + ;; This memory address is the PPUDATA, and #$0F corresponds to a black + ;; color. In each iteration, therefore, we are telling the PPU that we want + ;; a black color for all the colors from all palettes. Moreover, whenever + ;; you write into PPUDATA, the memory address for the next store is + ;; incremented by one automatically. This is why we don't have to index the + ;; write or anything like that, but we can simply write to the same memory + ;; address 32 times and it will be performed onto 32 consecutive positions. + lda #$0F + ldx #$20 +@palettes_reset_loop: + sta $2007 ; PPUDATA + dex + bne @palettes_reset_loop + + + ;; At this point everything is clear and with a state we know, now we can + ;; jump into our main subroutine and start loading sprites, palettes, etc.; + ;; and start the game proper. + jmp main + +;;; +;; This is our main subroutine. At this point we can assume that the hardware +;; has already been set to a proper and defined state. So now we can load all +;; the data we need for our game and enter the main game loop. +;;; +.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 init_sprites + + ;; 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: + ;; - 4: background pattern table starts at $1000. The CHR is divided by + ;; two pattern tables, and each table contains 256 8x8 tiles. In my + ;; case, my sprite data is located at the first pattern table (thus + ;; $0000, and that's why the 3rd bit from PPUCTRL is set to zero), + ;; and the background data is on the second pattern table (that + ;; starting at $1000). In the end this all depends on how you want + ;; to structure your CHR file. + ;; - 5: sprite size is 8x16. Set this to 0 if your sprites in the + ;; character file are 8x8. + ;; - 7: allow a NMI at the start of the vertical blanking interval. + ;; That is, whenever the screen has displayed one frame, send us + ;; an interrupt so we can generate the next one (see the code on + ;; `nmi`). + ;; - PPUMASK: we enable 4 bits: + ;; - 1: Show background in leftmost pixels on the screen. + ;; - 2: Show sprites in leftmost pixels on the screen. + ;; - 3: Show background. + ;; - 4: Show sprites. + ;; + ;; As for showing on the leftmost pixels, this might not be a good idea if + ;; the game is a scroller, since the game might flicker depending on how you + ;; manage the camera. This is out of scope. + cli + lda #%10110000 + sta $2000 ; PPUCTRL + lda #%00011110 + sta $2001 ; PPUMASK + +@main_game_loop: + ;;; + ;; NOTE: enter here your game loop logic. + ;;; + + ;; This is a hand-shake between the code on `main` and the code on the + ;; `nmi`. That is, whenever we are done with the game loop logic, we set + ;; this flag to indicate that the rendering is doable. After this, the code + ;; on `main` will be blocked until this flag is unset by the `nmi` code, + ;; which will indicate that it can proceed with another iteration of the + ;; game loop. On the contrary, the `nmi` code will be blocked until this + ;; flag is set, at which point it will start rendering and unset the flag + ;; whenever that is done. + lda #%10000000 + ora $20 + sta $20 +@wait_for_render: + bit $20 + 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 + ;; Remember these four instructions? That's what we also did when resetting + ;; palettes on the reset code. That is, we are preparing the PPU to write + ;; data starting from $3f00. + lda #$3F + sta $2006 ; PPUADDR + lda #$00 + sta $2006 ; PPUADDR + + ;; We are going to copy the eight palettes stored in the `palettes` tag into + ;; PPU. This is done by indexing with the `x` register, which is incremented + ;; until it reaches #$20 (32 in hexadecimal), which corresponds to 8 + ;; palettes * 4 bytes each (just like we did in the reset code). + ldx #0 +@load_palettes_loop: + lda palettes, x + sta $2007 ; PPUDATA + inx + cpx #$20 + bne @load_palettes_loop + rts +palettes: + ;; Let there be palettes! This is the data for all the palettes we have for + ;; our game. Each hexadecimal value is a byte that represents a color (you + ;; can use YY-CHR to quickly take a look at which color represents each + ;; hexadecimal value, or use any of the tables that can be found online, or + ;; even fceux has this with its "Palette editor" under "Tools"). There is, + ;; though, one big catch: the first color from all palettes *must* be the + ;; same. To be more precise, it's not like the NES will explode if you pick + ;; other colors, but the NES *will* assume that the first color from all + ;; palettes is the one found in $3F00 (that is, the first color from the + ;; first palette). In this case, $0F will be this "default color". Moreover, + ;; for this simple game we are not using all palettes, and that's why for + ;; some of them all four bytes are simply zero'ed. + + ;; 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 + +;; WRITE_PPU_DATA is a macro that will write into PPUADDR the given address and +;; into PPUDATA the given byte value. +.macro WRITE_PPU_DATA address, value + bit $2002 ; PPUSTATUS + lda #.HIBYTE(address) + sta $2006 ; PPUADDR + lda #.LOBYTE(address) + sta $2006 ; PPUADDR + lda #value + sta $2007 ; PPUDATA +.endmacro + +;; 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 + ;; The general idea here is that the background defaults to the + ;; "transparent" color, which is the first color on the palette. Then for + ;; the background we only need to store into the PPU those elements which + ;; are not the default thing. Hence, if we want to draw a couple of + ;; background elements in our game in some positions, we have to instruct + ;; the PPU where to put each item and where to find it on the pattern table. + ;; After that, we can change/toggle the palette for a background tile if we + ;; so want it, and we are done with it. + + ;; Anyways, before loading data into the PPU, we always have to check the + ;; PPUSTATUS, that's why the first instruction of each block is a `bit + ;; $2002`. This is a safe-guard so to reset the "address latch". That is, if + ;; instead of writing two consecutive bytes we only wrote one by mistake (or + ;; because we only wanted to write the high byte), it resets back its + ;; internal index. + + bit $2002 ; PPUSTATUS + + ;; Load the first item for the background! Let's make sense of the values. + ;; We first need to set the address that the PPU will use (that is, which + ;; couple of bytes we need to pass to PPUADDR). This can be tricky, but + ;; luckily some tools makes things easier for us. For example, the NEXXT + ;; tool allows users to draw a NES screen and then it gives the offset + ;; address that you can use for each drawn element. This way I got that the + ;; 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 + ;; (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 + sta $2006 ; PPUADDR + lda #$02 + sta $2007 ; PPUDATA + + ;; Now we have to do the same for the rest of the elements. I have gone + ;; through the same process of fetching the offset from a canvas I drew on + ;; NEXXT and here's the result. Instead of repeating te same code over and + ;; over but with slightly different values, I am using this macro I created + ;; to do the same. + 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 + WRITE_PPU_DATA $227A, $02 + + ;; Now everything we wanted to write for the background is there, but we + ;; might want to toggle the colors for some of the elements. You can do this + ;; with palettes, which for background tiles is done via attribute tables. + ;; Each attribute table lives right after each nametable, and it consists of + ;; 1KB of memory in which you can establish the palettes to be used for the + ;; tiles appearing on the background screen as defined on that specific + ;; nametable. Information on this is also given to us by tools like NEXXT. + ;; Shortly, for one of the elements I want to change their palette I was + ;; told that the "attribute offset" was $03CE. Again, added to the base + ;; address, it means that we need to pass $23CE to the PPUADDR. The value + ;; for PPUDATA is a bit more tricky: on the attribute table each 8x8 tile is + ;; broken down into 4 2-sized squares. Each of these 4 squares of the tile + ;; has a palette assigned to it ($00 by default). Thanks to this setup, a + ;; single byte can encode four palettes by addressing each square: + ;; %44332211. The tool also tells us in which of these smaller squares our + ;; element resides. With this info, and since we have two bits available for + ;; each of these smaller squares and, therefore, can encode up to 4 + ;; palettes, it means that we just have to assign either on the range of + ;; 00-11 to the pair of bits representing the smaller square that NEXXT is + ;; telling us that our background element resides in. In this case it's the + ;; first square, so we just need to assign $01 to the bits reserved to this + ;; square if I want to change the palette to 1. This is what we pass as a + ;; value. + WRITE_PPU_DATA $23CE, %00000001 + + rts +.endproc + +;; init_sprites loads all the sprites we want from our game. +.proc init_sprites + ;; This is tied to the number of sprites stored in the `initial_sprite_data` + ;; section. Remember that this can be 64 *maximum*: 64 sprites * 4 bytes per + ;; sprite = 256 bytes; which is the reserved space in memory we have for + ;; sprites: $0200-$02ff. + NUM_SPRITES = 2 + + ;; The loading is quite straight-forward. We just store whatever is on + ;; initial_sprite_data + x into $0200 + x, where x is our index that gets + ;; increased on each iteration. We will stop the loop whenever x reaches + ;; NUM_SPRITES * 4 (each sprite takes 4 bytes, more below). + ldx #$00 +@load_sprites_loop: + lda initial_sprite_data, x + sta $0200, x + inx + cpx #(4 * NUM_SPRITES) + bne @load_sprites_loop + rts +initial_sprite_data: + ;; Each sprite consists of 4 bytes: + ;; 1. The Y position. + ;; 2. The sprite position on the pattern table in hexadecimal. Make sure to + ;; get this position right by using YY-CHR (or whatever program you are + ;; using to manipulate the CHR file). You can get this wrong also + ;; depending if you are using 8x8 or 8x16 tiles mode (check the bit set + ;; on PPUCTRL on the `main` code). + ;; 3. The attributes for the sprite. The first two bits control which + ;; palette is used to draw the sprite (in our case palette $00). Bits 2 + ;; to 4 are not used. Bit 5 sets the sprite behind the background if set + ;; to 1; bit 6 flips the sprite horizontally; and bit 7 flips the sprite + ;; vertically. + ;; 4. The X position. + ;; + ;; In our case, notice that we are using the same sprite (located at $00), + ;; but the second sprite is moved eight pixels right and flipped + ;; horizontally. This is because the sprite located at $00 is just half of + ;; the real "meta-sprite", and so we can build the entire "meta-sprite" by + ;; simply flipping the same part horizontally. This is one of the many + ;; tricks we can use to save space on CHR-ROM. + .byte $B0, $00, %00000000, $7A + .byte $B0, $00, %01000000, $82 +.endproc + +;;; +;; Non-Maskable Interrupts handler. After being enabled by the initialization +;; code, you can count on this code being called at the end of each PPU +;; rendering frame during the Vertical Blanking Interval (VBLANK). This time +;; span is pretty tight (around 2273 CPU cycles), so you better update +;; everything to be rendered before that if you don't want graphical glitches. +;;; +nmi: + ;; As mentioned on the `main` subroutine, rendering will be skipped until + ;; the proper flag is set. + 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. + lda #$00 + sta $2003 ; OAMADDR + 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 + +;;; +;; Interrupt Requests handler. This is triggered by the NES' sound processor +;; (APU) or by some specific cartridge hardware (e.g. something specific to a +;; mapper). In our case we don't have to do anything here, so we just return +;; from the interrupt. +;;; +irq: + rti + +;;; +;; Include into this all the data that needs to go into the CHR ROM. One typical +;; implementation for this is by using the `.incbin` macro, which will blindly +;; copy the bitmap that you have generated through a program such as YY-CHR into +;; the CHR ROM. Note that not all games used the CHR-ROM for storing their +;; assets and instead used the PRG-ROM for that as well (e.g. The Legend of +;; Zelda). There are multiple technical reasons to do this, but this falls out +;; of the scope of this file and my expertise, to be honest. +;;; +.segment "CHARS" + .incbin "assets/basic.chr" diff --git a/examples/input.s b/examples/input.s deleted file mode 100644 index 25b17b0..0000000 --- a/examples/input.s +++ /dev/null @@ -1,170 +0,0 @@ -;; -;; 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 -;; pressed. When running this ROM, watch for the following RAM addresses: -;; -;; - $20: the bitmap of the current status of the controller (notice that since -;; we are constantly polling it and filling it, the value will move constantly). -;; - $21: the previous status of the right arrow. -;; - $42: the counter which is incremented on each press of the right arrow button. -;;; - -;;; -;; You can safely ignore all of this up until the `ReadController` subroutine. -;; This is boilerplate that is explained on the `sprite.s` example. -;;; - -.segment "HEADER" - .byte 'N', 'E', 'S', $1A - .byte $02 - .byte $01 - .byte $00 - .byte $00 - -.segment "VECTORS" - .addr nmi - .addr reset - .addr irq - -.segment "STARTUP" - -.segment "CODE" - -nmi: -irq: - rti - -reset: - sei - cld - ldx #$40 - stx $4017 - - ldx #$ff - txs - - inx - stx $2000 - stx $2001 - stx $4010 - -@vblankwait1: - bit $2002 - bpl @vblankwait1 - - ldx #0 - lda #0 -@ram_reset_loop: - sta $000, x - sta $100, x - sta $200, x - sta $300, x - sta $400, x - sta $500, x - sta $600, x - sta $700, x - inx - bne @ram_reset_loop - -@vblankwait2: - bit $2002 - bpl @vblankwait2 - - jmp main - -.proc ReadController - ;; The status of the eight buttons fits into a single byte. We start the whole - ;; dance by setting the first bit of the position we are storing this info - ;; ($20). This bit will act as a guard in the following code. - lda #1 - sta $20 - - ;; The 4021 chip is the one responsible to bring the input from the controller - ;; into the NES. The console reserves two addresses on the memory for the - ;; controllers: $4016 and $4017 (see - ;; https://www.nesdev.org/wiki/Input_devices). If you write into one of them - ;; first with a #1 and then with a #0, we activate the latch for the - ;; controller, and it will start to send a bit representing the state for each - ;; button upon each read. - ;; - ;; Thus, since we conveniently now have #1 into the 'a' register, we send this - ;; value to the 4021 chip, and we follow it by sending #0. This way we tell - ;; the controller to start to deliver the data. - sta $4016 - lda #0 - sta $4016 - - ;; The status of the buttons will be provided one by one following a specific - ;; order. The algorithm goes as follows: - ;; - ;; 1. Load the bit you get from the 4021 chip into `a`. After performing - ;; this read the controller will send the next one so it's ready for the - ;; next iteration. - ;; 2. Shift the value right so to set the carry flag as its comes (note: - ;; overflowing from the right also sets the carry flag on!). - ;; 3. Rotate one bit left from $20: C <- [$20] <- C. This way, we always get - ;; the result we put on the carry register at the right-most part of the - ;; byte on $20, and we clear the carry flag (the previous left-most bit - ;; moves into the carry register, which is 0 until we reach the one we - ;; planted as a guard). - ;; 4. We jump back into `read_loop` if the carry flag is clear. This is the - ;; case for most of the time until the #1 that we set at the very - ;; beginning as a guard flows into the carry flag as expected from the - ;; `rol` instruction. At this point, we have already read the full byte. -read_loop: - lda $4016 - lsr a - rol $20 - bcc read_loop - - rts -.endproc - -;; The main function will run indefinitely and it will continuously poll from -;; the controller and increment the value on $42 each time the user performs a -;; new press on the right arrow (that is, we want to count new presses on this -;; button, and we don't want to increment this value while the right arrow is -;; being pressed). -.proc main - ;; Initialize the value on $21 (previous state) and on $42 (counter). - lda #0 - sta $42 - sta $21 - -loop: - jsr ReadController - - ;; Was the right arrow being pressed? If that's the case, then jump into the - ;; `pressed` label to compare it with the previous state. - lda #1 - and $20 - bne pressed - - ;; The right arrow was not being pressed. Thus, we need to update the previous - ;; state to #0 before we read the controller again. - lda #0 - sta $21 - jmp loop - -pressed: - ;; Now the right arrow is being pressed, and we have the guarantee that `a = - ;; 1` (because of the `and $20` instruction returning a non-zero result). Now - ;; do the same with the previous state. If it's a non-zero result, then it - ;; means that the previous state was already of pressed. Hence, at this point - ;; we can return into the main loop. If this was not the case, then it's a new - ;; press. - and $21 - bne loop - - ;; It's a new press, set $21 to #1 accordinly and increment the counter on $42. - inc $21 - inc $42 - - ;; There and back again. - jmp loop - - rts -.endproc - -.segment "CHARS" diff --git a/examples/sprite.s b/examples/sprite.s deleted file mode 100644 index f429bb1..0000000 --- a/examples/sprite.s +++ /dev/null @@ -1,577 +0,0 @@ -;;; -;; 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 -;; memory, which are defined with the `.segment` macro and known by the compiler -;; through a linker configuration. You can provide a configuration of your own, -;; but bear in mind that compilers like `cc65` (the one used here, which is the -;; most common) already provide a default configuration for the linker that -;; glues a set of pretty common defined named segments. You can read about this -;; in `cfg/nes.cfg` from inside your cc65 installation. -;;; - -;;; -;; The "HEADER" is the first segment of any iNES binary and it contains basic -;; information about what this "cartridge" requires in order to work. Besides -;; basic identification, it defines some relevant things like PRG and CHR sizes, -;; plus mapping if desired. See https://www.nesdev.org/wiki/NES_2.0#Header for -;; documentation on this, or also: https://www.nesdev.org/neshdr20.txt. -;;; -.segment "HEADER" - ;; The first thing to do is to define the magic "NES\0" string identifier - ;; ($1A is the ASCII that MS-DOS wanted as end of string). Some people write - ;; it in hexadecimal through and through, but cc65 already compiles - ;; characters into their proper hexadecimal values, so there's no need to be - ;; extra cryptic here. - .byte 'N', 'E', 'S', $1A - - ;; The next two bytes define the size of the PRG and CHR ROMs in this order. - ;; More specifically, they define how many 8KB banks are available for PRG - ;; and CHR. Hence, the next two bytes define a 16KB (2x 8KB) of PRG-ROM, and - ;; 8KB of CHR-ROM. - .byte $02 - .byte $01 - - ;; Next we have two bytes for selecting a mapper. This is a huge topic (see - ;; the NesDev wiki for this), but it basically refers to the fact that some - ;; cartridges had specific requirements on how to place their data, or the - ;; amount of it they required, or how they expected their data to be - ;; mirrored in the memory, etc. During the life-time of the NES, and as - ;; developers pushed the boundaries of the NES hardware, more intricate - ;; mappers were used on the hardware of cartridges themselves. Thus, in - ;; these two bytes we are telling the emulator: "hey, act as if this was a - ;; cartridge that used this kind of mapper". - .byte $00 ; Horizontal mirroring (good for vertical scrollers) - .byte $00 ; No mapper nor special-case flags. - - ;; The previous are the mandatory bytes in order to get a "cartridge" going. - ;; After this there are some other bytes you can put into the header, like - ;; if the "cartridge" counts on a battery-backed RAM-mapped section to save - ;; states (e.g. The Legend of Zelda) or rare flags like specifying the - ;; region (NTSC vs PAL), but for now this falls out of my radar :-) - -;;; -;; This is the segment where we tell the processor where to find the code for -;; three important topics: the Non-Maskable Interrupts handler, the Reset -;; handler, and the IRQ handler. If you look at the configuration from `cc65` -;; that was provided on your installation (or if you are pesky enough to create -;; one yourself), you will notice that these vector addresses are placed at the -;; very end of memory ($fffa-ffff). The NES (and emulators) will look at these -;; three last positions in memory to know where to jump for each case. -;;; -.segment "VECTORS" - .addr nmi - .addr reset - .addr irq - -;;; -;; Required by the default linker configuration. Theoretically there should be a -;; semantical difference between this section and "CODE", but as for the linker -;; goes, there is no difference and everything will be put sequentially on the -;; resulting binary. Hence, if you want (and as I do here), you can leave this -;; empty (so to make the default configuration of the linker happy), and put -;; everything into the "CODE" segment. In fact, according to the Famicom Party -;; Book (https://famicom.party/book/04-hardwareoverview/), the "STARTUP" section -;; is only used by C programs compiled down into 6502 assembly, so it might not -;; be even relevant for us (and in fact said book actually removes this segment -;; in its linker configuration down the road). -;;; -.segment "STARTUP" - -.segment "CODE" - -;;; -;; The Reset handler is executed when the whole thing starts (i.e. the user has -;; pressed either the reset or the power on buttons on the NES). Thus, this -;; piece of code is pretty standard and an implementation is even given in the -;; NesDev wiki which is pretty much followed by everyone as I could see. The -;; main idea is to leave the hardware in a known state and then jump into the -;; main game subroutine. -;;; -reset: - ;; We first instruct the NES to disable everything. That is, we don't want - ;; any pesky interrupt to make us jump into the `nmi` section, for example, - ;; before we have configured everything. - - ;; Ignore IRQs and disable decimal mode (the NES 6502 chip, for copyright - ;; issues or legal reasons against MOS that I don't fully know nor care, - ;; does not have decimal mode anyway, but it's considered good practice). - sei - cld - - ;; Disable APU frame IRQ. This is the first instance we see of Memory-Mapped - ;; I/O. This is a core concept in NES programming and, to sum things up, the - ;; memory range $2000-$6000 is reserved to I/O operations, and each address - ;; is reserved to a specific hardware operation. This is because the NES CPU - ;; doesn't directly control the PPU nor other chips. In this case, ranges - ;; $4000-$4017 control the APU (Audio Processing Unit). More precisely, the - ;; $4017 address controls what is called the "Frame counter" from the APU - ;; (https://www.nesdev.org/wiki/APU#Frame_Counter_($4017)). Setting #$40 to - ;; it disables it completely, so we are in a known state. If we were to use - ;; sound, at the end of the reset code we should enable it back. We do *not* - ;; do it here because we don't need it. - ldx #$40 - stx $4017 ; APU Frame Counter - - ;; Set up the stack register with the proper value (the stack will grow in - ;; decreasing order from $01FF -> $0100). - 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. - ;; - ;; The one on $4010 refers again to the APU (as described before), and it - ;; directly controls the DMC. Again, if you wanted sound, you should enable - ;; this back after the whole reset block. - ;; - ;; On the other side, the low addresses of $2000 control the PPU. In - ;; particular, we disable NMIs from the PPU by setting the PPUCTRL address - ;; ($2000) to zero, and we do the same for the PPUMASK ($2001). Don't worry - ;; about them just now, we will go deeper down below. - inx - stx $2000 ; PPUCTRL - stx $2001 ; PPUMASK - stx $4010 ; 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. - ;;; - - ;; At this point, we have to wait for the PPU to stabilize. This is - ;; typically done by checking a flag from the PPUSTATUS address ($2002) and - ;; waiting until the proper value is set by the PPU. Since this wait can - ;; take a while, programmers typically put other initialization code here, - ;; like sprite resetting and such. - ;; - ;; The PPUSTATUS memory address contains general information on the status - ;; of the PPU, and is read-only. Moreover, reading from the PPUSTATUS has a - ;; convenient side-effect: it resets the "address latch" for PPUADDR. We - ;; will see how we can take advantage of this when we use the PPU properly - ;; down the road. - - ;; First of the two waits. -@vblankwait1: - bit $2002 ; PPUSTATUS - bpl @vblankwait1 - - ;; The PPU has at least started, now we have a bunch of cycles for it to - ;; stabilize, which will be properly announced through the PPUSTATUS memory - ;; address. Instead of just waiting for it again, we will take the chance to - ;; initialize more stuff. - - ;; One typical thing to do is to leave the RAM in a known state. That is, we - ;; will set to 0 addresses $0000 - $07FF. Apparently there are some people - ;; who say that doing this is bad because it will hide programming mistakes - ;; (e.g. bad initialization code). So, if you are one of these people, you - ;; can safely remove this loop. Otherwise let's get the RAM clean. That - ;; being said, notice that we are skipping $200-$2ff. This is no mistake as - ;; you will see below. - 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 ; 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). - ;; - ;; "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 - ;; $ef, which will give each "sprite" off-screen Y-coordinates. How any of - ;; this is the case will be shown whenever we deal with loading proper - ;; sprites below. - lda #$ef -@sprite_reset_loop: - sta $200, x - inx - bne @sprite_reset_loop - - ;; And write these resetted "sprites" into the PPU. How does this work? - ;; Well, address $2003 has the OAMADDR. That is, from which position the PPU - ;; should start the DMA process. In our case we set it to the very - ;; beginning. Then, if you write to the OAMDMA memory address ($4014), you - ;; will instruct the PPU to start a DMA process starting at $1XX*N. XX is - ;; 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. - lda #$00 - sta $2003 ; OAMADDR - lda #$02 - sta $4014 ; OAMDMA - - ;; As advertised, the other wait. Once we are out of this loop, we are 100% - ;; guaranteed to have a properly working PPU which is ready to render stuff - ;; into the screen. -@vblankwait2: - bit $2002 ; PPUSTATUS - bpl @vblankwait2 - - ;; And we also reset the palettes. How is this done? Well, the $2006 memory - ;; address is the PPU address (PPUADDR). This address is given byte by byte, - ;; the most significat byte first. Thus, the four lines below store into - ;; $2006 the value $3F00. This is the first address where palettes are - ;; stored. Palettes are the answer from old systems like the NES to: how can - ;; you display this amount of colors on screen without taking too much - ;; 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. - lda #$3F - sta $2006 ; PPUADDR - lda #$00 - sta $2006 ; PPUADDR - - ;; After setting the address for the first palette, now we loop #$20 times: - ;; 8 palettes * 4 bytes per palette (1 byte per color) = 32 (#$20 - ;; hexadecimal). At each iteration we will write into $2007 the value #$0F. - ;; This memory address is the PPUDATA, and #$0F corresponds to a black - ;; color. In each iteration, therefore, we are telling the PPU that we want - ;; a black color for all the colors from all palettes. Moreover, whenever - ;; you write into PPUDATA, the memory address for the next store is - ;; incremented by one automatically. This is why we don't have to index the - ;; write or anything like that, but we can simply write to the same memory - ;; address 32 times and it will be performed onto 32 consecutive positions. - lda #$0F - ldx #$20 -@palettes_reset_loop: - sta $2007 ; PPUDATA - dex - bne @palettes_reset_loop - - - ;; At this point everything is clear and with a state we know, now we can - ;; jump into our main subroutine and start loading sprites, palettes, etc.; - ;; and start the game proper. - jmp main - -;;; -;; This is our main subroutine. At this point we can assume that the hardware -;; has already been set to a proper and defined state. So now we can load all -;; the data we need for our game and enter the main game loop. -;;; -.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 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: - ;; - 4: background pattern table starts at $1000. The CHR is divided by - ;; two pattern tables, and each table contains 256 8x8 tiles. In my - ;; case, my sprite data is located at the first pattern table (thus - ;; $0000, and that's why the 3rd bit from PPUCTRL is set to zero), - ;; and the background data is on the second pattern table (that - ;; starting at $1000). In the end this all depends on how you want - ;; to structure your CHR file. - ;; - 5: sprite size is 8x16. Set this to 0 if your sprites in the - ;; character file are 8x8. - ;; - 7: allow a NMI at the start of the vertical blanking interval. - ;; That is, whenever the screen has displayed one frame, send us - ;; an interrupt so we can generate the next one (see the code on - ;; `nmi`). - ;; - PPUMASK: we enable 4 bits: - ;; - 1: Show background in leftmost pixels on the screen. - ;; - 2: Show sprites in leftmost pixels on the screen. - ;; - 3: Show background. - ;; - 4: Show sprites. - ;; - ;; As for showing on the leftmost pixels, this might not be a good idea if - ;; the game is a scroller, since the game might flicker depending on how you - ;; manage the camera. This is out of scope. - cli - lda #%10110000 - sta $2000 ; PPUCTRL - lda #%00011110 - sta $2001 ; PPUMASK - -@main_game_loop: - ;;; - ;; NOTE: enter here your game loop logic. - ;;; - - ;; This is a hand-shake between the code on `main` and the code on the - ;; `nmi`. That is, whenever we are done with the game loop logic, we set - ;; this flag to indicate that the rendering is doable. After this, the code - ;; on `main` will be blocked until this flag is unset by the `nmi` code, - ;; which will indicate that it can proceed with another iteration of the - ;; game loop. On the contrary, the `nmi` code will be blocked until this - ;; flag is set, at which point it will start rendering and unset the flag - ;; whenever that is done. - lda #%10000000 - ora $20 - sta $20 -@wait_for_render: - bit $20 - 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 - ;; Remember these four instructions? That's what we also did when resetting - ;; palettes on the reset code. That is, we are preparing the PPU to write - ;; data starting from $3f00. - lda #$3F - sta $2006 ; PPUADDR - lda #$00 - sta $2006 ; PPUADDR - - ;; We are going to copy the eight palettes stored in the `palettes` tag into - ;; PPU. This is done by indexing with the `x` register, which is incremented - ;; until it reaches #$20 (32 in hexadecimal), which corresponds to 8 - ;; palettes * 4 bytes each (just like we did in the reset code). - ldx #0 -@load_palettes_loop: - lda palettes, x - sta $2007 ; PPUDATA - inx - cpx #$20 - bne @load_palettes_loop - rts -palettes: - ;; Let there be palettes! This is the data for all the palettes we have for - ;; our game. Each hexadecimal value is a byte that represents a color (you - ;; can use YY-CHR to quickly take a look at which color represents each - ;; hexadecimal value, or use any of the tables that can be found online, or - ;; even fceux has this with its "Palette editor" under "Tools"). There is, - ;; though, one big catch: the first color from all palettes *must* be the - ;; same. To be more precise, it's not like the NES will explode if you pick - ;; other colors, but the NES *will* assume that the first color from all - ;; palettes is the one found in $3F00 (that is, the first color from the - ;; first palette). In this case, $0F will be this "default color". Moreover, - ;; for this simple game we are not using all palettes, and that's why for - ;; some of them all four bytes are simply zero'ed. - - ;; 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 - -;; WRITE_PPU_DATA is a macro that will write into PPUADDR the given address and -;; into PPUDATA the given byte value. -.macro WRITE_PPU_DATA address, value - bit $2002 ; PPUSTATUS - lda #.HIBYTE(address) - sta $2006 ; PPUADDR - lda #.LOBYTE(address) - sta $2006 ; PPUADDR - lda #value - sta $2007 ; PPUDATA -.endmacro - -;; 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 - ;; The general idea here is that the background defaults to the - ;; "transparent" color, which is the first color on the palette. Then for - ;; the background we only need to store into the PPU those elements which - ;; are not the default thing. Hence, if we want to draw a couple of - ;; background elements in our game in some positions, we have to instruct - ;; the PPU where to put each item and where to find it on the pattern table. - ;; After that, we can change/toggle the palette for a background tile if we - ;; so want it, and we are done with it. - - ;; Anyways, before loading data into the PPU, we always have to check the - ;; PPUSTATUS, that's why the first instruction of each block is a `bit - ;; $2002`. This is a safe-guard so to reset the "address latch". That is, if - ;; instead of writing two consecutive bytes we only wrote one by mistake (or - ;; because we only wanted to write the high byte), it resets back its - ;; internal index. - - bit $2002 ; PPUSTATUS - - ;; Load the first item for the background! Let's make sense of the values. - ;; We first need to set the address that the PPU will use (that is, which - ;; couple of bytes we need to pass to PPUADDR). This can be tricky, but - ;; luckily some tools makes things easier for us. For example, the NEXXT - ;; tool allows users to draw a NES screen and then it gives the offset - ;; address that you can use for each drawn element. This way I got that the - ;; 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). - lda #$20 - sta $2006 ; PPUADDR - lda #$C8 - sta $2006 ; PPUADDR - lda #$02 - sta $2007 ; PPUDATA - - ;; Now we have to do the same for the rest of the elements. I have gone - ;; through the same process of fetching the offset from a canvas I drew on - ;; NEXXT and here's the result. Instead of repeating te same code over and - ;; over but with slightly different values, I am using this macro I created - ;; to do the same. - 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 - WRITE_PPU_DATA $227A, $02 - - ;; Now everything we wanted to write for the background is there, but we - ;; might want to toggle the colors for some of the elements. You can do this - ;; with palettes, which for background tiles is done via attribute tables. - ;; Each attribute table lives right after each nametable, and it consists of - ;; 1KB of memory in which you can establish the palettes to be used for the - ;; tiles appearing on the background screen as defined on that specific - ;; nametable. Information on this is also given to us by tools like NEXXT. - ;; Shortly, for one of the elements I want to change their palette I was - ;; told that the "attribute offset" was $03CE. Again, added to the base - ;; address, it means that we need to pass $23CE to the PPUADDR. The value - ;; for PPUDATA is a bit more tricky: on the attribute table each 8x8 tile is - ;; broken down into 4 2-sized squares. Each of these 4 squares of the tile - ;; has a palette assigned to it ($00 by default). Thanks to this setup, a - ;; single byte can encode four palettes by addressing each square: - ;; %44332211. The tool also tells us in which of these smaller squares our - ;; element resides. With this info, and since we have two bits available for - ;; each of these smaller squares and, therefore, can encode up to 4 - ;; palettes, it means that we just have to assign either on the range of - ;; 00-11 to the pair of bits representing the smaller square that NEXXT is - ;; telling us that our background element resides in. In this case it's the - ;; first square, so we just need to assign $01 to the bits reserved to this - ;; square if I want to change the palette to 1. This is what we pass as a - ;; value. - WRITE_PPU_DATA $23CE, %00000001 - - rts -.endproc - -;; init_sprites loads all the sprites we want from our game. -.proc init_sprites - ;; This is tied to the number of sprites stored in the `initial_sprite_data` - ;; section. Remember that this can be 64 *maximum*: 64 sprites * 4 bytes per - ;; sprite = 256 bytes; which is the reserved space in memory we have for - ;; sprites: $0200-$02ff. - NUM_SPRITES = 2 - - ;; The loading is quite straight-forward. We just store whatever is on - ;; initial_sprite_data + x into $0200 + x, where x is our index that gets - ;; increased on each iteration. We will stop the loop whenever x reaches - ;; NUM_SPRITES * 4 (each sprite takes 4 bytes, more below). - ldx #$00 -@load_sprites_loop: - lda initial_sprite_data, x - sta $0200, x - inx - cpx #(4 * NUM_SPRITES) - bne @load_sprites_loop - rts -initial_sprite_data: - ;; Each sprite consists of 4 bytes: - ;; 1. The Y position. - ;; 2. The sprite position on the pattern table in hexadecimal. Make sure to - ;; get this position right by using YY-CHR (or whatever program you are - ;; using to manipulate the CHR file). You can get this wrong also - ;; depending if you are using 8x8 or 8x16 tiles mode (check the bit set - ;; on PPUCTRL on the `main` code). - ;; 3. The attributes for the sprite. The first two bits control which - ;; palette is used to draw the sprite (in our case palette $00). Bits 2 - ;; to 4 are not used. Bit 5 sets the sprite behind the background if set - ;; to 1; bit 6 flips the sprite horizontally; and bit 7 flips the sprite - ;; vertically. - ;; 4. The X position. - ;; - ;; In our case, notice that we are using the same sprite (located at $00), - ;; but the second sprite is moved eight pixels right and flipped - ;; horizontally. This is because the sprite located at $00 is just half of - ;; the real "meta-sprite", and so we can build the entire "meta-sprite" by - ;; simply flipping the same part horizontally. This is one of the many - ;; tricks we can use to save space on CHR-ROM. - .byte $B0, $00, %00000000, $7A - .byte $B0, $00, %01000000, $82 -.endproc - -;;; -;; Non-Maskable Interrupts handler. After being enabled by the initialization -;; code, you can count on this code being called at the end of each PPU -;; rendering frame during the Vertical Blanking Interval (VBLANK). This time -;; span is pretty tight (around 2273 CPU cycles), so you better update -;; everything to be rendered before that if you don't want graphical glitches. -;;; -nmi: - ;; As mentioned on the `main` subroutine, rendering will be skipped until - ;; the proper flag is set. - bit $20 - bpl @next - - ;; 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. - lda #$00 - sta $2003 ; OAMADDR - lda #$02 - sta $4014 ; OAMDMA - - ;; And unset the render flag so the `main` code is unblocked. - lda #%01111111 - and $20 - sta $20 -@next: - rti - -;;; -;; Interrupt Requests handler. This is triggered by the NES' sound processor -;; (APU) or by some specific cartridge hardware (e.g. something specific to a -;; mapper). In our case we don't have to do anything here, so we just return -;; from the interrupt. -;;; -irq: - rti - -;;; -;; Include into this all the data that needs to go into the CHR ROM. One typical -;; implementation for this is by using the `.incbin` macro, which will blindly -;; copy the bitmap that you have generated through a program such as YY-CHR into -;; the CHR ROM. Note that not all games used the CHR-ROM for storing their -;; assets and instead used the PRG-ROM for that as well (e.g. The Legend of -;; Zelda). There are multiple technical reasons to do this, but this falls out -;; of the scope of this file and my expertise, to be honest. -;;; -.segment "CHARS" - .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 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