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",
"serde",
] }
glob = "0.3.2"
tracing = { version = "0.1", default-features = false }
[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 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();

View File

@ -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();

View File

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

View File

@ -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.