Skip to main content

tiles/
tiles.rs

1//! A big tilemap with animated sprites on it.
2//!
3//!   `cargo run --release --example tiles`
4//!   `MAP=4000 cargo run --release --example tiles` (16 million tiles)
5//!
6//! - Right or middle mouse drag: pan. Scroll: zoom.
7//! - Left mouse: paint stone tiles.
8//!
9//! The tileset and the creature's animation frames are generated in code,
10//! so the example needs no image files. Only tiles the camera can see are
11//! drawn, so the map size barely affects the frame time.
12
13use ponko::*;
14
15const TILE: u32 = 16;
16/// Tile frames in the generated tileset.
17const WATER: u16 = 0;
18const SAND: u16 = 1;
19const GRASS: u16 = 2;
20const FOREST: u16 = 3;
21const STONE: u16 = 4;
22
23/// A 5-tile strip: water, sand, grass, forest, stone, with some texture.
24fn tileset(ctx: &mut Ctx) -> SpriteSheet {
25    let colors: [[u8; 3]; 5] = [
26        [38, 84, 160],
27        [222, 196, 132],
28        [86, 160, 70],
29        [40, 102, 48],
30        [128, 128, 136],
31    ];
32    let (w, h) = (TILE * colors.len() as u32, TILE);
33    let mut rng = Rng::new(1);
34    let mut rgba = Vec::with_capacity((w * h * 4) as usize);
35    for _y in 0..h {
36        for x in 0..w {
37            let c = colors[(x / TILE) as usize];
38            let shade = 0.88 + rng.f32() * 0.24;
39            rgba.extend(c.iter().map(|&v| (v as f32 * shade).min(255.0) as u8));
40            rgba.push(255);
41        }
42    }
43    let tex = ctx.create_texture(w, h, &rgba);
44    ctx.set_texture_filter(&tex, TextureFilter::Nearest);
45    SpriteSheet::new(tex, TILE, TILE)
46}
47
48/// 8 frames of a blob that squashes and stretches.
49fn creature(ctx: &mut Ctx) -> SpriteSheet {
50    let (frames, size) = (8u32, 16u32);
51    let mut rgba = vec![0u8; (frames * size * size * 4) as usize];
52    for f in 0..frames {
53        let phase = f as f32 / frames as f32 * std::f32::consts::TAU;
54        let (rx, ry) = (5.0 + phase.sin() * 1.5, 5.0 - phase.sin() * 1.5);
55        for y in 0..size {
56            for x in 0..size {
57                let d = vec2((x as f32 + 0.5 - 8.0) / rx, (y as f32 + 0.5 - 9.0) / ry);
58                if d.length() <= 1.0 {
59                    let i = ((y * frames * size + f * size + x) * 4) as usize;
60                    rgba[i..i + 4].copy_from_slice(&[240, 110, 80, 255]);
61                }
62            }
63        }
64    }
65    let tex = ctx.create_texture(frames * size, size, &rgba);
66    ctx.set_texture_filter(&tex, TextureFilter::Nearest);
67    SpriteSheet::new(tex, size, size)
68}
69
70/// Smooth-ish terrain from a few sine waves: no noise library needed.
71fn terrain(x: f32, y: f32) -> u16 {
72    let h = (x * 0.031).sin()
73        + (y * 0.027).cos()
74        + (x * 0.011 + y * 0.013).sin() * 1.5
75        + (x * 0.093 - y * 0.071).sin() * 0.3;
76    match h {
77        h if h < -0.6 => WATER,
78        h if h < -0.35 => SAND,
79        h if h < 0.8 => GRASS,
80        h if h < 1.6 => FOREST,
81        _ => STONE,
82    }
83}
84
85struct Wander(Vec2);
86
87// ANCHOR: setup
88fn setup(ctx: &mut Ctx, side: u32) {
89    let tiles = tileset(ctx);
90    let mut map = TileMap::new(tiles, side, side);
91    for y in 0..side {
92        for x in 0..side {
93            map.set(x, y, Some(terrain(x as f32, y as f32)));
94        }
95    }
96    ctx.world.spawn((Transform2D::default(), map));
97
98    let blob = creature(ctx);
99    let mut rng = Rng::new(7);
100    let world = (side * TILE) as f32;
101    ctx.world.spawn_batch((0..20_000).map(|_| {
102        let at = vec2(rng.range(0.0, world), rng.range(0.0, world));
103        let mut walk = Animation::new(blob, 0..8, rng.range(6.0, 12.0));
104        walk.time = rng.f32(); // out of step with each other
105        (
106            Transform2D::at(at),
107            blob.sprite(0),
108            walk,
109            Wander(rng.unit_vec2() * 20.0),
110        )
111    }));
112    ctx.camera.zoom = 2.0;
113    ctx.camera.look_at(vec2(world, world) / 2.0);
114}
115// ANCHOR_END: setup
116
117fn wander(ctx: &mut Ctx) {
118    let dt = ctx.dt();
119    ctx.world
120        .query::<(&mut Transform2D, &Wander)>()
121        .par_for_each(|(t, w)| t.position += w.0 * dt);
122}
123
124// ANCHOR: paint
125fn paint(ctx: &mut Ctx) {
126    if !ctx.mouse_down(MouseButton::Left) || ctx.mouse_over_ui() {
127        return;
128    }
129    let at = ctx.mouse_world();
130    ctx.world.query::<(&Transform2D, &mut TileMap)>().for_each(|(t, map)| {
131        if let Some(tile) = map.tile_at(t.position, at) {
132            map.set(tile.x, tile.y, Some(STONE));
133        }
134    });
135}
136// ANCHOR_END: paint
137
138fn hud(ctx: &mut Ctx) {
139    let r = ctx.render_stats();
140    let text = format!(
141        "{} sprites drawn ({} draw calls)\nzoom {:.2}x, {:.0} FPS\nleft mouse: paint, right drag: pan, wheel: zoom",
142        r.sprites,
143        r.draw_calls,
144        ctx.camera.zoom,
145        ctx.fps()
146    );
147    let size = ctx.measure_text(&text, ctx.ui_style.text_size) + 2.0 * ctx.ui_style.padding;
148    let panel = Rect::new(12.0, 12.0, size.x, size.y);
149    ctx.ui_panel(panel);
150    ctx.ui_label(panel.min + ctx.ui_style.padding, &text);
151}
152
153fn main() {
154    let side = std::env::var("MAP").ok().and_then(|v| v.parse().ok()).unwrap_or(1000);
155    Game::new("Crabby Engine: tiles", 1280, 720)
156        .vsync(false)
157        .system(camera_controls)
158        .system(wander)
159        .system(paint)
160        .system(hud)
161        .run(move |ctx| setup(ctx, side));
162}