bevy/examples/2d/tilemap_chunk.rs
Conner Petzold 3f187cf752
Add TilemapChunk rendering (#18866)
# Objective

An attempt to start building a base for first-party tilemaps (#13782).

The objective is to create a very simple tilemap chunk rendering plugin
that can be used as a building block for 3rd-party tilemap crates, and
eventually a first-party tilemap implementation.

## Solution

- Introduces two user-facing components, `TilemapChunk` and
`TilemapChunkIndices`, and a new material `TilemapChunkMaterial`.
- `TilemapChunk` holds the chunk and tile sizes, and the tileset image
- The tileset image is expected to be a layered image for use with
`texture_2d_array`, with the assumption that atlases or multiple images
would go through an asset loader/processor. Not sure if that should be
part of this PR or not..
- `TilemapChunkIndices` holds a 1d representation of all of the tile's
Option<u32> index into the tileset image.
- Indices are fixed to the size of tiles in a chunk (though maybe this
should just be an assertion instead?)
  - Indices are cloned and sent to the shader through a u32 texture.

## Testing

- Initial testing done with the `tilemap_chunk` example, though I need
to include some way to update indices as part of it.
- Tested wasm with webgl2 and webgpu
- I'm thinking it would probably be good to do some basic perf testing.

---

## Showcase

```rust
let chunk_size = UVec2::splat(64);
let tile_size = UVec2::splat(16);
let indices: Vec<Option<u32>> = (0..chunk_size.x * chunk_size.y)
    .map(|_| rng.gen_range(0..5))
    .map(|i| if i == 0 { None } else { Some(i - 1) })
    .collect();

commands.spawn((
    TilemapChunk {
        chunk_size,
        tile_size,
        tileset,
    },
    TilemapChunkIndices(indices),
));
```

![Screenshot 2025-04-17 at 11 54
56 PM](https://github.com/user-attachments/assets/850a53c1-16fc-405d-aad2-8ef5a0060fea)
2025-06-23 23:55:10 +00:00

71 lines
2.1 KiB
Rust

//! Shows a tilemap chunk rendered with a single draw call.
use bevy::{
prelude::*,
sprite::{TilemapChunk, TilemapChunkIndices},
};
use rand::{Rng, SeedableRng};
use rand_chacha::ChaCha8Rng;
fn main() {
App::new()
.add_plugins((DefaultPlugins.set(ImagePlugin::default_nearest()),))
.add_systems(Startup, setup)
.add_systems(Update, (update_tileset_image, update_tilemap))
.run();
}
#[derive(Component, Deref, DerefMut)]
struct UpdateTimer(Timer);
fn setup(mut commands: Commands, assets: Res<AssetServer>) {
let mut rng = ChaCha8Rng::seed_from_u64(42);
let chunk_size = UVec2::splat(64);
let tile_display_size = UVec2::splat(8);
let indices: Vec<Option<u16>> = (0..chunk_size.element_product())
.map(|_| rng.gen_range(0..5))
.map(|i| if i == 0 { None } else { Some(i - 1) })
.collect();
commands.spawn((
TilemapChunk {
chunk_size,
tile_display_size,
tileset: assets.load("textures/array_texture.png"),
..default()
},
TilemapChunkIndices(indices),
UpdateTimer(Timer::from_seconds(0.1, TimerMode::Repeating)),
));
commands.spawn(Camera2d);
}
fn update_tileset_image(
chunk_query: Single<&TilemapChunk>,
mut events: EventReader<AssetEvent<Image>>,
mut images: ResMut<Assets<Image>>,
) {
let chunk = *chunk_query;
for event in events.read() {
if event.is_loaded_with_dependencies(chunk.tileset.id()) {
let image = images.get_mut(&chunk.tileset).unwrap();
image.reinterpret_stacked_2d_as_array(4);
}
}
}
fn update_tilemap(time: Res<Time>, mut query: Query<(&mut TilemapChunkIndices, &mut UpdateTimer)>) {
for (mut indices, mut timer) in query.iter_mut() {
timer.tick(time.delta());
if timer.just_finished() {
let mut rng = ChaCha8Rng::from_entropy();
for _ in 0..50 {
let index = rng.gen_range(0..indices.len());
indices[index] = Some(rng.gen_range(0..5));
}
}
}
}