Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
53 commits
Select commit Hold shift + click to select a range
22e24fb
make ComponentId use Entity
Trashtalk217 Jun 23, 2026
96d54c9
removed ResourceEntities
Trashtalk217 Jun 23, 2026
45ff041
fix world_serialization tests
Trashtalk217 Jun 23, 2026
5e9d8fa
add PR number
Trashtalk217 Jun 23, 2026
16f2b9e
fix bevy_remote
Trashtalk217 Jun 23, 2026
c6d1670
fix dynamic example
Trashtalk217 Jun 23, 2026
a7d5879
fix doc test
Trashtalk217 Jun 23, 2026
eed38d9
fix bevy_settings test
Trashtalk217 Jun 23, 2026
a6d7ea3
forgot to remove a println!
Trashtalk217 Jun 23, 2026
9056e8a
fix benchmark
Trashtalk217 Jun 23, 2026
8430a66
Merge branch 'main' of https://github.com/bevyengine/bevy into compon…
Trashtalk217 Jun 24, 2026
63cd089
improved migration guide, added iter_registered_ids, addressed review
Trashtalk217 Jun 25, 2026
50ec233
markdown error
Trashtalk217 Jun 25, 2026
731b5e6
addressed most of chescocks review
Trashtalk217 Jun 27, 2026
05c6204
clippy
Trashtalk217 Jun 27, 2026
cc02f45
address review pt 2
Trashtalk217 Jun 28, 2026
958d2bb
fix test
Trashtalk217 Jun 28, 2026
50af152
address UB concerns
Trashtalk217 Jun 30, 2026
3f3d76a
I'm a big dumb idiot who should test their code
Trashtalk217 Jun 30, 2026
de65238
clippy
Trashtalk217 Jun 30, 2026
b02a412
migration guide
Trashtalk217 Jun 30, 2026
c2678eb
Apply suggestions from code review
Trashtalk217 Jul 1, 2026
56009dc
removed tests, changed bevy_settings
Trashtalk217 Jul 2, 2026
2c764e6
format
Trashtalk217 Jul 2, 2026
faf0adb
changed migration guide
Trashtalk217 Jul 5, 2026
0d4bcf7
markdown fix
Trashtalk217 Jul 5, 2026
ff207f8
cleaned up Res/Mut and hooks
Trashtalk217 Jul 7, 2026
38489bb
cleanup
Trashtalk217 Jul 7, 2026
9357ba2
add documentation
Trashtalk217 Jul 7, 2026
c0597ca
Merge branch 'main' of https://github.com/bevyengine/bevy into compon…
Trashtalk217 Jul 12, 2026
a7a2972
fix bevy_dev_tools
Trashtalk217 Jul 12, 2026
2d8488a
Merge branch 'main' of https://github.com/bevyengine/bevy into compon…
Trashtalk217 Aug 24, 2026
9137f52
small fix
Trashtalk217 Aug 24, 2026
31c9675
Merge branch 'main' of https://github.com/bevyengine/bevy into compon…
Trashtalk217 Sep 14, 2026
aacc2bb
doc fix
Trashtalk217 Sep 14, 2026
fb25d44
remove component sparse arrays
Trashtalk217 Sep 14, 2026
fe62ef1
dont use RemoteAllocator in component queued registration
ItsDoot Sep 17, 2026
ee670da
Remove ArchetypeComponentInfo and stop storing it in Archetype
ItsDoot Sep 17, 2026
2563181
Merge pull request #2 from ItsDoot/ecs/c-a-e
Trashtalk217 Sep 17, 2026
13a8779
Merge pull request #3 from ItsDoot/ecs/c-a-e-2
Trashtalk217 Sep 17, 2026
876989f
Merge branch 'main' of https://github.com/bevyengine/bevy into compon…
Trashtalk217 Sep 17, 2026
35c6b0e
fix tests
Trashtalk217 Sep 17, 2026
bf50639
Merge branch 'main' into components-as-entities-alt
Trashtalk217 Sep 17, 2026
97fcd15
remove migration guide
Trashtalk217 Sep 19, 2026
0309e8d
fix world_serialization
Trashtalk217 Sep 19, 2026
43d6314
fix clippy warnings
Trashtalk217 Sep 19, 2026
f6789d8
Merge branch 'main' of https://github.com/bevyengine/bevy into compon…
Trashtalk217 Sep 19, 2026
bf97958
cleaned up AI code
Trashtalk217 Sep 19, 2026
be0ccf4
fix clippy tests
Trashtalk217 Sep 19, 2026
d53700e
fix issues
Trashtalk217 Sep 20, 2026
8256f1d
scatter shot debuggin
Trashtalk217 Sep 20, 2026
7bd9d4b
fix soundness bug
Trashtalk217 Sep 20, 2026
0bdcb2b
fix test
Trashtalk217 Sep 20, 2026
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
36 changes: 36 additions & 0 deletions _release-content/migration-guides/components-as-entities.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
---

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

