Assets & Scenes
Load textures, models, and scenes in Bevy - handles, asset states, and hot-reload workflows.
Recipe
fn setup(asset_server: Res<AssetServer>, mut commands: Commands) {
let texture: Handle<Image> = asset_server.load("sprites/player.png");
commands.spawn(Sprite::from_image(texture));
}When to reach for this:
- Any game with art/audio not procedurally generated
- Level composition via scene files instead of code-only spawns
- Iterating on assets without restarting game (dev hot-reload)
- Separating content pipeline from Rust gameplay code
Working Example
use bevy::prelude::*;
fn main() {
App::new()
.add_plugins(DefaultPlugins)
.add_systems(Startup, load_scene)
.add_systems(Update, watch_loading)
.run();
}
#[derive(Resource)]
struct LevelHandle(Handle<Scene>);
fn load_scene(mut commands: Commands, asset_server: Res<AssetServer>) {
let scene = asset_server.load("scenes/level1.scn.ron");
commands.insert_resource(LevelHandle(scene));
}
fn watch_loading(
level: Res<LevelHandle>,
asset_server: Res<AssetServer>,
mut scenes: ResMut<Assets<Scene>>,
mut done: Local<bool>,
mut commands: Commands,
) {
if *done {
return;
}
match asset_server.load_state(&level.0) {
bevy::asset::LoadState::Loaded => {
if scenes.get(&level.0).is_some() {
commands.spawn(DynamicSceneRoot(level.0.clone()));
*done = true;
}
}
bevy::asset::LoadState::Failed(err) => eprintln!("load failed: {err:?}"),
_ => {}
}
}What this demonstrates:
- Scene handle stored as resource
- Polling
LoadStatebefore spawningDynamicSceneRoot - Failure logging for missing assets
Deep Dive
How It Works
- AssetServer IO thread loads files into typed
Assets<T>storages. - Handle<T> UUID-like reference safe to clone and store on components.
- Scene files list reflected components to spawn entity tree.
- File watcher (dev) triggers reload when disk changes.
Folder Layout
| Path | Content |
|---|---|
assets/sprites/ | PNG/WebP textures |
assets/audio/ | OGG/WAV |
assets/scenes/ | .scn.ron levels |
assets/models/ | GLTF/OBJ 3D |
Gotchas
- Wrong asset path case on Linux builds - works on macOS, fails in CI. Fix: consistent lowercase paths.
- Spawning before load completes - invisible entities. Fix: gate on
LoadState::Loaded. - Huge textures unoptimized - GPU memory blowup. Fix: compress, atlas, mipmaps.
- Non-reflected components in scenes - silently omitted. Fix: register
Reflecton custom components. - Hot-reload in release - unintended behavior. Fix: disable watcher in release builds.
Alternatives
| Alternative | Use When | Don't Use When |
|---|---|---|
Embed with include_bytes! | Tiny jam binary | Large art pipeline |
| External pack files | DRM or DLC | Simple open mods |
| Procedural only | Shader toys | Artist-driven content |
| Asset streaming crate | Open world | Small linear levels |
FAQs
GLTF loading?
SceneRoot from glTF path spawns meshes/materials - common 3D path in Bevy examples.
Audio assets?
asset_server.load("audio/hit.ogg") -> Handle<AudioSource> on AudioPlayer components.
Asset labels?
asset_server.load("player.png#Texture") style labels for multi-asset files like GLTF.
Preloading screen?
Collect handles in menu state; transition when all is_loaded_with_dependencies.
Modding?
Load assets from user dir with separate AssetSource mounting - advanced plugin pattern.
Versioning art?
Content hash filenames or manifest JSON for cache busting in shipped builds.
Scene vs prefab?
Scenes are serialized hierarchies; prefabs often custom Bundle spawn functions in code.
Memory pressure?
Drop handles when level unloads; Assets::remove unused entries in scene transitions.
CI without GPU?
Use headless/minimal plugins and mock handles for logic tests.
Bevy integration?
Core to Bevy Engine asset plugin architecture.
Related
- Bevy Engine - ECS spawning
- Performance for Games - texture budgets
- Game Dev Basics - sprite spawn example
- Physics & Audio - audio handles
Stack versions: This page was written for Rust 1.97.0 (edition 2024), Tokio 1.x, Axum 0.8, serde 1.0, sqlx 0.8, clap 4, and Polars 0.46+.