//! 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::() .init_asset::() .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>, asset_server: Res) { // 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::>(); 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) { 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); /// 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>, one_boxes: Res>, existing_boxes: Query>, 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`], 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>, } /// A serializable version of [`ManyBoxes`]. #[derive(Serialize, Deserialize)] struct SerializableManyBoxes { /// The boxes that exist in this scene. boxes: Vec, } /// 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::(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 { 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( press: On, window: Query<(), With>, camera: Single<(&Camera, &GlobalTransform)>, mut commands: Commands, ) { if press.button != PointerButton::Primary { return; } if !window.contains(press.entity) { return; } let (camera, camera_transform) = camera.into_inner(); let Ok(click_point) = camera.viewport_to_world_2d(camera_transform, press.pointer.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