From 3808ea9edeb93fc36bac1181a296d0b313c80f47 Mon Sep 17 00:00:00 2001 From: shishanyue <62888460+shishanyue@users.noreply.github.com> Date: Thu, 19 Jun 2025 11:42:35 +0800 Subject: [PATCH] add LoadBatch to LoadedFolder --- Cargo.toml | 8 +- crates/bevy_asset/Cargo.toml | 2 + crates/bevy_asset/src/folder.rs | 61 +++++++- crates/bevy_asset/src/lib.rs | 2 +- crates/bevy_asset/src/server/mod.rs | 138 ++++++++++++++---- examples/2d/texture_atlas.rs | 4 +- examples/README.md | 2 +- examples/asset/asset_loading.rs | 2 +- .../{folder_filter.rs => folder_batch.rs} | 18 +-- 9 files changed, 184 insertions(+), 53 deletions(-) rename examples/asset/{folder_filter.rs => folder_batch.rs} (96%) diff --git a/Cargo.toml b/Cargo.toml index be7ae76c1d..cd2f26d4ec 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1782,13 +1782,13 @@ category = "Assets" wasm = false [[example]] -name = "folder_filter" -path = "examples/asset/folder_filter.rs" +name = "folder_batch" +path = "examples/asset/folder_batch.rs" doc-scrape-examples = true -[package.metadata.example.folder_filter] +[package.metadata.example.folder_batch] name = "Folder Filter" -description = "Load Folder With Filter" +description = "Load Folder With Batch" category = "Assets" wasm = false diff --git a/crates/bevy_asset/Cargo.toml b/crates/bevy_asset/Cargo.toml index e91987f40a..03a2c0218d 100644 --- a/crates/bevy_asset/Cargo.toml +++ b/crates/bevy_asset/Cargo.toml @@ -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] diff --git a/crates/bevy_asset/src/folder.rs b/crates/bevy_asset/src/folder.rs index 6fa5e4263b..9471dd6d0b 100644 --- a/crates/bevy_asset/src/folder.rs +++ b/crates/bevy_asset/src/folder.rs @@ -1,10 +1,66 @@ -use std::{path::Path, sync::Arc}; +use std::sync::Arc; use alloc::vec::Vec; use crate::{Asset, UntypedHandle}; use bevy_reflect::TypePath; +/// This is use for [`AssetServer::load_folderload_folder_with_batch`](crate::prelude::AssetServer::load_folder_with_batch). +#[derive(Debug, Clone, Copy, Default)] +pub enum LoadBatchKind { + #[default] + White, //Allow loading + Black, //Disallow loading +} + +impl LoadBatchKind { + pub fn apply(&self, expr: bool) -> bool { + match self { + LoadBatchKind::White => expr, + LoadBatchKind::Black => !expr, + } + } +} + +#[derive(Debug, Clone, Default)] +pub struct LoadBatch { + pub paths: Option>>, + pub paths_kind: LoadBatchKind, + pub extensions: Option>>, + pub extensions_kind: LoadBatchKind, +} + +impl LoadBatch { + pub fn new( + paths: Vec<&'static str>, + paths_kind: LoadBatchKind, + extensions: Vec<&'static str>, + extensions_kind: LoadBatchKind, + ) -> Self { + Self { + paths: Some(Arc::new(paths)), + paths_kind, + extensions: Some(Arc::new(extensions)), + extensions_kind, + } + } + pub fn paths(paths: Vec<&'static str>, paths_kind: LoadBatchKind) -> Self { + Self { + paths: Some(Arc::new(paths)), + paths_kind, + extensions: None, + ..Default::default() + } + } + pub fn extensions(extensions: Vec<&'static str>, extensions_kind: LoadBatchKind) -> Self { + Self { + paths: None, + extensions: Some(Arc::new(extensions)), + extensions_kind, + ..Default::default() + } + } +} /// 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). @@ -15,5 +71,6 @@ pub struct LoadedFolder { /// The handles of all assets stored in the folder. #[dependency] pub handles: Vec, - pub filter: Option bool + Send + Sync + 'static>>, + /// For filtering files that are required or not required. + pub load_batch: Option, } diff --git a/crates/bevy_asset/src/lib.rs b/crates/bevy_asset/src/lib.rs index 1859ea8f80..4b29beae79 100644 --- a/crates/bevy_asset/src/lib.rs +++ b/crates/bevy_asset/src/lib.rs @@ -1618,7 +1618,7 @@ mod tests { .init_asset::() .register_asset_loader(CoolTextLoader); let asset_server = app.world().resource::().clone(); - let handle: Handle = asset_server.load_folder("text", None); + let handle: Handle = asset_server.load_folder("text"); gate_opener.open(a_path); gate_opener.open(b_path); gate_opener.open(c_path); diff --git a/crates/bevy_asset/src/server/mod.rs b/crates/bevy_asset/src/server/mod.rs index bd60d6d2af..ed7e0da490 100644 --- a/crates/bevy_asset/src/server/mod.rs +++ b/crates/bevy_asset/src/server/mod.rs @@ -2,7 +2,7 @@ mod info; mod loaders; use crate::{ - folder::LoadedFolder, + folder::{LoadBatch, LoadedFolder}, io::{ AssetReaderError, AssetSource, AssetSourceEvent, AssetSourceId, AssetSources, AssetWriterError, ErasedAssetReader, MissingAssetSourceError, MissingAssetWriterError, @@ -15,8 +15,8 @@ use crate::{ }, path::AssetPath, Asset, AssetEvent, AssetHandleProvider, AssetId, AssetLoadFailedEvent, AssetMetaCheck, Assets, - DeserializeMetaError, ErasedLoadedAsset, Handle, LoadedUntypedAsset, UnapprovedPathMode, - UntypedAssetId, UntypedAssetLoadFailedEvent, UntypedHandle, + DeserializeMetaError, ErasedLoadedAsset, Handle, LoadBatchKind, LoadedUntypedAsset, + UnapprovedPathMode, UntypedAssetId, UntypedAssetLoadFailedEvent, UntypedHandle, }; use alloc::{borrow::ToOwned, boxed::Box, vec, vec::Vec}; use alloc::{ @@ -32,6 +32,7 @@ use core::{any::TypeId, future::Future, panic::AssertUnwindSafe, task::Poll}; use crossbeam_channel::{Receiver, Sender}; use either::Either; use futures_lite::{FutureExt, StreamExt}; +use glob::Pattern; use info::*; use loaders::*; use parking_lot::{RwLock, RwLockWriteGuard}; @@ -944,10 +945,38 @@ impl AssetServer { /// 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>( + pub fn load_folder<'a>(&self, path: impl Into>) -> Handle { + let path = path.into().into_owned(); + let (handle, should_load) = self + .data + .infos + .write() + .get_or_create_path_handle::( + path.clone(), + HandleLoadingMode::Request, + None, + ); + if !should_load { + return handle; + } + let id = handle.id().untyped(); + self.load_folder_internal(id, path, None); + + handle + } + /// Loads all assets from the specified folder recursively with batch. 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 + /// [`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 + /// 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_with_batch<'a>( &self, path: impl Into>, - filter: Option bool + Send + Sync + 'static>>, + load_batch: LoadBatch, ) -> Handle { let path = path.into().into_owned(); let (handle, should_load) = self @@ -963,16 +992,14 @@ impl AssetServer { return handle; } let id = handle.id().untyped(); - self.load_folder_internal(id, path, filter); - + self.load_folder_internal(id, path, Some(load_batch)); handle } - pub(crate) fn load_folder_internal( &self, id: UntypedAssetId, path: AssetPath, - filter: Option bool + Send + Sync + 'static>>, + load_batch: Option, ) { async fn load_folder<'a>( source: AssetSourceId<'static>, @@ -980,36 +1007,51 @@ impl AssetServer { reader: &'a dyn ErasedAssetReader, server: &'a AssetServer, handles: &'a mut Vec, - filter: Option bool + Send + Sync + 'static>>, + patterns: &'a Vec, + patterns_kind: LoadBatchKind, + extensions: &'a Option>>, + extensions_kind: LoadBatchKind, ) -> 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 { - let child_is_dir = reader.is_directory(&child_path).await?; - if let Some(ref filter_fn) = filter { - if !filter_fn(&child_path, child_is_dir) { - continue; - } - } - if child_is_dir { + if reader.is_directory(&child_path).await? { Box::pin(load_folder( source.clone(), &child_path, reader, server, handles, - filter.clone(), + patterns, + patterns_kind, + extensions, + extensions_kind, )) .await?; } else { let path = child_path.to_str().expect("Path should be a valid string."); - if let Some(ref filter_fn) = filter { - if !filter_fn(&child_path, false) { - continue; - } + + if !patterns_kind + .apply(patterns.iter().any(|pattern| pattern.matches(path))) + { + continue; + } + + if let Some(extensions) = extensions { + match child_path.extension() { + Some(ext) => { + if !extensions_kind.apply(extensions.contains( + &ext.to_str().expect("Path should be a valid string."), + )) { + continue; + } + } + None => break, + }; } 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 @@ -1026,6 +1068,32 @@ impl AssetServer { } let path = path.into_owned(); + let patterns: Vec = load_batch + .as_ref() + .map(|batch| { + batch + .paths + .clone() + .unwrap_or(Arc::new(vec!["*/*"])) + .iter() + .map(|path| Pattern::new(path).expect("Failed to create pattern from path")) + .collect() + }) + .unwrap_or(vec![Pattern::new("*/*").unwrap()]); + let patterns_kind = load_batch + .as_ref() + .map(|batch| batch.paths_kind) + .unwrap_or_default(); + + let extensions = load_batch + .as_ref() + .map(|batch| batch.extensions.clone()) + .unwrap_or_default(); + let extensions_kind = load_batch + .as_ref() + .map(|batch| batch.extensions_kind) + .unwrap_or_default(); + let server = self.clone(); IoTaskPool::get() .spawn(async move { @@ -1052,12 +1120,11 @@ impl AssetServer { }; let mut handles = Vec::new(); - let filter_clone = filter.clone(); - match load_folder(source.id(), path.path(), asset_reader, &server, &mut handles,filter).await { + match load_folder(source.id(), path.path(), asset_reader, &server, &mut handles,&patterns,patterns_kind,&extensions,extensions_kind).await { Ok(_) => server.send_asset_event(InternalAssetEvent::Loaded { id, loaded_asset: LoadedAsset::new_with_dependencies( - LoadedFolder { handles,filter:filter_clone }, + LoadedFolder { handles,load_batch }, ) .into(), }), @@ -1643,12 +1710,16 @@ impl AssetServer { /// A system that manages internal [`AssetServer`] events, such as finalizing asset loads. pub fn handle_internal_asset_events(world: &mut World) { world.resource_scope(|world, server: Mut| { - let mut folder_filters: HashMap< - AssetPath<'_>, - Option bool + Send + Sync + 'static>>, - > = HashMap::new(); - for (id, loaded_folder) in world.get_resource::>().unwrap().iter() { - folder_filters.insert(server.get_path(id).unwrap(), loaded_folder.filter.clone()); + let mut load_batchs: HashMap, Option> = HashMap::new(); + for (id, loaded_folder) in world + .get_resource::>() + .expect("Could not get LoadedFolder Assets") + .iter() + { + load_batchs.insert( + server.get_path(id).expect("Path should be a valid string."), + loaded_folder.load_batch.clone(), + ); } let mut infos = server.data.infos.write(); let var_name = vec![]; @@ -1723,7 +1794,10 @@ pub fn handle_internal_asset_events(world: &mut World) { server.load_folder_internal( folder_handle.id(), parent_asset_path.clone(), - folder_filters.get(&parent_asset_path).unwrap().clone(), + load_batchs + .get(&parent_asset_path) + .expect("parent folder is doesn't loaded") + .clone(), ); } } diff --git a/examples/2d/texture_atlas.rs b/examples/2d/texture_atlas.rs index 8e9b2b819e..25106adcfb 100644 --- a/examples/2d/texture_atlas.rs +++ b/examples/2d/texture_atlas.rs @@ -31,9 +31,7 @@ struct RpgSpriteFolder(Handle); fn load_textures(mut commands: Commands, asset_server: Res) { // Load multiple, individual sprites from a folder - commands.insert_resource(RpgSpriteFolder( - asset_server.load_folder("textures/rpg", None), - )); + commands.insert_resource(RpgSpriteFolder(asset_server.load_folder("textures/rpg"))); } fn check_textures( diff --git a/examples/README.md b/examples/README.md index 3d0cfd97c6..9e1d3c4b4d 100644 --- a/examples/README.md +++ b/examples/README.md @@ -254,7 +254,7 @@ Example | Description [Custom Asset IO](../examples/asset/custom_asset_reader.rs) | Implements a custom AssetReader [Embedded Asset](../examples/asset/embedded_asset.rs) | Embed an asset in the application binary and load it [Extra asset source](../examples/asset/extra_source.rs) | Load an asset from a non-standard asset source -[Folder Filter](../examples/asset/folder_filter.rs) | Load Folder With Filter +[Folder Filter](../examples/asset/folder_batch.rs) | Load Folder With Batch [Hot Reloading of Assets](../examples/asset/hot_asset_reloading.rs) | Demonstrates automatic reloading of assets when modified on disk [Multi-asset synchronization](../examples/asset/multi_asset_sync.rs) | Demonstrates how to wait for multiple assets to be loaded. [Repeated texture configuration](../examples/asset/repeated_texture.rs) | How to configure the texture to repeat instead of the default clamp to edges diff --git a/examples/asset/asset_loading.rs b/examples/asset/asset_loading.rs index 5e53be3724..49d2dca279 100644 --- a/examples/asset/asset_loading.rs +++ b/examples/asset/asset_loading.rs @@ -52,7 +52,7 @@ 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 = asset_server.load_folder("models/torus", None); + let _loaded_folder: Handle = asset_server.load_folder("models/torus"); // 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. diff --git a/examples/asset/folder_filter.rs b/examples/asset/folder_batch.rs similarity index 96% rename from examples/asset/folder_filter.rs rename to examples/asset/folder_batch.rs index 4e7eed052c..741d23403a 100644 --- a/examples/asset/folder_filter.rs +++ b/examples/asset/folder_batch.rs @@ -1,6 +1,6 @@ //! In this example we generate four texture atlases (sprite sheets) from a folder containing //! individual sprites.But the folder containing waste files called "waste.txt" -//! So we use filter to ingrone the waste files +//! So we use load_folder_with_batch to ingrone the waste files and textures/rpg_with_waste/tiles/* //! //! The texture atlases are generated with different padding and sampling to demonstrate the //! effect of these settings, and how bleeding issues can be resolved by padding the sprites. @@ -8,9 +8,8 @@ //! 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 std::sync::Arc; - use bevy::{asset::LoadedFolder, image::ImageSampler, prelude::*}; +use bevy_asset::{LoadBatch, LoadBatchKind}; fn main() { App::new() @@ -34,13 +33,14 @@ struct RpgSpriteFolder(Handle); fn load_textures(mut commands: Commands, asset_server: Res) { // Load multiple, individual sprites from a folder - commands.insert_resource(RpgSpriteFolder(asset_server.load_folder( + commands.insert_resource(RpgSpriteFolder(asset_server.load_folder_with_batch( "textures/rpg_with_waste", - Some(Arc::new(|path, is_dir| { - let need = is_dir || path.extension().unwrap_or_default() == "png"; - info!("is_dir:{} path:{:?} need:{}", is_dir, path, need); - need - })), + LoadBatch::new( + vec!["textures/rpg_with_waste/tiles/*"], + LoadBatchKind::Black, + vec!["png"], + LoadBatchKind::White, + ), ))); }