diff options
| -rw-r--r-- | README.md | 50 | ||||
| -rw-r--r-- | basics/README.md | 107 | ||||
| -rw-r--r-- | basics/flicker.s | 8 | ||||
| -rw-r--r-- | basics/sprite.s | 100 | ||||
| -rw-r--r-- | docs/flicker.gif | bin | 0 -> 658484 bytes | |||
| -rw-r--r-- | docs/input.png | bin | 0 -> 33370 bytes | |||
| -rw-r--r-- | docs/space.gif | bin | 0 -> 304309 bytes | |||
| -rw-r--r-- | docs/sprite.png | bin | 0 -> 22863 bytes | |||
| -rw-r--r-- | docs/unrom.png | bin | 0 -> 37125 bytes | |||
| -rw-r--r-- | space/README.md | 2 |
10 files changed, 184 insertions, 83 deletions
@@ -1,15 +1,16 @@ -<p align="center"> - <a href="https://github.com/mssola/NES/actions?query=workflow%3ACI" title="CI status for the main branch"><img src="https://github.com/mssola/NES/workflows/CI/badge.svg" alt="Build Status for main branch" /></a> -</p> +Examples for the Famicom/NES. These examples are not full blown games nor +standalone projects. Rather, they should be seen as learning material and +experiments. If you want to check out standalone projects that I have written, +check out [this list](#other-projects). ---- +## Build -This repository consists of some examples of programs for the NES. You can build -them by just calling `make` and they will then be available at the `out` -directory. Before doing that, though, you will need a compiler for the 6052 -platform. A good option is [cc65](https://github.com/cc65/cc65), which is -available on all major platforms. Otherwise, if you want to use another -compiler, you can pass the `CC65` and `CCOPTS` variables to the Makefile. +You can build everything by just calling `make` and binaries will then be +available in the `out` directory. Before doing that, though, you will need a +compiler for the 6052 platform. A good option is +[cc65](https://github.com/cc65/cc65), which is available on all major platforms. +Otherwise, if you want to use another compiler, you can pass the `CC65` and +`CCOPTS` variables to the Makefile. 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 @@ -19,33 +20,28 @@ NES memory. A safe bet is to go with either [Mesen](https://github.com/SourMesen/Mesen2/), which provide tools like RAM watchers or a full debugger. -Notice also that I will use some terms without actually introducing them. That -is, I expect you to go over the [NES Dev -wiki](https://www.nesdev.org/wiki/Nesdev_Wiki) for glossary or for full -documentation on the stuff being shown here. For example, if I am writing an -example code using the UNROM or the MMC1 chips, I assume that you will go over -the [NES Dev wiki](https://www.nesdev.org/wiki/Nesdev_Wiki) for details on what -these chips actually are or how they were used. That is, if you find that on a -bunch of comments I write stuff like "memory mapper", "MMC3 chip", "OAM" or -stuff like that, just go to the [NES Dev -wiki](https://www.nesdev.org/wiki/Nesdev_Wiki) to get a better picture. +## Examples The examples are distributed like this: - `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. + description for each example is covered by an initial comment on each file, + and you can also find more info in the [basics/README.md](./basics/README.md) + file. - `space`: example in which you can move a spaceship with subpixel movement and - shoot bullets. Consider this an evolution from the `basics/sprite.s` example. - That is, we are no longer just showing a sprite, but we make it move and - perform an action like shooting bullets. + shoot bullets. Consider this an evolution from the `basics/sprite.s` and + `basics/input.s` examples. That is, we are no longer just showing a sprite, + but we make it move and perform an action like shooting bullets. - `scroll`: different scrolling tactics. Read the [scroll/README.md](./scroll/README.md) file for more info. - `fx`: miscellanous effects that can be achieved with this humble machine. -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. +## Other projects + +- Full games: [jetpac.nes](https://github.com/mssola/jetpac.nes). +- Libraries: [list.nes](https://github.com/mssola/list.nes). +- Misc: [Advent of Code 2023](https://github.com/mssola/aoc2023.nes). ## License diff --git a/basics/README.md b/basics/README.md index 84d43a6..3ffb285 100644 --- a/basics/README.md +++ b/basics/README.md @@ -1,4 +1,4 @@ -## Basics +## Showing a sprite and reading from a controller Here we have simple programs which try to explain a particular topic of NES programming. They are single files which already contain all you need in order @@ -6,22 +6,97 @@ to have a working program. This means that there is quite a lot of boilerplate you might not be aware. For this reason I encourage you to start with the `sprite.s` file, which has a step by step explanation on how the NES is initialized and what do all these magic "HEADER" and other jargon actually mean. -Thus, for absolute beginners go to: +Thus, read the file thoroughly, and when you build the example you will see the +following: -- `sprite.s`: detailed explanation on how to initialize the NES in order to have - some background and a sprite shown on screen. Follow along the code in order - to get a detailed explanation on each section. -- `input.s`: how to read the input from one controller. + -After this, you should be good to go for full examples like the one on the -`space` directory, in which we take the example on `sprite.s` and make it move -and shoot bullets depending on the given input. +Now that you know how to show a sprite on screen, let's read a controller. This +is demonstrated with the `input.s` file, but note that a safer and more +comprehensive version of reading from a controller is shown in +[shared/joypad.s](../shared/joypad.s). Read the `input.s` file, but note that +nothing useful will be displayed on screen. Out of simplicity, you are required +to just look at a RAM value. If you do so, just watch on the `$42` memory +address, which contains how many times the right button has been pressed. This +is an example with FCEUX showing a run which has pressed the right button three +times: -Whenever you are done with that, you can then move into other topics like: + -- `flicker.s`: flickers sprites which are aligned so they don't disappear due to - NES horizontal sprites limit. -- `persist.s`: using the MMC1 chip in order to persist data. -- `unrom.s`: bank switching using the UNROM chip. -- `chr-ram.s`: how to show background and sprites via CHR-RAM instead of - CHR-ROM. This is a combination of `sprite.s` and `unrom.s`. +We could put these two examples together, and that's the role of the +[space/](../space/) directory. + +## Flickering sprites + +The NES had a limitation which developers found out pretty quickly: there can +only be 8 sprites on a single scanline. If this is not considered and more than +eight sprites are on a given scanline, those "extra" sprites are going to be +skipped from rendering. This is of course bad, and because of this most games +implemented some logic to avoid it: either by placing sprites in clever ways, or +using background elements as if they were sprites, or, more famously, by +applying a flicker effect. + +A dumb flicker effect on the NES is pretty easy to achieve, it's a matter of +mindlessly re-ordering sprites as found on the +[OAM](https://www.nesdev.org/wiki/PPU_OAM). The PPU will respect this order and +thus each sprite will be shown most of the times except for a few frames. To the +human eye this looks like flickering images, and it's a technique which is used +a lot on NES games. A dumb and conservative approach has been implemented in +`flicker.s`, which looks like this: + + + +A more clever way to implement this flickering effect stems from being more +aware about the priorities of your game: + +1. Which sprites can never be "flickered"? Such as in this example, it might be + a good idea to leave the main character out of this flickering technique for + the player's convenience. +2. Is there a way to lay out the OAM memory in a way in which we always get the + proper results without having to re-arrange objects on the OAM? Imagine a + very simple game such as [jetpac.nes](https://github.com/mssola/jetpac.nes), + in which we know in advance the slots for enemies, bonuses, etc. + +## Showing a sprite through CHR-RAM instead of CHR-ROM + +Rendering a sprite onto the screen is easy as shown on the `sprite.s` example, +but loading sprites from ROM space might be in some cases not in our interest. +This is a huge topic, covered on the [NESdev +wiki](https://www.nesdev.org/wiki/CHR_ROM_vs._CHR_RAM). That is, some games, for +a handful of very specific reasons, might want to load sprites first from ROM +into RAM, and then only rely on RAM for manipulating and showing sprites on +screen. + +The [UNROM](https://www.nesdev.org/wiki/UxROM) chip is possibly one of the first +to ever implement this technique. Moreover, this is also the first chip to ever +implement "bank switching". This is a way to map way more memory than the +original hardware allowed, in which cartridges brought a multiplexer which +selected one memory chip or another depending on the "bank" being selected. So, +to begin with this, I have added the `unrom.s` example, which shows a very basic +way to setup the UNROM chip, and how bank switching can be performed. The end +result for this example has to be shown from the RAM viewer. For example, on +FCEUX we can see: + + + +These are the expected values as described on `unrom.s`. After that, we already +have the foundations for `chr-ram.s`, which will use one of the banks for the +assets, and then upon initialization move it to RAM. The result is the same as +with `sprite.s`, but the goal of this example is to appreciate the technique and +understand when it's useful. + +## Persisting memory + +Another area explored here is a way to persist data across runs. This was +probably to be achieved via the Famicom Disk System, but once that was scrapped +outside of Japan, a replacement had to be delivered for games built for the Disk +System like 'The Legend of Zelda'. This solution came first in the form of the +MMC1, the first memory mapper to support a memory region where persistence was +guaranteed via a battery included into the cartridge itself. + +Again, there's not much to show other than RAM data, but at least FCEUX allows +us to retrieve back the data that was explicitely persisted. That is, after you +run this example, simply check on `$HOME/.fceux/sav/` (or at least that's on +Linux). Hence, just run `hexdump -C $HOME/.fceux/sav/persist.sav`, and that will +print the bytes stored starting from `$6000`. The first two bytes is data +explicitely changed by `persist.s`. diff --git a/basics/flicker.s b/basics/flicker.s index 640817a..34fc312 100644 --- a/basics/flicker.s +++ b/basics/flicker.s @@ -34,7 +34,7 @@ sta $2001 ; PPUMASK @main_game_loop: - ;; NOTE: the logic is pretty simply: read the pad, move the player + ;; NOTE: the logic is pretty simple: read the pad, move the player ;; accordingly, and apply the flickering effect. jsr joypad_read jsr Diskun::update @@ -121,9 +121,9 @@ adc #4 sta $40 - ;; We know that that last byte from the last sprite is held at $24F. Thus, - ;; if the sprite pointer is already passed this point, we can break the - ;; loop. Otherwise just carry on. + ;; We know that the last byte from the last sprite is held at $24F. Thus, if + ;; the sprite pointer is already passed this point, we can break the loop. + ;; Otherwise just carry on. cmp #$50 bne @loop diff --git a/basics/sprite.s b/basics/sprite.s index 4e9c573..34f9dba 100644 --- a/basics/sprite.s +++ b/basics/sprite.s @@ -396,6 +396,12 @@ palettes: ;; init_nametable loads the relevant data to the nametable that is then going to ;; be used in order to build up the background. +;; +;; NOTE: this function is called after the PPU has been initialized, but NMIs +;; are still disabled. This is important because you **cannot** write into PPU +;; data outside of VBlank space (during `nmi` code). If you do so it will result +;; into rendering glitches. This is better explained at the `scroll` examples, +;; where VRAM buffering techniques are applied and explained. .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 @@ -416,21 +422,38 @@ palettes: 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). + ;; couple of bytes we need to pass to PPUADDR). That is: which tile index is + ;; to be used from the nametable (read more about PPU Nametables here: + ;; https://www.nesdev.org/wiki/PPU_nametables). Think of this as the screen + ;; being set as a grid of 8x8 pixels (tile), and each of these tiles is + ;; indexed in PPU memory: 960 bytes of actual data, and 64 bytes for the + ;; attribute table (see more on attribute tables below). You can draw this + ;; by hand by using resources like Morhpcat's developer spreadsheet + ;; (https://ko-fi.com/s/ad5d7601e0), based on the ones being used at + ;; Nintendo. This is good but there are also handy tools that make things + ;; easier for us. For example the NEXXT tool allows users to draw a full NES + ;; screen and then it gives the offset address that you can use for each + ;; drawn element. I did this and I got that the offset for the first element + ;; was $0C8, which added to the base address of $2000 (start of the first + ;; nametable), gave me the address $20C8. If you want to make more sense of + ;; it by just reading the number, notice that each row has exactly 32 tiles. + ;; Thus, you can divide 0x0C8 by 32 and you will get 6.25: that is, on the + ;; vertical axis it's the row '6'. Then subtract 0xC8 by 192 (6 rows of 32 + ;; tiles) to get '8' as the value on the horizontal axis. As you can see, + ;; doing things manually can be tedious, that's why people either use a dev + ;; spreadsheet or a computer assisted tool. + ;; + ;; Anyways, if I want this background element to be rendered in the position + ;; I envisioned on this tool, I need to write $20C8 into PPUADDR, which adds + ;; for 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). You can check that by + ;; opening the `.chr` file for this example on YY-CHR. lda #$20 sta $2006 ; PPUADDR lda #$C8 @@ -453,26 +476,25 @@ palettes: ;; 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. + ;; Each attribute table lives right after each nametable, 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 @@ -529,6 +551,11 @@ initial_sprite_data: ;; 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. +;; +;; NOTE: NMI code can be more complex as we want to do more complex stuff, but +;; we ought to keep things as simple and fast as possible. You can take a look +;; at the examples from `scroll` for more complex NMI code that have to handle +;; stuff like VRAM buffering or setting other PPU registers. ;;; nmi: ;; As mentioned on the `main` subroutine, rendering will be skipped until @@ -595,8 +622,9 @@ irq: ;; 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. +;; Zelda). Take a look at the `basics/chr-ram.s` example for more. There are +;; multiple technical reasons to do this, but this falls out of the scope of +;; this example. ;;; .segment "CHARS" .incbin "../assets/basic.chr" diff --git a/docs/flicker.gif b/docs/flicker.gif Binary files differnew file mode 100644 index 0000000..e55cd42 --- /dev/null +++ b/docs/flicker.gif diff --git a/docs/input.png b/docs/input.png Binary files differnew file mode 100644 index 0000000..b99ad8a --- /dev/null +++ b/docs/input.png diff --git a/docs/space.gif b/docs/space.gif Binary files differnew file mode 100644 index 0000000..3bfb5de --- /dev/null +++ b/docs/space.gif diff --git a/docs/sprite.png b/docs/sprite.png Binary files differnew file mode 100644 index 0000000..7413075 --- /dev/null +++ b/docs/sprite.png diff --git a/docs/unrom.png b/docs/unrom.png Binary files differnew file mode 100644 index 0000000..f0fb3a4 --- /dev/null +++ b/docs/unrom.png diff --git a/space/README.md b/space/README.md index 8a0b451..cf59034 100644 --- a/space/README.md +++ b/space/README.md @@ -8,3 +8,5 @@ The movement is done by taking into consideration subpixels. For this, I have taken most of the code/idea from [NES Hacker](https://github.com/NesHacker/PlatformerMovement). Thus, this part is mostly attributed to him. + + |
