Path: blob/main/examples/asset/asset_saving_with_subassets.rs
30632 views
//! This example demonstrates how to save assets that include subassets.12use bevy::{3asset::{4io::{Reader, Writer},5saver::{save_using_saver, AssetSaver, SavedAsset, SavedAssetBuilder},6AssetLoader, AssetPath, AsyncWriteExt, LoadContext,7},8color::palettes::tailwind,9input::common_conditions::input_just_pressed,10prelude::*,11tasks::IoTaskPool,12};13use serde::{Deserialize, Serialize};1415fn main() {16App::new()17.add_plugins(DefaultPlugins.set(AssetPlugin {18// This is just overriding the default asset paths to scope this to the correct example19// folder. You can generally skip this in your own projects.20file_path: "examples/asset/saved_assets".to_string(),21..Default::default()22}))23.add_plugins(box_editing_plugin)24.init_asset::<OneBox>()25.init_asset::<ManyBoxes>()26.register_asset_loader(ManyBoxesLoader)27.add_systems(28PreUpdate,29(30perform_save.run_if(input_just_pressed(KeyCode::F5)),31(32start_load.run_if(input_just_pressed(KeyCode::F6)),33wait_for_pending_loads,34)35.chain(),36),37)38.run();39}4041const ASSET_PATH: &str = "my_scene.boxes";4243/// A system that takes the scene data, passes it to a task, and saves that scene data to44/// [`ASSET_PATH`].45fn perform_save(boxes: Query<(&Sprite, &Transform), With<Box>>, asset_server: Res<AssetServer>) {46// First we extract all the data needed to produce an asset we can save.47let boxes = boxes48.iter()49.map(|(sprite, transform)| OneBox {50position: transform.translation.xy(),51color: sprite.color,52})53.collect::<Vec<_>>();5455let asset_server = asset_server.clone();56IoTaskPool::get()57.spawn(async move {58// Build a `SavedAsset` instance from the boxes we extracted.59let mut builder = SavedAssetBuilder::new(asset_server.clone(), ASSET_PATH.into());60let mut many_boxes = ManyBoxes { boxes: vec![] };61for (index, one_box) in boxes.iter().enumerate() {62many_boxes63.boxes64.push(builder.add_labeled_asset_with_new_handle(65index.to_string(),66SavedAsset::from_asset(one_box),67));68}6970let saved_asset = builder.build(&many_boxes);71// Save the asset using the provided saver.72match save_using_saver(73asset_server.clone(),74&ManyBoxesSaver,75&ASSET_PATH.into(),76saved_asset,77&(),78)79.await80{81Ok(()) => info!("Completed save of {ASSET_PATH}"),82Err(err) => error!("Failed to save asset: {err}"),83}84})85.detach();86}8788/// A system the starts loading [`ASSET_PATH`].89fn start_load(mut commands: Commands, asset_server: Res<AssetServer>) {90commands.spawn(PendingLoad(asset_server.load(ASSET_PATH)));91}9293/// Marks that a handle is currently loading.94///95/// Once loading is complete, the [`ManyBoxes`] data will be spawned.96#[derive(Component)]97struct PendingLoad(Handle<ManyBoxes>);9899/// Waits for any [`PendingLoad`]s to complete, and spawns in their boxes when they do.100fn wait_for_pending_loads(101loads: Populated<(Entity, &PendingLoad)>,102many_boxes: Res<Assets<ManyBoxes>>,103one_boxes: Res<Assets<OneBox>>,104existing_boxes: Query<Entity, With<Box>>,105mut commands: Commands,106) {107for (entity, load) in loads.iter() {108let Some(many_boxes) = many_boxes.get(&load.0) else {109continue;110};111112commands.entity(entity).despawn();113for entity in existing_boxes.iter() {114commands.entity(entity).despawn();115}116117for box_handle in many_boxes.boxes.iter() {118let Some(one_box) = one_boxes.get(box_handle) else {119return;120};121commands.spawn((122Sprite::from_color(one_box.color, Vec2::new(100.0, 100.0)),123Transform::from_translation(one_box.position.extend(0.0)),124Pickable::default(),125Box,126));127}128}129}130131/// An asset representing a single box.132#[derive(Asset, TypePath, Clone, Serialize, Deserialize)]133struct OneBox {134/// The position of the box.135position: Vec2,136/// The color of the box.137color: Color,138}139140/// An asset representing many boxes.141#[derive(Asset, TypePath)]142struct ManyBoxes {143/// Stores handles to all the boxes that should be spawned.144///145/// Note: in this trivial example, it seems more reasonable to just store [`Vec<OneBox>`], but146/// in a more realistic example this could be something like a whole [`Mesh`] (where a handle147/// makes more sense). We use a handle here to demonstrate saving subassets as well.148boxes: Vec<Handle<OneBox>>,149}150151/// A serializable version of [`ManyBoxes`].152#[derive(Serialize, Deserialize)]153struct SerializableManyBoxes {154/// The boxes that exist in this scene.155boxes: Vec<OneBox>,156}157158/// Am asset saver to save [`ManyBoxes`] assets.159#[derive(TypePath)]160struct ManyBoxesSaver;161162impl AssetSaver for ManyBoxesSaver {163type Asset = ManyBoxes;164type Error = BevyError;165type OutputLoader = ManyBoxesLoader;166type Settings = ();167168async fn save(169&self,170writer: &mut Writer,171asset: SavedAsset<'_, '_, Self::Asset>,172_settings: &Self::Settings,173_asset_path: AssetPath<'_>,174) -> Result<(), Self::Error> {175let boxes = asset176.boxes177.iter()178.map(|handle| {179asset180.get_labeled_by_id::<OneBox>(handle)181.unwrap()182.get()183.clone()184})185.collect();186187// Note: serializing to string isn't ideal since we can't do a streaming write, but this is188// fine for an example.189let serialized = ron::to_string(&SerializableManyBoxes { boxes })?;190writer.write_all(serialized.as_bytes()).await?;191192Ok(())193}194}195196/// An asset loader for loading [`ManyBoxes`] assets.197#[derive(TypePath)]198struct ManyBoxesLoader;199200impl AssetLoader for ManyBoxesLoader {201type Asset = ManyBoxes;202type Error = BevyError;203type Settings = ();204205async fn load(206&self,207reader: &mut dyn Reader,208_settings: &Self::Settings,209load_context: &mut LoadContext<'_>,210) -> Result<Self::Asset, Self::Error> {211let mut bytes = vec![];212reader.read_to_end(&mut bytes).await?;213214let serialized: SerializableManyBoxes = ron::de::from_bytes(&bytes)?;215216// Add the boxes as subassets.217let mut result_boxes = vec![];218for (index, one_box) in serialized.boxes.into_iter().enumerate() {219result_boxes.push(load_context.add_labeled_asset(index.to_string(), one_box));220}221222Ok(ManyBoxes {223boxes: result_boxes,224})225}226227fn extensions(&self) -> &[&str] {228&["boxes"]229}230}231232/// Plugin for doing all the box-editing.233///234/// This doesn't really have anything to do with asset saving, but provides a real use-case.235fn box_editing_plugin(app: &mut App) {236app.add_systems(Startup, setup)237.add_observer(spawn_box)238.add_observer(start_rotate_box_hue)239.add_observer(end_rotate_box_hue_on_release)240.add_observer(end_rotate_box_hue_on_out)241.add_systems(Update, rotate_hue)242.add_observer(stop_propagate_on_clicked_box)243.add_observer(drag_box);244}245246#[derive(Component)]247struct Box;248249/// Spawns the initial scene.250fn setup(mut commands: Commands) {251commands.spawn(Camera2d);252253commands.spawn(Text(254r"LMB (on background) - spawn new box255LMB (on box) - drag to move256RMB (on box) - rotate colors257F5 - Save boxes258F6 - Load boxes"259.into(),260));261}262263/// Spawns a new box whenever you left-click on the background.264fn spawn_box(265event: On<Pointer<Press>>,266window: Query<(), With<Window>>,267camera: Single<(&Camera, &GlobalTransform)>,268mut commands: Commands,269) {270if event.button != PointerButton::Primary {271return;272}273if !window.contains(event.entity) {274return;275}276277let (camera, camera_transform) = camera.into_inner();278let Ok(click_point) =279camera.viewport_to_world_2d(camera_transform, event.pointer_location.position)280else {281return;282};283commands.spawn((284Sprite::from_color(tailwind::RED_500, Vec2::new(100.0, 100.0)),285Transform::from_translation(click_point.extend(0.0)),286Pickable::default(),287Box,288));289}290291/// A component to rotate the hue of a sprite every frame.292#[derive(Component)]293struct RotateHue;294295/// Rotates the hue of each [`Sprite`] tagged with [`RotateHue`].296fn rotate_hue(time: Res<Time>, mut sprites: Query<&mut Sprite, With<RotateHue>>) {297for mut sprite in sprites.iter_mut() {298// Make a full rotation every 2 seconds.299sprite.color = sprite.color.rotate_hue(time.delta_secs() * 180.0);300}301}302303/// Starts rotating the hue of a box that has been right-clicked.304fn start_rotate_box_hue(305event: On<Pointer<Press>>,306boxes: Query<(), With<Box>>,307mut commands: Commands,308) {309if event.button != PointerButton::Secondary {310return;311}312if !boxes.contains(event.entity) {313return;314}315commands.entity(event.entity).insert(RotateHue);316}317318/// Stops rotating the box hue if it's right-click is released.319fn end_rotate_box_hue_on_release(320event: On<Pointer<Release>>,321boxes: Query<(), (With<Box>, With<RotateHue>)>,322mut commands: Commands,323) {324if event.button != PointerButton::Secondary {325return;326}327if !boxes.contains(event.entity) {328return;329}330commands.entity(event.entity).remove::<RotateHue>();331}332333/// Stops rotating the box hue if the cursor moves off the entity.334fn end_rotate_box_hue_on_out(335event: On<Pointer<Out>>,336boxes: Query<(), (With<Box>, With<RotateHue>)>,337mut commands: Commands,338) {339if !boxes.contains(event.entity) {340return;341}342commands.entity(event.entity).remove::<RotateHue>();343}344345/// Blocks propagation of pointer press events on left-clicked boxes.346fn stop_propagate_on_clicked_box(mut event: On<Pointer<Press>>, boxes: Query<(), With<Box>>) {347if event.button != PointerButton::Primary {348return;349}350if !boxes.contains(event.entity) {351return;352}353event.propagate(false);354}355356/// Drags a box when you left-click on one.357fn drag_box(event: On<Pointer<Drag>>, mut boxes: Query<&mut Transform, With<Box>>) {358if event.button != PointerButton::Primary {359return;360}361let Ok(mut transform) = boxes.get_mut(event.entity) else {362return;363};364365// This is wrong in general (e.g., doesn't consider scale), but it's close enough for our366// purposes.367transform.translation += Vec3::new(event.delta.x, -event.delta.y, 0.0);368}369370371