From my unaddressed comment in the linked PR:

In order to get access to a resource entity, you usually need to go through two lookups: TypeId -> ComponentId -> Entity. The ComponentId -> Entity lookup can potentially be removed, speeding up resource lookup.

On the other hand note that you have introduced a HashMap lookup into every ComponentId -> ComponentInfo path! This happens for example when calling hooks, which is arguably pretty common too.

Sidenote: do we have a benchmark for hooks to measure this?

Currently, one big downside of resources-as-components is that any components on resource entities aren't serialized. This forms a barrier for implementing required components for resources. Implementing this correctly is tricky. One of the problems we run into is that IsResource(ComponentId) cannot be directly copied over because ComponentIds are not consistent between worlds.

I'm not sure how this PR solves the issue. You'll still have the issue that the EntityId in the new world needs to match the ComponentId of the resource.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

As far as I know, we don't have benchmarks to measure hook access. The particular performance problem you describe can be fixed through #24102.

With regards to the bevy_world_serialization problem, this is where it gets clever. Mapping entities between worlds has already been solved through SceneEntityMapper. The idea is that we can use this just as well for ComponentIds when ComponentIds are just entities.

@SkiFire13 SkiFire13 Jul 6, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

cargo bench -p benches --bench ecs -> benchmarks.txt

I can see some rather big regressions here:

