# bevy_asset_loader: loading screens that guarantee your assets are ready

> bevy_asset_loader is a Bevy plugin that turns a struct of asset handles into a derived AssetCollection, loads it during a Bevy state, and only makes the collection available as a resource once every handle has finished loading. It suits Bevy projects that need a real loading screen, and it is more machinery than a game with a handful of assets needs.

**NiklasEi/bevy_asset_loader** — Bevy plugin helping with asset loading and organization

- Repository: https://github.com/NiklasEi/bevy_asset_loader
- Stars: 711 · Forks: 71
- Language: Rust
- License: Apache-2.0
- Published: 2026-10-21 · Updated: 2026-10-21 · Language: en
- Canonical page: https://hysenlabs.com/projects/niklasei-bevy-asset-loader

## A collection is a struct of handles, and the derive macro writes it for you

bevy_asset_loader is a Bevy plugin that reduces boilerplate for handling game assets, and the crate offers a derivable AssetCollection trait that can automatically load structs that implement it. Asset collections contain handles to your game assets and are available to your systems as resources after loading.

The shape of the API is a derive macro and a struct. A collection is declared as a plain Rust struct with asset attributes on the fields:

```rust
#[derive(AssetCollection, Resource)]
struct AudioAssets {
    #[asset(path = "audio/background.ogg")]
    background: Handle<AudioSource>,
}
```

The field type says what kind of asset it is, and the derive macro turns that into the loading logic. The simplest supported field is a Handle<T> loaded from a single file, and the field type can be any asset type that has a handle, so audio sources, images and textures all work the same way.

The second example in the project documentation shows a second collection, PlayerAssets with a sprite field pointing at images/player.png, which is the shape most collections end up taking.

The crate is published on crates.io as bevy_asset_loader, with API documentation on docs.rs, and the licence is dual: the repository tree contains both LICENSE-APACHE and LICENSE-MIT files, and the crate is Apache-2.0. The source is a Cargo workspace with two members, bevy_asset_loader and bevy_asset_loader_derive, the second being the proc macro crate, and the workspace carries a set of clippy lint settings in its own lints table, including doc_markdown, redundant_else, match_same_arms, semicolon_if_nothing_returned, map_flatten and manual_let_else.

## The loading state is a gate, not a progress bar

The core idea is a Bevy state that manages the loading process, and it is worth understanding what it actually guarantees before deciding whether you want it.

The loading state is responsible for managing loading during a configurable Bevy state. If your LoadingState is set up, you start your game logic from the configured next state and use the asset collections as resources in your systems. The guarantee is that all handles in your collections are fully loaded by the time the next state starts.

So it is a gate, not a progress bar. The collection is not available to your gameplay systems until every handle in it has finished loading, and only then is it inserted into the ECS as a resource. That is the mechanism that removes the class of bug where a system runs against a handle that has not resolved yet.

The setup code shows the whole shape:

```rust
use bevy::prelude::*;
use bevy_asset_loader::prelude::*;

fn main() {
    App::new()
        .add_plugins(DefaultPlugins)
        .init_state::<MyStates>()
        .add_loading_state(
            LoadingState::new(MyStates::AssetLoading)
                .continue_to_state(MyStates::Next)
                .load_collection::<AudioAssets>(),
        )
        .add_systems(OnEnter(MyStates::Next), start_background_audio)
        .run();
}
```

The state enum itself is an ordinary Bevy States derive with a default variant, and the loading state names the state to run in and the state to continue to.

The documentation notes that the loading state guarantees the handles are loaded, but it does not describe a progress callback or a loading screen UI in this material. So the progress observation the introduction mentions, that all assets are loaded and their loading progress is observed, is described as part of the state, and the UI part is left to you. If you need a progress bar with a percentage, that is not something the material describes.

## Adding collections later, from a plugin, with configure_loading_state

