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.
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 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(
mut bevy_image: ResMut<BevyImage>,
asset_server: Res<AssetServer>,
mut commands: Commands,
) {
// This will not block, the asset will be loaded in the background
bevy_image.0 = asset_server.load("example_images/bevy.png");
commands.spawn(Sprite {
image: bevy_image.0.clone(),
..default()
});
}
Here we are loading an image from the assets/example_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, Default)]
struct BevyImage(Handle<Image>);
fn load_images(
mut bevy_image: ResMut<BevyImage>,
asset_server: Res<AssetServer>,
) {
bevy_image.0 = asset_server.load("example_images/bevy.png");
}
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 its ready.
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(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:
- 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. Bevy will by default expect our assets at ./assets according to our BEVY_ASSET_ROOT env variable.
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<Scene>,
}
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 load it inside a system using a SceneRoot, which is a component used to render predefined scenes.
fn spawn_car(car: Res<CarAssets>, mut commands: Commands) {
info!("Spawning ambulance scene");
commands.spawn((
SceneRoot(car.scene.clone()),
Transform::from_xyz(0., 0., 0.),
));
}
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 type | Loader |
|---|---|---|
ttf, otf | Font | FontLoader |
hdr | Image | HdrTextureLoader |
exr | Image | ExrTextureLoader |
scn, scn.ron | DynamicScene | SceneLoader |
gz | GzAsset | GzAssetLoader |
meshlet_mesh | MeshletMesh | MeshletMeshLoader |
mp3, flac, oga, ogg, spx | AudioSource | AudioLoader |
bmp, dds, ff, gif, ico, jpeg, ktx2, png, pnm, qoi, tga, tiff, webp | Image | ImageLoader |
animgraph, animgraph.ron | AnimationGraph | AnimationGraphAssetLoader |
gltf, glb | Gltf | GltfLoader |
spv, wgsl, vert, frag, comp | Shader | ShaderLoader |
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. 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 its fully loaded:
use bevy::asset::LoadState;
fn on_asset_event(asset_server: Res<AssetServer>, car: Res<CarAssets>) {
match asset_server.get_load_state(&car.scene) {
Some(LoadState::NotLoaded) => {
info!("Ambulance glb scene not loaded");
}
Some(LoadState::Loading) => {
info!("Loading ambulance glb scene");
}
Some(LoadState::Loaded) => {
info!("Loaded ambulance glb scene");
}
Some(LoadState::Failed(_)) => {
info!("Failed to load ambulance glb scene");
}
None => {}
}
}
Asset paths
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:
BEVY_ASSET_ROOTCARGO_MANIFEST_DIRwhich automatically set to the root folder of our crate (workspace).
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, which 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 } => {
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.
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:
use bevy::prelude::*;
use std::path::Path;
use bevy::asset::{
AssetPath,
io::{AssetSourceBuilder, AssetSourceId},
};
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 anywhere else in our project directory.
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 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:
- We register a new
Asset<T>type if its custom - We Register an
AssetLoaderfor that asset if its 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 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.
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();
}