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(orset(x, y, None)) leaves a hole.tile_sizedefaults to the sheet’s frame size; set it to scale tiles.- Maps are drawn before sprites, so sprites with the same
zare on top.
cargo run --release --example tiles, or MAP=4000 for 16 million tiles.