use bevy::{
asset::{
io::{Reader, Writer},
saver::{save_using_saver, AssetSaver, SavedAsset, SavedAssetBuilder},
AssetLoader, 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 {
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";
fn perform_save(boxes: Query<(&Sprite, &Transform), With<Box>>, asset_server: Res<AssetServer>) {
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 {
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);
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();
}
fn start_load(mut commands: Commands, asset_server: Res<AssetServer>) {
commands.spawn(PendingLoad(asset_server.load(ASSET_PATH)));
}
#[derive(Component)]
struct PendingLoad(Handle<ManyBoxes>);
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,
));
}
}
}
#[derive(Asset, TypePath, Clone, Serialize, Deserialize)]
struct OneBox {
position: Vec2,
color: Color,
}
#[derive(Asset, TypePath)]
struct ManyBoxes {
boxes: Vec<Handle<OneBox>>,
}
#[derive(Serialize, Deserialize)]
struct SerializableManyBoxes {
boxes: Vec<OneBox>,
}
#[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,
) -> Result<(), Self::Error> {
let boxes = asset
.boxes
.iter()
.map(|handle| {
let label = handle
.path()
.and_then(|path| path.label())
.ok_or_else(|| format!("Failed to get label for handle {handle:?}"))?;
asset
.get_labeled::<OneBox>(label)
.map(|subasset| subasset.get().clone())
.ok_or_else(|| format!("Failed to find labeled asset for label {label}"))
})
.collect::<Result<Vec<_>, _>>()?;
let serialized = ron::to_string(&SerializableManyBoxes { boxes })?;
writer.write_all(serialized.as_bytes()).await?;
Ok(())
}
}
#[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)?;
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"]
}
}
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;
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(),
));
}
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,
));
}
#[derive(Component)]
struct RotateHue;
fn rotate_hue(time: Res<Time>, mut sprites: Query<&mut Sprite, With<RotateHue>>) {
for mut sprite in sprites.iter_mut() {
sprite.color = sprite.color.rotate_hue(time.delta_secs() * 180.0);
}
}
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);
}
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>();
}
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>();
}
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);
}
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;
};
transform.translation += Vec3::new(event.delta.x, -event.delta.y, 0.0);
}