This commit is contained in:
shishanyue 2025-07-17 17:38:04 +08:00 committed by GitHub
commit eee6dccff4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 144 additions and 133 deletions

View File

@ -62,6 +62,8 @@ uuid = { version = "1.13.1", default-features = false, features = [
"v4", "v4",
"serde", "serde",
] } ] }
glob = "0.3.2"
tracing = { version = "0.1", default-features = false } tracing = { version = "0.1", default-features = false }
[target.'cfg(target_os = "android")'.dependencies] [target.'cfg(target_os = "android")'.dependencies]

View 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>,
}

View File

@ -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>,
}

View File

@ -174,9 +174,9 @@ pub mod prelude {
mod asset_changed; mod asset_changed;
mod assets; mod assets;
mod batch;
mod direct_access_ext; mod direct_access_ext;
mod event; mod event;
mod folder;
mod handle; mod handle;
mod id; mod id;
mod loader; mod loader;
@ -187,10 +187,10 @@ mod render_asset;
mod server; mod server;
pub use assets::*; pub use assets::*;
pub use batch::*;
pub use bevy_asset_macros::Asset; pub use bevy_asset_macros::Asset;
pub use direct_access_ext::DirectAssetAccessExt; pub use direct_access_ext::DirectAssetAccessExt;
pub use event::*; pub use event::*;
pub use folder::*;
pub use futures_lite::{AsyncReadExt, AsyncWriteExt}; pub use futures_lite::{AsyncReadExt, AsyncWriteExt};
pub use handle::*; pub use handle::*;
pub use id::*; pub use id::*;
@ -410,7 +410,7 @@ impl Plugin for AssetPlugin {
} }
} }
app.insert_resource(embedded) app.insert_resource(embedded)
.init_asset::<LoadedFolder>() .init_asset::<LoadedBatch>()
.init_asset::<LoadedUntypedAsset>() .init_asset::<LoadedUntypedAsset>()
.init_asset::<()>() .init_asset::<()>()
.add_event::<UntypedAssetLoadFailedEvent>() .add_event::<UntypedAssetLoadFailedEvent>()
@ -668,7 +668,7 @@ pub type AssetEvents = AssetEventSystems;
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use crate::{ use crate::{
folder::LoadedFolder, batch::LoadedBatch,
handle::Handle, handle::Handle,
io::{ io::{
gated::{GateOpener, GatedReader}, gated::{GateOpener, GatedReader},
@ -677,7 +677,7 @@ mod tests {
}, },
loader::{AssetLoader, LoadContext}, loader::{AssetLoader, LoadContext},
Asset, AssetApp, AssetEvent, AssetId, AssetLoadError, AssetLoadFailedEvent, AssetPath, Asset, AssetApp, AssetEvent, AssetId, AssetLoadError, AssetLoadFailedEvent, AssetPath,
AssetPlugin, AssetServer, Assets, LoadState, UnapprovedPathMode, AssetPlugin, AssetServer, Assets, LoadBatchRequest, LoadState, UnapprovedPathMode,
}; };
use alloc::{ use alloc::{
boxed::Box, boxed::Box,
@ -1574,7 +1574,7 @@ mod tests {
} }
#[test] #[test]
fn load_folder() { fn load_batch() {
// The particular usage of GatedReader in this test will cause deadlocking if running single-threaded // The particular usage of GatedReader in this test will cause deadlocking if running single-threaded
#[cfg(not(feature = "multi_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"); 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>() .init_asset::<SubText>()
.register_asset_loader(CoolTextLoader); .register_asset_loader(CoolTextLoader);
let asset_server = app.world().resource::<AssetServer>().clone(); 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(a_path);
gate_opener.open(b_path); gate_opener.open(b_path);
gate_opener.open(c_path); gate_opener.open(c_path);
let mut reader = EventCursor::default(); let mut reader = EventCursor::default();
run_app_until(&mut app, |world| { 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 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>>(); let cool_texts = world.resource::<Assets<CoolText>>();
for event in reader.read(events) { for event in reader.read(events) {
if let AssetEvent::LoadedWithDependencies { id } = event { if let AssetEvent::LoadedWithDependencies { id } = event {
if *id == handle.id() { 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> = let a_handle: Handle<CoolText> =
asset_server.get_handle("text/a.cool.ron").unwrap(); asset_server.get_handle("text/a.cool.ron").unwrap();
let c_handle: Handle<CoolText> = let c_handle: Handle<CoolText> =
@ -1640,7 +1641,7 @@ mod tests {
let mut found_a = false; let mut found_a = false;
let mut found_c = 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() { if asset_handle.id() == a_handle.id().untyped() {
found_a = true; found_a = true;
} else if asset_handle.id() == c_handle.id().untyped() { } else if asset_handle.id() == c_handle.id().untyped() {
@ -1649,7 +1650,7 @@ mod tests {
} }
assert!(found_a); assert!(found_a);
assert!(found_c); 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 a_text = cool_texts.get(&a_handle).unwrap();
let b_text = cool_texts.get(&a_text.dependencies[0]).unwrap(); let b_text = cool_texts.get(&a_text.dependencies[0]).unwrap();

View File

@ -2,10 +2,9 @@ mod info;
mod loaders; mod loaders;
use crate::{ use crate::{
folder::LoadedFolder,
io::{ io::{
AssetReaderError, AssetSource, AssetSourceEvent, AssetSourceId, AssetSources, AssetReaderError, AssetSource, AssetSourceEvent, AssetSourceId, AssetSources,
AssetWriterError, ErasedAssetReader, MissingAssetSourceError, MissingAssetWriterError, AssetWriterError, MissingAssetSourceError, MissingAssetWriterError,
MissingProcessedAssetReaderError, Reader, MissingProcessedAssetReaderError, Reader,
}, },
loader::{AssetLoader, ErasedAssetLoader, LoadContext, LoadedAsset}, loader::{AssetLoader, ErasedAssetLoader, LoadContext, LoadedAsset},
@ -15,8 +14,9 @@ use crate::{
}, },
path::AssetPath, path::AssetPath,
Asset, AssetEvent, AssetHandleProvider, AssetId, AssetLoadFailedEvent, AssetMetaCheck, Assets, Asset, AssetEvent, AssetHandleProvider, AssetId, AssetLoadFailedEvent, AssetMetaCheck, Assets,
DeserializeMetaError, ErasedLoadedAsset, Handle, LoadedUntypedAsset, UnapprovedPathMode, DeserializeMetaError, ErasedLoadedAsset, Handle, LoadBatchRequest, LoadedBatch,
UntypedAssetId, UntypedAssetLoadFailedEvent, UntypedHandle, LoadedUntypedAsset, UnapprovedPathMode, UntypedAssetId, UntypedAssetLoadFailedEvent,
UntypedHandle,
}; };
use alloc::{borrow::ToOwned, boxed::Box, vec, vec::Vec}; use alloc::{borrow::ToOwned, boxed::Box, vec, vec::Vec};
use alloc::{ use alloc::{
@ -31,7 +31,7 @@ use bevy_tasks::IoTaskPool;
use core::{any::TypeId, future::Future, panic::AssertUnwindSafe, task::Poll}; use core::{any::TypeId, future::Future, panic::AssertUnwindSafe, task::Poll};
use crossbeam_channel::{Receiver, Sender}; use crossbeam_channel::{Receiver, Sender};
use either::Either; use either::Either;
use futures_lite::{FutureExt, StreamExt}; use futures_lite::FutureExt;
use info::*; use info::*;
use loaders::*; use loaders::*;
use parking_lot::{RwLock, RwLockWriteGuard}; use parking_lot::{RwLock, RwLockWriteGuard};
@ -954,59 +954,42 @@ impl AssetServer {
handle.typed_debug_checked() handle.typed_debug_checked()
} }
/// Loads all assets from the specified folder recursively. The [`LoadedFolder`] asset (when it loads) will /// 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 [`LoadedFolder`]'s /// contain handles to all assets in the folder. You can wait for all assets to load by checking the [`LoadedBatch`]'s
/// [`RecursiveDependencyLoadState`]. /// [`RecursiveDependencyLoadState`].
/// ///
/// Loading the same folder multiple times will return the same handle. If the `file_watcher` /// 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, /// removed, added or moved. This includes files in subdirectories and moving, adding,
/// or removing complete subdirectories. /// or removing complete subdirectories.
#[must_use = "not using the returned strong handle may result in the unexpected release of the assets"] #[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> { pub fn load_batch(&self, load_batch_request: LoadBatchRequest) -> Handle<LoadedBatch> {
let path = path.into().into_owned(); let handle = self.data.infos.write().create_loading_handle_untyped(
let (handle, should_load) = self TypeId::of::<LoadedBatch>(),
.data core::any::type_name::<LoadedBatch>(),
.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);
handle self.load_batch_internal(handle.id(), load_batch_request);
handle.typed_debug_checked()
} }
pub(crate) fn load_batch_internal(
pub(crate) fn load_folder_internal(&self, id: UntypedAssetId, path: AssetPath) { &self,
async fn load_folder<'a>( id: UntypedAssetId,
load_batch_request: LoadBatchRequest,
) {
async fn load_file<'a>(
source: AssetSourceId<'static>, source: AssetSourceId<'static>,
path: &'a Path, path: &'a Path,
reader: &'a dyn ErasedAssetReader,
server: &'a AssetServer, server: &'a AssetServer,
handles: &'a mut Vec<UntypedHandle>, handles: &'a mut Vec<UntypedHandle>,
) -> Result<(), AssetLoadError> { ) -> Result<(), AssetLoadError> {
let is_dir = reader.is_directory(path).await?; let path = path
if is_dir { .strip_prefix("assets\\")
let mut path_stream = reader.read_directory(path.as_ref()).await?; .unwrap()
while let Some(child_path) = path_stream.next().await { .to_str()
if reader.is_directory(&child_path).await? { .expect("Path should be a valid string.");
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()); let asset_path = AssetPath::parse(path).with_source(source.clone());
match server.load_untyped_async(asset_path).await { match server.load_untyped_async(asset_path).await {
Ok(handle) => handles.push(handle), Ok(handle) => handles.push(handle),
// skip assets that cannot be loaded // skip assets that cannot be loaded
@ -1016,52 +999,64 @@ impl AssetServer {
) => {} ) => {}
Err(err) => return Err(err), Err(err) => return Err(err),
} }
}
}
}
Ok(()) Ok(())
} }
let path = path.into_owned();
let server = self.clone(); let server = self.clone();
IoTaskPool::get() IoTaskPool::get()
.spawn(async move { .spawn(async move {
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;
}
};
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 { let Ok(source) = server.get_source(path.source()) else {
error!( error!(
"Failed to load {path}. AssetSource {} does not exist", "Failed to load {}. AssetSource {} does not exist",
path,
path.source() path.source()
); );
return; return;
}; };
let asset_reader = match server.data.mode { if let Err(err) =
AssetServerMode::Unprocessed => source.reader(), load_file(source.id(), path.path(), &server, &mut handles).await
AssetServerMode::Processed => match source.processed_reader() { {
Ok(reader) => reader, error!("Failed to load {}: {}", path, err);
Err(_) => { server.send_asset_event(InternalAssetEvent::Failed {
error!(
"Failed to load {path}. AssetSource {} does not have a processed AssetReader",
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, id,
loaded_asset: LoadedAsset::new_with_dependencies( error: err,
LoadedFolder { handles }, path: path.clone(),
) });
.into(),
}),
Err(err) => {
error!("Failed to load folder. {err}");
server.send_asset_event(InternalAssetEvent::Failed { id, error: err, path });
},
} }
}
}
server.send_asset_event(InternalAssetEvent::Loaded {
id,
loaded_asset: LoadedAsset::new_with_dependencies(LoadedBatch { handles })
.into(),
});
}) })
.detach(); .detach();
} }
@ -1701,17 +1696,8 @@ pub fn handle_internal_asset_events(world: &mut World) {
} }
} }
let reload_parent_folders = |path: PathBuf, source: &AssetSourceId<'static>| { let reload_parent_folders = |_path: PathBuf, _source: &AssetSourceId<'static>| {
let mut current_folder = path; info!("reload_parent_folders");
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 mut paths_to_reload = <HashSet<_>>::default(); let mut paths_to_reload = <HashSet<_>>::default();

View File

@ -7,7 +7,11 @@
//! Only one padded and one unpadded texture atlas are rendered to the screen. //! 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. //! 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() { fn main() {
App::new() App::new()
@ -27,17 +31,19 @@ enum AppState {
} }
#[derive(Resource, Default)] #[derive(Resource, Default)]
struct RpgSpriteFolder(Handle<LoadedFolder>); struct RpgSpriteFolder(Handle<LoadedBatch>);
fn load_textures(mut commands: Commands, asset_server: Res<AssetServer>) { fn load_textures(mut commands: Commands, asset_server: Res<AssetServer>) {
// Load multiple, individual sprites from a folder // 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( fn check_textures(
mut next_state: ResMut<NextState<AppState>>, mut next_state: ResMut<NextState<AppState>>,
rpg_sprite_folder: Res<RpgSpriteFolder>, 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` // Advance the `AppState` once all sprite handles have been loaded by the `AssetServer`
for event in events.read() { for event in events.read() {
@ -52,7 +58,7 @@ fn setup(
rpg_sprite_handles: Res<RpgSpriteFolder>, rpg_sprite_handles: Res<RpgSpriteFolder>,
asset_server: Res<AssetServer>, asset_server: Res<AssetServer>,
mut texture_atlases: ResMut<Assets<TextureAtlasLayout>>, mut texture_atlases: ResMut<Assets<TextureAtlasLayout>>,
loaded_folders: Res<Assets<LoadedFolder>>, loaded_folders: Res<Assets<LoadedBatch>>,
mut textures: ResMut<Assets<Image>>, mut textures: ResMut<Assets<Image>>,
) { ) {
let loaded_folder = loaded_folders.get(&rpg_sprite_handles.0).unwrap(); 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 /// Create a texture atlas with the given padding and sampling settings
/// from the individual sprites in the given folder. /// from the individual sprites in the given folder.
fn create_texture_atlas( fn create_texture_atlas(
folder: &LoadedFolder, folder: &LoadedBatch,
padding: Option<UVec2>, padding: Option<UVec2>,
sampling: Option<ImageSampler>, sampling: Option<ImageSampler>,
textures: &mut ResMut<Assets<Image>>, textures: &mut ResMut<Assets<Image>>,

View File

@ -1,6 +1,6 @@
//! This example illustrates various ways to load assets. //! This example illustrates various ways to load assets.
use bevy::{asset::LoadedFolder, prelude::*}; use bevy::{asset::LoadBatchRequest, prelude::*};
fn main() { fn main() {
App::new() App::new()
@ -52,7 +52,8 @@ fn setup(
// to load. // to load.
// If you want to keep the assets in the folder alive, make sure you store the returned handle // If you want to keep the assets in the folder alive, make sure you store the returned handle
// somewhere. // 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. // 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. // It will _not_ be loaded a second time.