Skip to main content

tilemap_chunk/
tilemap_chunk.rs

1//! Shows a tilemap chunk rendered with a single draw call.
2
3use bevy::{
4    color::palettes::tailwind::RED_400,
5    image::{ImageArrayLayout, ImageLoaderSettings},
6    prelude::*,
7    sprite_render::{TileData, TilemapChunk, TilemapChunkTileData},
8};
9use chacha20::ChaCha8Rng;
10use rand::{RngExt, SeedableRng};
11
12fn main() {
13    App::new()
14        .add_plugins(DefaultPlugins.set(ImagePlugin::default_nearest()))
15        .add_systems(Startup, (setup, spawn_fake_player).chain())
16        .add_systems(Update, (update_tilemap, move_player, log_tile))
17        .run();
18}
19
20#[derive(Component, Deref, DerefMut)]
21struct UpdateTimer(Timer);
22
23#[derive(Resource, Deref, DerefMut)]
24struct SeededRng(ChaCha8Rng);
25
26fn setup(mut commands: Commands, assets: Res<AssetServer>) {
27    // We're seeding the PRNG here to make this example deterministic for testing purposes.
28    // This isn't strictly required in practical use unless you need your app to be deterministic.
29    let mut rng = ChaCha8Rng::seed_from_u64(42);
30
31    let chunk_size = UVec2::splat(64);
32    let tile_display_size = UVec2::splat(8);
33    let tile_data: Vec<Option<TileData>> = (0..chunk_size.element_product())
34        .map(|_| rng.random_range(0..5))
35        .map(|i| {
36            if i == 0 {
37                None
38            } else {
39                Some(TileData::from_tileset_index(i - 1))
40            }
41        })
42        .collect();
43
44    commands.spawn((
45        TilemapChunk {
46            chunk_size,
47            tile_display_size,
48            tileset: assets
49                .load_builder()
50                .with_settings(|settings: &mut ImageLoaderSettings| {
51                    // The tileset texture is expected to be an array of tile textures, so we tell the
52                    // `ImageLoader` that our texture is composed of 4 stacked tile images.
53                    settings.array_layout = Some(ImageArrayLayout::RowCount { rows: 4 });
54                })
55                .load("textures/array_texture.png"),
56            ..default()
57        },
58        TilemapChunkTileData(tile_data),
59        UpdateTimer(Timer::from_seconds(0.1, TimerMode::Repeating)),
60    ));
61
62    commands.spawn(Camera2d);
63
64    commands.insert_resource(SeededRng(rng));
65}
66
67#[derive(Component)]
68struct MovePlayer;
69
70fn spawn_fake_player(
71    mut commands: Commands,
72    mut meshes: ResMut<Assets<Mesh>>,
73    mut materials: ResMut<Assets<ColorMaterial>>,
74    chunk: Single<&TilemapChunk>,
75) {
76    let mut transform = chunk.calculate_tile_transform(UVec2::new(0, 0));
77    transform.translation.z = 1.;
78
79    commands.spawn((
80        Mesh2d(meshes.add(Rectangle::new(8., 8.))),
81        MeshMaterial2d(materials.add(Color::from(RED_400))),
82        transform,
83        MovePlayer,
84    ));
85
86    let mut transform = chunk.calculate_tile_transform(UVec2::new(5, 6));
87    transform.translation.z = 1.;
88
89    // second "player" to visually test a non-zero position
90    commands.spawn((
91        Mesh2d(meshes.add(Rectangle::new(8., 8.))),
92        MeshMaterial2d(materials.add(Color::from(RED_400))),
93        transform,
94    ));
95}
96
97fn move_player(
98    mut player: Single<&mut Transform, With<MovePlayer>>,
99    time: Res<Time>,
100    chunk: Single<&TilemapChunk>,
101) {
102    let t = (ops::sin(time.elapsed_secs()) + 1.) / 2.;
103
104    let origin = chunk
105        .calculate_tile_transform(UVec2::new(0, 0))
106        .translation
107        .x;
108    let destination = chunk
109        .calculate_tile_transform(UVec2::new(63, 0))
110        .translation
111        .x;
112
113    player.translation.x = origin.lerp(destination, t);
114}
115
116fn update_tilemap(
117    time: Res<Time>,
118    mut query: Query<(&mut TilemapChunkTileData, &mut UpdateTimer)>,
119    mut rng: ResMut<SeededRng>,
120) {
121    for (mut tile_data, mut timer) in query.iter_mut() {
122        timer.tick(time.delta());
123
124        if timer.just_finished() {
125            for _ in 0..50 {
126                let index = rng.random_range(0..tile_data.len());
127                tile_data[index] = Some(TileData::from_tileset_index(rng.random_range(0..5)));
128            }
129        }
130    }
131}
132
133// find the data for an arbitrary tile in the chunk and log its data
134fn log_tile(tilemap: Single<(&TilemapChunk, &TilemapChunkTileData)>, mut local: Local<u16>) {
135    let (chunk, data) = tilemap.into_inner();
136    let Some(tile_data) = data.tile_data_from_tile_pos(chunk.chunk_size, UVec2::new(3, 4)) else {
137        return;
138    };
139    // log when the tile changes
140    if tile_data.tileset_index != *local {
141        info!(?tile_data, "tile_data changed");
142        *local = tile_data.tileset_index;
143    }
144}