Bevy Assets
Assets are resources that need to be loaded into our games, usually from the disk.
The problem is that assets can be quite large so this can be rather slow. We'd rather store them in memory, but memory is limited so there is an art to loading assets and figuring out when a good time to unload unused ones is.
Bevy's asset system works hard to make this loading and unloading simple. It handles asynchronous loading of assets in the background, and unloads assets when they are no longer needed.
Bevy's asset system is also a bespoke ball of code, with its own unique idioms and complex API surface. Much of it will change as more of the asset pipeline is moved inside the ECS.
Our assets are managed through two key resources:
Assets<T>: stores the loaded assets of each typeAssetServer: loads our assets from files asynchronously
Loading assets
To add assets from the file system, we tell our AssetServer to load them for us. This will return a Handle<T> to the loading asset.
fn load_images(
mut bevy_image: ResMut<BevyImage>,
asset_server: Res<AssetServer>,
) {
// This will not block, the asset will be loaded in the background
bevy_image.0 = asset_server.load("example_images/bevy.png");
}
Here we are loading an image from the assets/example_images/bevy.png file. Bevy is immediately returning us a Handle<T>. Components hold these handles instead of the actual asset data, and the handles can be used to get the actual asset data from the Assets<T> collection.
Often it is convenient to separate the system that loads an asset from the system that uses it:
#[derive(Resource, Default)]
struct BevyImage(Handle<Image>);
fn spawn_sprite(bevy_image: Res<BevyImage>, mut commands: Commands) {
commands.spawn(Sprite {
image: bevy_image.0.clone(),
..default()
});
}
The Sprite takes a Handle<Image> which does not have to be loaded yet. Bevy will handle loading it in the background and rendering it when it's ready.
As of 0.19 Bevy introduced Bevy Scene Notation (BSN) which simplifies things even more. Sprite and other things that take a Handle<T> can initialize their dependencies automatically when spawned:
fn sprite() -> impl Scene {
bsn! {
Sprite { image: "example_images/bevy.png" }
}
}
Inside of a bsn! macro types will have .into() called on them when spawned which is how our string transforms into a Handle<Image>.
Creating shapes
Not all assets need to be loaded from files. Sometimes we want to create them procedurally, especially during prototyping.
We can create our own shapes and colors directly in code. By giving a component a Mesh and Material, Bevy's rendering system will draw it for us.
fn spawn_ball(
mut commands: Commands,
mut meshes: ResMut<Assets<Mesh>>,
mut materials: ResMut<Assets<ColorMaterial>>,
) {
let circle = Circle::new(5.);
let color = Color::BLACK;
let mesh = meshes.add(circle);
let material = materials.add(color);
commands.spawn((Mesh2d(mesh), MeshMaterial2d(material)));
}
The same thing can be accomplished with BSN:
fn ball() -> impl Scene {
bsn! {
Mesh2d(asset_value(Circle::new(5.)))
MeshMaterial2d<ColorMaterial>(asset_value(Color::BLACK))
}
}
Changing an entity's assets
To change assets that have already spawned on an entity you have 3 choices:
- Change the handle stored on the component
- Despawn the entity and spawn a new one with the new asset data.
- Use the
Assetscollection to modify the current handle's asset data
Loading 3D models
Loading models works a lot like loading images. We can imagine we want to add a 3D .glb file from Kenney.nl.
These .glb files are binary glTF files that pack up the meshes, textures and potentially animations into a single file.
We start by adding the unzipped assets to our ./assets/models folder in the root of our project. By default Bevy looks for assets in the assets folder relative to our project.
First, we have to load the model using the AssetServer. We will need to tell Bevy that we want to load a particular scene from the .glb file by describing the asset using a GltfAssetLabel.
#[derive(Resource, Default)]
struct CarAssets {
scene: Handle<WorldAsset>,
}
fn setup(mut commands: Commands) {
info!("Spawning camera");
commands.spawn((
Camera3d::default(),
Transform::from_xyz(0.0, 3.0, 10.0).looking_at(Vec3::ZERO, Vec3::Y),
));
}
fn load_car_body(asset_server: Res<AssetServer>, mut car: ResMut<CarAssets>) {
info!("Loading ambulance glb scene");
car.scene = asset_server
.load(GltfAssetLabel::Scene(0).from_asset("models/ambulance.glb"));
}
Then we can render it inside a system using a WorldAssetRoot, which is a component used to render predefined scenes:
fn spawn_car(car: Res<CarAssets>, mut commands: Commands) {
info!("Spawning ambulance scene");
commands.spawn((
WorldAssetRoot(car.scene.clone()),
Transform::from_xyz(0., 0., 0.),
));
}
Asset handles
The Assets<T> collection and the AssetServer both give us back a Handle<T>.
A handle is like a reference counted pointer but cooler. These handles let Bevy lookup the correct asset without the need to manage lifetimes and complicated borrowing rules by using a handle as a reference to the actual asset data.
The handle points to a particular index inside the corresponding Assets<T>. This is subtly different from a normal reference because the actual asset data can be moved around in memory without invalidating the handle.
There are two kinds of handle. A Handle::Strong is reference counted: the asset is kept alive until every strong handle to it has been dropped. A Handle::Uuid is built from a known AssetId and does not keep the asset alive, which makes it useful for referencing assets that may not exist yet.
When all strong handles have been removed then the asset is also removed and unloaded from memory. So to keep our assets loaded they must be referenced by at least one Component or Resource.
Each asset loaded by the AssetServer is mapped to a Handle<T> where T depends on the file's extension:
| Extension(s) | Handle type | Loader |
|---|---|---|
ttf, otf | Font | FontLoader |
hdr | Image | HdrTextureLoader |
exr | Image | ExrTextureLoader |
scn, scn.ron | DynamicWorld | WorldAssetLoader |
meshlet_mesh | MeshletMesh | MeshletMeshLoader |
mp3, flac, wav, oga, ogg, spx | AudioSource | AudioLoader |
basis, bmp, dds, ff, farbfeld, gif, ico, jpg, jpeg, ktx2, pam, pbm, pgm, png, ppm, qoi, tga, tif, tiff, webp | Image | ImageLoader |
animgraph, animgraph.ron | AnimationGraph | AnimationGraphAssetLoader |
gltf, glb | Gltf | GltfLoader |
spv, wgsl, wesl, vert, frag, comp | Shader | ShaderLoader |
Many of these loaders are behind optional cargo features, such as meshlet, animation, hdr, exr, or the individual audio and image formats. If a format doesn't load, make sure the matching feature is enabled.
These handles are what we pass to our other components which then in turn know what to do with the underlying data. That way it can resolve only if it needs to.
For example, if we give a .glb file to the asset server, then Bevy will use the GltfLoader. Loading the whole file gives back a Handle<Gltf>, but to render a specific scene we load it with a GltfAssetLabel, which gives back a Handle<WorldAsset>. We then pass that handle to a WorldAssetRoot on our entity which will actually render the 3D asset.
fn spawn_car_with_handle(
asset_server: Res<AssetServer>,
mut commands: Commands,
) {
// From our table above we know that the `.glb` extension is
// handled by the `GltfLoader`. The `#Scene0` label loads the first
// scene as a `WorldAsset` rather than the whole file as a `Gltf`.
let handle = asset_server.load("models/ambulance.glb#Scene0");
commands.spawn((
Car,
Transform::from_xyz(0.0, 0.0, 0.0),
WorldAssetRoot(handle),
));
}
Asset loading state
The AssetServer tracks the loading state of every asset it manages. We can query this state to figure out if the asset is loaded or not.
Usually this all happens so fast that we don't need to worry about it. Components that take these handles will just render them when they are ready.
However, the larger our assets are, the more likely you will want to group up the loading of assets in a single stage and only proceed when they are all ready.
From our example above, we might want to query the loading state of the model and only do something when it's fully loaded:
fn on_asset_event(asset_server: Res<AssetServer>, car: Res<CarAssets>) {
match asset_server.get_load_state(&car.scene) {
Some(bevy::asset::LoadState::NotLoaded) => {
info!("Ambulance glb scene not loaded");
}
Some(bevy::asset::LoadState::Loading) => {
info!("Loading ambulance glb scene");
}
Some(bevy::asset::LoadState::Loaded) => {
info!("Loaded ambulance glb scene");
}
Some(bevy::asset::LoadState::Failed(_)) => {
info!("Failed to load ambulance glb scene");
}
None => {}
}
}
Loads can fail. When they do, the LoadState becomes LoadState::Failed and Bevy sends an AssetLoadFailedEvent for that asset, so it is worth handling both outcomes.
Asset paths
By default Bevy looks for our assets inside the assets folder. The base directory it uses depends on how the app is run: cargo run sets CARGO_MANIFEST_DIR to our crate root, while a standalone executable falls back to the directory containing the executable.
The simplest way to change where Bevy looks is to set file_path on the AssetPlugin:
App::new()
.add_plugins(DefaultPlugins.set(AssetPlugin {
file_path: "my_assets".to_string(),
..default()
}))
.run();
The base directory can also be overridden with one of the following environment variables:
BEVY_ASSET_ROOTCARGO_MANIFEST_DIRwhich Cargo sets to the root folder of our crate
The provided path to asset_server.load MUST include the file extension.
You can also define a separate AssetSource as mentioned above.
Asset events
Assets fire certain events which are sent during the PostUpdate schedule, which lets us react to changes to our assets:
fn react_to_images(mut events: MessageReader<AssetEvent<Image>>) {
for event in events.read() {
match event {
AssetEvent::Added { id } => {
info!("Image added: {:?}", id);
}
AssetEvent::LoadedWithDependencies { id } => {
info!("Image loaded with dependencies: {:?}", id);
}
AssetEvent::Modified { id } => {
info!("Image modified: {:?}", id);
}
AssetEvent::Removed { id } => {
info!("Image removed: {:?}", id);
}
AssetEvent::Unused { id } => {
info!("Image unused: {:?}", id);
}
}
}
}
Each asset event will yield an AssetId<T> which is a weaker version of a Handle<T>. An AssetId may point to an Asset that no longer exists.
In most cases when you want to react to an event that represents the asset being "fully loaded" you should use the AssetEvent::LoadedWithDependencies. Because events are written during PostUpdate, a system that reads them in Update sees them one frame late; if you need to react immediately, add the reader to the Last schedule after AssetEventSystems.
Asset sources
Before Bevy 0.12 there was only one asset source: the file system. But the asset system after that now allows handling of multiple types.
This is handled through AssetSource which abstracts finding assets from a particular source that is specified when we define our app:
fn main() {
App::new()
// This must be done before AssetPlugin (included in DefaultPlugins)
// finalizes building assets.
.register_asset_source(
"example_images",
AssetSourceBuilder::platform_default("assets/example_images", None),
)
.add_plugins(DefaultPlugins)
.add_systems(Startup, setup)
.run();
}
Normally assets are sourced from the assets/ folder, but we can choose another folder like assets/other, or any other folder we register as a source. Loading files outside those approved folders is forbidden by default, so keep assets inside a registered source.
fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
commands.spawn(Camera2d);
let path = Path::new("bevy.png");
let source = AssetSourceId::from("example_images");
let asset_path = AssetPath::from_path(path).with_source(source);
commands.spawn(Sprite {
image: asset_server.load(asset_path),
..default()
});
}
We can also use the shorthand if we don't want to construct the path dynamically:
commands.spawn(Sprite {
image: asset_server.load("example_images://bevy.png"),
..default()
});
Asset server
The AssetServer is a resource that loads assets asynchronously in the background. It's responsible for tracking the loading state of the assets it manages.
All assets follow the same general process:
- We register a new
Asset<T>type if it's custom - We register an
AssetLoaderfor that asset if it's custom - We add the asset to our
assetsfolder - Then we call
AssetServer::loadto get aHandle<T>to the asset
When you load up an asset, Bevy sends a task to the IoTaskPool, which manages the asynchronous loading of assets on separate threads.
How assets are processed
Assets are processed by the AssetProcessor which is designed to crash and still pick up where it left off.
To do this Bevy is using write-ahead logging to recover from crashes. So it will be working hard to not have to re-process assets during an error.
This processor will create .meta files alongside our assets which we can configure exactly how each one can be loaded or processed.
A meta file for an image might look like this:
(
meta_format_version: "1.0",
asset: Load(
loader: "bevy_image::image_loader::ImageLoader",
settings: (
format: FromExtension,
is_srgb: true,
sampler: Default,
asset_usage: RenderAssetUsages("RENDER_WORLD | MAIN_WORLD"),
),
),
)
We can edit these files to customize how this particular image is loaded by Bevy. Fields that don't have a default must be present, so if you leave one out the file will fail to parse.
Asset processing is optional. It is enabled with the asset_processor cargo feature and by setting AssetPlugin { mode: AssetMode::Processed, .. }. The .meta files themselves are separate: the asset server always checks for them unless configured otherwise with AssetMetaCheck.
Hot reloading
To enable hot reloading it's recommended to do so using one of the optional Bevy features:
[dependencies]
bevy = {
version = "0.19",
features = [
"file_watcher",
"embedded_watcher"
]
}
The file_watcher feature will watch our filesystem for changes in our assets and hot-reload them.
The embedded_watcher feature will watch our in memory assets and hot-reload when they change.
We can also enable the feature by using the watch_for_changes_override setting:
fn main() {
App::new()
.add_plugins(DefaultPlugins.set(AssetPlugin {
watch_for_changes_override: Some(true),
..default()
}))
.run();
}
Loading assets from a folder
If we have a great number of assets we can make things easier by loading all of them in a single system:
fn load_models_from_folder(asset_server: Res<AssetServer>) {
// You can load all assets in a folder like this. They will be loaded in
// parallel without blocking
let _scenes: Handle<LoadedFolder> = asset_server.load_folder("models");
}
This will load all of the assets in the models folder.
Custom asset loader
If our assets don't fall into the normal file types supported by Bevy you can create your own custom asset loader.
#![allow(dead_code)]
use bevy::{
asset::{AssetLoader, LoadContext, io::Reader},
prelude::*,
};
use futures_lite::AsyncReadExt;
#[derive(Asset, TypePath)]
struct TextFile {
content: String,
}
#[derive(Default, TypePath)]
struct TextFileLoader;
impl AssetLoader for TextFileLoader {
type Asset = TextFile;
type Settings = ();
type Error = std::io::Error;
async fn load(
&self,
reader: &mut dyn Reader,
_settings: &(),
_load_context: &mut LoadContext<'_>,
) -> Result<Self::Asset, Self::Error> {
let mut content = String::new();
reader.read_to_string(&mut content).await?;
Ok(TextFile { content })
}
fn extensions(&self) -> &[&str] {
&["txt"]
}
}
fn main() {
App::new()
.add_plugins(DefaultPlugins)
.init_asset::<TextFile>()
.init_asset_loader::<TextFileLoader>()
.run();
}
Advanced asset tracking
This is an explanation of a common way of tracking assets popularized by bevy_new_2d. We will create a plugin that is going to add systems to track resources.
Some assets can take a while to load. Eventually you will want to handle some kind of state transition when all of your assets are loaded. This is an example of building a small plugin which does just that.
The first thing we need is a resource we can store all our handles:
/// A function that inserts a loaded resource.
type InsertLoadedResource = fn(&mut World, &UntypedHandle);
#[derive(Resource, Default)]
pub struct ResourceHandles {
// Use a queue for waiting assets so they can be cycled through and moved to
// `finished` one at a time.
waiting: VecDeque<(UntypedHandle, InsertLoadedResource)>,
finished: Vec<UntypedHandle>,
}
For convenience we can implement a method that tells us when everything is loaded:
impl ResourceHandles {
/// Returns true if all requested [`Asset`]s have finished loading and are
/// available as [`Resource`]s.
pub fn is_all_done(&self) -> bool {
self.waiting.is_empty()
}
}
Then we are going to create a function that will manage tracking these dependencies and their loading status and putting them into the proper field of our ResourceHandles.
fn load_resource_assets(world: &mut World) {
world.resource_scope(|world, mut resource_handles: Mut<ResourceHandles>| {
world.resource_scope(|world, assets: Mut<AssetServer>| {
for _ in 0..resource_handles.waiting.len() {
let (handle, insert_fn) = resource_handles.waiting.pop_front().unwrap();
if assets.is_loaded_with_dependencies(&handle) {
insert_fn(world, &handle);
resource_handles.finished.push(handle);
} else {
resource_handles.waiting.push_back((handle, insert_fn));
}
}
});
});
}
pub fn resource_plugin(app: &mut App) {
app.init_resource::<ResourceHandles>();
app.add_systems(PreUpdate, load_resource_assets);
}
With all that done we still need an easy way to add resources to this queue.
It would be nice to be able to say:
pub fn level_plugin(app: &mut App) {
app
.register_type::<LevelAssets>()
.load_resource::<LevelAssets>();
}
We want to extend App with our load_resource method which we can do by creating a trait:
pub trait LoadResource {
/// This will load the [`Resource`] as an [`Asset`]. When all of its asset
/// dependencies have been loaded, it will be inserted as a resource. This
/// ensures that the resource only exists when the assets are ready.
fn load_resource<T: Resource + Asset + Clone + FromWorld>(
&mut self,
) -> &mut Self;
}
impl LoadResource for App {
fn load_resource<T: Resource + Asset + Clone + FromWorld>(
&mut self,
) -> &mut Self {
self.init_asset::<T>();
let world = self.world_mut();
let value = T::from_world(world);
let assets = world.resource::<AssetServer>();
let handle = assets.add(value);
let mut handles = world.resource_mut::<ResourceHandles>();
handles
.waiting
.push_back((handle.untyped(), |world, handle| {
let assets = world.resource::<Assets<T>>();
if let Some(value) = assets.get(handle.id().typed::<T>()) {
world.insert_resource(value.clone());
}
}));
self
}
}
Now we are ready to call this from our other plugins that need to load their assets. Lets create a LevelAssets resource that will hold our level specific assets:
#[derive(Resource, Asset, Clone, Reflect)]
#[reflect(Resource)]
pub struct LevelAssets {
#[dependency]
logo: Handle<Image>,
}
impl FromWorld for LevelAssets {
fn from_world(world: &mut World) -> Self {
let assets = world.resource::<AssetServer>();
Self {
logo: assets.load("example_images/bevy.png"),
}
}
}
Implementing FromWorld lets our load_resource extension load this specific asset and add it to our World so its available to other systems.
Then finally we can use our ResourceHandles to control the state of our app changing from Loading to an InGame state once everything has loaded:
fn check_assets(
resource_handles: Res<ResourceHandles>,
mut next_state: ResMut<NextState<AppState>>,
) {
if resource_handles.is_all_done() {
next_state.set(AppState::InGame);
} else {
info!("Resources are still loading...");
}
}
fn display_logo(level_assets: Res<LevelAssets>, mut commands: Commands) {
commands.spawn(Camera2d);
commands.spawn(Sprite {
image: level_assets.logo.clone(),
..default()
});
}
fn main() {
App::new()
.add_plugins(DefaultPlugins)
.add_plugins((resource_plugin, level_plugin))
.init_state::<AppState>()
.add_systems(Update, check_assets)
.add_systems(OnEnter(AppState::InGame), display_logo)
.run();
}