group                                                                                                     after                                    before
-----                                                                                                     -----                                    ------
added_archetypes/archetype_count/10000                                                                    2.99     36.5±1.69ms        ? ?/sec      1.00     12.2±0.16ms        ? ?/sec
all_added_detection/50000_entities_ecs::change_detection::Sparse                                          1.72     84.7±0.81µs        ? ?/sec      1.00     49.4±2.98µs        ? ?/sec
all_added_detection/5000_entities_ecs::change_detection::Sparse                                           1.71      8.5±0.52µs        ? ?/sec      1.00      4.9±0.44µs        ? ?/sec
despawn_world_recursive/10000_entities                                                                    1.12  1547.4±24.08µs        ? ?/sec      1.00   1375.8±8.73µs        ? ?/sec
despawn_world_recursive/100_entities                                                                      1.14     16.4±1.42µs        ? ?/sec      1.00     14.4±1.31µs        ? ?/sec
ecs::resources::get                                                                                       1.30      8.1±0.45ns        ? ?/sec      1.00      6.2±0.52ns        ? ?/sec
ecs::resources::get_mut                                                                                   1.23      9.0±0.47ns        ? ?/sec      1.00      7.4±0.60ns        ? ?/sec
ecs::world::world_get::world_query_get_components_mut/unchecked_5_components_50000_entities               2.02  1547.9±74.62µs        ? ?/sec      1.00   766.5±38.31µs        ? ?/sec
empty_archetypes/for_each/1000                                                                            2.46     18.4±1.52µs        ? ?/sec      1.00      7.5±1.00µs        ? ?/sec
empty_archetypes/for_each/10000                                                                           25.88  276.3±58.63µs        ? ?/sec      1.00     10.7±0.97µs        ? ?/sec
empty_archetypes/iter/100                                                                                 1.18      8.4±1.02µs        ? ?/sec      1.00      7.1±0.51µs        ? ?/sec
empty_archetypes/iter/1000                                                                                2.58     19.1±2.33µs        ? ?/sec      1.00      7.4±0.84µs        ? ?/sec
empty_archetypes/iter/10000                                                                               27.51  279.2±54.97µs        ? ?/sec      1.00     10.1±2.66µs        ? ?/sec
empty_archetypes/par_for_each/10                                                                          1.15     10.1±1.13µs        ? ?/sec      1.00      8.7±0.65µs        ? ?/sec
empty_archetypes/par_for_each/100                                                                         3.70     32.4±3.23µs        ? ?/sec      1.00      8.8±1.06µs        ? ?/sec
empty_archetypes/par_for_each/1000                                                                        33.88  329.2±23.02µs        ? ?/sec      1.00      9.7±0.85µs        ? ?/sec
empty_archetypes/par_for_each/10000                                                                       191.38     3.3±0.06ms        ? ?/sec     1.00     17.3±2.24µs        ? ?/sec
empty_systems/0_systems                                                                                   1.14     16.1±4.09ns        ? ?/sec      1.00     14.2±1.49ns        ? ?/sec
events_iter/size_16_events_1000                                                                           1.20  776.3±115.84ns        ? ?/sec      1.00   647.5±49.46ns        ? ?/sec
events_iter/size_16_events_10000                                                                          1.32      8.5±0.57µs        ? ?/sec      1.00      6.4±0.53µs        ? ?/sec
events_iter/size_4_events_100                                                                             1.32     85.9±5.74ns        ? ?/sec      1.00     64.9±5.00ns        ? ?/sec
events_iter/size_4_events_1000                                                                            1.27   826.7±94.24ns        ? ?/sec      1.00   648.9±66.35ns        ? ?/sec
multiple_archetypes_none_changed_detection/100_archetypes_10000_entities_ecs::change_detection::Table     1.33    727.2±4.41µs        ? ?/sec      1.00    546.0±5.28µs        ? ?/sec
multiple_archetypes_none_changed_detection/100_archetypes_1000_entities_ecs::change_detection::Sparse     2.76    215.7±5.87µs        ? ?/sec      1.00     78.1±2.80µs        ? ?/sec
multiple_archetypemultiple_archetypes_none_changed_detection/5_archetypes_1000_entities_ecs::change_detection::Table        1.48      3.4±0.05µs        ? ?/sec      1.00      2.3±0.25µs        ? ?/sec
s_none_changed_detection/100_archetypes_1000_entities_ecs::change_detection::Table      2.11    100.3±2.18µs        ? ?/sec      1.00     47.5±0.95µs        ? ?/sec
multiple_archetypes_none_changed_detection/100_archetypes_100_entities_ecs::change_detection::Table       1.53      9.2±0.37µs        ? ?/sec      1.00      6.0±0.24µs        ? ?/sec
multiple_archetypes_none_changed_detection/100_archetypes_10_entities_ecs::change_detection::Sparse       1.24  1420.8±139.83ns        ? ?/sec     1.00  1147.1±161.44ns        ? ?/sec
multiple_archetypes_none_changed_detection/100_archetypes_10_entities_ecs::change_detection::Table        1.39  1212.4±121.47ns        ? ?/sec     1.00   869.3±85.26ns        ? ?/sec
multiple_archetypes_none_changed_detection/20_archetypes_10000_entities_ecs::change_detection::Sparse     1.22   325.4±22.68µs        ? ?/sec      1.00   266.6±11.61µs        ? ?/sec
multiple_archetypes_none_changed_detection/20_archetypes_10000_entities_ecs::change_detection::Table      1.49    140.3±3.66µs        ? ?/sec      1.00     94.2±0.89µs        ? ?/sec
multiple_archetypes_none_changed_detection/20_archetypes_1000_entities_ecs::change_detection::Table       1.52     13.9±0.24µs        ? ?/sec      1.00      9.1±0.17µs        ? ?/sec
multiple_archetypes_none_changed_detection/20_archetypes_100_entities_ecs::change_detection::Table        1.52  1526.8±146.79ns        ? ?/sec     1.00  1007.4±15.22ns        ? ?/sec

