Files
bevy/examples/asset/asset_saving_with_subassets.rs
andriyDev 935936ded5 Add an ImageSaver AssetSaver, and simplify / split the asset saving example (#23105)
# Objective

- Allow users to save images.
- Make a "simple" example of asset saving (as opposed to the existing
complex example).

## Solution

- Pass in the asset path to `AssetSaver`s.
- Make `get_full_extension` return a `&str` instead of an owned
`String`.
- Created a new `ImageSaver` that currently only supports PNG.
- We can extend this in the future if we want more support. It's not
super straightforward since e.g., JPEG doesn't support alpha and wgpu
TextureFormat doesn't have a regular RGB8 format.
- Renamed the old `asset_saving` example to
`asset_saving_with_subassets`.
- Created a new `asset_saving` example which allows drawing an image and
then saving it.

## Testing

- The new example works!
2026-03-10 05:11:21 +00:00

370 lines
11 KiB
Rust

//! This example demonstrates how to save assets that include subassets.
use bevy::{
asset::{
io::{Reader, Writer},
saver::{save_using_saver, AssetSaver, SavedAsset, SavedAssetBuilder},
AssetLoader, AssetPath, AsyncWriteExt, LoadContext,
},
color::palettes::tailwind,
input::common_conditions::input_just_pressed,
prelude::*,
tasks::IoTaskPool,
};
use serde::{Deserialize, Serialize};
fn main() {
App::new()
.add_plugins(DefaultPlugins.set(AssetPlugin {
// This is just overriding the default asset paths to scope this to the correct example
// folder. You can generally skip this in your own projects.
file_path: "examples/asset/saved_assets".to_string(),
..Default::default()
}))
.add_plugins(box_editing_plugin)
.init_asset::<OneBox>()
.init_asset::<ManyBoxes>()
.register_asset_loader(ManyBoxesLoader)
.add_systems(
PreUpdate,
(
perform_save.run_if(input_just_pressed(KeyCode::F5)),
(
start_load.run_if(input_just_pressed(KeyCode::F6)),
wait_for_pending_loads,
)
.chain(),
),
)
.run();
}
const ASSET_PATH: &str = "my_scene.boxes";
/// A system that takes the scene data, passes it to a task, and saves that scene data to
/// [`ASSET_PATH`].
fn perform_save(boxes: Query<(&Sprite, &Transform), With<Box>>, asset_server: Res<AssetServer>) {
// First we extract all the data needed to produce an asset we can save.
let boxes = boxes
.iter()
.map(|(sprite, transform)| OneBox {
position: transform.translation.xy(),
color: sprite.color,
})
.collect::<Vec<_>>();
let asset_server = asset_server.clone();
IoTaskPool::get()
.spawn(async move {
// Build a `SavedAsset` instance from the boxes we extracted.
let mut builder = SavedAssetBuilder::new(asset_server.clone(), ASSET_PATH.into());
let mut many_boxes = ManyBoxes { boxes: vec![] };
for (index, one_box) in boxes.iter().enumerate() {
many_boxes
.boxes
.push(builder.add_labeled_asset_with_new_handle(
index.to_string(),
SavedAsset::from_asset(one_box),
));
}
let saved_asset = builder.build(&many_boxes);
// Save the asset using the provided saver.
match save_using_saver(
asset_server.clone(),
&ManyBoxesSaver,
&ASSET_PATH.into(),
saved_asset,
&(),
)
.await
{
Ok(()) => info!("Completed save of {ASSET_PATH}"),
Err(err) => error!("Failed to save asset: {err}"),
}
})
.detach();
}
/// A system the starts loading [`ASSET_PATH`].
fn start_load(mut commands: Commands, asset_server: Res<AssetServer>) {
commands.spawn(PendingLoad(asset_server.load(ASSET_PATH)));
}
/// Marks that a handle is currently loading.
///
/// Once loading is complete, the [`ManyBoxes`] data will be spawned.
#[derive(Component)]
struct PendingLoad(Handle<ManyBoxes>);
/// Waits for any [`PendingLoad`]s to complete, and spawns in their boxes when they do.
fn wait_for_pending_loads(
loads: Populated<(Entity, &PendingLoad)>,
many_boxes: Res<Assets<ManyBoxes>>,
one_boxes: Res<Assets<OneBox>>,
existing_boxes: Query<Entity, With<Box>>,
mut commands: Commands,
) {
for (entity, load) in loads.iter() {
let Some(many_boxes) = many_boxes.get(&load.0) else {
continue;
};
commands.entity(entity).despawn();
for entity in existing_boxes.iter() {
commands.entity(entity).despawn();
}
for box_handle in many_boxes.boxes.iter() {
let Some(one_box) = one_boxes.get(box_handle) else {
return;
};
commands.spawn((
Sprite::from_color(one_box.color, Vec2::new(100.0, 100.0)),
Transform::from_translation(one_box.position.extend(0.0)),
Pickable::default(),
Box,
));
}
}
}
/// An asset representing a single box.
#[derive(Asset, TypePath, Clone, Serialize, Deserialize)]
struct OneBox {
/// The position of the box.
position: Vec2,
/// The color of the box.
color: Color,
}
/// An asset representing many boxes.
#[derive(Asset, TypePath)]
struct ManyBoxes {
/// Stores handles to all the boxes that should be spawned.
///
/// Note: in this trivial example, it seems more reasonable to just store [`Vec<OneBox>`], but
/// in a more realistic example this could be something like a whole [`Mesh`] (where a handle
/// makes more sense). We use a handle here to demonstrate saving subassets as well.
boxes: Vec<Handle<OneBox>>,
}
/// A serializable version of [`ManyBoxes`].
#[derive(Serialize, Deserialize)]
struct SerializableManyBoxes {
/// The boxes that exist in this scene.
boxes: Vec<OneBox>,
}
/// Am asset saver to save [`ManyBoxes`] assets.
#[derive(TypePath)]
struct ManyBoxesSaver;
impl AssetSaver for ManyBoxesSaver {
type Asset = ManyBoxes;
type Error = BevyError;
type OutputLoader = ManyBoxesLoader;
type Settings = ();
async fn save(
&self,
writer: &mut Writer,
asset: SavedAsset<'_, '_, Self::Asset>,
_settings: &Self::Settings,
_asset_path: AssetPath<'_>,
) -> Result<(), Self::Error> {
let boxes = asset
.boxes
.iter()
.map(|handle| {
asset
.get_labeled_by_id::<OneBox>(handle)
.unwrap()
.get()
.clone()
})
.collect();
// Note: serializing to string isn't ideal since we can't do a streaming write, but this is
// fine for an example.
let serialized = ron::to_string(&SerializableManyBoxes { boxes })?;
writer.write_all(serialized.as_bytes()).await?;
Ok(())
}
}
/// An asset loader for loading [`ManyBoxes`] assets.
#[derive(TypePath)]
struct ManyBoxesLoader;
impl AssetLoader for ManyBoxesLoader {
type Asset = ManyBoxes;
type Error = BevyError;
type Settings = ();
async fn load(
&self,
reader: &mut dyn Reader,
_settings: &Self::Settings,
load_context: &mut LoadContext<'_>,
) -> Result<Self::Asset, Self::Error> {
let mut bytes = vec![];
reader.read_to_end(&mut bytes).await?;
let serialized: SerializableManyBoxes = ron::de::from_bytes(&bytes)?;
// Add the boxes as subassets.
let mut result_boxes = vec![];
for (index, one_box) in serialized.boxes.into_iter().enumerate() {
result_boxes.push(load_context.add_labeled_asset(index.to_string(), one_box));
}
Ok(ManyBoxes {
boxes: result_boxes,
})
}
fn extensions(&self) -> &[&str] {
&["boxes"]
}
}
/// Plugin for doing all the box-editing.
///
/// This doesn't really have anything to do with asset saving, but provides a real use-case.
fn box_editing_plugin(app: &mut App) {
app.add_systems(Startup, setup)
.add_observer(spawn_box)
.add_observer(start_rotate_box_hue)
.add_observer(end_rotate_box_hue_on_release)
.add_observer(end_rotate_box_hue_on_out)
.add_systems(Update, rotate_hue)
.add_observer(stop_propagate_on_clicked_box)
.add_observer(drag_box);
}
#[derive(Component)]
struct Box;
/// Spawns the initial scene.
fn setup(mut commands: Commands) {
commands.spawn(Camera2d);
commands.spawn(Text(
r"LMB (on background) - spawn new box
LMB (on box) - drag to move
RMB (on box) - rotate colors
F5 - Save boxes
F6 - Load boxes"
.into(),
));
}
/// Spawns a new box whenever you left-click on the background.
fn spawn_box(
event: On<Pointer<Press>>,
window: Query<(), With<Window>>,
camera: Single<(&Camera, &GlobalTransform)>,
mut commands: Commands,
) {
if event.button != PointerButton::Primary {
return;
}
if !window.contains(event.entity) {
return;
}
let (camera, camera_transform) = camera.into_inner();
let Ok(click_point) =
camera.viewport_to_world_2d(camera_transform, event.pointer_location.position)
else {
return;
};
commands.spawn((
Sprite::from_color(tailwind::RED_500, Vec2::new(100.0, 100.0)),
Transform::from_translation(click_point.extend(0.0)),
Pickable::default(),
Box,
));
}
/// A component to rotate the hue of a sprite every frame.
#[derive(Component)]
struct RotateHue;
/// Rotates the hue of each [`Sprite`] tagged with [`RotateHue`].
fn rotate_hue(time: Res<Time>, mut sprites: Query<&mut Sprite, With<RotateHue>>) {
for mut sprite in sprites.iter_mut() {
// Make a full rotation every 2 seconds.
sprite.color = sprite.color.rotate_hue(time.delta_secs() * 180.0);
}
}
/// Starts rotating the hue of a box that has been right-clicked.
fn start_rotate_box_hue(
event: On<Pointer<Press>>,
boxes: Query<(), With<Box>>,
mut commands: Commands,
) {
if event.button != PointerButton::Secondary {
return;
}
if !boxes.contains(event.entity) {
return;
}
commands.entity(event.entity).insert(RotateHue);
}
/// Stops rotating the box hue if it's right-click is released.
fn end_rotate_box_hue_on_release(
event: On<Pointer<Release>>,
boxes: Query<(), (With<Box>, With<RotateHue>)>,
mut commands: Commands,
) {
if event.button != PointerButton::Secondary {
return;
}
if !boxes.contains(event.entity) {
return;
}
commands.entity(event.entity).remove::<RotateHue>();
}
/// Stops rotating the box hue if the cursor moves off the entity.
fn end_rotate_box_hue_on_out(
event: On<Pointer<Out>>,
boxes: Query<(), (With<Box>, With<RotateHue>)>,
mut commands: Commands,
) {
if !boxes.contains(event.entity) {
return;
}
commands.entity(event.entity).remove::<RotateHue>();
}
/// Blocks propagation of pointer press events on left-clicked boxes.
fn stop_propagate_on_clicked_box(mut event: On<Pointer<Press>>, boxes: Query<(), With<Box>>) {
if event.button != PointerButton::Primary {
return;
}
if !boxes.contains(event.entity) {
return;
}
event.propagate(false);
}
/// Drags a box when you left-click on one.
fn drag_box(event: On<Pointer<Drag>>, mut boxes: Query<&mut Transform, With<Box>>) {
if event.button != PointerButton::Primary {
return;
}
let Ok(mut transform) = boxes.get_mut(event.entity) else {
return;
};
// This is wrong in general (e.g., doesn't consider scale), but it's close enough for our
// purposes.
transform.translation += Vec3::new(event.delta.x, -event.delta.y, 0.0);
}