Skip to content
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

Merged
merged 27 commits into from
Sep 17, 2024
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
55c96e8
Revert "Revert accidentally added asset docs (#15054)"
alice-i-cecile Sep 5, 2024
938ab47
Change ordering of suggestions for modifying assets
alice-i-cecile Sep 8, 2024
7967aac
Copyediting suggestions
alice-i-cecile Sep 8, 2024
f66fa09
Mirrored to complementary :(
alice-i-cecile Sep 9, 2024
1e6d453
More clarity about nature of storage system interaction
alice-i-cecile Sep 9, 2024
4ebd44a
Remove asset zoo reference
alice-i-cecile Sep 9, 2024
d02a31d
Remove line about clone_weak
alice-i-cecile Sep 9, 2024
7eeb495
Merge branch 'lets-try-this-again' of https://github.com/alice-i-ceci…
alice-i-cecile Sep 9, 2024
8c7bd34
Improve docs comparing AssetSaver/Loader with Reader/Writer
alice-i-cecile Sep 9, 2024
f1e54d6
AssetLoader is about settings, not intermediate state
alice-i-cecile Sep 9, 2024
f2d19e5
No stuttering
alice-i-cecile Sep 9, 2024
e41f384
No ```rust
alice-i-cecile Sep 9, 2024
014363e
Copyediting
alice-i-cecile Sep 9, 2024
244efbe
Copyediting
alice-i-cecile Sep 17, 2024
920716a
Handles are not assets
alice-i-cecile Sep 17, 2024
ee3c853
Briefly explain weak handles
alice-i-cecile Sep 17, 2024
b5d2d78
Remove non-compiling code snippet
alice-i-cecile Sep 17, 2024
a67698c
Add note about procedural assets
alice-i-cecile Sep 17, 2024
5ae4ac3
Fix ordering of options for changing assets
alice-i-cecile Sep 17, 2024
5c3f047
More guidance on mutating handles
alice-i-cecile Sep 17, 2024
57ce386
More headings
alice-i-cecile Sep 17, 2024
886f606
Add section on hot reloading assets
alice-i-cecile Sep 17, 2024
c6bd067
Caveat about desktop only
alice-i-cecile Sep 17, 2024
8f641f2
Notes on asset dependencies
alice-i-cecile Sep 17, 2024
37c205e
Redundant target for doc link
alice-i-cecile Sep 17, 2024
8fadd6c
Improve wording
alice-i-cecile Sep 17, 2024
d2ca650
Mention that Assets / AssetServer are resources
alice-i-cecile Sep 17, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 8 additions & 2 deletions crates/bevy_asset/src/io/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Copy link
Contributor

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.

Copy link
Member Author

@alice-i-cecile alice-i-cecile Sep 17, 2024

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

/// 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.
///
Expand Down Expand Up @@ -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).
Copy link
Contributor

Choose a reason for hiding this comment

The 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>(
Expand Down
120 changes: 120 additions & 0 deletions crates/bevy_asset/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,112 @@
//! In the context of game development, an "asset" is a piece of multimedia content which must be loaded from disk and displayed in the game.
alice-i-cecile marked this conversation as resolved.
Show resolved Hide resolved
//! 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.
//!
alice-i-cecile marked this conversation as resolved.
Show resolved Hide resolved
//! ## Loading and using 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.
//!
//! ```rust
alice-i-cecile marked this conversation as resolved.
Show resolved Hide resolved
//! if asset_server.is_loaded_with_dependencies(&my_asset) {
//! // Start the Game!
//! }
//! ```
//!
//! Keep track of what you're waiting on using a [`HashSet`] or similar data structure,
alice-i-cecile marked this conversation as resolved.
Show resolved Hide resolved
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.
Copy link
Member

Choose a reason for hiding this comment

The 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 bevy_quickstart, for example.

Copy link
Member Author

Choose a reason for hiding this comment

The 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!
//!
//! If we later want to change the asset data a given Component uses (such as changing an entity's material), we have three options:
alice-i-cecile marked this conversation as resolved.
Show resolved Hide resolved
//!
//! 1. Change the handle field on the Component to the handle of a different asset
alice-i-cecile marked this conversation as resolved.
Show resolved Hide resolved
//! 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 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 second option is the most common: just query for the component that holds the handle, and mutate it, pointing to the new asset.
alice-i-cecile marked this conversation as resolved.
Show resolved Hide resolved
//!
alice-i-cecile marked this conversation as resolved.
Show resolved Hide resolved
//! 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
//! ## 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: think through the lifetime of your assets carefully!
alice-i-cecile marked this conversation as resolved.
Show resolved Hide resolved
//! 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.
//!
//! # 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))]
Expand Down Expand Up @@ -231,13 +340,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.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
/// Generally, assets are large, complex, and/or expensive to load from disk, and are often authored by artists or designers.
/// Generally, assets are large, complex, and/or expensive to load from disk, and are often authored by artists or designers.
/// Examples include textures, sounds, 3D models, etc.

Copy link
Contributor

Choose a reason for hiding this comment

The 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.

Copy link
Contributor

Choose a reason for hiding this comment

The 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 :)

Copy link
Contributor

@richchurcher richchurcher Sep 10, 2024

Choose a reason for hiding this comment

The 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 asset_server.load in an Update system, because my instinct is "no" but I don't really have anything to back that up except the number of times updates get called in a second! So, something around general runtime changes: we're making after the initial load, do we:

  • place an asset load request in a queue to be processed
  • dispatch an event, add something to a resource tracking loads to be accomplished
  • load the asset in update systems, because this is considered safe (?)
  • something else

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 😆

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, IMO this deserves a full example to demonstrate the patterns here.

Copy link
Contributor

Choose a reason for hiding this comment

The 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));
}
Expand Down
4 changes: 4 additions & 0 deletions crates/bevy_asset/src/loader.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@ use thiserror::Error;

/// Loads an [`Asset`] from a given byte [`Reader`]. This can accept [`AssetLoader::Settings`], which configure how the [`Asset`]
/// should be loaded.
///
/// This trait is generally used in concert with [`AssetReader`](crate::io::AssetReader) to load assets from a byte source.
///
/// For a complementary version of this trait that can save assets, see [`AssetSaver`](crate::saver::AssetSaver).
pub trait AssetLoader: Send + Sync + 'static {
/// The top level [`Asset`] loaded by this [`AssetLoader`].
type Asset: Asset;
Expand Down
4 changes: 4 additions & 0 deletions crates/bevy_asset/src/saver.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ use std::{borrow::Borrow, hash::Hash, ops::Deref};

/// Saves an [`Asset`] of a given [`AssetSaver::Asset`] type. [`AssetSaver::OutputLoader`] will then be used to load the saved asset
/// in the final deployed application. The saver should produce asset bytes in a format that [`AssetSaver::OutputLoader`] can read.
///
/// This trait is generally used in concert with [`AssetWriter`](crate::io::AssetWriter) to write assets as bytes.
///
/// For a complementary version of this trait that can load assets, see [`AssetLoader`](crate::loader::AssetLoader).
pub trait AssetSaver: Send + Sync + 'static {
/// The top level [`Asset`] saved by this [`AssetSaver`].
type Asset: Asset;
Expand Down
3 changes: 3 additions & 0 deletions crates/bevy_asset/src/server/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Copy link
Contributor

Choose a reason for hiding this comment

The 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 :/.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

#11111 is the relevant issue.

Copy link
Member

Choose a reason for hiding this comment

The 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:
Expand Down
Loading