10. Sprite Graphics
« 9. The Picture Processing Unit (PPU) 11. More Assembly: Branching and Loops »
Table of Contents:
Now that we have talked about the PPU at a high level, we are ready to dive into the details of drawing sprites. By the end of this chapter, you will know how to create sprite tiles and draw them to the screen.
Internally, the PPU uses 256 bytes of memory to store sprite information. It takes four bytes of data to describe a sprite, which is where the limit of 64 sprites at a time comes from. Those four bytes of data encode the following information:
- Y position of the top left corner of the sprite (0-255)
- Tile number from the sprite pattern table (0-255)
- Special attribute flags (horizontal/vertical flipping, palette number, etc.)
- X position of the top left corner of the sprite (0-255)
The third byte (attribute flags) uses its eight bits to store multiple pieces of information in a compact format. The eight bits control the following properties of a sprite:
|7||Flips sprite vertically (if "1")|
|6||Flips sprite horizontally (if "1")|
|5||Sprite priority (behind background if "1")|
|1-0||Palette for sprite|
Object Attribute Memory (OAM)
The region in PPU memory where sprite data is stored is called "Object Attribute Memory", or "OAM". This region of memory is different in that there are special MMIO addresses that the CPU can use to update the contents of OAM all at once, at high speed. Being able to update OAM quickly is a necessity for fast-paced games, so that all 64 sprites can move smoothly every frame.
To use this high-speed copying, the CPU needs to have all of the sprite
data ready to go in a contiguous page of memory. (A page
is a block of 256 bytes.) Generally, this "sprite buffer" is placed
in CPU memory addresses
A few things to note about the memory map image here. First, "RAM"
is listed as extending from
$0800. In reality, the entire
$0800 is RAM (2KB); the region from
$0800 is just
the portion of RAM that is not commonly allocated for a specific
purpose. The region from
$2000 in the memory map is empty
space - writes to that region of memory will silently fail,
and reads from that range have undefined behavior. Finally, note
that the entire area from
$FFFF comes from the PRG-ROM
chip on the cartridge, including the six bytes that define
the locations of interrupt handlers.
Within the sprite buffer (and in OAM itself), every four bytes defines one sprite. So, the first eight bytes of the sprite buffer look like this:
|$0200||Y position of sprite 0 (first sprite)|
|$0201||Tile number of sprite 0|
|$0202||Attribute flags for sprite 0|
|$0203||X position of sprite 0|
|$0204||Y position of sprite 1 (second sprite)|
|$0205||Tile number of sprite 1|
|$0206||Attribute flags for sprite 1|
|$0207||X position of sprite 1|
$2003: OAMADDR and
Once we have set up all of the sprite data we want to transfer, we use
two new MMIO addresses in our code to send all of the sprite data
to the PPU.
OAMADDR is used to set where in OAM we
want to write to; for all of our projects (and for most commercial
games), this will always be
$00, the beginning of the
OAMDMA initiates the transfer of an entire
page of memory into OAM. Writing the high byte of a memory address
OAMDMA will transfer that page.
While it might seem like we only need to write to OAM when something has changed, the OAM portion of PPU memory is implemented with "dynamic RAM", which means that it is highly unstable and needs to be continuously refreshed, even if nothing has changed. In practice, this means that we want to write to OAM once per frame of graphics (60 times per second).
Non-Maskable Interrupts (NMI)
Fortunately, the NES has an easy-to-use system for running code once per frame: the Non-Maskable Interrupt (NMI). NMI is one of the three interrupt vectors the 6502 knows how to handle. The NMI event is triggered each time the PPU enters "vblank", which occurs at the end of each frame of graphics. "Vblank" stands for "vertical blank"; there is a similar "Hblank" or "horizontal blank" as well. To understand what these terms mean, we need to look at how the CRT televisions and monitors of the era work.
Until new technologies like plasma and LCD took over in the mid-2000's, most televisions used a technology called "cathode-ray tube" or CRT. CRTs work by shooting a beam of electrons through an "electron gun" to hit the inside of a phosphorescent screen, which absorbs the energy of the electrons and converts it into light. The electron gun sweeps across the screen continuously in horizontal lines from top to bottom, starting at the top-left corner and ending at the bottom-right corner before beginning again. The speed of these sweeps is determined by the video signal the television is designed to display. The NTSC standard used in the US and Japan calls for 60 frames per second, while the competing PAL standard used in Europe uses 50 frames per second.
When the electron gun is resetting itself — either to start a new horizontal line from the left edge or when moving from bottom right to top left to start a new frame — the stream of electrons is temporarily stopped, so as not to inadvertently cause graphical issues. These "blanking periods" are the only times when the display on screen is not changing. "Hblank" occurs at the end of each horizontal line and is incredibly brief, lasting only 10.9 microseconds for NTSC. "Vblank" is comparatively much longer, though still short: about 1250 microseconds, or 0.00125 seconds.
Since Vblank is one of the only times that nothing is being output to the screen, and since Hblank is far too short to do meaningful work, it is common practice to perform most graphical updates during Vblank, i.e. as part of the NMI handler. Currently, the NMI handler in the test project looks like this:
.proc nmi_handler RTI .endproc
RTI, as discussed previously, is the opcode for Return from Interrupt.
Let's update the NMI handler to copy the memory from
into OAM each time it runs:
.proc nmi_handler LDA #$00 STA OAMADDR LDA #$02 STA OAMDMA RTI .endproc
A quick review of the assembly we learned back in
Chapter 5 is in order. On line 2,
we load the literal value zero into the accumulator.
In case you've forgotten: a number given to an instruction like
is, by default, a memory address.
LDA $00 means "load
the accumulator with the value stored at memory address zero".
Adding a "#" tells the assembler that this is a literal value,
not a memory address.
On line 3, we store (write) this zero to the OAMADDR address.
This tells the PPU to prepare for a transfer to OAM starting at byte zero.
Next, we load the literal value two into the accumulator, and write
it to OAMDMA. This tells the PPU to initiate a high-speed transfer
of the 256 bytes from $0200-$02ff into OAM.
In order to use
OAMDMA in our code,
we need to update our constants file to include these new constants.
Here is the updated
PPUCTRL = $2000 PPUMASK = $2001 PPUSTATUS = $2002 PPUADDR = $2006 PPUDATA = $2007 OAMADDR = $2003 OAMDMA = $4014
We now have a reliable and automated way to keep OAM up to date. But where do our sprite graphics come from? As mentioned in the last chapter, the CHR-ROM chip in a cartridge holds two pattern tables, one for sprites and one for backgrounds. We will need to create our own pattern tables to display sprites on screen. This is where NES Lightbox comes in handy.
Using NES Lightbox
Open NES Lightbox. You should see a screen similar to this:
Before we dive into creating sprites, let's take a quick tour of how NES Lightbox is organized. The large area on the left half of the screen is a sort of canvas that can be used to draw backgrounds using tiles from a pattern table. We will not use this area until we start discussing background graphics. The right half of the interface is broadly divided into pattern tables ("Tileset") and palettes.
Within the "Tileset" area, the main element is a display of a pattern table. Just below the pattern table display are toggle switches for "Bank A / Bank B". As mentioned before, generally one pattern table is used for sprites, and the other is used for backgrounds. The A/B switch lets you flip between the two pattern tables. There is also a "Grid" button, which will turn on and off a grid showing the boundaries of each tile in the pattern table. The "Edit" button will open a separate tile editor window once you select a tile in the tileset display.
The "Palettes" area lets you preview how different palette choices will affect your tiles. There are four numbered palettes. Clicking any color will change the display of the sprites in the pattern table to the palette that color is part of. You can change a color by clicking a color in a palette, then choosing a color from the display of all possible colors at the bottom of this section. Hovering over any color in the pop-up will display the hexadecimal value the NES uses for that color, to use in your code.
Changing the first color of any palette in the palettes area will change the background color of the large canvas area as well as the background of the pattern table display.
Making Your Own Tiles
To help you get started, I have created a starter .chr file that features some basic sprites and a full font in the background table. Download graphics.chr and open it in NES Lightbox ("Tilesets" → "Open CHR..." → select graphics.chr).
To edit or create tiles, click on the space in the pattern table display for the tile you wish to alter, then click the "Edit" button. This will open a separate tile-editing window.
To edit a tile (or create new tiles!), select a palette index from the four colors below the tile, then click pixels in the Edit Tile window to set them to that palette index. Note that clicking on different palettes in the main window changes all displayed colors to the colors from that palette. This is extremely useful for testing out what your tiles will look like in the palettes used by your game. The "Grid" button will turn on a dotted-line grid that makes it easier to see where each individual pixel of the tile is. The rotate and flip buttons allow you to easily make large-scale edits. Once you have created a set of tiles to work with, save your work by choosing "Tileset" → "Save CHR As...".
Displaying Sprites In Your Game
Download the full source code for this example: 10-spritegraphics.zip
In order to display our tiles in a game, we first need to make sure
we load the .chr file that contains them. In our last project, we
simply reserved 8,192 bytes of empty space for the CHR-ROM chip.
Now that we have actual tiles, we will load them from a .chr file
directly. Change the
.segment "CHR" section to this:
.segment "CHR" .incbin "graphics.chr"
.incbin, as you might expect, is a new assembler directive
that instructs ca65 to include raw binary data (as opposed to
which is processed by the assembler).
With our tiles in place, it's time to draw something. Here, we will use
the four "spaceship" tiles from graphics.chr, but feel free to use your own
Next, we will need to fill out an entire palette, instead of just setting
the first palette color to green (
$29). We'll extend
20 .proc main 21 ; write a palette 22 LDX PPUSTATUS 23 LDX #$3f 24 STX PPUADDR 25 LDX #$00 26 STX PPUADDR 27 LDA #$29 28 STA PPUDATA 29 LDA #$19 30 STA PPUDATA 31 LDA #$09 32 STA PPUDATA 33 LDA #$0f 34 STA PPUDATA
Notice that we only have to set an address with
each time we write to
PPUDATA, the PPU memory address is
automatically increased by one.
Next, we need to store the data for our sprites. We are going to start by drawing
one sprite, the top-left "corner" of the spaceship. As discussed earlier, we
will store all our sprite information in memory between
and copy it to the PPU using DMA transfer (in our NMI handler). Let's continue
.proc main to copy sprite data into
36 ; write sprite data 37 LDA #$70 38 STA $0200 ; Y-coord of first sprite 39 LDA #$05 40 STA $0201 ; tile number of first sprite 41 LDA #$00 42 STA $0202 ; attributes of first sprite 43 LDA #$80 44 STA $0203 ; X-coord of first sprite
Finally, we need to make one more change to
.proc main. In our
previous examples, after writing palette data, we turn on the screen by
PPUMASK. However, now that we are using the NMI
handler, we need to tell the CPU to generate NMI events. We can do that
by writing to
PPUCTRL, like this:
For full details of the possible options that can be set via
PPUMASK, see the
46 vblankwait: ; wait for another vblank before continuing 47 BIT PPUSTATUS 48 BPL vblankwait 49 50 LDA #%10010000 ; turn on NMIs, sprites use first pattern table 51 STA PPUCTRL 52 LDA #%00011110 ; turn on screen 53 STA PPUMASK
At this point, our code will draw a single sprite (the top-left
of the spaceship) to the screen, at X
$70 (near the middle of the screen).
The sprite data that we wrote to CPU memory between
will be copied to PPU memory once per frame by our NMI handler.
Assembling, linking, and running this code gives the output seen
While this works, our code is very inefficient. To draw all 64 sprites this way, we would need 128 lines of code. To make our sprite code more manageable, we will store the sprite data separately from the code that writes it to memory, and use a loop to iterate over the data. This requires a few new assembly opcodes, which we will learn next chapter.
« 9. The Picture Processing Unit (PPU) 11. More Assembly: Branching and Loops »