Tainted \\ Coders

Bevy Assets

Last updated:

This guide has been updated to the v2 asset update introduced in Bevy 0.12.

Assets are resources that need to be loaded into our games, such as:

  • Textures
  • Audio
  • 3D models
  • Other media files

Usually these assets are quite large so loading them all into memory at once would be slow and unfeasible. Bevy’s asset system works hard to make this loading easy and able to be done asynchronously.

In Bevy your assets are managed through two key resources:

  • Assets<T> which is what stores the loaded assets of each type
  • AssetServer which loads your assets from files asynchronously

Adding assets

To add our assets from the file system, we tell our AssetServer to load them for us. This will return a handle to the loading asset.

We then attach these handles to some kind of component that will render them onto the screen.

As a quick example we can imagine we want to add a 3D .glb file from Kenney.nl.

We start by adding it to our ./assets/models folder in the root of our project.

Then we can load it inside a system using a SceneBundle:

fn spawn_car(
    mut commands: Commands,
    asset_server: Res<AssetServer>,
) {
    let model = asset_server.load("models/ambulance.glb#Scene0");

    commands.spawn((
        Car,
        SceneBundle {
            scene: model,
            transform: Transform::from_xyz(0.0, 0.0, 0.0),
            ..default()
        }
    );
}

We can also choose to store the handles to the assets we load inside a resource so they are easily usable in other systems:

#[derive(Resource)]
struct CarAssets {
    body: Option<Handle<Mesh>>,
}

fn load_car_body(
    asset_server: Res<AssetServer>,
    mut meshes: ResMut<Assets<Mesh>>,
    mut car: ResMut<CarAssets>,
) {
    let car_handle = asset_server.load(
        "models/cars/basic.gltf#Mesh0/Primitive0"
    );
    car.body = Some(car_handle);
}

Later, perhaps in some other system we can access this resource:

fn spawn_car(
    car: Res<CarAssets>,
    mut commands: Commands
) {
    if let Some(body) = car.body.as_ref() {
        commands.spawn(PbrBundle {
            mesh: body.clone(),
            ..default()
        });
    }
}

We can also create assets procedurally from our code:

use bevy::sprite::MaterialMesh2dBundle;

fn spawn_ball(
    mut commands: Commands,
    mut meshes: ResMut<Assets<Mesh>>,
    mut materials: ResMut<Assets<ColorMaterial>>,
) {
    let mesh = Mesh::from(Circle::new(5.));
    let material = ColorMaterial::from(Color::BLACK);

    // `Assets::add` will load these into memory and
    // return a `Handle` (an ID) to these assets.
    // When all references to this `Handle` are 
    // cleaned up the asset is cleaned up.
    let mesh_handle = meshes.add(mesh);
    let material_handle = materials.add(material);

    commands.spawn((
        MaterialMesh2dBundle {
            mesh: mesh_handle.into(),
            material: material_handle,
            ..default()
        },
    ));
}

This kind of asset loading is perfect for smaller prototypes when you need to draw and animate simple shapes on a screen.

Asset handles

Each asset is mapped to a Handle<T>. We can use these handles to retrieve the asset’s data or pass this handle to a bundle like in the example above.

The Asset<T> and AssetServer resources both give these handles back to us when we ask them to load something. These handles can be cloned as strong or weak and will be counted by the Asset<T> resource.

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 on Component or Resource.

Asset events

Our assets fire certain events when they are created, modified or removed:

  • AssetEvent::Created
  • AssetEvent::LoadedWithDependencies
  • AssetEvent::Modified
  • AssetEvent::Removed
  • AssetEvent::Unused

This lets us react to changes to our assets:

use bevy::asset::AssetEvent;

fn react_to_images(
    mut events: EventReader<AssetEvent<Image>>
) {
    for event in events.read() {
        match event {
            AssetEvent::Added { id } => {
                // React to the image being created
            }
            AssetEvent::LoadedWithDependencies { id } => {
                // React to the image being loaded
                // after all dependencies
            }
            AssetEvent::Modified { id } => {
                // React to the image being modified
            }
            AssetEvent::Removed { id } => {
                // React to the image being removed
            },
            AssetEvent::Unused { id } => {
                // React to the last strong handle for the asset being dropped
            }
        }
    }
}

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.

Asset sources

Before Bevy 0.12 there was only one asset source: the file system. But the new AssetSource now allows handling of multiple types.

sprite.texture = assets.load("path/to/sprite.png");

This would load an asset from your file system looking in the default assets/ path. But we could create our own asset source to handle loading from an alternative folder:

// reads assets from the "other" folder, rather than the default "assets" folder
app.register_asset_source(
    // This is the "name" of the new source, used in asset paths.
    // Ex: "other://path/to/sprite.png"
    "other",
    // This is a repeatable source builder. You can configure readers, writers,
    // processed readers, processed writers, asset watchers, etc.
    AssetSource::build()
        .with_reader(|| Box::new(FileAssetReader::new("other")))
    )
)

Now we can load assets from the other/ folder:

sprite.texture = assets.load("other://path/to/sprite.png");

Asset server

The AssetServer is a resource that uses the file system to load assets in the asynchronously in the background. Its responsible for tracking the loading state of assets it manages.

