Skip to main content

asset_saving_with_subassets/
asset_saving_with_subassets.rs

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