You do not have to declare every collection in one place. The documentation shows adding collections to a loading state at any point in the application, using configure_loading_state, and gives the concrete case of adding a PlayerPlugin to the App after the loading state has been added, where the plugin contains everything player related including the sprite.

```rust
use bevy::prelude::*;
use bevy_asset_loader::prelude::*;

struct PlayerPlugin;

impl Plugin for PlayerPlugin {
    fn build(&self, app: &mut App) {
        app
            .configure_loading_state(
                LoadingStateConfig::new(MyStates::AssetLoading)
                    .load_collection::<PlayerAssets>(),
            );
    }
}
```

The configuration type differs from the initial one, LoadingStateConfig rather than LoadingState, and it is constructed with the same state name. The pattern is a plugin that owns a collection and registers it against the loading state, which is a cleaner arrangement than one enormous collection struct, because the collection lives next to the plugin that uses it.

The documentation also makes the point that you do not have to use a loading state at all. If you do not want one, asset collections can still result in cleaner code and improved maintainability, and there is a section on usage without a loading state. That is a real option for a project that streams assets in rather than gating on them, and it is the same derive macro either way, since the collection is the unit that the macro generates.

## Compile time paths versus dynamic assets, and the split between code and data

Asset configuration, such as the file path or the dimensions of a sprite sheet, can be given at compile time through derive macro attributes, or at run time as dynamic assets. The second option means managing asset configurations as assets themselves, so a list of your asset files and their properties can live in an asset file.

The stated benefits of the dynamic route are a cleaner split of code and data, fewer recompiles while working on your assets, and a game that is more approachable for contributors who do not want to touch code. That last point is the interesting one for a project with community contributions, since a designer can add an asset entry without a Rust change.

The dynamic form uses a key attribute rather than a path:

```rust
use bevy::prelude::*;
use bevy_asset_loader::asset_collection::AssetCollection;

#[derive(AssetCollection, Resource)]
struct ImageAssets {
  #[asset(key = "player")]
  player: Handle<Image>,
  #[asset(key = "tree")]
  tree: Handle<Image>,
}
```

The keys have to be set somewhere, either manually in the DynamicAssets resource before the loading state, or as part of a dynamic assets file. The file is in ron format:

```ron
({
    "player": File (
        path: "images/player.png",
    ),
    "tree": File (
        path: "images/tree.png",
    ),
})
```

Using dynamic assets like File and loading ron files requires the standard_dynamic_assets feature to be enabled. The file ending is .assets.ron by default and can be changed with LoadingState::set_standard_dynamic_asset_collection_file_endings.

The trade-off is explicit in the documentation. With compile time assets, the path of the player sprite is hardcoded to images/player.png, and changing it requires recompiling the application. With dynamic assets, the path lives in data and the recompile disappears, at the cost of the feature flag, the ron file, and the requirement that the AssetServer resource is available, since adding a dynamic asset file to a loading state needs it. In most cases that means adding the DefaultPlugins before configuring the loading state, which is visible in the first example where add_plugins(DefaultPlugins) comes before add_loading_state.

## Custom asset types and the field types the macro understands

The field types a collection can contain are broader than a Handle. The simplest is a Handle<T> loaded from a single file, and the documentation points at a full_collection example as the place to see all the different kinds of fields a collection can contain using only derive macro attributes.

There is also a full_dynamic_collection example showing all supported field types for dynamic assets, and a custom_dynamic_assets example showing how to define your own types to load as dynamic assets. That last one matters if your project has a bespoke asset type: the plugin is not limited to the built-in Bevy asset kinds, since the mechanism is a trait that your types can implement.

The manual_dynamic_asset example covers the other half of the dynamic story, setting the keys by hand in the DynamicAssets resource before the loading state rather than through a file.

The compatibility story is handled by a version table linked from the readme, so the Bevy versions this plugin supports are published rather than left to guesswork. The repository has no GitHub releases, so the crate version on crates.io is the thing to track, and the last push was on 2026-10-13 with the repository not archived.

