36 releases

0.15.0 Nov 29, 2024
0.15.0-rc.2 Oct 27, 2024
0.14.0 Jul 4, 2024
0.13.1 Mar 18, 2024
0.3.0 Nov 3, 2020

#1505 in Game dev

Download history 21095/week @ 2024-08-17 23497/week @ 2024-08-24 22698/week @ 2024-08-31 20487/week @ 2024-09-07 17896/week @ 2024-09-14 21286/week @ 2024-09-21 18347/week @ 2024-09-28 13340/week @ 2024-10-05 14819/week @ 2024-10-12 15497/week @ 2024-10-19 16028/week @ 2024-10-26 17149/week @ 2024-11-02 9559/week @ 2024-11-09 12090/week @ 2024-11-16 16977/week @ 2024-11-23 22617/week @ 2024-11-30

63,851 downloads per month
Used in 981 crates (73 directly)

MIT/Apache

4.5MB
76K SLoC

Bevy Asset

License Crates.io Downloads Docs Discord


lib.rs:

In the context of game development, an "asset" is a piece of content that is loaded from disk and displayed in the game. Typically, these are authored by artists and designers (in contrast to code), are relatively large in size, and include everything from textures and models to sounds and music to levels and scripts.

This presents two main challenges:

  • Assets take up a lot of memory; simply storing a copy for each instance of an asset in the game would be prohibitively expensive.
  • Loading assets from disk is slow, and can cause long load times and delays.

These problems play into each other, for if assets are expensive to store in memory, then larger game worlds will need to load them from disk as needed, ideally without a loading screen.

As is common in Rust, non-blocking asset loading is done using async, with background tasks used to load assets while the game is running. Bevy coordinates these tasks using the AssetServer resource, storing each loaded asset in a strongly-typed Assets<T> collection (also a resource). Handles serve as an id-based reference to entries in the Assets collection, allowing them to be cheaply shared between systems, and providing a way to initialize objects (generally entities) before the required assets are loaded. In short: Handles are not the assets themselves, they just tell how to look them up!

Loading assets

The AssetServer is the main entry point for loading assets. Typically, you'll use the AssetServer::load method to load an asset from disk, which returns a Handle. Note that this method does not attempt to reload the asset if it has already been loaded: as long as at least one handle has not been dropped, calling AssetServer::load on the same path will return the same handle. The handle that's returned can be used to instantiate various Components that require asset data to function, which will then be spawned into the world as part of an entity.

To avoid assets "popping" into existence, you may want to check that all of the required assets are loaded before transitioning to a new scene. This can be done by checking the LoadState of the asset handle using AssetServer::is_loaded_with_dependencies, which will be true when the asset is ready to use.

Keep track of what you're waiting on by using a HashSet of asset handles or similar data structure, which iterate over and poll in your update loop, and transition to the new scene once all assets are loaded. Bevy's built-in states system can be very helpful for this!

Modifying entities that use assets

