-
-
Notifications
You must be signed in to change notification settings - Fork 3.7k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add module and supporting documentation to bevy_assets
#15056
Changes from 24 commits
55c96e8
938ab47
7967aac
f66fa09
1e6d453
4ebd44a
d02a31d
7eeb495
8c7bd34
f1e54d6
f2d19e5
e41f384
014363e
244efbe
920716a
ee3c853
b5d2d78
a67698c
5ae4ac3
5c3f047
57ce386
886f606
c6bd067
8f641f2
37c205e
8fadd6c
d2ca650
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -130,7 +130,10 @@ where | |
/// API, where asset bytes and asset metadata bytes are both stored and accessible for a given | ||
/// `path`. This trait is not object safe, if needed use a dyn [`ErasedAssetReader`] instead. | ||
/// | ||
/// Also see [`AssetWriter`]. | ||
/// This trait defines asset-agnostic mechanisms to read bytes from a storage system. | ||
/// For the per-asset-type saving/loading logic, see [`AssetSaver`](crate::saver::AssetSaver) and [`AssetLoader`](crate::loader::AssetLoader). | ||
/// | ||
/// For a complementary version of this trait that can write assets to storage, see [`AssetWriter`]. | ||
alice-i-cecile marked this conversation as resolved.
Show resolved
Hide resolved
|
||
pub trait AssetReader: Send + Sync + 'static { | ||
/// Returns a future to load the full file data at the provided path. | ||
/// | ||
|
@@ -261,7 +264,10 @@ pub enum AssetWriterError { | |
/// API, where asset bytes and asset metadata bytes are both stored and accessible for a given | ||
/// `path`. This trait is not object safe, if needed use a dyn [`ErasedAssetWriter`] instead. | ||
/// | ||
/// Also see [`AssetReader`]. | ||
/// This trait defines asset-agnostic mechanisms to write bytes to a storage system. | ||
/// For the per-asset-type saving/loading logic, see [`AssetSaver`](crate::saver::AssetSaver) and [`AssetLoader`](crate::loader::AssetLoader). | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Same here. |
||
/// | ||
/// For a complementary version of this trait that can read assets from storage, see [`AssetReader`]. | ||
pub trait AssetWriter: Send + Sync + 'static { | ||
/// Writes the full asset bytes at the provided path. | ||
fn write<'a>( | ||
|
Original file line number | Diff line number | Diff line change | ||||||||
---|---|---|---|---|---|---|---|---|---|---|
@@ -1,3 +1,143 @@ | ||||||||||
//! 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. | ||||||||||
alice-i-cecile marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||
//! | ||||||||||
//! 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. | ||||||||||
alice-i-cecile marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||
//! | ||||||||||
//! 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! | ||||||||||
alice-i-cecile marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||
//! | ||||||||||
//! 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`], storing each loaded asset in a strongly-typed [`Assets<T>`] collection. | ||||||||||
//! [`Handle`]s 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: [`Handle`]s are not the assets themselves, they just tell how to look them up! | ||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
Suggested change
If people still remember what a bookmark is, of course 🙂 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Hmm, I think that terminology is a bit confusing :) |
||||||||||
//! | ||||||||||
//! ## 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`]. | ||||||||||
alice-i-cecile marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||
//! 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 [`Component`](bevy_ecs::prelude::Component)s 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`], | ||||||||||
alice-i-cecile marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||
//! which will be `true` when the asset is ready to use. | ||||||||||
//! | ||||||||||
//! Keep track of what you're waiting on by using a [`HashSet`] or similar data structure, | ||||||||||
alice-i-cecile marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||
//! which you poll in your update loop, and transition to the new scene when all assets are loaded. | ||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can we link something here? Cart himself wrote a version of this for There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. IMO this is out of scope.
alice-i-cecile marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||
//! 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! | ||||||||||
alice-i-cecile marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||
//! | ||||||||||
alice-i-cecile marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||
//! # 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 | ||||||||||
alice-i-cecile marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||
//! | ||||||||||
//! [`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. | ||||||||||
//! | ||||||||||
alice-i-cecile marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||
//! With a new asset type in place, we now need to figure out how to load it. | ||||||||||
//! While [`AssetReader`](io::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`](io::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. | ||||||||||
alice-i-cecile marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||
//! | ||||||||||
alice-i-cecile marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||
//! After the loader is implemented, it needs to be registered with the [`AssetServer`] using [`App::register_asset_loader`](AssetApp::register_asset_loader). | ||||||||||
//! Once your asset type is loaded, you can use it in your game like any other asset type! | ||||||||||
//! | ||||||||||
alice-i-cecile marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||
//! If you want to save your assets back to disk, you should implement [`AssetSaver`](saver::AssetSaver) as well. | ||||||||||
//! This trait mirrors [`AssetLoader`] in structure, and works in tandem with [`AssetWriter`](io::AssetWriter), which mirrors [`AssetReader`](io::AssetReader). | ||||||||||
|
||||||||||
// FIXME(3492): remove once docs are ready | ||||||||||
#![allow(missing_docs)] | ||||||||||
alice-i-cecile marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||
#![cfg_attr(docsrs, feature(doc_auto_cfg))] | ||||||||||
|
@@ -231,13 +371,24 @@ impl Plugin for AssetPlugin { | |||||||||
} | ||||||||||
} | ||||||||||
|
||||||||||
/// Declares that this type is an asset, | ||||||||||
/// which can be loaded and managed by the [`AssetServer`] and stored in [`Assets`] collections. | ||||||||||
/// | ||||||||||
/// Generally, assets are large, complex, and/or expensive to load from disk, and are often authored by artists or designers. | ||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The other thing you might want to mention is hot reloading "data", e.g. how much damage an enemy does or what items they drop or their spawn rate or whatever. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Actually, I'm not seeing any mention of hot reloading at all in these docs, seems like a missing piece :) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. So when I was reading through with #3130 in mind, the biggest question remaining for me is timing-sensitive code, i.e. should we be calling
And touch on whether any of these mechanisms should be state-controlled. That's the missing piece of the puzzle for me, I guess as a junior apprentice 😆 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah, IMO this deserves a full example to demonstrate the patterns here. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If only someone was working on such a thing! Oh, wait... |
||||||||||
/// | ||||||||||
/// [`TypePath`] is largely used for diagnostic purposes, and should almost always be implemented by deriving [`Reflect`] on your type. | ||||||||||
/// [`VisitAssetDependencies`] is used to track asset dependencies, and an implementation is automatically generated when deriving [`Asset`]. | ||||||||||
#[diagnostic::on_unimplemented( | ||||||||||
message = "`{Self}` is not an `Asset`", | ||||||||||
label = "invalid `Asset`", | ||||||||||
note = "consider annotating `{Self}` with `#[derive(Asset)]`" | ||||||||||
)] | ||||||||||
pub trait Asset: VisitAssetDependencies + TypePath + Send + Sync + 'static {} | ||||||||||
|
||||||||||
/// This trait defines how to visit the dependencies of an asset. | ||||||||||
/// For example, a 3D model might require both textures and meshes to be loaded. | ||||||||||
/// | ||||||||||
/// Note that this trait is automatically implemented when deriving [`Asset`]. | ||||||||||
pub trait VisitAssetDependencies { | ||||||||||
fn visit_dependencies(&self, visit: &mut impl FnMut(UntypedAssetId)); | ||||||||||
} | ||||||||||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -266,6 +266,9 @@ impl AssetServer { | |
/// it returns a "strong" [`Handle`]. When the [`Asset`] is loaded (and enters [`LoadState::Loaded`]), it will be added to the | ||
/// associated [`Assets`] resource. | ||
/// | ||
/// Note that if the asset at this path is already loaded, this function will return the existing handle, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Relevant context: This is a (already documented) problem, since different loader settings on the same asset don't work :/. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. #11111 is the relevant issue. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nice number :D |
||
/// and will not waste work spawning a new load task. | ||
/// | ||
/// In case the file path contains a hashtag (`#`), the `path` must be specified using [`Path`] | ||
/// or [`AssetPath`] because otherwise the hashtag would be interpreted as separator between | ||
/// the file path and the label. For example: | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The first and second paragraphs seem like they should be combined somehow? It feels a bit duplicated to me.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I agree, but I don't immediately see a good way to do this, so I'm going to merge as is <3