Some are pretty niche, but I find interesting that ecs::resources::get and ecs::resources::get_mut are there given that this PR claimed to speed up resource lookup.

Edit: or are after/before perhaps switched?

@Trashtalk217 Trashtalk217 Jul 7, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I don't think after/before were switched. As per my comment, while the micro benchmarks show some regressions, performance is not the only reason for this PR. Furthermore, the stress tests (which are more representative for real world usecases) don't show a significant decrease in performance.

Further furthermore: A fix for any performance issues already exists in #24102, but that's something best left for a separate PR.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

It's also possible that my laptop (bless their soul) is not super reliable for benchmarks, but it tried its best.

title: "Components as Entities"
pull_requests: [24728]
---

- `ComponentId::new` now takes `Entity` as an argument instead of `usize`. For debugging, you can use `ComponentId::from_u32`.
- `ComponentId::index` was removed in favor of implementing `ContainsEntity`, call `ComponentId::entity` to get the underlying entity.
- `ComponentIdSet` is now an `EntityEquivalentHashSet` instead of a `FixedBitSet`.
- `ComponentIdSet::is_clear` has changed to `ComponentIdSet::is_empty`.
- `ComponentIdSet::difference` has changed to `-`, i.e.: `difference = set - other`.
- `ComponentIdSet::intersection` has changed to `&`, i.e.: `intersection = set & other`.
- `ComponentIdSet::union` has changed to `|`, i.e.: `union = set | other`.
- Other methods have remained the same.
- `ComponentIds` has been removed. Instead of `ComponentIds`, `ComponentsRegistrator::new` now takes `EntityAllocator`, while `ComponentsQueuedRegistrator::new` now takes `RemoteAllocator`.
Comment thread
Trashtalk217 marked this conversation as resolved.
- `Access` and `EcsAccessType` no longer derive `Hash`.
- `ResourceEntities` was removed. The following methods have been removed with it: `World::resource_entities`, `EntityWorldMut::resource_entities`, `UnsafeWorldCell::resource_entities`. It can also no longer be used as a system param. If you need the entity linked with a `ComponentId`, simply call `component_id.entity()`.
- The `component_id` field has been removed from the `IsResource` component, along with `IsResource::new` and `IsResource::resource_component_id`. If you need the `ComponentId` for a resource dynamically, you can simply query `Entity` alongside the resource you want and wrap it: `ComponentId::new(entity)`.

In 0.19, you could attach components to a resource by simply calling `world.spawn((Res1, Comp1, Comp2))`. In 0.20, this no longer works as `Res1` needs to be on the resource entity allocated by `world.register_component<Res1>()`. In 0.20, adding components looks as follows:

```rust
let entity = world.register_component::<R>().entity();
world.spawn_at(entity, (Res1, Comp1, Comp2));
```

Additionally, manually implementing `Resource` through

```rust
#[derive(Component, Default)]
struct R;

impl Resource for R {}
```

has become less viable, as now `Res` and `ResMut` panic when `IsResource` has not been made a required component for a resource.
Use `#[derive(Resource)]` instead.
4 changes: 1 addition & 3 deletions benches/benches/bevy_ecs/empty_archetypes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,7 @@ fn add_archetypes(world: &mut World, count: u16) {
if i & (1 << 15) != 0 {
e.insert(A::<28>(1.0));
}
e.despawn();
}
}

