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

2D Sprites

Flint’s 2D sprite system provides GPU-instanced flat-quad rendering for side-scrollers, top-down games, and any project that needs sprite-based visuals. It sits alongside the existing 3D billboard sprite pipeline but uses an orthographic camera and XY-plane alignment instead of camera-facing quads.

How It Works

2D sprites render as axis-aligned quads on the XY plane using a dedicated Sprite2dPipeline in flint-render. The pipeline follows the same storage-buffer instancing pattern as the particle system — one draw call per texture batch, quads generated from vertex_index in the shader, no vertex buffers needed.

Scene TOML              CPU collection              GPU rendering
sprite component  ──►  Collect instances   ──►  Sprite2dPipeline
  mode = "sprite2d"    batch by texture           instanced draw
  texture, layer       sort by layer              storage buffer
  tint, flip           pack Sprite2dInstanceGpu    orthographic proj

Key differences from 3D billboard sprites:

BillboardSprite2D
OrientationAlways faces cameraFixed on XY plane
CameraPerspective (3D scene)Orthographic
Z-orderingDepth bufferLayer field (integer)
Depth writeYes (binary alpha discard)No (layer-based sorting)
Use caseItems/NPCs in 3D world2D games

Setting Up a 2D Scene

A 2D scene needs an orthographic camera and sprites set to sprite2d mode:

[scene]
name = "My 2D Game"

[camera]
projection = "orthographic"
ortho_height = 10.0            # visible height in world units
position = [0.0, 0.0, 50.0]   # Z offset doesn't affect ortho rendering
target = [0.0, 0.0, 0.0]

[entities.player]
archetype = "sprite"
[entities.player.transform]
position = [0.0, 2.0, 0.0]
[entities.player.sprite]
mode = "sprite2d"
texture = "player.png"
width = 1.0
height = 2.0
layer = 10
tint = [1.0, 1.0, 1.0, 1.0]

[entities.background]
archetype = "sprite"
[entities.background.transform]
position = [0.0, 0.0, 0.0]
[entities.background.sprite]
mode = "sprite2d"
texture = "sky.png"
width = 20.0
height = 12.0
layer = 0

The layer field controls draw order — higher layers render in front. Unlike depth-buffer sorting, this gives you explicit, deterministic control over what overlaps what.

Sprite Component

The sprite component works for both billboard and sprite2d modes. Set mode = "sprite2d" to switch to flat rendering.

FieldTypeDefaultDescription
texturestring""Sprite texture file (empty = solid color from tint)
widthf321.0World-space width
heightf321.0World-space height
modestring"billboard""billboard" (3D camera-facing) or "sprite2d" (flat XY quad)
layeri320Z-order layer for 2D sorting (higher = in front)
tintvec4[1,1,1,1]RGBA color multiplier
flip_xboolfalseMirror horizontally
flip_yboolfalseMirror vertically
source_rectvec4[0,0,0,0]Source rectangle in pixels [x, y, w, h]; [0,0,0,0] = full texture
anchor_yf320.0Vertical anchor (0 = bottom, 0.5 = center)
framei320Current sprite sheet frame index
frames_xi321Columns in sprite sheet
frames_yi321Rows in sprite sheet
fullbrightbooltrueBypass PBR lighting (always true for 2D)
visiblebooltrueShow/hide the sprite

Texture Atlases

For sprite sheets and texture packing, Flint supports .atlas.toml sidecar files that define named regions within a texture:

# sprites/player_sheet.atlas.toml
[atlas]
texture = "player_sheet.png"

[atlas.regions]
idle_0  = [0,   0, 64, 64]
idle_1  = [64,  0, 64, 64]
run_0   = [0,  64, 64, 64]
run_1   = [64, 64, 64, 64]
run_2   = [128, 64, 64, 64]
run_3   = [192, 64, 64, 64]

Each region is [x, y, width, height] in pixel coordinates. The AtlasRegistry loads all .atlas.toml files from a directory and resolves region names to UV rectangles at runtime.

Sprite Sheet Animation

The flint-animation crate provides frame-based sprite animation through .sprite.toml clip files and the sprite_animator component.

Defining Clips

Animation clips live in .sprite.toml files alongside your scene:

