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 typeAssetServer
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:
- The asset type
- The asset settings type
- 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()
}