Files
bevy/examples/2d/tilemap_chunk.rs
andriyDev 61127f6d01 Replace all different load variants in AssetServer with a builder. (#23663)
# Objective

- In 0.18, we had 10 different functions that load assets (I'm not even
counting `load_folder`).
- In 0.19, we've even added `load_erased` - but it unfortunately doesn't
support all the features that the other variants support.
- We apparently needed `load_acquire_with_settings_override` which 1)
loads the asset, 2) uses the settings provided, 3) allows reading
unapproved asset paths, and 4) drops a guard once the load completes.
- That's fine if that's necessary. But we needed to create an explicit
variant for that.
- We need fewer load paths!

## Solution

- Create a builder.
- Store all these options dynamically instead of statically handling
each case.
- Have the caller choose a particular "kind" of load when they are
ready: `load`, `load_erased`, `load_untyped`, or `load_untyped_async`.
- I intentionally didn't provide a `load_async` or `load_erased_async`,
since those can be replicated using `load`/`load_erased` +
`AssetServer::wait_for_asset_id` to get the exact same effect.

I am also intentionally leaving `NestedLoader` untouched in this PR, but
a followup will duplicate this API for `NestedLoader`, which should make
it easier to understand.

Unlike the `NestedLoader` API, we aren't doing any type-state craziness,
so the docs are much more clear: users don't need to understand how
type-state stuff works, they just call the handful of methods on the
type. The "cost" here is we now need to be careful about including the
cross product of loads between static asset type, runtime asset type, or
dynamic asset type, crossed with deferred or async. In theory, if we
added more kinds on either side, we would need to expand this cross
product a lot. In practice though, it seems unlikely there will be any
more variants there. (maybe there could be a blocking variant? I don't
think this is a popular opinion though).

A big con here is some somewhat common calls are now more verbose.
Specifically, `asset_server.load_with_settings()` has become
`asset_server.load_builder().with_settings().load()`. I am not really
concerned about this though, since it really isn't that painful.

## Testing

- Tests all pass!

---

## Showcase

Now instead of:

```rust
asset_server.load_acquire_with_settings_override("some_path", |settings: &mut GltfLoaderSettings| { ... }, my_lock_guard);
```

You can instead do:

```rust
asset_server.load_builder()
    .with_guard(my_lock_guard)
    .with_settings(|settings: &mut GltfLoaderSettings| { ... })
    .override_unapproved()
    .load("some_path");
```

We also now cover more variants! For example, you can now load an asset
untyped with a guard, or with override_unapproved, etc.

---------

Co-authored-by: Alice Cecile <alice.i.cecile@gmail.com>
2026-04-15 18:32:25 +00:00

145 lines
4.4 KiB
Rust

//! Shows a tilemap chunk rendered with a single draw call.
use bevy::{
color::palettes::tailwind::RED_400,
image::{ImageArrayLayout, ImageLoaderSettings},
prelude::*,
sprite_render::{TileData, TilemapChunk, TilemapChunkTileData},
};
use chacha20::ChaCha8Rng;
use rand::{RngExt, SeedableRng};
fn main() {
App::new()
.add_plugins(DefaultPlugins.set(ImagePlugin::default_nearest()))
.add_systems(Startup, (setup, spawn_fake_player).chain())
.add_systems(Update, (update_tilemap, move_player, log_tile))
.run();
}
#[derive(Component, Deref, DerefMut)]
struct UpdateTimer(Timer);
#[derive(Resource, Deref, DerefMut)]
struct SeededRng(ChaCha8Rng);
fn setup(mut commands: Commands, assets: Res<AssetServer>) {
// We're seeding the PRNG here to make this example deterministic for testing purposes.
// This isn't strictly required in practical use unless you need your app to be deterministic.
let mut rng = ChaCha8Rng::seed_from_u64(42);
let chunk_size = UVec2::splat(64);
let tile_display_size = UVec2::splat(8);
let tile_data: Vec<Option<TileData>> = (0..chunk_size.element_product())
.map(|_| rng.random_range(0..5))
.map(|i| {
if i == 0 {
None
} else {
Some(TileData::from_tileset_index(i - 1))
}
})
.collect();
commands.spawn((
TilemapChunk {
chunk_size,
tile_display_size,
tileset: assets
.load_builder()
.with_settings(|settings: &mut ImageLoaderSettings| {
// The tileset texture is expected to be an array of tile textures, so we tell the
// `ImageLoader` that our texture is composed of 4 stacked tile images.
settings.array_layout = Some(ImageArrayLayout::RowCount { rows: 4 });
})
.load("textures/array_texture.png"),
..default()
},
TilemapChunkTileData(tile_data),
UpdateTimer(Timer::from_seconds(0.1, TimerMode::Repeating)),
));
commands.spawn(Camera2d);
commands.insert_resource(SeededRng(rng));
}
#[derive(Component)]
struct MovePlayer;
fn spawn_fake_player(
mut commands: Commands,
mut meshes: ResMut<Assets<Mesh>>,
mut materials: ResMut<Assets<ColorMaterial>>,
chunk: Single<&TilemapChunk>,
) {
let mut transform = chunk.calculate_tile_transform(UVec2::new(0, 0));
transform.translation.z = 1.;
commands.spawn((
Mesh2d(meshes.add(Rectangle::new(8., 8.))),
MeshMaterial2d(materials.add(Color::from(RED_400))),
transform,
MovePlayer,
));
let mut transform = chunk.calculate_tile_transform(UVec2::new(5, 6));
transform.translation.z = 1.;
// second "player" to visually test a non-zero position
commands.spawn((
Mesh2d(meshes.add(Rectangle::new(8., 8.))),
MeshMaterial2d(materials.add(Color::from(RED_400))),
transform,
));
}
fn move_player(
mut player: Single<&mut Transform, With<MovePlayer>>,
time: Res<Time>,
chunk: Single<&TilemapChunk>,
) {
let t = (ops::sin(time.elapsed_secs()) + 1.) / 2.;
let origin = chunk
.calculate_tile_transform(UVec2::new(0, 0))
.translation
.x;
let destination = chunk
.calculate_tile_transform(UVec2::new(63, 0))
.translation
.x;
player.translation.x = origin.lerp(destination, t);
}
fn update_tilemap(
time: Res<Time>,
mut query: Query<(&mut TilemapChunkTileData, &mut UpdateTimer)>,
mut rng: ResMut<SeededRng>,
) {
for (mut tile_data, mut timer) in query.iter_mut() {
timer.tick(time.delta());
if timer.just_finished() {
for _ in 0..50 {
let index = rng.random_range(0..tile_data.len());
tile_data[index] = Some(TileData::from_tileset_index(rng.random_range(0..5)));
}
}
}
}
// find the data for an arbitrary tile in the chunk and log its data
fn log_tile(tilemap: Single<(&TilemapChunk, &TilemapChunkTileData)>, mut local: Local<u16>) {
let (chunk, data) = tilemap.into_inner();
let Some(tile_data) = data.tile_data_from_tile_pos(chunk.chunk_size, UVec2::new(3, 4)) else {
return;
};
// log when the tile changes
if tile_data.tileset_index != *local {
info!(?tile_data, "tile_data changed");
*local = tile_data.tileset_index;
}
}