Tainted\\Coders

Bevy Assets

Bevy version: 0.14Last updated:

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

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:

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:

#[derive(Component)]
struct Car;

fn spawn_ambulance(
  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 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 circle = Circle::new(BALL_SIZE);
  let color = 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 = meshes.add(circle);
  let material = materials.add(color);

  // Here we are using `spawn` instead of `spawn_empty` followed by an
  // `insert`. They mean the same thing, letting us spawn many components on a
  // new entity at once.
  commands.spawn((MaterialMesh2dBundle {
    mesh: mesh.into(),
    material,
    ..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

The Asset<T> and AssetServer resources both give us back a Handle<T> when we ask them to load something.

These handles are like pointers that let use fetch the actual asset when we need it, instead of passing it around directly. 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.

Each asset loaded by the AssetServer is mapped to a Handle<T> depending on the files extension:

Extension(s)Handle typeLoader
ttf, otfFontFontLoader
hdrImageHdrTextureLoader
exrImageExrTextureLoader
scn, scn.ronDynamicSceneSceneLoader
gzGzAssetGzAssetLoader
meshlet_meshMeshletMeshMeshletMeshLoader
mp3, flac, oga, ogg, spxAudioSourceAudioLoader
bmp, dds, ff, gif, ico, jpeg, ktx2, png, pnm, qoi, tga, tiff, webpImageImageLoader
animgraph, animgraph.ronAnimationGraphAnimationGraphAssetLoader
gltf, glbGltfGltfLoader
spv, wgsl, vert, frag, compShaderShaderLoader

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 it needs to.

// From our table above we know that because of the `.glb`
// extension it will give us back a `Handle<Gltf>` provided by
// a `GltfLoader`
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()
  },
));

Asset events

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

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 modified
      }
      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 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 you define your app:

fn main() {
  App::new()
    // This must be done before `AssetPlugin` (included in `DefaultPlugins`)
    // finalizes building assets.
    .register_asset_source(
      "other",
      AssetSourceBuilder::platform_default("assets/other", 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 anywhere else in your project directory.

fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
  commands.spawn(Camera2dBundle::default());

  let path = Path::new("some_image.png");
  let source = AssetSourceId::from("other");
  let asset_path = AssetPath::from_path(path).with_source(source);

  commands.spawn(SpriteBundle {
    texture: asset_server.load(asset_path),
    ..default()
  });
}

We can also use the shorthand if we don't want to construct the path dynamically:

commands.spawn(SpriteBundle {
  texture: asset_server.load("other://some_image.png"),
  ..default()
});

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 spawn_boid(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) {
    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:

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.14.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::{Deserialize, Serialize};

#[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,
    io::Reader
};
use futures_lite::AsyncReadExt;

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

  async fn load<'a>(
    &'a self,
    reader: &'a mut Reader<'_>,
    _settings: &'a MyAssetSettings,
    _load_context: &'a mut LoadContext<'_>,
  ) -> Result<Self::Asset, Self::Error> {
    let mut bytes = Vec::new();
    reader.read(&mut bytes).await?;
    // convert bytes to value somehow
    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();
}