fn setup(
    mut commands: Commands,
    asset_server: Res<AssetServer>,
) {
    // Spawns the bevy logo in the center of the screen
    commands.spawn((
        SpriteBundle {
            transform: Transform::from_xyz(0., 0., 0.),
            // The asset server will return a `Handle<Image>`
            // but that does not mean the asset has
            // been fully loaded yet.
            texture: asset_server.load("images/bevy.png"),
            ..default()
        },
    ));
}

Assets are loaded async. This means that when we first spawn this SpriteBundle even though the asset server gave us back a Handle<Image>, the actual asset might not yet be available.

We can use AssetServer::get_load_state to check if a asset has been loaded and is ready to use in the Assets collection.

Loading assets

First we can load our image from a file using the asset server:

#[derive(Resource)]
struct BevyImage(Handle<Image>);

fn load_sprites(
    mut bevy_image: ResMut<BevyImage>,
    asset_server: Res<AssetServer>
) {
    bevy_image.0 = asset_server.load("images/bevy.png");
}

Here we load the image and store the Handle<Image> in a resource. Then in another system we can query the loading state of that asset and only spawn our entity when we know the asset is loaded:

use bevy::asset::LoadState;

fn on_asset_event(
    mut commands: Commands,
    asset_server: Res<AssetServer>,
    bevy_image: Res<BevyImage>,
) {
    match asset_server.get_load_state(bevy_image.0.clone()) {
        Some(LoadState::NotLoaded) => {}
        Some(LoadState::Loading) => {}
        Some(LoadState::Loaded) => {
            commands.spawn(
                SpriteBundle {
                    transform: Transform::from_xyz(0., 0., 0.),
                    texture: bevy_image.0.clone(),
                    ..default()
                }
            );
        }
        Some(LoadState::Failed) => {}
        None => {}
    }
}

By default it will expect your assets to be inside the assets folder inside the root directory of your application.

You can change where Bevy will assume your assets are by setting one of the following environment variables:

  • BEVY_ASSET_ROOT
  • CARGO_MANIFEST_DIR which automatically set to the root folder of your crate (workspace).

The provided path to assets.load MUST include the file extension.

You can also define a separate AssetSource as mentioned above.

How assets are processed

Your 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 along side your 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_render::texture::image_loader::ImageLoader",
        settings: (
            format: FromExtension,
            is_srgb: true,
            sampler: Default,
        ),
    ),
)

We can edit these files to customize how this particular image is loaded by Bevy.

This processing is optional and is controlled by setting the AssetServerMode or AssetMode when loading an asset.

Hot reloading

To enable hot reloading its recommended to do so using one of the optional Bevy features:

[dependencies]
bevy = {
    version = "0.12.0",
    features = [
        "file_watcher",
        "embedded_watcher"
    ]
}

The file_watcher feature will watch your filesystem for changes in your assets and hot-reload them.

The embedded_watcher feature willw atch your 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:

use bevy::asset::LoadedFolder;

fn load_models(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/monkey");
}

This will load all of the assets in the models/cars folder.

Custom asset loader

If your assets don’t fall into the normal file types supported by Bevy you can create your own custom asset loader.

To start we need to create our loader, a custom asset to load and optionally some settings for our asset:

use serde::{Serialize, Deserialize};

#[derive(Default)]
pub struct MyAssetLoader;

#[derive(Asset, TypePath)]
pub struct MyAsset {
    value: String,
}

#[derive(Serialize, Deserialize, Default)]
pub struct MyAssetSettings {
    some_setting: bool
}

An asset loader has 3 types to define:

  1. The asset type
  2. The asset settings type
  3. An error type

The error can be defined by deriving Error from a library such as thiserror:

use thiserror::Error;

#[non_exhaustive]
#[derive(Debug, Error)]
pub enum MyAssetLoaderError {
    /// An [IO](std::io) Error
    #[error("Could load shader: {0}")]
    Io(#[from] std::io::Error),
}

With all three types defined we can implement AssetLoader for our custom asset loader:

use bevy::asset::{
    AssetLoader,
    LoadContext,
    BoxedFuture,
    io::Reader
};
use futures_lite::AsyncReadExt;

impl AssetLoader for MyAssetLoader {
    type Asset = MyAsset;
    type Settings = MyAssetSettings;
    type Error = MyAssetLoaderError;

    fn load<'a>(
        &'a self,
        reader: &'a mut Reader,
        _settings: &'a MyAssetSettings,
        _load_context: &'a mut LoadContext,
    ) -> BoxedFuture<'a, Result<Self::Asset, Self::Error>> {
        Box::pin(async move {
            let mut bytes = Vec::new();
            reader.read(&mut bytes).await?;
            // convert bytes to value somehow, we will be cheating:
            let value = "Hello World!";
            Ok(MyAsset {
                value: value.into()
            })
        })
    }

    fn extensions(&self) -> &[&str] {
        &["thing"]
    }
}

To read the async values with the reader you will need to use the futures_lite extension or you will get errors that reader does not define the read method as its still a future value.

And finally load our asset and loader into our app:

fn main() {
    App::new()
        .add_plugins(DefaultPlugins)
        .init_asset_loader::<MyAssetLoader>()
        .init_asset::<MyAsset>()
        .run()
}

Read more