Skip to content

Commit a728ac6

Browse files
committed
MVP with an Example
1 parent 436c3e2 commit a728ac6

File tree

5 files changed

+215
-38
lines changed

5 files changed

+215
-38
lines changed

Cargo.toml

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1413,6 +1413,17 @@ description = "How to configure the texture to repeat instead of the default cla
14131413
category = "Assets"
14141414
wasm = true
14151415

1416+
[[example]]
1417+
name = "temp_asset"
1418+
path = "examples/asset/temp_asset.rs"
1419+
doc-scrape-examples = true
1420+
1421+
[package.metadata.example.temp_asset]
1422+
name = "Temporary assets"
1423+
description = "How to use the temporary asset source"
1424+
category = "Assets"
1425+
wasm = false
1426+
14161427
# Async Tasks
14171428
[[example]]
14181429
name = "async_compute"

crates/bevy_asset/src/lib.rs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ pub use server::*;
4747

4848
/// Rusty Object Notation, a crate used to serialize and deserialize bevy assets.
4949
pub use ron;
50-
use temp::TempAssetPlugin;
50+
use temp::get_temp_source;
5151

5252
use crate::{
5353
io::{embedded::EmbeddedAssetRegistry, AssetSourceBuilder, AssetSourceBuilders, AssetSourceId},
@@ -157,6 +157,8 @@ impl AssetPlugin {
157157

158158
impl Plugin for AssetPlugin {
159159
fn build(&self, app: &mut App) {
160+
let temp_source = get_temp_source(app.world_mut(), self.temporary_file_path.clone());
161+
160162
let embedded = EmbeddedAssetRegistry::default();
161163
{
162164
let mut sources = app
@@ -168,6 +170,7 @@ impl Plugin for AssetPlugin {
168170
.then_some(self.processed_file_path.as_str()),
169171
);
170172
embedded.register_source(&mut sources);
173+
sources.insert("temp", temp_source);
171174
}
172175
{
173176
let mut watch = cfg!(feature = "watch");
@@ -225,10 +228,7 @@ impl Plugin for AssetPlugin {
225228
.add_event::<UntypedAssetLoadFailedEvent>()
226229
.configure_sets(PreUpdate, TrackAssets.after(handle_internal_asset_events))
227230
.add_systems(PreUpdate, handle_internal_asset_events)
228-
.register_type::<AssetPath>()
229-
.add_plugins(TempAssetPlugin {
230-
temporary_file_path: self.temporary_file_path.clone(),
231-
});
231+
.register_type::<AssetPath>();
232232
}
233233
}
234234

crates/bevy_asset/src/temp.rs

Lines changed: 15 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,8 @@
11
use std::path::{Path, PathBuf};
22

3-
use bevy_app::Plugin;
4-
use bevy_ecs::system::Resource;
3+
use bevy_ecs::{system::Resource, world::World};
54

6-
use crate::io::{AssetSourceBuilder, AssetSourceBuilders};
5+
use crate::io::AssetSourceBuilder;
76

87
#[derive(Resource)]
98
struct TempAssetDirectory {
@@ -30,37 +29,20 @@ impl TempDirectory {
3029
}
3130
}
3231

33-
/// Plugin providing a temporary asset source.
34-
#[derive(Default)]
35-
pub(crate) struct TempAssetPlugin {
36-
/// The path to use for temporary assets (relative to the project root).
37-
/// If not provided, a platform specific folder will be created and deleted upon exit.
38-
pub temporary_file_path: Option<String>,
39-
}
40-
41-
impl Plugin for TempAssetPlugin {
42-
fn build(&self, app: &mut bevy_app::App) {
43-
let source = {
44-
let temp_dir = app
45-
.world_mut()
46-
.get_resource_or_insert_with::<TempAssetDirectory>(|| {
47-
let folder = match &self.temporary_file_path {
48-
Some(path) => TempDirectory::Persist(path.into()),
49-
None => TempDirectory::Delete(tempfile::tempdir().expect("todo")),
50-
};
51-
52-
TempAssetDirectory { folder }
53-
});
54-
55-
let path = temp_dir.path().to_str().expect("todo");
56-
57-
AssetSourceBuilder::platform_default(path, None)
32+
pub(crate) fn get_temp_source(
33+
world: &mut World,
34+
temporary_file_path: Option<String>,
35+
) -> AssetSourceBuilder {
36+
let temp_dir = world.get_resource_or_insert_with::<TempAssetDirectory>(|| {
37+
let folder = match temporary_file_path {
38+
Some(path) => TempDirectory::Persist(path.into()),
39+
None => TempDirectory::Delete(tempfile::tempdir().expect("todo")),
5840
};
5941

60-
let mut sources = app
61-
.world_mut()
62-
.get_resource_or_insert_with::<AssetSourceBuilders>(Default::default);
42+
TempAssetDirectory { folder }
43+
});
6344

64-
sources.insert("temp", source);
65-
}
45+
let path = temp_dir.path().to_str().expect("todo");
46+
47+
AssetSourceBuilder::platform_default(path, None)
6648
}

examples/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -215,6 +215,7 @@ Example | Description
215215
[Extra asset source](../examples/asset/extra_source.rs) | Load an asset from a non-standard asset source
216216
[Hot Reloading of Assets](../examples/asset/hot_asset_reloading.rs) | Demonstrates automatic reloading of assets when modified on disk
217217
[Repeated texture configuration](../examples/asset/repeated_texture.rs) | How to configure the texture to repeat instead of the default clamp to edges
218+
[Temporary assets](../examples/asset/temp_asset.rs) | How to use the temporary asset source
218219

219220
## Async Tasks
220221

examples/asset/temp_asset.rs

Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
1+
//! This example shows how to use the temporary asset source, `temp://`.
2+
//! First, a [`TextAsset`] is created in-memory, then saved into the temporary asset source.
3+
//! Once the save operation is completed, we load the asset just like any other file, and display its contents!
4+
5+
use bevy::{
6+
asset::{
7+
saver::{AssetSaver, ErasedAssetSaver},
8+
AssetPath, ErasedLoadedAsset, LoadedAsset,
9+
},
10+
prelude::*,
11+
tasks::{block_on, IoTaskPool, Task},
12+
};
13+
14+
use futures_lite::future;
15+
use text_asset::{TextAsset, TextLoader, TextSaver};
16+
17+
fn main() {
18+
App::new()
19+
.add_plugins(DefaultPlugins)
20+
.init_asset::<TextAsset>()
21+
.register_asset_loader(TextLoader)
22+
.add_systems(Startup, (save_temp_asset, setup_ui))
23+
.add_systems(Update, (wait_until_temp_saved, display_text))
24+
.run();
25+
}
26+
27+
/// Attempt to save an asset to the temporary asset source.
28+
fn save_temp_asset(assets: Res<AssetServer>, mut commands: Commands) {
29+
// This is the asset we will attempt to save.
30+
let my_text_asset = TextAsset("Hello World!".to_owned());
31+
32+
// To ensure the `Task` can outlive this function, we must provide owned versions
33+
// of the `AssetServer` and our desired path.
34+
let path = AssetPath::from("temp://message.txt").into_owned();
35+
let server = assets.clone();
36+
37+
let task = IoTaskPool::get().spawn(async move {
38+
save_asset(my_text_asset, path, server, TextSaver)
39+
.await
40+
.unwrap();
41+
});
42+
43+
// To ensure the task completes before we try loading, we will manually poll this task
44+
// so we can react to its completion.
45+
commands.spawn(SavingTask(task));
46+
}
47+
48+
/// Poll the save tasks until completion, and then start loading our temporary text asset.
49+
fn wait_until_temp_saved(
50+
assets: Res<AssetServer>,
51+
mut tasks: Query<(Entity, &mut SavingTask)>,
52+
mut commands: Commands,
53+
) {
54+
for (entity, mut task) in tasks.iter_mut() {
55+
if let Some(()) = block_on(future::poll_once(&mut task.0)) {
56+
commands.insert_resource(MyTempText {
57+
text: assets.load("temp://message.txt"),
58+
});
59+
60+
commands.entity(entity).despawn_recursive();
61+
}
62+
}
63+
}
64+
65+
/// Setup a basic UI to display our [`TextAsset`] once it's loaded.
66+
fn setup_ui(mut commands: Commands) {
67+
commands.spawn(Camera2dBundle::default());
68+
69+
commands.spawn((TextBundle::from_section("Loading...", default())
70+
.with_text_justify(JustifyText::Center)
71+
.with_style(Style {
72+
position_type: PositionType::Absolute,
73+
bottom: Val::Percent(50.),
74+
right: Val::Percent(50.),
75+
..default()
76+
}),));
77+
}
78+
79+
/// Once the [`TextAsset`] is loaded, update our display text to its contents.
80+
fn display_text(
81+
mut query: Query<&mut Text>,
82+
my_text: Option<Res<MyTempText>>,
83+
texts: Res<Assets<TextAsset>>,
84+
) {
85+
let message = my_text
86+
.as_ref()
87+
.and_then(|resource| texts.get(&resource.text))
88+
.map(|text| text.0.as_str())
89+
.unwrap_or("Loading...");
90+
91+
for mut text in query.iter_mut() {
92+
*text = Text::from_section(message, default());
93+
}
94+
}
95+
96+
/// Save an [`Asset`] at the provided path. Returns [`None`] on failure.
97+
async fn save_asset<A: Asset>(
98+
asset: A,
99+
path: AssetPath<'_>,
100+
server: AssetServer,
101+
saver: impl AssetSaver<Asset = A> + ErasedAssetSaver,
102+
) -> Option<()> {
103+
let asset = ErasedLoadedAsset::from(LoadedAsset::from(asset));
104+
let source = server.get_source(path.source()).ok()?;
105+
let writer = source.writer().ok()?;
106+
107+
let mut writer = writer.write(path.path()).await.ok()?;
108+
ErasedAssetSaver::save(&saver, &mut writer, &asset, &())
109+
.await
110+
.ok()?;
111+
112+
Some(())
113+
}
114+
115+
#[derive(Component)]
116+
struct SavingTask(Task<()>);
117+
118+
#[derive(Resource)]
119+
struct MyTempText {
120+
text: Handle<TextAsset>,
121+
}
122+
123+
mod text_asset {
124+
//! Putting the implementation of an asset loader and writer for a text asset in this module to avoid clutter.
125+
//! While this is required for this example to function, it isn't the focus.
126+
127+
use bevy::{
128+
asset::{
129+
io::{Reader, Writer},
130+
saver::{AssetSaver, SavedAsset},
131+
AssetLoader, LoadContext,
132+
},
133+
prelude::*,
134+
};
135+
use futures_lite::{AsyncReadExt, AsyncWriteExt};
136+
137+
#[derive(Asset, TypePath, Debug)]
138+
pub struct TextAsset(pub String);
139+
140+
#[derive(Default)]
141+
pub struct TextLoader;
142+
143+
impl AssetLoader for TextLoader {
144+
type Asset = TextAsset;
145+
type Settings = ();
146+
type Error = std::io::Error;
147+
async fn load<'a>(
148+
&'a self,
149+
reader: &'a mut Reader<'_>,
150+
_settings: &'a Self::Settings,
151+
_load_context: &'a mut LoadContext<'_>,
152+
) -> Result<TextAsset, Self::Error> {
153+
let mut bytes = Vec::new();
154+
reader.read_to_end(&mut bytes).await?;
155+
let value = String::from_utf8(bytes).unwrap();
156+
Ok(TextAsset(value))
157+
}
158+
159+
fn extensions(&self) -> &[&str] {
160+
&["txt"]
161+
}
162+
}
163+
164+
#[derive(Default)]
165+
pub struct TextSaver;
166+
167+
impl AssetSaver for TextSaver {
168+
type Asset = TextAsset;
169+
type Settings = ();
170+
type OutputLoader = TextLoader;
171+
type Error = std::io::Error;
172+
173+
async fn save<'a>(
174+
&'a self,
175+
writer: &'a mut Writer,
176+
asset: SavedAsset<'a, Self::Asset>,
177+
_settings: &'a Self::Settings,
178+
) -> Result<(), Self::Error> {
179+
writer.write_all(asset.0.as_bytes()).await?;
180+
Ok(())
181+
}
182+
}
183+
}

0 commit comments

Comments
 (0)