When to use this skill
- Loading a model, texture, audio file, or scene.
- Reading the loaded data back from
Assets<T>once it's ready. - Reacting to load progress (
AssetEvent::Added/Modified). - Spawning glTF scenes, waiting for
WorldInstanceReady, reading names/extras, or owning/despawning an instance cleanly. - Enabling hot-reload during development.
- Writing your own loader → see
bevy-custom-assets.
Canonical pattern
use bevy::prelude::*;
fn main() { App::new() .add_plugins(DefaultPlugins.set(AssetPlugin { // Hot-reload on file change — dev-only. watch_for_changes_override: Some(true), ..default() })) .init_resource::() .add_systems(Startup, load_handles) .add_systems(Update, react_to_loads) .run(); }
#[derive(Resource, Default)] struct MyHandles { hero: Handle, bricks: Handle, }
fn load_handles(asset_server: Res, mut handles: ResMut) { // GLTF scenes are addressed by sub-asset label. handles.hero = asset_server.load("models/hero.glb#Scene0"); handles.bricks = asset_server.load("textures/bricks.png"); }
fn react_to_loads( mut ev: MessageReader<AssetEvent>, images: Res<Assets>, ) { for event in ev.read() { if let AssetEvent::LoadedWithDependencies { id } = event { if let Some(img) = images.get(*id) { info!("image loaded: {}x{}", img.width(), img.height()); } } } }
## Asset paths
```rust
use bevy::asset::AssetPath;
// `LoadContext::path()` returns `AssetPath`, not `&Path`.
// Build paths explicitly when generating handles inside a custom loader:
let path = AssetPath::from("textures/bricks.png");
let path_with_label = AssetPath::from("models/hero.glb").with_label("Scene0");
let _ = path;
let _ = path_with_label;
Asset readiness check
use bevy::prelude::*;
# fn _check(
asset_server: Res<AssetServer>,
handles: Res<MyHandles>,
# ) {
use bevy::asset::LoadState;
if asset_server.load_state(&handles.hero) == LoadState::Loaded {
// Safe to spawn WorldAssetRoot(handles.hero.clone()).
}
# }
# #[derive(Resource)] struct MyHandles { hero: Handle<WorldAsset> }
Bevy 0.19 gotchas
LoadContext::path()returnsAssetPath, not&Path. Callers that didctx.path().to_string_lossy()need toctx.path().path().to_string_lossy()or use theAssetPathAPI directly.SeekableReaderis new in 0.18. Loaders that need random access into the underlying file can ask:if let Ok(s) = reader.seekable() { /* s: &mut dyn SeekableReader */ }.- Every custom
Readerimplementsseekable()in 0.19. ReturnOk(self)when it also implementsAsyncSeek; otherwise returnErr(ReaderNotSeekableError).AsyncSeekForwardwas removed. - Advanced loads use builders in 0.19. Prefer
AssetServer::load_builder()for settings, guards, untyped loads, or approval overrides; the many specializedload_*variants are deprecated. AssetPath::resolvenow takes&AssetPath. Useresolve_str/resolve_embed_strwhen the child path starts as text.AssetSourceBuilder::new(...)replacesAssetSource::build().with_reader(...). Existing custom asset sources need to be re-shaped.AssetSourcechannel isasync_channel::Senderin 0.18 (wascrossbeam_channel). Usesend_blocking(...).Image::reinterpret_size(size)returnsResultin 0.18.- Sub-asset labels.
path.glb#Scene0,path.glb#Mesh0/Primitive0— distinct handles, can be loaded independently. Forgetting the label gives you the root asset, not the named one. - Hot reload is dev-only. Don't ship
watch_for_changes_override: Some(true)in a release build — it polls the filesystem. asset_server.load(...)is non-blocking. QueryingAssets<T>::getimmediately after returnsNone. Wait forAssetEvent::LoadedWithDependenciesor pollload_state(...).AssetEvent<T>is aMessage, so iterate withMessageReader<AssetEvent<T>>, not the oldEventReader.
See also
- glTF scenes —
GltfAssetLabel,WorldAssetRoot, instance readiness/ownership, names/extras, metadata proxies, and despawning. bevy-custom-assets— writing anAssetLoaderand nested load builder.bevy-save-load— durable stable IDs and save schema; glTF names/entity IDs are not persistence contracts.bevy-migration-0-17-to-0-18—LoadContext::pathand channel-type renames.bevy-migration-0-18-to-0-19— load builders and requiredReader::seekable.