Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Animated Sprite Sheets

gba::embed::indexed4_sheet<FrameW, FrameH>() turns one sprite-sheet image into frame-packed OBJ tile data at compile time. It is the animation-oriented sibling to Embedding Images: same file formats, same supplier-lambda pattern, but a different output shape tuned for OBJ 1D mapping.

For procedural sprite generation without source image files, see Shapes. For type-level API details, see Animated Sprite Sheet Type Reference.

When to use indexed4_sheet

Use indexed4_sheet when:

  • one source image contains multiple animation frames
  • every frame has the same width and height
  • you want each frame’s tiles laid out contiguously in OBJ VRAM
  • you want compile-time flipbook helpers instead of manual tile math

Use plain indexed4<dedup::none>() when you only need one static sprite frame.

Quick start

#include <cstring>
#include <gba/embed>
#include <gba/video>

static constexpr auto actor = gba::embed::indexed4_sheet<16, 16>([] {
	return std::to_array<unsigned char>({
#embed "actor.png"
	});
});

static constexpr auto walk = actor.ping_pong<0, 3>();

const auto base_tile = gba::tile_index(gba::memory_map(gba::mem_vram_obj));
std::memcpy(gba::memory_map(gba::mem_vram_obj), actor.sprite.data(), actor.sprite.size());

unsigned int frame = walk.frame(tick / 8);

gba::obj_mem[0] = actor.frame_obj(base_tile, frame, 0);

The converter validates at compile time that:

  • the full image width is a multiple of FrameW
  • the full image height is a multiple of FrameH
  • FrameW x FrameH is a valid GBA OBJ size
  • the whole sheet fits a single 15-colour palette plus transparent index 0

What sheet4_result gives you

Member / helperPurpose
paletteShared OBJ palette bank for every frame
spriteFrame-packed 4bpp tile payload ready for OBJ VRAM upload
tile_offset(frame)Tile offset for a frame, useful with manual tile_index management
frame_obj(base, frame, pal)Regular OAM helper for one frame
frame_obj_aff(base, frame, pal)Affine OAM helper for one frame
forward<Start, Count>()Compile-time sequential flipbook
ping_pong<Start, Count>()Compile-time forward-then-reverse flipbook
sequence<"...">()Explicit frame order via string literal
row<R>()Row-scoped flipbook builder for multi-row sheets

How frames are laid out

The important difference from plain indexed4() is tile order. indexed4_sheet() repacks tiles frame-by-frame so the GBA can step through animation frames with simple tile offsets.

Source sheet (2 rows x 4 columns, 16x16 frames)

+----+----+----+----+
| f0 | f1 | f2 | f3 |
+----+----+----+----+
| f4 | f5 | f6 | f7 |
+----+----+----+----+

OBJ tile payload emitted by indexed4_sheet

[f0 tiles][f1 tiles][f2 tiles][f3 tiles][f4 tiles][f5 tiles][f6 tiles][f7 tiles]

That means tile_offset(frame) is simply:

frame * tiles_per_frame

No runtime repacking step is needed.

Flipbook builders

Sequential animation

static constexpr auto idle = actor.forward<0, 4>();

Frames: 0, 1, 2, 3

Ping-pong animation

static constexpr auto walk = actor.ping_pong<0, 4>();

Frames: 0, 1, 2, 3, 2, 1

Explicit frame order

static constexpr auto attack = actor.sequence<"01232100">();

Each character selects a frame index. 0-9 map to frames 0-9, a-z continue from 10 upward, and A-Z map the same way as lowercase.

Row-based sheets

For RPG Maker style character sheets with one direction per row, use row<R>() to scope animations to a single row.

static constexpr auto actor = gba::embed::indexed4_sheet<16, 16>([] {
	return std::to_array<unsigned char>({
#embed "hero_walk.png"
	});
});

static constexpr auto down  = actor.row<0>().ping_pong<0, 3>();
static constexpr auto left  = actor.row<1>().ping_pong<0, 3>();
static constexpr auto right = actor.row<2>().ping_pong<0, 3>();
static constexpr auto up    = actor.row<3>().ping_pong<0, 3>();

Row helpers still produce sheet-global frame indices, so the result plugs directly into frame_obj() and tile_offset().

A practical render loop

#include <algorithm>
#include <cstring>
#include <gba/bios>
#include <gba/embed>
#include <gba/video>

static constexpr auto actor = gba::embed::indexed4_sheet<16, 16>([] {
	return std::to_array<unsigned char>({
#embed "actor.png"
	});
});

static constexpr auto walk = actor.ping_pong<0, 4>();

int main() {
	gba::reg_dispcnt = {
		.video_mode = 0,
		.linear_obj_tilemap = true,
		.enable_obj = true,
	};

	std::copy(actor.palette.begin(), actor.palette.end(), gba::pal_obj_bank[0]);
	std::memcpy(gba::memory_map(gba::mem_vram_obj), actor.sprite.data(), actor.sprite.size());

	unsigned int tick = 0;
	const auto base_tile = gba::tile_index(gba::memory_map(gba::mem_vram_obj));

	while (true) {
		gba::VBlankIntrWait();
		const unsigned int frame = walk.frame(tick / 8);
		auto obj = actor.frame_obj(base_tile, frame, 0);
		obj.x = 112;
		obj.y = 72;
		gba::obj_mem[0] = obj;
		++tick;
	}
}

Palette and colour limits

indexed4_sheet builds one shared 16-entry OBJ palette:

  • palette index 0 stays transparent
  • the whole sheet may use at most 15 opaque colours total
  • unlike background-oriented indexed4(), sheet conversion does not spread tiles across multiple palette banks

That trade-off keeps every frame interchangeable at one base tile and one OBJ palette bank.

Compile-time failure modes

Typical compile-time diagnostics are:

  • frame width or height not divisible into the source image
  • source image not aligned to 8x8 tile boundaries
  • frame dimensions not matching a legal OBJ size
  • more than 15 opaque colours across the whole sheet
  • invalid frame index in forward, ping_pong, sequence, or row

Choosing between the asset paths

WorkflowBest for
ShapesSimple geometric sprites, HUD markers, debug art, zero external assets
Embedding ImagesStatic backgrounds, portraits, logos, and one-frame sprites
indexed4_sheet()Animated sprite sheets with compile-time frame selection