Merge 5b6ac36b50
into f964ee1e3a
This commit is contained in:
commit
eee6dccff4
@ -62,6 +62,8 @@ uuid = { version = "1.13.1", default-features = false, features = [
|
||||
"v4",
|
||||
"serde",
|
||||
] }
|
||||
glob = "0.3.2"
|
||||
|
||||
tracing = { version = "0.1", default-features = false }
|
||||
|
||||
[target.'cfg(target_os = "android")'.dependencies]
|
||||
|
31
crates/bevy_asset/src/batch.rs
Normal file
31
crates/bevy_asset/src/batch.rs
Normal file
@ -0,0 +1,31 @@
|
||||
use alloc::vec::Vec;
|
||||
|
||||
use crate::{Asset, AssetPath, UntypedHandle};
|
||||
use bevy_reflect::TypePath;
|
||||
|
||||
pub struct LoadBatchRequest {
|
||||
pub requests: Vec<AssetPath<'static>>,
|
||||
}
|
||||
|
||||
impl LoadBatchRequest {
|
||||
pub fn new<T>(requests: Vec<T>) -> Self
|
||||
where
|
||||
T: Into<AssetPath<'static>>,
|
||||
{
|
||||
Self {
|
||||
requests: requests.into_iter().map(Into::into).collect(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A "loaded batch" containing handles for all assets stored in a given [`AssetPath`].
|
||||
///
|
||||
/// This is produced by [`AssetServer::load_batch`](crate::prelude::AssetServer::load_batch).
|
||||
///
|
||||
/// [`AssetPath`]: crate::AssetPath
|
||||
#[derive(Asset, TypePath)]
|
||||
pub struct LoadedBatch {
|
||||
/// The handles of all assets stored in the batch.
|
||||
#[dependency]
|
||||
pub handles: Vec<UntypedHandle>,
|
||||
}
|
@ -1,16 +0,0 @@
|
||||
use alloc::vec::Vec;
|
||||
|
||||
use crate::{Asset, UntypedHandle};
|
||||
use bevy_reflect::TypePath;
|
||||
|
||||
/// A "loaded folder" containing handles for all assets stored in a given [`AssetPath`].
|
||||
///
|
||||
/// This is produced by [`AssetServer::load_folder`](crate::prelude::AssetServer::load_folder).
|
||||
///
|
||||
/// [`AssetPath`]: crate::AssetPath
|
||||
#[derive(Asset, TypePath)]
|
||||
pub struct LoadedFolder {
|
||||
/// The handles of all assets stored in the folder.
|
||||
#[dependency]
|
||||
pub handles: Vec<UntypedHandle>,
|
||||
}
|
@ -174,9 +174,9 @@ pub mod prelude {
|
||||
|
||||
mod asset_changed;
|
||||
mod assets;
|
||||
mod batch;
|
||||
mod direct_access_ext;
|
||||
mod event;
|
||||
mod folder;
|
||||
mod handle;
|
||||
mod id;
|
||||
mod loader;
|
||||
@ -187,10 +187,10 @@ mod render_asset;
|
||||
mod server;
|
||||
|
||||
pub use assets::*;
|
||||
pub use batch::*;
|
||||
pub use bevy_asset_macros::Asset;
|
||||
pub use direct_access_ext::DirectAssetAccessExt;
|
||||
pub use event::*;
|
||||
pub use folder::*;
|
||||
pub use futures_lite::{AsyncReadExt, AsyncWriteExt};
|
||||
pub use handle::*;
|
||||
pub use id::*;
|
||||
@ -410,7 +410,7 @@ impl Plugin for AssetPlugin {
|
||||
}
|
||||
}
|
||||
app.insert_resource(embedded)
|
||||
.init_asset::<LoadedFolder>()
|
||||
.init_asset::<LoadedBatch>()
|
||||
.init_asset::<LoadedUntypedAsset>()
|
||||
.init_asset::<()>()
|
||||
.add_event::<UntypedAssetLoadFailedEvent>()
|
||||
@ -668,7 +668,7 @@ pub type AssetEvents = AssetEventSystems;
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::{
|
||||
folder::LoadedFolder,
|
||||
batch::LoadedBatch,
|
||||
handle::Handle,
|
||||
io::{
|
||||
gated::{GateOpener, GatedReader},
|
||||
@ -677,7 +677,7 @@ mod tests {
|
||||
},
|
||||
loader::{AssetLoader, LoadContext},
|
||||
Asset, AssetApp, AssetEvent, AssetId, AssetLoadError, AssetLoadFailedEvent, AssetPath,
|
||||
AssetPlugin, AssetServer, Assets, LoadState, UnapprovedPathMode,
|
||||
AssetPlugin, AssetServer, Assets, LoadBatchRequest, LoadState, UnapprovedPathMode,
|
||||
};
|
||||
use alloc::{
|
||||
boxed::Box,
|
||||
@ -1574,7 +1574,7 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn load_folder() {
|
||||
fn load_batch() {
|
||||
// The particular usage of GatedReader in this test will cause deadlocking if running single-threaded
|
||||
#[cfg(not(feature = "multi_threaded"))]
|
||||
panic!("This test requires the \"multi_threaded\" feature, otherwise it will deadlock.\ncargo test --package bevy_asset --features multi_threaded");
|
||||
@ -1618,21 +1618,22 @@ mod tests {
|
||||
.init_asset::<SubText>()
|
||||
.register_asset_loader(CoolTextLoader);
|
||||
let asset_server = app.world().resource::<AssetServer>().clone();
|
||||
let handle: Handle<LoadedFolder> = asset_server.load_folder("text");
|
||||
let handle: Handle<LoadedBatch> =
|
||||
asset_server.load_batch(LoadBatchRequest::new(vec!["text/*", "b.cool.ron"]));
|
||||
gate_opener.open(a_path);
|
||||
gate_opener.open(b_path);
|
||||
gate_opener.open(c_path);
|
||||
|
||||
let mut reader = EventCursor::default();
|
||||
run_app_until(&mut app, |world| {
|
||||
let events = world.resource::<Events<AssetEvent<LoadedFolder>>>();
|
||||
let events = world.resource::<Events<AssetEvent<LoadedBatch>>>();
|
||||
let asset_server = world.resource::<AssetServer>();
|
||||
let loaded_folders = world.resource::<Assets<LoadedFolder>>();
|
||||
let loaded_batchs = world.resource::<Assets<LoadedBatch>>();
|
||||
let cool_texts = world.resource::<Assets<CoolText>>();
|
||||
for event in reader.read(events) {
|
||||
if let AssetEvent::LoadedWithDependencies { id } = event {
|
||||
if *id == handle.id() {
|
||||
let loaded_folder = loaded_folders.get(&handle).unwrap();
|
||||
let loaded_batch = loaded_batchs.get(&handle).unwrap();
|
||||
let a_handle: Handle<CoolText> =
|
||||
asset_server.get_handle("text/a.cool.ron").unwrap();
|
||||
let c_handle: Handle<CoolText> =
|
||||
@ -1640,7 +1641,7 @@ mod tests {
|
||||
|
||||
let mut found_a = false;
|
||||
let mut found_c = false;
|
||||
for asset_handle in &loaded_folder.handles {
|
||||
for asset_handle in &loaded_batch.handles {
|
||||
if asset_handle.id() == a_handle.id().untyped() {
|
||||
found_a = true;
|
||||
} else if asset_handle.id() == c_handle.id().untyped() {
|
||||
@ -1649,7 +1650,7 @@ mod tests {
|
||||
}
|
||||
assert!(found_a);
|
||||
assert!(found_c);
|
||||
assert_eq!(loaded_folder.handles.len(), 2);
|
||||
assert_eq!(loaded_batch.handles.len(), 2);
|
||||
|
||||
let a_text = cool_texts.get(&a_handle).unwrap();
|
||||
let b_text = cool_texts.get(&a_text.dependencies[0]).unwrap();
|
||||
|
@ -2,10 +2,9 @@ mod info;
|
||||
mod loaders;
|
||||
|
||||
use crate::{
|
||||
folder::LoadedFolder,
|
||||
io::{
|
||||
AssetReaderError, AssetSource, AssetSourceEvent, AssetSourceId, AssetSources,
|
||||
AssetWriterError, ErasedAssetReader, MissingAssetSourceError, MissingAssetWriterError,
|
||||
AssetWriterError, MissingAssetSourceError, MissingAssetWriterError,
|
||||
MissingProcessedAssetReaderError, Reader,
|
||||
},
|
||||
loader::{AssetLoader, ErasedAssetLoader, LoadContext, LoadedAsset},
|
||||
@ -15,8 +14,9 @@ use crate::{
|
||||
},
|
||||
path::AssetPath,
|
||||
Asset, AssetEvent, AssetHandleProvider, AssetId, AssetLoadFailedEvent, AssetMetaCheck, Assets,
|
||||
DeserializeMetaError, ErasedLoadedAsset, Handle, LoadedUntypedAsset, UnapprovedPathMode,
|
||||
UntypedAssetId, UntypedAssetLoadFailedEvent, UntypedHandle,
|
||||
DeserializeMetaError, ErasedLoadedAsset, Handle, LoadBatchRequest, LoadedBatch,
|
||||
LoadedUntypedAsset, UnapprovedPathMode, UntypedAssetId, UntypedAssetLoadFailedEvent,
|
||||
UntypedHandle,
|
||||
};
|
||||
use alloc::{borrow::ToOwned, boxed::Box, vec, vec::Vec};
|
||||
use alloc::{
|
||||
@ -31,7 +31,7 @@ use bevy_tasks::IoTaskPool;
|
||||
use core::{any::TypeId, future::Future, panic::AssertUnwindSafe, task::Poll};
|
||||
use crossbeam_channel::{Receiver, Sender};
|
||||
use either::Either;
|
||||
use futures_lite::{FutureExt, StreamExt};
|
||||
use futures_lite::FutureExt;
|
||||
use info::*;
|
||||
use loaders::*;
|
||||
use parking_lot::{RwLock, RwLockWriteGuard};
|
||||
@ -954,114 +954,109 @@ impl AssetServer {
|
||||
handle.typed_debug_checked()
|
||||
}
|
||||
|
||||
/// Loads all assets from the specified folder recursively. The [`LoadedFolder`] asset (when it loads) will
|
||||
/// contain handles to all assets in the folder. You can wait for all assets to load by checking the [`LoadedFolder`]'s
|
||||
/// Loads all assets from the specified folder recursively with batch. The [`LoadedBatch`] asset (when it loads) will
|
||||
/// contain handles to all assets in the folder. You can wait for all assets to load by checking the [`LoadedBatch`]'s
|
||||
/// [`RecursiveDependencyLoadState`].
|
||||
///
|
||||
/// Loading the same folder multiple times will return the same handle. If the `file_watcher`
|
||||
/// feature is enabled, [`LoadedFolder`] handles will reload when a file in the folder is
|
||||
/// feature is enabled, [`LoadedBatch`] handles will reload when a file in the folder is
|
||||
/// removed, added or moved. This includes files in subdirectories and moving, adding,
|
||||
/// or removing complete subdirectories.
|
||||
#[must_use = "not using the returned strong handle may result in the unexpected release of the assets"]
|
||||
pub fn load_folder<'a>(&self, path: impl Into<AssetPath<'a>>) -> Handle<LoadedFolder> {
|
||||
let path = path.into().into_owned();
|
||||
let (handle, should_load) = self
|
||||
.data
|
||||
.infos
|
||||
.write()
|
||||
.get_or_create_path_handle::<LoadedFolder>(
|
||||
path.clone(),
|
||||
HandleLoadingMode::Request,
|
||||
None,
|
||||
);
|
||||
if !should_load {
|
||||
return handle;
|
||||
}
|
||||
let id = handle.id().untyped();
|
||||
self.load_folder_internal(id, path);
|
||||
pub fn load_batch(&self, load_batch_request: LoadBatchRequest) -> Handle<LoadedBatch> {
|
||||
let handle = self.data.infos.write().create_loading_handle_untyped(
|
||||
TypeId::of::<LoadedBatch>(),
|
||||
core::any::type_name::<LoadedBatch>(),
|
||||
);
|
||||
|
||||
handle
|
||||
self.load_batch_internal(handle.id(), load_batch_request);
|
||||
handle.typed_debug_checked()
|
||||
}
|
||||
|
||||
pub(crate) fn load_folder_internal(&self, id: UntypedAssetId, path: AssetPath) {
|
||||
async fn load_folder<'a>(
|
||||
pub(crate) fn load_batch_internal(
|
||||
&self,
|
||||
id: UntypedAssetId,
|
||||
load_batch_request: LoadBatchRequest,
|
||||
) {
|
||||
async fn load_file<'a>(
|
||||
source: AssetSourceId<'static>,
|
||||
path: &'a Path,
|
||||
reader: &'a dyn ErasedAssetReader,
|
||||
server: &'a AssetServer,
|
||||
handles: &'a mut Vec<UntypedHandle>,
|
||||
) -> Result<(), AssetLoadError> {
|
||||
let is_dir = reader.is_directory(path).await?;
|
||||
if is_dir {
|
||||
let mut path_stream = reader.read_directory(path.as_ref()).await?;
|
||||
while let Some(child_path) = path_stream.next().await {
|
||||
if reader.is_directory(&child_path).await? {
|
||||
Box::pin(load_folder(
|
||||
source.clone(),
|
||||
&child_path,
|
||||
reader,
|
||||
server,
|
||||
handles,
|
||||
))
|
||||
.await?;
|
||||
} else {
|
||||
let path = child_path.to_str().expect("Path should be a valid string.");
|
||||
let asset_path = AssetPath::parse(path).with_source(source.clone());
|
||||
match server.load_untyped_async(asset_path).await {
|
||||
Ok(handle) => handles.push(handle),
|
||||
// skip assets that cannot be loaded
|
||||
Err(
|
||||
AssetLoadError::MissingAssetLoaderForTypeName(_)
|
||||
| AssetLoadError::MissingAssetLoaderForExtension(_),
|
||||
) => {}
|
||||
Err(err) => return Err(err),
|
||||
}
|
||||
}
|
||||
}
|
||||
let path = path
|
||||
.strip_prefix("assets\\")
|
||||
.unwrap()
|
||||
.to_str()
|
||||
.expect("Path should be a valid string.");
|
||||
let asset_path = AssetPath::parse(path).with_source(source.clone());
|
||||
|
||||
match server.load_untyped_async(asset_path).await {
|
||||
Ok(handle) => handles.push(handle),
|
||||
// skip assets that cannot be loaded
|
||||
Err(
|
||||
AssetLoadError::MissingAssetLoaderForTypeName(_)
|
||||
| AssetLoadError::MissingAssetLoaderForExtension(_),
|
||||
) => {}
|
||||
Err(err) => return Err(err),
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
let path = path.into_owned();
|
||||
let server = self.clone();
|
||||
IoTaskPool::get()
|
||||
.spawn(async move {
|
||||
let Ok(source) = server.get_source(path.source()) else {
|
||||
error!(
|
||||
"Failed to load {path}. AssetSource {} does not exist",
|
||||
path.source()
|
||||
);
|
||||
return;
|
||||
};
|
||||
let mut handles = Vec::new();
|
||||
for request_path in load_batch_request.requests.iter() {
|
||||
let glob_pattern = format!("assets/{}", request_path);
|
||||
let glob_result = match glob::glob(&glob_pattern) {
|
||||
Ok(g) => g,
|
||||
Err(e) => {
|
||||
error!("Invalid glob pattern {}: {}", request_path, e);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
let asset_reader = match server.data.mode {
|
||||
AssetServerMode::Unprocessed => source.reader(),
|
||||
AssetServerMode::Processed => match source.processed_reader() {
|
||||
Ok(reader) => reader,
|
||||
Err(_) => {
|
||||
for entry in glob_result {
|
||||
let path = match entry {
|
||||
Ok(path) => path,
|
||||
Err(e) => {
|
||||
error!("Failed to read path matching {}: {}", request_path, e);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
if path.is_dir() {
|
||||
continue;
|
||||
}
|
||||
|
||||
let path = AssetPath::from_path_buf(path);
|
||||
|
||||
let Ok(source) = server.get_source(path.source()) else {
|
||||
error!(
|
||||
"Failed to load {path}. AssetSource {} does not have a processed AssetReader",
|
||||
"Failed to load {}. AssetSource {} does not exist",
|
||||
path,
|
||||
path.source()
|
||||
);
|
||||
return;
|
||||
}
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
let mut handles = Vec::new();
|
||||
match load_folder(source.id(), path.path(), asset_reader, &server, &mut handles).await {
|
||||
Ok(_) => server.send_asset_event(InternalAssetEvent::Loaded {
|
||||
id,
|
||||
loaded_asset: LoadedAsset::new_with_dependencies(
|
||||
LoadedFolder { handles },
|
||||
)
|
||||
.into(),
|
||||
}),
|
||||
Err(err) => {
|
||||
error!("Failed to load folder. {err}");
|
||||
server.send_asset_event(InternalAssetEvent::Failed { id, error: err, path });
|
||||
},
|
||||
if let Err(err) =
|
||||
load_file(source.id(), path.path(), &server, &mut handles).await
|
||||
{
|
||||
error!("Failed to load {}: {}", path, err);
|
||||
server.send_asset_event(InternalAssetEvent::Failed {
|
||||
id,
|
||||
error: err,
|
||||
path: path.clone(),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
server.send_asset_event(InternalAssetEvent::Loaded {
|
||||
id,
|
||||
loaded_asset: LoadedAsset::new_with_dependencies(LoadedBatch { handles })
|
||||
.into(),
|
||||
});
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
@ -1701,17 +1696,8 @@ pub fn handle_internal_asset_events(world: &mut World) {
|
||||
}
|
||||
}
|
||||
|
||||
let reload_parent_folders = |path: PathBuf, source: &AssetSourceId<'static>| {
|
||||
let mut current_folder = path;
|
||||
while let Some(parent) = current_folder.parent() {
|
||||
current_folder = parent.to_path_buf();
|
||||
let parent_asset_path =
|
||||
AssetPath::from(current_folder.clone()).with_source(source.clone());
|
||||
for folder_handle in infos.get_path_handles(&parent_asset_path) {
|
||||
info!("Reloading folder {parent_asset_path} because the content has changed");
|
||||
server.load_folder_internal(folder_handle.id(), parent_asset_path.clone());
|
||||
}
|
||||
}
|
||||
let reload_parent_folders = |_path: PathBuf, _source: &AssetSourceId<'static>| {
|
||||
info!("reload_parent_folders");
|
||||
};
|
||||
|
||||
let mut paths_to_reload = <HashSet<_>>::default();
|
||||
|
@ -7,7 +7,11 @@
|
||||
//! Only one padded and one unpadded texture atlas are rendered to the screen.
|
||||
//! An upscaled sprite from each of the four atlases are rendered to the screen.
|
||||
|
||||
use bevy::{asset::LoadedFolder, image::ImageSampler, prelude::*};
|
||||
use bevy::{
|
||||
asset::{LoadBatchRequest, LoadedBatch},
|
||||
image::ImageSampler,
|
||||
prelude::*,
|
||||
};
|
||||
|
||||
fn main() {
|
||||
App::new()
|
||||
@ -27,17 +31,19 @@ enum AppState {
|
||||
}
|
||||
|
||||
#[derive(Resource, Default)]
|
||||
struct RpgSpriteFolder(Handle<LoadedFolder>);
|
||||
struct RpgSpriteFolder(Handle<LoadedBatch>);
|
||||
|
||||
fn load_textures(mut commands: Commands, asset_server: Res<AssetServer>) {
|
||||
// Load multiple, individual sprites from a folder
|
||||
commands.insert_resource(RpgSpriteFolder(asset_server.load_folder("textures/rpg")));
|
||||
commands.insert_resource(RpgSpriteFolder(asset_server.load_batch(
|
||||
LoadBatchRequest::new(vec!["textures/rpg/**/*", "textures/rpg/chars/**/*"]),
|
||||
)));
|
||||
}
|
||||
|
||||
fn check_textures(
|
||||
mut next_state: ResMut<NextState<AppState>>,
|
||||
rpg_sprite_folder: Res<RpgSpriteFolder>,
|
||||
mut events: EventReader<AssetEvent<LoadedFolder>>,
|
||||
mut events: EventReader<AssetEvent<LoadedBatch>>,
|
||||
) {
|
||||
// Advance the `AppState` once all sprite handles have been loaded by the `AssetServer`
|
||||
for event in events.read() {
|
||||
@ -52,7 +58,7 @@ fn setup(
|
||||
rpg_sprite_handles: Res<RpgSpriteFolder>,
|
||||
asset_server: Res<AssetServer>,
|
||||
mut texture_atlases: ResMut<Assets<TextureAtlasLayout>>,
|
||||
loaded_folders: Res<Assets<LoadedFolder>>,
|
||||
loaded_folders: Res<Assets<LoadedBatch>>,
|
||||
mut textures: ResMut<Assets<Image>>,
|
||||
) {
|
||||
let loaded_folder = loaded_folders.get(&rpg_sprite_handles.0).unwrap();
|
||||
@ -215,7 +221,7 @@ fn setup(
|
||||
/// Create a texture atlas with the given padding and sampling settings
|
||||
/// from the individual sprites in the given folder.
|
||||
fn create_texture_atlas(
|
||||
folder: &LoadedFolder,
|
||||
folder: &LoadedBatch,
|
||||
padding: Option<UVec2>,
|
||||
sampling: Option<ImageSampler>,
|
||||
textures: &mut ResMut<Assets<Image>>,
|
||||
|
@ -1,6 +1,6 @@
|
||||
//! This example illustrates various ways to load assets.
|
||||
|
||||
use bevy::{asset::LoadedFolder, prelude::*};
|
||||
use bevy::{asset::LoadBatchRequest, prelude::*};
|
||||
|
||||
fn main() {
|
||||
App::new()
|
||||
@ -52,7 +52,8 @@ fn setup(
|
||||
// to load.
|
||||
// If you want to keep the assets in the folder alive, make sure you store the returned handle
|
||||
// somewhere.
|
||||
let _loaded_folder: Handle<LoadedFolder> = asset_server.load_folder("models/torus");
|
||||
let _loaded_folder =
|
||||
asset_server.load_batch(LoadBatchRequest::new(vec!["models/torus/torus.gltf"]));
|
||||
|
||||
// If you want a handle to a specific asset in a loaded folder, the easiest way to get one is to call load.
|
||||
// It will _not_ be loaded a second time.
|
||||
|
Loading…
Reference in New Issue
Block a user