## Where bevy_asset_loader is the wrong tool

The first limit is the gate. The loading state blocks your game until every handle in every registered collection has finished loading. For a game with a large asset set that is what you want, and for a game that streams content in over a session, it is the wrong shape, because the guarantee it provides is a guarantee that everything is already there, which is the opposite of streaming. The documentation mentions that progress is observed during the state, but the project documentation does not describe a per-collection progress API, so a progress bar with a percentage is something to verify separately.

The second limit is the compile time path. Hardcoded paths in derive attributes mean a change to an asset path is a code change and a recompile, which is exactly the problem the dynamic route exists to solve. A project that expects to move assets around frequently, or that wants non-programmers to manage the asset list, will end up on the dynamic path and will need the standard_dynamic_assets feature and the ron files.

The third is that this is a Bevy plugin and nothing else. The derive macro generates code against Bevy's asset and state APIs, so there is no way to use the collection concept outside Bevy, and the plugin is tied to a Bevy version. The compatibility table exists because the plugin tracks Bevy's release cycle closely, and a project on an older Bevy has to find a matching plugin version.

The fourth is the amount of machinery for a small project. A struct with a couple of handles and a spawn in the right system is not worth a derive macro and a state pair. The plugin earns its keep when there are several collections, several plugins, and a real loading screen.

The fifth is a licensing point to check rather than assume. The repository carries both LICENSE-APACHE and LICENSE-MIT, which is the permissive dual-licence pattern and should be straightforward for most organisations, but the crate metadata lists a single Apache-2.0 identifier while the tree holds both files, so the terms of the two files and which one governs the crates.io artifact are worth a glance.

## Conclusion

bevy_asset_loader is worth adopting for a Bevy game that has more than a handful of assets and a real loading screen, because the derive macro turns a struct of handles into a typed collection and the loading state guarantees the collection is complete before your gameplay systems can touch it, which removes a class of use-after-load bugs. It is the wrong choice for a small project, and it is the wrong choice if you want to stream assets in over time rather than gate on them up front. Before you adopt it, check the three things the documentation is specific about: the Bevy version compatibility, since the project publishes a version table for it; whether you want compile time or dynamic assets, because the dynamic path needs the standard_dynamic_assets feature and a ron file, while the compile time path hardcodes paths in the derive attributes and needs a recompile to change them; and that adding a dynamic asset file to a loading state requires the AssetServer resource to be available, which in practice means adding the DefaultPlugins before configuring the loading state.

## FAQ

### What does the AssetCollection derive macro in bevy_asset_loader generate?

It turns a struct of asset handles into a loadable collection. You derive AssetCollection and Resource on the struct and mark each field with an asset attribute such as path or key, and the plugin loads the handles and inserts the collection into the ECS as a resource once loading is complete.

### How does bevy_asset_loader handle dynamic assets?

Dynamic assets use the key attribute instead of path, and the keys are either set manually in the DynamicAssets resource before the loading state or supplied in a dynamic assets file. The file format is ron, the default ending is .assets.ron, and using File and loading ron files requires the standard_dynamic_assets feature to be enabled.

### Can I use bevy_asset_loader without a loading state?

Yes. The documentation states that if you do not want to use a loading state, asset collections can still result in cleaner code and improved maintainability, and it has a section on usage without a loading state. The same AssetCollection derive is used either way.

## Sources

- [Issues](https://github.com/NiklasEi/bevy_asset_loader/issues)
- [License: Apache-2.0](https://github.com/NiklasEi/bevy_asset_loader/blob/main/LICENSE)
- [NiklasEi/bevy_asset_loader on GitHub](https://github.com/NiklasEi/bevy_asset_loader)
- [README](https://github.com/NiklasEi/bevy_asset_loader/blob/main/README.md)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/niklasei-bevy-asset-loader