Expand All @@ -166,7 +167,6 @@ fn empty_archetypes(criterion: &mut Criterion) {
schedule.add_systems(iter);
});
add_archetypes(&mut world, archetype_count);
world.clear_entities();
Comment thread
Trashtalk217 marked this conversation as resolved.
let mut e = world.spawn_empty();
e.insert(A::<0>(1.0));
e.insert(A::<1>(1.0));
Expand Down Expand Up @@ -197,7 +197,6 @@ fn empty_archetypes(criterion: &mut Criterion) {
schedule.add_systems(for_each);
});
add_archetypes(&mut world, archetype_count);
world.clear_entities();
let mut e = world.spawn_empty();
e.insert(A::<0>(1.0));
e.insert(A::<1>(1.0));
Expand Down Expand Up @@ -228,7 +227,6 @@ fn empty_archetypes(criterion: &mut Criterion) {
schedule.add_systems(par_for_each);
});
add_archetypes(&mut world, archetype_count);
world.clear_entities();
let mut e = world.spawn_empty();
e.insert(A::<0>(1.0));
e.insert(A::<1>(1.0));
Expand Down
18 changes: 8 additions & 10 deletions crates/bevy_dev_tools/src/inspection/component_inspection.rs
Original file line number Diff line number Diff line change
Expand Up @@ -175,10 +175,9 @@ impl ComponentMetadataMap {
pub fn generate(world: &World) -> Self {
let mut map = HashMap::new();

for index in 0..world.components().num_registered() {
let component_id = ComponentId::new(index);
if let Ok(metadata) = ComponentTypeMetadata::new(world, component_id) {
map.insert(component_id, metadata);
for (id, _) in world.components().iter_registered() {
if let Ok(metadata) = ComponentTypeMetadata::new(world, id) {
map.insert(id, metadata);
}
}

Expand Down Expand Up @@ -209,12 +208,11 @@ impl ComponentMetadataMap {

/// Adds entries for component types that are not yet in the map, leaving existing entries alone.
pub fn update(&mut self, world: &World) {
for index in 0..world.components().num_registered() {
let component_id = ComponentId::new(index);
if !self.map.contains_key(&component_id)
&& let Ok(metadata) = ComponentTypeMetadata::new(world, component_id)
for (id, _) in world.components().iter_registered() {
if !self.map.contains_key(&id)
&& let Ok(metadata) = ComponentTypeMetadata::new(world, id)
{
self.map.insert(component_id, metadata);
self.map.insert(id, metadata);
}
}
}
Expand Down Expand Up @@ -449,7 +447,7 @@ mod tests {
#[test]
fn unregistered_component_id_returns_error() {
let world = test_world();
let component_id = ComponentId::new(usize::MAX);
let component_id = ComponentId::from_u32(464149);

let result = ComponentTypeMetadata::new(&world, component_id);

Expand Down
9 changes: 6 additions & 3 deletions crates/bevy_dev_tools/src/inspection/world_summary.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,10 @@
//!
//! See [`WorldSummary`] for the output, and [`WorldSummaryExt`] to generate.

use bevy_ecs::{archetype::ArchetypeId, component::ComponentId, system::Commands, world::World};
use bevy_ecs::{
archetype::ArchetypeId, component::ComponentId, storage::SparseSetIndex, system::Commands,
world::World,
};
use bevy_log::info;
use bevy_utils::{memory_size::MemorySize, prelude::DebugName};
use core::{cmp::Reverse, fmt};
Expand Down Expand Up @@ -143,7 +146,7 @@ pub trait WorldSummaryExt {
impl WorldSummaryExt for World {
fn summarize(&self, settings: SummarySettings) -> WorldSummary {
let total_entities = self.entities().count_spawned();
let total_send_resources = self.resource_entities().iter().count();
let total_send_resources = self.iter_resources().count();
let total_non_send_resources = self.storages().non_sends.len();
let total_archetypes = self.archetypes().len();
let mut archetype_summaries: Vec<ArchetypeSummary> = self
Expand All @@ -161,7 +164,7 @@ impl WorldSummaryExt for World {
self.components()
.get_name(*component_id)
.unwrap_or_else(|| {
let component_index = component_id.index();
let component_index = component_id.sparse_set_index();
DebugName::owned(format!("Component #{component_index}"))
})
})
Expand Down
10 changes: 5 additions & 5 deletions crates/bevy_dev_tools/src/schedule_data/serde.rs
Original file line number Diff line number Diff line change
Expand Up @@ -129,11 +129,11 @@ impl AccessData {
let writes = value.writes().as_finite_set();

let (reads_inverted, reads) = match reads {
Some(reads) => (false, trace.get_indexes(reads.iter())),
Some(reads) => (false, trace.get_indexes(reads.iter().copied())),
None => (true, vec![]),
};
let (writes_inverted, writes) = match writes {
Some(writes) => (false, trace.get_indexes(writes.iter())),
Some(writes) => (false, trace.get_indexes(writes.iter().copied())),
None => (true, vec![]),
};

Expand All @@ -142,7 +142,7 @@ impl AccessData {
writes,
reads_inverted,
writes_inverted,
archetypal: trace.get_indexes(value.archetypal().iter()),
archetypal: trace.get_indexes(value.archetypal().iter().copied()),
}
}
}
Expand All @@ -164,8 +164,8 @@ pub struct AccessFiltersData {
impl AccessFiltersData {
fn new(value: &bevy_ecs::query::AccessFilters, trace: &mut ComponentTrace) -> Self {
Self {
with: trace.get_indexes(value.with().iter()),
without: trace.get_indexes(value.without().iter()),
with: trace.get_indexes(value.with().iter().copied()),
without: trace.get_indexes(value.without().iter().copied()),
}
}
}
Expand Down
7 changes: 7 additions & 0 deletions crates/bevy_ecs/macro_logic/src/component.rs
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,8 @@ pub struct DeriveComponent {
pub map_entities: Option<MapEntitiesAttributeKind>,
/// Additional required component registrations that are added in `Component::register_required_components`
pub additional_requires: Vec<TokenStream>,
/// Additional `on_insert` hook
pub additional_insert_hook: Option<TokenStream>,
}

impl DeriveComponent {
Expand All @@ -73,6 +75,7 @@ impl DeriveComponent {
clone_behavior: None,
map_entities: None,
additional_requires: Vec::new(),
additional_insert_hook: None,
};

let mut require_paths = HashSet::new();
Expand Down Expand Up @@ -250,6 +253,10 @@ impl DeriveComponent {
let mut on_despawn_path =
Vec::from_iter(self.on_despawn.map(|path| path.to_token_stream(bevy_ecs)));

if let Some(extra_insert_hook) = self.additional_insert_hook {
on_insert_path.push(extra_insert_hook);
}

if relationship.is_some() {
on_insert_path.push(quote!(<Self as #bevy_ecs::relationship::Relationship>::on_insert));
on_discard_path
Expand Down
12 changes: 3 additions & 9 deletions crates/bevy_ecs/macros/src/resource.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
use bevy_ecs_macro_logic::component::{DeriveComponent, StorageAttribute, StorageTy};
use bevy_macro_utils::fq_std::FQOption;
use proc_macro2::TokenStream;
use quote::quote;
use syn::{DeriveInput, Path};
Expand All @@ -11,17 +10,12 @@ pub fn derive_resource(ast: &mut DeriveInput) -> TokenStream {
Err(e) => return e.into_compile_error(),
};

let struct_name = &ast.ident;
let (_, type_generics, _) = &ast.generics.split_for_impl();
// We add an extra insert hook to the resource
derive_component.additional_insert_hook = Some(quote!(#bevy_ecs::resource::on_resource_insert));

// We add the component_id existence check here to avoid recursive init during required components initialization.
derive_component.additional_requires.push(quote! {
let resource_component_id = if let #FQOption::Some(id) = required_components.components_registrator().component_id::<#struct_name #type_generics>() {
id
} else {
required_components.components_registrator().register_component::<#struct_name #type_generics>()
};
required_components.register_required::<#bevy_ecs::resource::IsResource>(move || #bevy_ecs::resource::IsResource::new(resource_component_id));
required_components.register_required::<#bevy_ecs::resource::IsResource>(|| #bevy_ecs::resource::IsResource);
});

let component_impl = match derive_component.impl_component(ast, &bevy_ecs, StorageTy::SparseSet)
Expand Down
Loading
Loading