# animations/character.sprite.toml

[animation.idle]
texture = "character_sheet.png"
frame_size = [64, 64]
frames = [
    { col = 0, row = 0, duration_ms = 300 },
    { col = 1, row = 0, duration_ms = 300 },
]
loop_mode = "loop"

[animation.run]
texture = "character_sheet.png"
frame_size = [64, 64]
frames = [
    { col = 0, row = 1, duration_ms = 100 },
    { col = 1, row = 1, duration_ms = 100 },
    { col = 2, row = 1, duration_ms = 100 },
    { col = 3, row = 1, duration_ms = 100 },
]
loop_mode = "loop"

[animation.jump]
texture = "character_sheet.png"
frame_size = [64, 64]
frames = [
    { col = 0, row = 2, duration_ms = 150 },
    { col = 1, row = 2, duration_ms = 200 },
    { col = 2, row = 2, duration_ms = 300 },
]
loop_mode = "once"

Each frame specifies a column and row in the sprite sheet, plus a duration in milliseconds. A single file can contain multiple named clips.

Loop Modes

ModeBehavior
loopRepeats indefinitely from the start
ping_pongPlays forward then backward, repeating
oncePlays once and stops on the last frame

Attaching Animation

Add a sprite_animator component alongside the sprite component:

[entities.player]
archetype = "animated_sprite"
[entities.player.transform]
position = [0.0, 2.0, 0.0]
[entities.player.sprite]
mode = "sprite2d"
texture = "character_sheet.png"
width = 2.0
height = 2.0
[entities.player.sprite_animator]
clip = "idle"
playing = true
speed = 1.0

The animated_sprite archetype bundles transform, sprite (mode = sprite2d), and sprite_animator together.

Sprite Animator Component

FieldTypeDefaultDescription
clipstring""Active animation clip name
playingboolfalseWhether animation is playing
speedf321.0Playback speed multiplier (0–10)

How Playback Works

SpriteAnimSync runs each frame during the animation update:

  1. Sync from world — reads sprite_animator.clip and sprite_animator.playing from the ECS, detects changes
  2. Advance — steps playback time by delta * speed, finds the current frame in the clip
  3. Write back — computes the source rectangle from the frame’s col/row and writes sprite.source_rect back to the ECS

The source rectangle tells the GPU which portion of the sprite sheet to display. This happens automatically — scripts only need to set the clip name and playing state.

Scripting Integration

Control sprite animation from Rhai scripts:

FunctionDescription
sprite_play(entity_id, clip_name)Start playing a clip
sprite_stop(entity_id)Stop playback
sprite_set_speed(entity_id, speed)Set playback speed
sprite_is_playing(entity_id)Check if currently playing

The on_animation_end(clip_name) callback fires when a once clip finishes:

#![allow(unused)]
fn main() {
// Rhai script: character animation controller
fn on_init() {
    let me = self_entity();
    sprite_play(me, "idle");
}

fn on_update() {
    let me = self_entity();
    let vx = 0.0;

    if action_held("move_right") { vx = 1.0; }
    if action_held("move_left")  { vx = -1.0; }

    if vx != 0.0 {
        sprite_play(me, "run");
        set_field(me, "sprite", "flip_x", vx < 0.0);
    } else {
        sprite_play(me, "idle");
    }
}

fn on_animation_end(clip) {
    if clip == "jump" {
        sprite_play(self_entity(), "idle");
    }
}
}

Architecture

  • Sprite2dPipeline (flint-render) — wgpu render pipeline with orthographic projection, storage buffer instancing, per-texture batching
  • Sprite2dInstanceGpu — 80-byte per-instance struct: position, size, UV rect, tint, flip flags
  • TextureAtlas / AtlasRegistry (flint-render) — parses .atlas.toml files, resolves region names to pixel rectangles
  • SpriteAnimClip (flint-animation) — frame sequence with per-frame duration and loop mode
  • SpriteAnimSync (flint-animation) — bridges ECS sprite_animator components to playback state, writes source_rect back each frame

Further Reading

  • Touch Input — mobile-friendly input for 2D games
  • Animation — property tweens and skeletal animation
  • Scripting — full scripting API including sprite animation functions
  • Rendering — the GPU pipeline architecture