This tutorial covers SNES sprites (OBJ layer) including OAM management and animation.
SNES Sprite Basics
- Up to 128 sprites on screen
- Sizes: 8x8, 16x16, 32x32, 64x64 (two sizes per mode)
- 4bpp (16 colors per palette)
- 8 palettes available (palettes 8-15 in CGRAM)
- Stored in OAM (Object Attribute Memory) - 544 bytes
OAM Structure
Each sprite uses 4 bytes in main OAM + 2 bits in high table:
Main OAM (4 bytes per sprite):
| Byte | Content |
| 0 | X position (low 8 bits) |
| 1 | Y position |
| 2 | Tile number (low 8 bits) |
| 3 | Attributes: vhoopppN |
Attributes:
- v = Vertical flip
- h = Horizontal flip
- oo = Priority (0-3)
- ppp = Palette (0-7, maps to CGRAM 128-255)
- N = Tile number bit 8
Using OpenSNES Sprite Functions
Initialize OAM
}
int main(void)
Definition main.c:66
void consoleInit(void)
Initialize SNES hardware.
void setScreenOn(void)
Enable screen display.
Definition console.h:115
#define REG_TM
Main screen designation (W).
Definition registers.h:187
#define TM_OBJ
Definition registers.h:449
OpenSNES common-case master header.
void oamInit(u16 size, u16 name_base)
Initialize the sprite (OAM) system.
#define OAM_DEFAULT_SIZE
Default sprite size mode — small=8×8 / large=16×16.
Definition sprite.h:261
#define OAM_DEFAULT_TILE_BASE
Default sprite tile base — 0 = tiles at VRAM word $0000.
Definition sprite.h:263
Setting a Sprite
oamSet(0, 100, 80, 0, 0, 0, 0);
oamSet(1, 120, 80, 0, 1, 0, 0);
void oamSet(u16 id, u16 x, u16 y, u16 tile, u16 palette, u16 priority, u16 flags)
Set sprite properties.
Updating OAM
while (1) {
}
static s16 player_y
Player Y position in screen coordinates.
Definition main.c:58
static s16 player_x
Player X position in screen coordinates.
Definition main.c:56
void WaitForVBlank(void)
Wait for next VBlank period.
void oamUpdate(void)
Copy OAM buffer to hardware.
Hiding Sprites
void oamHide(u8 id)
Park a sprite off screen.
void oamClear(void)
Clear all sprites.
Re-initialising is not how you hide sprites — oamInit() reconfigures OBJSEL. Call oamHide(id) per sprite, or oamClear() to hide them all.
The name_base argument is a page number, not a VRAM address
oamInit(size, name_base) takes name_base as a page number 0-7, not a VRAM address. Each page is $2000 word addresses (16 KB), so tiles DMA'd to word $4000 need base 2. The value is masked to 3 bits: passing a VRAM address — the natural mistake, since every other VRAM parameter in the SDK takes one — silently yields a wrong base (0x6000 & 7 is 0) and the sprites render whatever tiles sit at word 0, with no diagnostic. Use the OBJ_NAME_BASE(addr) macro to convert, so the intent survives the call:
#define OBJ_NAME_BASE(vram_word_addr)
Convert a VRAM word address into an OBJ name base (0-7).
Definition sprite.h:307
#define OBJ_SIZE8_L16
Sprite size indices (for oamInit, oamInitGfxSet).
Definition sprite.h:49
Loading Sprite Tiles
Sprite tiles go in VRAM (location set by REG_OBJSEL):
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00
};
void load_sprite_tiles(void) {
for (
i = 0;
i < 32;
i += 2) {
}
}
static u8 i
Definition main.c:156
#define REG_VMADDH
VRAM address high (W).
Definition registers.h:124
#define REG_VMAIN
VRAM address increment mode (W).
Definition registers.h:112
#define REG_VMADDL
VRAM address low (W).
Definition registers.h:121
#define REG_VMDATAL
VRAM data write low (W).
Definition registers.h:127
#define REG_OBJSEL
Object (sprite) size and base (W).
Definition registers.h:52
#define REG_VMDATAH
VRAM data write high (W).
Definition registers.h:130
unsigned short u16
16-bit unsigned integer (0 to 65535)
Definition types.h:53
unsigned char u8
8-bit unsigned integer (0 to 255)
Definition types.h:47
static const u8 sprite_tile[32]
Inline 8x8 solid square sprite tile in SNES 4bpp format (32 bytes).
Definition main.c:43
Converting a sheet: -s must match the sprite size
A 16x16 OBJ does not read four consecutive tiles. It reads n, n+1, n+16, n+17 — the second row comes from 16 tiles later, because the hardware treats the sheet as 16 tiles (128 px) wide. Same idea for 32x32 (n, n+1, n+2, n+3, n+16 …) and 64x64.
gfx4snes produces exactly that layout when you tell it the sprite size:
gfx4snes -s 8 -p -i cursor.png # a sheet of 8x8 sprites
gfx4snes -s 16 -p -i hero.png # a sheet of 16x16 sprites
gfx4snes -s 32 -p -i boss.png # a sheet of 32x32 sprites
With -s 16 the converter cuts the image into 16x16 blocks and emits each one as the four tiles the hardware expects, whatever the sheet's width or height.
The trap: converting a sheet of 16x16 frames with -s 8. It succeeds, produces the right number of tiles, and the sprite renders — wrong. The top half is one frame and the bottom half is whatever tile happened to land 16 slots later, which depends on the PNG's width. A sheet 16 tiles wide happens to work; make it 18 tiles wide by adding a frame and every sprite in the game breaks at once, with no diagnostic.
The rule is one line: -s is the sprite's size, not the tile size.
gfx4snes cannot catch the mistake — with -s 8 it has no way to know you meant 16x16 sprites. It does warn when the image is not an exact multiple of the block size, since the last row or column of blocks would then be built from pixels that are not in the image.
Sprite Palettes
Sprite palettes use CGRAM addresses 128-255:
void load_sprite_palette(void) {
}
#define REG_CGADD
CGRAM address (W).
Definition registers.h:154
#define REG_CGDATA
CGRAM data write (W).
Definition registers.h:157
Animation
Frame-based Animation
void update_animation(void) {
anim_timer++;
if (anim_timer >= ANIM_SPEED) {
anim_timer = 0;
anim_frame++;
if (anim_frame >= 4) {
anim_frame = 0;
}
}
}
update_animation();
void oamSetTile(u8 id, u16 tile)
Set sprite tile.
Movement with Animation
void update_player(
u16 pad) {
facing_right = 0;
}
facing_right = 1;
}
}
#define OBJ_FLIPX
Metasprite horizontal flip flag.
Definition sprite.h:524
Sprite Sizes
Configure sprite sizes with REG_OBJSEL:
Example: Two Players
See examples/input/two_players/ for a complete example with two independently controlled sprites.
Performance Tips
- Minimize oamUpdate() calls - Only call once per frame
- Use sprite pooling - Reuse sprite slots instead of creating/destroying
- Check sprite limits - Max 32 sprites per scanline, 128 total
- Batch similar sprites - Group sprites using same tiles/palettes
Next Steps