Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
bevyengine
GitHub Repository: bevyengine/bevy
Path: blob/main/examples/asset/asset_saving_with_subassets.rs
30632 views
1
//! This example demonstrates how to save assets that include subassets.
2
3
use 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
};
14
use serde::{Deserialize, Serialize};
15
16
fn 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
42
const 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`].
46
fn 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`].
90
fn 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)]
98
struct PendingLoad(Handle<ManyBoxes>);
99
100
/// Waits for any [`PendingLoad`]s to complete, and spawns in their boxes when they do.
101
fn 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)]
134
struct 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)]
143
struct 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)]
154
struct 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)]
161
struct ManyBoxesSaver;
162
163
impl 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)]
199
struct ManyBoxesLoader;
200
201
impl 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.
236
fn 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)]
248
struct Box;
249
250
/// Spawns the initial scene.
251
fn setup(mut commands: Commands) {
252
commands.spawn(Camera2d);
253
254
commands.spawn(Text(
255
r"LMB (on background) - spawn new box
256
LMB (on box) - drag to move
257
RMB (on box) - rotate colors
258
F5 - Save boxes
259
F6 - Load boxes"
260
.into(),
261
));
262
}
263
264
/// Spawns a new box whenever you left-click on the background.
265
fn 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)]
294
struct RotateHue;
295
296
/// Rotates the hue of each [`Sprite`] tagged with [`RotateHue`].
297
fn 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.
305
fn 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.
320
fn 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.
335
fn 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.
347
fn 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.
358
fn 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
}
370
371