Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
24147ff
Implement assets-as-entities.
andriyDev Jan 31, 2026
0488ba3
Reimplement ReflectAsset to take entities into account.
andriyDev Feb 10, 2026
a90e545
Migrate most uses of assets within Bevy to assets-as-entities.
andriyDev Feb 7, 2026
4cfd9ab
Update most examples to use assets-as-entities.
andriyDev Feb 7, 2026
20fef63
Fix the asset decompression example to use assets as entities.
andriyDev Feb 8, 2026
7c4e117
Rewrite bevy_text to defer spawning of new atlases until the system e…
andriyDev Feb 12, 2026
3950845
Replace solari's StandardMaterialAssets extraction with a hand-coded …
andriyDev Feb 13, 2026
2463376
Update the error docs to be described in terms of assets-as-entities.
andriyDev Feb 7, 2026
60b5c1d
Add a migration guide.
andriyDev Feb 13, 2026
f0c07d8
Create release notes.
andriyDev Feb 13, 2026
0e4e2f2
Delete `asset-mut` migration guide.
andriyDev Feb 13, 2026
0759e81
Send asset events for UUID asset IDs (duplicating their entity's asse…
andriyDev Feb 14, 2026
e0817d9
Improve docs on some methods and types.
andriyDev Mar 8, 2026
625eb3c
Merge 'main' branch into asset-v1.
andriyDev Apr 7, 2026
6735834
Fix up migration errors.
andriyDev Apr 7, 2026
ac0172f
Add some methods for making it easier to juggle `Assets` and `AssetsM…
andriyDev Apr 7, 2026
2df5388
Migrate `bevy_scene` to use assets-as-entities.
andriyDev Apr 7, 2026
7d3be54
Fix editable_text_system to use the deferred atlases.
andriyDev Apr 7, 2026
e0e5a77
Fix up bevy_gizmos_render to use assets-as-entities.
andriyDev Apr 7, 2026
26bbc43
Fix up any new/updated examples.
andriyDev Apr 7, 2026
7ca3759
Merge 'main' branch into 'asset-v1'.
andriyDev Apr 7, 2026
1356f60
More random migrations.
andriyDev Apr 7, 2026
29bbb97
Fix accidentally ambiguous systems after adding Commands (through Ass…
andriyDev Apr 8, 2026
5d15b10
Merge 'main' branch into 'asset-v1'.
andriyDev Apr 8, 2026
29cff47
Fixup bevy_city.
andriyDev Apr 8, 2026
161fa3f
More migrations.
andriyDev Apr 8, 2026
bd6fbe8
Replace all `.into()` usages in `spawn_asset` to use `A::from` instead.
andriyDev Apr 9, 2026
3be8021
Implement Deref/DerefMut to deref into the asset type instead of into…
andriyDev Apr 9, 2026
a8c86ea
Update accesses to AssetMut and remove triple derefs.
andriyDev Apr 9, 2026
7227e12
Add doc comments for `AssetSelfHandle`.
andriyDev Apr 9, 2026
a99ec9c
Fixup doc comments in lib.rs.
andriyDev Apr 9, 2026
05f3a50
Merge 'main' branch into 'asset-v1'.
andriyDev Jun 13, 2026
af092aa
Fixup compile errors from merge.
andriyDev Jun 14, 2026
3e238bc
Move migration guide and release notes to _release-content.
andriyDev Jun 14, 2026
f6dc2db
Fixup the migration guides.
andriyDev Jun 14, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
195 changes: 195 additions & 0 deletions _release-content/migration-guides/assets_as_entities.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,195 @@
---
title: Assets-as-entities
pull_requests: [22939]
---

Previously assets were stored in a resource named `Assets`. Assets are now stored as components on
entities in the ECS world. This has required us to redesign some of our APIs. Here are some common
cases and their replacements:

## 1. Reading asset data

- Replace `Res<Assets<A>>` with `Assets<A>`.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I would really love a regular expression that I can toss into VSCode's find and replace to do this here.


Before:
Comment thread
andriyDev marked this conversation as resolved.

```rust
fn my_system(meshes: Res<Assets<Mesh>>) {
let handle = ...;
let data: &Mesh = meshes.get(&handle).unwrap();
}
```

After:

```rust
fn my_system(meshes: Assets<Mesh>) {
let handle = ...;
let data: &Mesh = meshes.get(&handle).unwrap();
}
```

## 2. Mutating asset data

- Replace `ResMut<Assets<A>>` with `AssetsMut<A>`.

Before:

```rust
fn my_system(mut meshes: ResMut<Assets<Mesh>>) {
let handle = ...;
let data: AssetMut<Mesh> = meshes.get_mut(&handle).unwrap();
}
```

After:

```rust
fn my_system(mut meshes: AssetsMut<Mesh>) {
let handle = ...;
let mut data: AssetMut<Mesh> = meshes.get_mut(&handle).unwrap();
}
```

## 3. Adding new assets

- Replace `ResMut<Assets<T>>` with `AssetCommands`.
- For multiple asset types, you only need one `AssetCommands`.
- Replace `.add()` with `.spawn_asset()`.
- For types that previously implicitly converted to your type (e.g., `Cuboid` implements
Comment thread
andriyDev marked this conversation as resolved.
`Into<Mesh>`), you must surround the value in `MyType::from`.
- If the asset type is "constrained" (e.g., you store the handle into `Handle<Mesh>`), you can
use `.into()` to convert your value instead.

Before:

```rust
fn my_system(mut meshes: ResMut<Assets<Mesh>>) {
// Cuboid gets implicitly converted to Mesh.
let handle = meshes.add(Cuboid::new(1.0, 2.0, 3.0));
}
```

After:

```rust
fn my_system(mut asset_commands: AssetCommands) {
let handle = asset_commands.spawn_asset(Mesh::from(Cuboid::new(1.0, 2.0, 3.0)));
}
```

Or:

```rust
fn my_system(mut asset_commands: AssetCommands) {
let handle: Handle<Mesh> = asset_commands.spawn_asset(Cuboid::new(1.0, 2.0, 3.0).into());
}
```

## 4. Removing assets

- Replace `ResMut<Assets<A>>` with `AssetCommands`.
- For multiple asset types, you only need one `AssetCommands`.
- Replace `.remove()` with `.remove_asset()`.
- **This does not return the asset**. To get the asset back, you can enqueue a command in
`Commands`, then use `world.remove_asset()`.

Before:

```rust
fn my_system(mut meshes: ResMut<Assets<Mesh>>) {
let handle = ...;
// We get back the data here.
let mesh = meshes.remove(&handle).unwrap();
}
```

After:

```rust
fn my_system(mut asset_commands: AssetCommands) {
let handle = ...;
// We don't get the data here, since this action is deferred. You need exclusive world access.
asset_commands.remove(&handle);
}
```

## 5. Spawning materials

- Since we can no longer deduce the asset type (since we have an untyped `AssetCommands` and
`MeshMaterial3d` is generic), we need to explicitly convert colors to a material. Wrap your value
in `StandardMaterial::from` or `ColorMaterial::from` for 3D or 2D respectively.

Before:

```rust
fn my_system(mut commands: Commands, mut materials: ResMut<Assets<StandardMaterial>>) {
commands.spawn((
Mesh3d(...),
MeshMaterial3d(materials.add(Color::BLACK)),
));
}
```

After:

```rust
fn my_system(mut commands: Commands, mut asset_commands: AssetCommands) {
commands.spawn((
Mesh3d(...),
MeshMaterial3d(asset_commands.spawn_asset(StandardMaterial::from(Color::BLACK))),
));
}
```

## 6. UUID assets

- Instead of accessing the `Assets<A>` resource and inserting the asset into the handle, call
`world.spawn_uuid_asset()` or `app.world_mut().spawn_uuid_asset()`.

Before:

```rust
const IMAGE: Handle<Image> = uuid_handle!("1347c9b7-c46a-48e7-b7b8-023a354b7cac");

fn my_plugin(app: &mut App) {
app.world_mut().resource_mut::<Assets<Image>>().insert(&IMAGE, create_some_image());
}
```

After:

```rust
const IMAGE: Handle<Image> = uuid_handle!("1347c9b7-c46a-48e7-b7b8-023a354b7cac");

fn my_plugin(app: &mut App) {
app.world_mut().spawn_uuid_asset(IMAGE.uuid().unwrap(), create_some_image());
}
```

## Dealing with "deferred" assets

Some existing code may assume that adding an asset is instant - in other words, you can call
`Assets::add` and then `Assets::get_mut` to mutate that added asset in the same system. Now that
assets need to be spawned, calling `AssetCommands::spawn_asset` followed by `AssetsMut::get_mut`
does not allow you to mutably access the asset. This acts the same way how calling `Commands::spawn`
followed by `Query::get_mut` does not allow you to mutably access a component.

There are several ways to deal with this. One possibility is to change your system to be exclusive,
spawning assets with `DirectAssetAccessExt::spawn_asset`, and mutating the asset with
`DirectAssetAccessExt::get_asset_mut`. Since these operate on a world, spawning the asset happens
immediately.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

IMO we should create an EntityCommands::entry-style API, and promote it heavily in the migration guide and docs. That can be left to follow-up though.

Another possibility is to defer the spawning of assets until the end of your system: allow accessing
an asset either through `AssetsMut` or through a local "pending" collection. Create assets in this
pending collection instead of spawning them, and then at the end of your system, spawn all pending
assets. This approach won't work in all cases, but can be straight forward when possible!

## Misc changes

- `Assets::len` -> `Assets::count`
- `Assets::reserve_handle` -> `AssetCommands::reserve_handle` / `DirectAssetAccessExt::reserve_asset_handle`.
- `AssetServer::get_id_handle` -> `AssetServer::get_entity_handle`
- `ReflectAsset::assets_resource_type_id` -> `ReflectAsset::asset_data_type_id`
- `ReflectAsset::add` -> `ReflectAsset::spawn`
- `ReflectAsset::len` -> `ReflectAsset::count`
18 changes: 18 additions & 0 deletions _release-content/release-notes/assets_as_entities.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
---
title: Assets-as-entities
authors: ["@andriyDev"]
pull_requests: [22939]
---

In previous versions of Bevy, assets were stored in a big `Assets` resource (per asset type).
Continuing our traditions, assets are now represented as entities! This allows us to remove some
bespoke implementations (like `AssetEvent`) and replace it with more generic ECS features (like
change detection or hooks/observers). We can now also take advantage of ECS features like
relationships in the implementation of assets.

While the simplification of Bevy internals is nice, what's more interesting is how users can
**also** benefit from these ECS features. For example, users can attach arbitrary components to
entities, whether that be to extend an asset's data, or to "tag" assets for better observability in
their apps. While users can take advantage of these today, their usage is still limited (for
example, users cannot add arbitrary components from within loaders, only from the ECS). We expect
this API to evolve to bring even more ergonomic access and more features to users!
5 changes: 2 additions & 3 deletions benches/benches/bevy_render/extract_render_asset.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use bevy_app::{App, AppLabel};
use bevy_asset::{Asset, AssetApp, AssetEvent, AssetId, Assets, RenderAssetUsages};
use bevy_asset::{Asset, AssetApp, AssetEvent, AssetId, DirectAssetAccessExt, RenderAssetUsages};
use bevy_ecs::prelude::*;
use bevy_reflect::TypePath;
use bevy_render::{
Expand Down Expand Up @@ -53,9 +53,8 @@ fn extract_render_asset_bench(c: &mut Criterion) {

let mut handles = Vec::with_capacity(size);
{
let mut assets = app.world_mut().resource_mut::<Assets<DummyAsset>>();
for _ in 0..size {
handles.push(assets.add(DummyAsset));
handles.push(app.world_mut().spawn_asset(DummyAsset));
}
}

Expand Down
10 changes: 3 additions & 7 deletions benches/benches/bevy_scene/spawn.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ use bevy_asset::{
memory::{Dir, MemoryAssetReader},
AssetSourceBuilder, AssetSourceId,
},
Asset, AssetApp, AssetLoader, AssetServer, Assets, Handle,
Asset, AssetApp, AssetLoader, AssetServer, DirectAssetAccessExt, Handle,
};
use bevy_ecs::prelude::*;
use bevy_scene::{prelude::*, ScenePatch};
Expand Down Expand Up @@ -49,15 +49,11 @@ fn spawn(c: &mut Criterion) {
dir.insert_asset_text(Path::new("button.bsn"), "");

let asset_server = app.world().resource::<AssetServer>().clone();
let handle = asset_server.load("button.bsn");
let handle: Handle<ScenePatch> = asset_server.load("button.bsn");

run_app_until(&mut app, || asset_server.is_loaded(&handle));

let patch = app
.world()
.resource::<Assets<ScenePatch>>()
.get(&handle)
.unwrap();
let patch = app.world().get_asset(handle.id()).unwrap();
assert!(patch.resolved.is_some());

b.iter(move || {
Expand Down
10 changes: 3 additions & 7 deletions crates/bevy_animation/src/graph.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,8 @@ use bevy_asset::{
};
use bevy_derive::{Deref, DerefMut};
use bevy_ecs::{
component::Component,
message::MessageReader,
reflect::ReflectComponent,
resource::Resource,
system::{Res, ResMut},
template::FromTemplate,
component::Component, message::MessageReader, reflect::ReflectComponent, resource::Resource,
system::ResMut, template::FromTemplate,
};
use bevy_platform::collections::HashMap;
use bevy_reflect::{prelude::ReflectDefault, Reflect, TypePath};
Expand Down Expand Up @@ -851,7 +847,7 @@ pub struct NonPathHandleError;
/// for quick evaluation of that graph's animations.
pub(crate) fn thread_animation_graphs(
mut threaded_animation_graphs: ResMut<ThreadedAnimationGraphs>,
animation_graphs: Res<Assets<AnimationGraph>>,
animation_graphs: Assets<AnimationGraph>,
mut animation_graph_asset_events: MessageReader<AssetEvent<AnimationGraph>>,
) {
for animation_graph_asset_event in animation_graph_asset_events.read() {
Expand Down
17 changes: 10 additions & 7 deletions crates/bevy_animation/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ use crate::{
};

use bevy_app::{AnimationSystems, App, Plugin, PostUpdate};
use bevy_asset::{Asset, AssetApp, AssetEventSystems, Assets};
use bevy_asset::{Asset, AssetApp, AssetData, AssetEventSystems, AssetSelfHandle, Assets};
use bevy_ecs::{prelude::*, resource::IsResource, world::EntityMutExcept};
use bevy_math::FloatOrd;
use bevy_platform::{collections::HashMap, hash::NoOpHash};
Expand Down Expand Up @@ -988,8 +988,8 @@ impl AnimationPlayer {
/// A system that triggers untargeted animation events for the currently-playing animations.
fn trigger_untargeted_animation_events(
mut commands: Commands,
clips: Res<Assets<AnimationClip>>,
graphs: Res<Assets<AnimationGraph>>,
clips: Assets<AnimationClip>,
graphs: Assets<AnimationGraph>,
players: Query<(Entity, &AnimationPlayer, &AnimationGraphHandle)>,
) {
for (entity, player, graph_id) in &players {
Expand Down Expand Up @@ -1030,8 +1030,8 @@ fn trigger_untargeted_animation_events(
/// A system that advances the time for all playing animations.
pub fn advance_animations(
time: Res<Time>,
animation_clips: Res<Assets<AnimationClip>>,
animation_graphs: Res<Assets<AnimationGraph>>,
animation_clips: Assets<AnimationClip>,
animation_graphs: Assets<AnimationGraph>,
mut players: Query<(&mut AnimationPlayer, &AnimationGraphHandle)>,
) {
let delta_seconds = time.delta_secs();
Expand Down Expand Up @@ -1074,15 +1074,18 @@ pub type AnimationEntityMut<'w, 's> = EntityMutExcept<
AnimatedBy,
AnimationPlayer,
AnimationGraphHandle,
AssetSelfHandle,
AssetData<AnimationClip>,
AssetData<AnimationGraph>,
),
>;

/// A system that modifies animation targets (e.g. bones in a skinned mesh)
/// according to the currently-playing animations.
pub fn animate_targets(
par_commands: ParallelCommands,
clips: Res<Assets<AnimationClip>>,
graphs: Res<Assets<AnimationGraph>>,
clips: Assets<AnimationClip>,
graphs: Assets<AnimationGraph>,
threaded_animation_graphs: Res<ThreadedAnimationGraphs>,
players: Query<(&AnimationPlayer, &AnimationGraphHandle)>,
mut targets: Query<
Expand Down
6 changes: 2 additions & 4 deletions crates/bevy_anti_alias/src/smaa/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -320,15 +320,13 @@ impl Plugin for SmaaPlugin {
};
#[cfg(not(feature = "smaa_luts"))]
let smaa_luts = {
use bevy_asset::RenderAssetUsages;
use bevy_asset::{DirectAssetAccessExt, RenderAssetUsages};
use bevy_image::ImageSampler;
use bevy_render::render_resource::{Extent3d, TextureDataOrder};

let mut images = app.world_mut().resource_mut::<bevy_asset::Assets<Image>>();

let format = TextureFormat::Rgba8Unorm;
let data = vec![255, 0, 255, 255];
let handle = images.add(Image {
let handle = app.world_mut().spawn_asset(Image {
data: Some(data),
data_order: TextureDataOrder::default(),
texture_descriptor: TextureDescriptor {
Expand Down
6 changes: 3 additions & 3 deletions crates/bevy_asset/macros/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ const DEPENDENCY_ATTRIBUTE: &str = "dependency";
#[proc_macro_derive(Asset, attributes(dependency))]
pub fn derive_asset(input: TokenStream) -> TokenStream {
let ast = parse_macro_input!(input as DeriveInput);
let bevy_asset_path: Path = bevy_asset_path();
let bevy_asset_path = bevy_asset_path();

let struct_name = &ast.ident;
let (impl_generics, type_generics, where_clause) = &ast.generics.split_for_impl();
Expand All @@ -36,7 +36,7 @@ pub fn derive_asset(input: TokenStream) -> TokenStream {
#[proc_macro_derive(VisitAssetDependencies, attributes(dependency))]
pub fn derive_asset_dependency_visitor(input: TokenStream) -> TokenStream {
let ast = parse_macro_input!(input as DeriveInput);
let bevy_asset_path: Path = bevy_asset_path();
let bevy_asset_path = bevy_asset_path();
match derive_dependency_visitor_internal(&ast, &bevy_asset_path) {
Ok(dependency_visitor) => TokenStream::from(dependency_visitor),
Err(err) => err.into_compile_error().into(),
Expand Down Expand Up @@ -102,7 +102,7 @@ fn derive_dependency_visitor_internal(

Ok(quote! {
impl #impl_generics #bevy_asset_path::VisitAssetDependencies for #struct_name #type_generics #where_clause {
fn visit_dependencies(&self, #visit: &mut impl ::core::ops::FnMut(#bevy_asset_path::UntypedAssetId)) {
fn visit_dependencies(&self, #visit: &mut impl ::core::ops::FnMut(#bevy_asset_path::AssetEntity)) {
#body
}
}
Expand Down
Loading
Loading