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