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

Sprite sheets, animation and tilemaps

Sprite sheets

A sprite sheet is one texture holding many frames of the same size: an animation strip, a tileset, a set of icons. Frames are numbered left to right, top to bottom.

#![allow(unused)]
fn main() {
let tex = ctx.load_texture("units.png");
let sheet = SpriteSheet::new(tex, 32, 32);   // 32 × 32 frames
ctx.world.spawn((Transform2D::at(pos), sheet.sprite(5))); // frame 5
}
    auto tex = ctx.load_texture("units.png");
    auto sheet = crabby::sheet_of(tex, 32, 32);   // 32 x 32 frames
    world.spawn(crabby::transform_at(pos), crabby::sprite_of(sheet, 5));   // frame 5

Under the hood a sprite has a region, the part of its texture to show in texture pixels. sheet.sprite(i) is Sprite::new(tex).with_region(sheet.frame(i)), and ctx.draw_ex takes the same region in DrawParams.

For pixel art, turn smoothing off so pixels stay square:

#![allow(unused)]
fn main() {
ctx.set_texture_filter(&tex, TextureFilter::Nearest);
}
    ctx.set_texture_filter(tex, crabby::texture_filter::nearest);

Animation

Add an Animation next to the Sprite; the built-in animate_sprites system (on by default, all cores) advances it every frame:

#![allow(unused)]
fn main() {
ctx.world.spawn((
    Transform2D::at(pos),
    sheet.sprite(0),
    Animation::new(sheet, 0..8, 12.0), // frames 0-7 at 12 fps, looping
));
let boom = Animation::new(sheet, 8..16, 24.0).once(); // stops on the last frame
}
    world.spawn(crabby::transform_at(pos), crabby::sprite_of(sheet, 0),
                crabby::animation_of(sheet, 0, 8, 12));   // frames 0-7 at 12 fps
    auto boom = crabby::animation_of(sheet, 8, 8, 24);
    boom.looping = false;   // stops on the last frame; crabby::finished(boom) says when

animation.finished() tells you when a once() animation is done, so you can despawn the explosion. In C++, crabby::animation is a plain struct, so an each can take crabby::animation& like crabby::transform2d&.

Tilemaps

A TileMap is a grid of sheet frames on an entity with a Transform2D (its position is the map’s top-left corner). The built-in draw_tilemaps system draws only the tiles the camera can see, so a map can be huge: the tiles example has a million tiles, and zoomed out to 380 000 visible tiles it still runs at over 500 FPS.

#![allow(unused)]
fn main() {
fn setup(ctx: &mut Ctx, side: u32) {
    let tiles = tileset(ctx);
    let mut map = TileMap::new(tiles, side, side);
    for y in 0..side {
        for x in 0..side {
            map.set(x, y, Some(terrain(x as f32, y as f32)));
        }
    }
    ctx.world.spawn((Transform2D::default(), map));

    let blob = creature(ctx);
    let mut rng = Rng::new(7);
    let world = (side * TILE) as f32;
    ctx.world.spawn_batch((0..20_000).map(|_| {
        let at = vec2(rng.range(0.0, world), rng.range(0.0, world));
        let mut walk = Animation::new(blob, 0..8, rng.range(6.0, 12.0));
        walk.time = rng.f32(); // out of step with each other
        (
            Transform2D::at(at),
            blob.sprite(0),
            walk,
            Wander(rng.unit_vec2() * 20.0),
        )
    }));
    ctx.camera.zoom = 2.0;
    ctx.camera.look_at(vec2(world, world) / 2.0);
}
}
static crabby::entity make_map(crabby::world_view world, crabby::sprite_sheet tiles, std::uint32_t side) {
    const crabby::entity map = world.spawn(crabby::transform_at({0, 0}));
    auto t = world.add_tilemap(map, tiles, side, side);   // the transform is its top-left corner
    for (std::uint32_t y = 0; y < side; ++y)
        for (std::uint32_t x = 0; x < side; ++x)
            t.set(x, y, (x * 7 + y * 3) % 5 == 0 ? STONE : GRASS);
    return map;
}

Changing tiles is writing into the map. In C++, world.tilemap(e) gives the engine’s tiles directly (set, get, tiles() for bulk writes), with no copy:

#![allow(unused)]
fn main() {
fn paint(ctx: &mut Ctx) {
    if !ctx.mouse_down(MouseButton::Left) || ctx.mouse_over_ui() {
        return;
    }
    let at = ctx.mouse_world();
    ctx.world.query::<(&Transform2D, &mut TileMap)>().for_each(|(t, map)| {
        if let Some(tile) = map.tile_at(t.position, at) {
            map.set(tile.x, tile.y, Some(STONE));
        }
    });
}
}
static void paint(crabby::ctx& ctx, crabby::entity map) {
    if (!ctx.mouse_down(CRABBY_MOUSE_LEFT) || ctx.mouse_over_ui()) return;
    auto world = ctx.world();
    const crabby::vec2 origin = world.get<crabby::transform2d>(map).position;
    auto tiles = world.tilemap(map);
    if (auto tile = tiles.tile_at(origin, ctx.mouse_world())) tiles.set((*tile)[0], (*tile)[1], STONE);
}
  • TileMap::EMPTY (or set(x, y, None)) leaves a hole.
  • tile_size defaults to the sheet’s frame size; set it to scale tiles.
  • Maps are drawn before sprites, so sprites with the same z are on top.

cargo run --release --example tiles, or MAP=4000 for 16 million tiles.