Non-blocking load_untyped using a wrapper asset (#10198)
# Objective
- Assets v2 does not currently offer a public API to load untyped assets
## Solution
- Wrap the untyped handle in a `LoadedUntypedAsset` asset to offer a
non-blocking load for untyped assets. The user does not need to know the
actual asset type.
- Handles to `LoadedUntypedAsset` have the same path as the wrapped
asset, but their handles are shared using a label.
The user side of `load_untyped` looks like this:
```rust
use bevy::prelude::*;
use bevy_internal::asset::LoadedUntypedAsset;
fn main() {
App::new()
.add_plugins(DefaultPlugins)
.add_systems(Startup, setup)
.add_systems(Update, check)
.run();
}
#[derive(Resource)]
struct UntypedAsset {
handle: Handle<LoadedUntypedAsset>,
}
fn setup(
mut commands: Commands,
asset_server: Res<AssetServer>,
) {
let handle = asset_server.load_untyped("branding/banner.png");
commands.insert_resource(UntypedAsset { handle });
commands.spawn(Camera2dBundle::default());
}
fn check(
mut commands: Commands,
res: Option<Res<UntypedAsset>>,
assets: Res<Assets<LoadedUntypedAsset>>,
) {
if let Some(untyped_asset) = res {
if let Some(asset) = assets.get(&untyped_asset.handle) {
commands.spawn(SpriteBundle {
texture: asset.handle.clone().typed(),
..default()
});
commands.remove_resource::<UntypedAsset>();
}
}
}
```
---
## Changelog
- `load_untyped` on the asset server now returns a handle to a
`LoadedUntypedAsset` instead of an untyped handle to the asset at the
given path. The untyped handle for the given path can be retrieved from
the `LoadedUntypedAsset` once it is done loading.
## Migration Guide
Whenever possible use the typed API in order to directly get a handle to
your asset. If you do not know the type or need to use `load_untyped`
for a different reason, Bevy 0.12 introduces an additional layer of
indirection. The asset server will return a handle to a
`LoadedUntypedAsset`, which will load in the background. Once it is
loaded, the untyped handle to the asset file can be retrieved from the
`LoadedUntypedAsset`s field `handle`.
---------
Co-authored-by: Carter Anderson <mcanders1@gmail.com>
This commit is contained in:
parent
befbf52a18
commit
77309ba5d8
@ -1,9 +1,10 @@
|
|||||||
use crate::{Asset, AssetEvent, AssetHandleProvider, AssetId, AssetServer, Handle};
|
use crate as bevy_asset;
|
||||||
|
use crate::{Asset, AssetEvent, AssetHandleProvider, AssetId, AssetServer, Handle, UntypedHandle};
|
||||||
use bevy_ecs::{
|
use bevy_ecs::{
|
||||||
prelude::EventWriter,
|
prelude::EventWriter,
|
||||||
system::{Res, ResMut, Resource},
|
system::{Res, ResMut, Resource},
|
||||||
};
|
};
|
||||||
use bevy_reflect::{Reflect, Uuid};
|
use bevy_reflect::{Reflect, TypePath, Uuid};
|
||||||
use bevy_utils::HashMap;
|
use bevy_utils::HashMap;
|
||||||
use crossbeam_channel::{Receiver, Sender};
|
use crossbeam_channel::{Receiver, Sender};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
@ -74,6 +75,15 @@ impl AssetIndexAllocator {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A "loaded asset" containing the untyped handle for an asset stored in a given [`AssetPath`].
|
||||||
|
///
|
||||||
|
/// [`AssetPath`]: crate::AssetPath
|
||||||
|
#[derive(Asset, TypePath)]
|
||||||
|
pub struct LoadedUntypedAsset {
|
||||||
|
#[dependency]
|
||||||
|
pub handle: UntypedHandle,
|
||||||
|
}
|
||||||
|
|
||||||
// PERF: do we actually need this to be an enum? Can we just use an "invalid" generation instead
|
// PERF: do we actually need this to be an enum? Can we just use an "invalid" generation instead
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
enum Entry<A: Asset> {
|
enum Entry<A: Asset> {
|
||||||
|
|||||||
@ -177,6 +177,7 @@ impl Plugin for AssetPlugin {
|
|||||||
}
|
}
|
||||||
app.insert_resource(embedded)
|
app.insert_resource(embedded)
|
||||||
.init_asset::<LoadedFolder>()
|
.init_asset::<LoadedFolder>()
|
||||||
|
.init_asset::<LoadedUntypedAsset>()
|
||||||
.init_asset::<()>()
|
.init_asset::<()>()
|
||||||
.configure_sets(
|
.configure_sets(
|
||||||
UpdateAssets,
|
UpdateAssets,
|
||||||
|
|||||||
@ -660,7 +660,7 @@ impl AssetProcessor {
|
|||||||
source: &AssetSource,
|
source: &AssetSource,
|
||||||
asset_path: &AssetPath<'static>,
|
asset_path: &AssetPath<'static>,
|
||||||
) -> Result<ProcessResult, ProcessError> {
|
) -> Result<ProcessResult, ProcessError> {
|
||||||
// TODO: The extension check was removed now tht AssetPath is the input. is that ok?
|
// TODO: The extension check was removed now that AssetPath is the input. is that ok?
|
||||||
// TODO: check if already processing to protect against duplicate hot-reload events
|
// TODO: check if already processing to protect against duplicate hot-reload events
|
||||||
debug!("Processing {:?}", asset_path);
|
debug!("Processing {:?}", asset_path);
|
||||||
let server = &self.server;
|
let server = &self.server;
|
||||||
|
|||||||
@ -13,12 +13,12 @@ use crate::{
|
|||||||
},
|
},
|
||||||
path::AssetPath,
|
path::AssetPath,
|
||||||
Asset, AssetEvent, AssetHandleProvider, AssetId, Assets, DeserializeMetaError,
|
Asset, AssetEvent, AssetHandleProvider, AssetId, Assets, DeserializeMetaError,
|
||||||
ErasedLoadedAsset, Handle, UntypedAssetId, UntypedHandle,
|
ErasedLoadedAsset, Handle, LoadedUntypedAsset, UntypedAssetId, UntypedHandle,
|
||||||
};
|
};
|
||||||
use bevy_ecs::prelude::*;
|
use bevy_ecs::prelude::*;
|
||||||
use bevy_log::{error, info, warn};
|
use bevy_log::{error, info, warn};
|
||||||
use bevy_tasks::IoTaskPool;
|
use bevy_tasks::IoTaskPool;
|
||||||
use bevy_utils::{HashMap, HashSet};
|
use bevy_utils::{CowArc, HashMap, HashSet};
|
||||||
use crossbeam_channel::{Receiver, Sender};
|
use crossbeam_channel::{Receiver, Sender};
|
||||||
use futures_lite::StreamExt;
|
use futures_lite::StreamExt;
|
||||||
use info::*;
|
use info::*;
|
||||||
@ -288,6 +288,71 @@ impl AssetServer {
|
|||||||
self.load_internal(None, path, false, None).await
|
self.load_internal(None, path, false, None).await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Load an asset without knowing it's type. The method returns a handle to a [`LoadedUntypedAsset`].
|
||||||
|
///
|
||||||
|
/// Once the [`LoadedUntypedAsset`] is loaded, an untyped handle for the requested path can be
|
||||||
|
/// retrieved from it.
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// use bevy_asset::{Assets, Handle, LoadedUntypedAsset};
|
||||||
|
/// use bevy_ecs::system::{Res, Resource};
|
||||||
|
///
|
||||||
|
/// #[derive(Resource)]
|
||||||
|
/// struct LoadingUntypedHandle(Handle<LoadedUntypedAsset>);
|
||||||
|
///
|
||||||
|
/// fn resolve_loaded_untyped_handle(loading_handle: Res<LoadingUntypedHandle>, loaded_untyped_assets: Res<Assets<LoadedUntypedAsset>>) {
|
||||||
|
/// if let Some(loaded_untyped_asset) = loaded_untyped_assets.get(&loading_handle.0) {
|
||||||
|
/// let handle = loaded_untyped_asset.handle.clone();
|
||||||
|
/// // continue working with `handle` which points to the asset at the originally requested path
|
||||||
|
/// }
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// This indirection enables a non blocking load of an untyped asset, since I/O is
|
||||||
|
/// required to figure out the asset type before a handle can be created.
|
||||||
|
#[must_use = "not using the returned strong handle may result in the unexpected release of the assets"]
|
||||||
|
pub fn load_untyped<'a>(&self, path: impl Into<AssetPath<'a>>) -> Handle<LoadedUntypedAsset> {
|
||||||
|
let path = path.into().into_owned();
|
||||||
|
let untyped_source = AssetSourceId::Name(match path.source() {
|
||||||
|
AssetSourceId::Default => CowArc::Borrowed(UNTYPED_SOURCE_SUFFIX),
|
||||||
|
AssetSourceId::Name(source) => {
|
||||||
|
CowArc::Owned(format!("{source}--{UNTYPED_SOURCE_SUFFIX}").into())
|
||||||
|
}
|
||||||
|
});
|
||||||
|
let (handle, should_load) = self
|
||||||
|
.data
|
||||||
|
.infos
|
||||||
|
.write()
|
||||||
|
.get_or_create_path_handle::<LoadedUntypedAsset>(
|
||||||
|
path.clone().with_source(untyped_source),
|
||||||
|
HandleLoadingMode::Request,
|
||||||
|
None,
|
||||||
|
);
|
||||||
|
if !should_load {
|
||||||
|
return handle;
|
||||||
|
}
|
||||||
|
let id = handle.id().untyped();
|
||||||
|
|
||||||
|
let server = self.clone();
|
||||||
|
IoTaskPool::get()
|
||||||
|
.spawn(async move {
|
||||||
|
match server.load_untyped_async(path).await {
|
||||||
|
Ok(handle) => server.send_asset_event(InternalAssetEvent::Loaded {
|
||||||
|
id,
|
||||||
|
loaded_asset: LoadedAsset::new_with_dependencies(
|
||||||
|
LoadedUntypedAsset { handle },
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
.into(),
|
||||||
|
}),
|
||||||
|
Err(_) => server.send_asset_event(InternalAssetEvent::Failed { id }),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.detach();
|
||||||
|
|
||||||
|
handle
|
||||||
|
}
|
||||||
|
|
||||||
/// Performs an async asset load.
|
/// Performs an async asset load.
|
||||||
///
|
///
|
||||||
/// `input_handle` must only be [`Some`] if `should_load` was true when retrieving `input_handle`. This is an optimization to
|
/// `input_handle` must only be [`Some`] if `should_load` was true when retrieving `input_handle`. This is an optimization to
|
||||||
@ -982,3 +1047,7 @@ impl std::fmt::Debug for AssetServer {
|
|||||||
.finish()
|
.finish()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// This is appended to asset sources when loading a [`LoadedUntypedAsset`]. This provides a unique
|
||||||
|
/// source for a given [`AssetPath`].
|
||||||
|
const UNTYPED_SOURCE_SUFFIX: &str = "--untyped";
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user