If we later want to change the asset data a given component uses (such as changing an entity's material), we have three options:

  1. Change the handle stored on the responsible component to the handle of a different asset
  2. Despawn the entity and spawn a new one with the new asset data.
  3. Use the Assets collection to directly modify the current handle's asset data

The first option is the most common: just query for the component that holds the handle, and mutate it, pointing to the new asset. Check how the handle was passed in to the entity when it was spawned: if a mesh-related component required a handle to a mesh asset, you'll need to find that component via a query and change the handle to the new mesh asset. This is so commonly done that you should think about strategies for how to store and swap handles in your game.

The second option is the simplest, but can be slow if done frequently, and can lead to frustrating bugs as references to the old entity (such as what is targeting it) and other data on the entity are lost. Generally, this isn't a great strategy.

The third option has different semantics: rather than modifying the asset data for a single entity, it modifies the asset data for all entities using this handle. While this might be what you want, it generally isn't!

Hot reloading assets

Bevy supports asset hot reloading, allowing you to change assets on disk and see the changes reflected in your game without restarting. When enabled, any changes to the underlying asset file will be detected by the AssetServer, which will then reload the asset, mutating the asset data in the Assets collection and thus updating all entities that use the asset. While it has limited uses in published games, it is very useful when developing, as it allows you to iterate quickly.

To enable asset hot reloading on desktop platforms, enable bevy's file_watcher cargo feature. To toggle it at runtime, you can use the watch_for_changes_override field in the AssetPlugin to enable or disable hot reloading.

Procedural asset creation

Not all assets are loaded from disk: some are generated at runtime, such as procedural materials, sounds or even levels. After creating an item of a type that implements Asset, you can add it to the Assets collection using Assets::add. Once in the asset collection, this data can be operated on like any other asset.

Note that, unlike assets loaded from a file path, no general mechanism currently exists to deduplicate procedural assets: calling Assets::add for every entity that needs the asset will create a new copy of the asset for each entity, quickly consuming memory.

Handles and reference counting

Handle (or their untyped counterpart UntypedHandle) are used to reference assets in the Assets collection, and are the primary way to interact with assets in Bevy. As a user, you'll be working with handles a lot!

The most important thing to know about handles is that they are reference counted: when you clone a handle, you're incrementing a reference count. When the object holding the handle is dropped (generally because an entity was despawned), the reference count is decremented. When the reference count hits zero, the asset it references is removed from the Assets collection.

This reference counting is a simple, largely automatic way to avoid holding onto memory for game objects that are no longer in use. However, it can lead to surprising behavior if you're not careful!

There are two categories of problems to watch out for:

  • never dropping a handle, causing the asset to never be removed from memory
  • dropping a handle too early, causing the asset to be removed from memory while it's still in use

The first problem is less critical for beginners, as for tiny games, you can often get away with simply storing all of the assets in memory at once, and loading them all at the start of the game. As your game grows, you'll need to be more careful about when you load and unload assets, segmenting them by level or area, and loading them on-demand. This problem generally arises when handles are stored in a persistent "collection" or "manifest" of possible objects (generally in a resource), which is convenient for easy access and zero-latency spawning, but can result in high but stable memory usage.

The second problem is more concerning, and looks like your models or textures suddenly disappearing from the game. Debugging reveals that the entities are still there, but nothing is rendering! This is because the assets were removed from memory while they were still in use. You were probably too aggressive with the use of weak handles (which don't increment the reference count of the asset): think through the lifecycle of your assets carefully! As soon as an asset is loaded, you must ensure that at least one strong handle is held to it until all matching entities are out of sight of the player.

Asset dependencies

Some assets depend on other assets to be loaded before they can be loaded themselves. For example, a 3D model might require both textures and meshes to be loaded, or a 2D level might require a tileset to be loaded.

The assets that are required to load another asset are called "dependencies". An asset is only considered fully loaded when it and all of its dependencies are loaded. Asset dependencies can be declared when implementing the Asset trait by implementing the VisitAssetDependencies trait, and the #[dependency] attribute can be used to automatically derive this implementation.

Custom asset types

While Bevy comes with implementations for a large number of common game-oriented asset types (often behind off-by-default feature flags!), implementing a custom asset type can be useful when dealing with unusual, game-specific, or proprietary formats.

Defining a new asset type is as simple as implementing the Asset trait. This requires TypePath for metadata about the asset type, and VisitAssetDependencies to track asset dependencies. In simple cases, you can derive Asset and Reflect and be done with it: the required supertraits will be implemented for you.

With a new asset type in place, we now need to figure out how to load it. While AssetReader describes strategies to read asset bytes from various sources, AssetLoader is the trait that actually turns those into your desired in-memory format. Generally, (only) AssetLoader needs to be implemented for custom assets, as the AssetReader implementations are provided by Bevy.

However, AssetLoader shouldn't be implemented for your asset type directly: instead, this is implemented for a "loader" type that can store settings and any additional data required to load your asset, while your asset type is used as the AssetLoader::Asset associated type. As the trait documentation explains, this allows various AssetLoader::Settings to be used to configure the loader.

After the loader is implemented, it needs to be registered with the AssetServer using App::register_asset_loader. Once your asset type is loaded, you can use it in your game like any other asset type!

If you want to save your assets back to disk, you should implement AssetSaver as well. This trait mirrors AssetLoader in structure, and works in tandem with AssetWriter, which mirrors AssetReader.

Dependencies

~9–26MB
~344K SLoC