Tainted\\Coders

Bevy Assets

Bevy version: 0.17Last updated:

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.

Our assets are managed through two key resources:

Loading 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.

fn load_images(asset_server: Res<AssetServer>, mut commands: Commands) {
  // This will not block, the asset will be loaded in the background
  let image_handle: Handle<Image> = asset_server.load("images/bevy.png");

  commands.spawn(Sprite {
    image: image_handle,
    ..default()
  });
}

Here we are loading an image from the assets/images/bevy.png file. Bevy is immediately returning us a Handle. Components use these handles to avoid holding the actual asset data.

Often it is more convenient to separate the system that loads all of our assets and then use them through a resource in another system:

#[derive(Resource)]
struct MySprites {
  bevy_logo: Handle<Image>,
}

fn load_sprites_into_resource(
  asset_server: Res<AssetServer>,
  mut sprites: ResMut<MySprites>,
) {
  sprites.bevy_logo = asset_server.load("images/bevy.png");
}

fn use_sprites_from_resource(sprites: Res<MySprites>, mut commands: Commands) {
  commands.spawn(Sprite {
    image: sprites.bevy_logo.clone(),
    ..default()
  });
}

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(BALL_SIZE);
  let color = Color::BLACK;

  let mesh = meshes.add(circle);
  let material = materials.add(color);

  commands.spawn((Mesh2d(mesh), MeshMaterial2d(material)));
}

Changing an entity's assets

To change assets that have already spawned on an entity you have 3 choices:

  1. Change the handle stored on the component
  2. Despawn the entity and spawn a new one with the new asset data.
  3. Use the Assets collection 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.

We start by adding the unzipped assets to our ./assets/models folder in the root of our project. Bevy will by default expect our assets at ./assets according to our BEVY_ASSET_ROOT env variable.

Then we can load it inside a system using a SceneRoot, which is a component used to render pre-defined scenes.

#[derive(Component)]
struct Car;

fn spawn_ambulance(mut commands: Commands, asset_server: Res<AssetServer>) {
    let model = asset_server.load(
        GltfAssetLabel::Scene(0).from_asset("glb/cars/vehicle-racer.glb#Scene0")
    );

    commands.spawn((
      Car,
      Transform::from_xyz(0.0, 0.0, 0.0),
      SceneRoot(model)
    ));
}

For convenience, if you need to pass around these assets often, you can store these handles inside a resource.

#[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(
    GltfAssetLabel::Scene(0).from_asset("glb/cars/vehicle-racer.glb#Scene0")
  );
  car.body = Some(car_handle);
}

Which makes them much easier to access 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(Mesh3d(body.clone()));
  }
}

Asset handles

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

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 weak 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.

Handles can be cloned as strong or weak and will be reference 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 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 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 if it needs to.

For example, if we give a .glb file to the asset server, then Bevy will use the GltfLoader and give back a Handle<Gltf>. We then pass this handle to a SceneRoot on our entity which will actually render the 3D asset.

// 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 handle = asset_server.load("models/ambulance.glb#Scene0");

commands.spawn((
  Car,
  Transform::from_xyz(0.0, 0.0, 0.0),
  SceneRoot(handle),
));

Asset loading state

An Asset tracks its loading state internally. 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. The larger our assets are the more likely you are to need to track their loading state.

For example, we might want to load an image and only spawn our entity when the image is fully loaded.

#[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:

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((
        Transform::from_xyz(0., 0., 0.),
        Sprite {
          image: bevy_image.0.clone(),
          ..default()
        },
      ));
    }
    Some(LoadState::Failed(_)) => {}
    None => {}
  }
}

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

You can change where Bevy will assume our 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.

Asset events

Assets fire certain events which are sent during the PostUpdate schedule:

This lets us react to changes to our assets:

use bevy::asset::AssetEvent;

fn react_to_images(mut events: MessageReader<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 our 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 our project directory.

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

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

  // You could also parse this URL-like string representation for the asset
  // path.
  assert_eq!(asset_path, "example_files://bevy_pixel_light.png".into());

  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("other://some_image.png"),
  ..default()
});

Asset server

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

All assets follow the same general process:

  1. We register a new Asset<T> type if its custom
  2. We Register an AssetLoader for that asset if its custom
  3. We add the asset to our assets folder
  4. Then we call AssetServer::load to get a Handle<T> to the asset

When you load up an asset Bevy is sending a task its TaskPool which handles managing the asynchronous loading of assets in a separate thread.

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 along side 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_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 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:

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 our assets don't fall into the normal file types supported by Bevy you can create our own custom asset loader.

#[derive(Asset, TypePath)]
struct TextFile {
  content: String,
}

impl AssetLoader for TextFile {
  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>()
    .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.

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 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.

A very ergonomic technique is to extend App with our own load_resource function that will add them to the queue and only make them available as resources when they are fully loaded.

This makes it much easier to query for resources and not have to manually track their loaded status in every system.

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. The tracking will be handled for us.

#[derive(Resource, Asset, Clone, Reflect)]
#[reflect(Resource)]
pub struct LevelAssets {
  #[dependency]
  music: Handle<AudioSource>,
}

impl FromWorld for LevelAssets {
  fn from_world(world: &mut World) -> Self {
    let assets = world.resource::<AssetServer>();
    Self {
      music: assets.load("audio/music/Fluffing A Duck.ogg"),
    }
  }
}

pub fn level_plugin(app: &mut App) {
  app.register_type::<LevelAssets>();
  app.load_resource::<LevelAssets>();
}

Implementing FromWorld lets our load_resource extension load this specific asset and add it to our World so its available to other systems.