Store Resources as components on singleton entities
This is part of #19731.
Resources as Components
Motivation
More things should be entities. This simplifies the API, the lower-level implementation and the tools we have for entities and components can be used for other things in the engine. In particular, for resources, it is really handy to have observers, which we currently don't have. See #20821 under 1A, for a more specific use.
Current Work
This removes the resources field from the world storage and instead store the resources on singleton entities. For easy lookup, we add a HashMap<ComponentId, Entity> to World, in order to quickly find the singleton entity where the resource is stored.
Because we store resources on entities, we derive Component alongside Resource, this means that
#[derive(Resource)]
struct Foo;
turns into
#[derive(Resource, Component)]
struct Foo;
This was also done for reflections, meaning that
#[derive(Resource, Reflect)]
#[refect(Resource)]
struct Bar;
becomes
#[derive(Resource, Component, Reflect)]
#[refect(Resource, Component)]
struct Bar;
In order to distinguish resource entities, they are tagged with the IsResource component. Additionally, to ensure that they aren't queried by accident, they are also tagged as being internal entities, which means that they don't show up in queries by default.
Drawbacks
- Currently you can't have a struct that is both a
Resourceand aComponent, becauseResourceexpands to also implementComponent, this means that this throws a compiler error as it's implemented twice. - Because every reflected Resource must also implement
ReflectComponentyou need to importbevy_ecs::reflect::ReflectComponentevery time you use#[reflect(Resource)]. This is kind of unintuitive.
Future Work
- Simplify
Accessin the ECS, to only deal with components (and not components and resources). - Newtype
Res<Resource>toSingle<Ref<Resource>>(or something similair). - Eliminate
ReflectResource. - Take stabs at simplifying the public facing API.
The access control is currently not enough since Query<&mut MyResource, With<Internal>> and ResMut<MyResource> don't conflict but still provide mutable access to the same data. The following test should not reach the unreachable:
#[test]
fn resource_conflict() {
use crate::prelude::*;
#[derive(Resource)]
struct Foo;
let mut world = World::default();
world.insert_resource(Foo);
fn system(mut q: Query<&mut Foo, With<Internal>>, r: ResMut<Foo>) {
let _foo1 = q.single_mut().unwrap();
let _foo2 = r;
unreachable!("This should not be possible")
}
world.run_system_once(system);
}
How big the difference between singleton components and regular components is going to be? If resources are singleton components should we still call them resources? Maybe Singleton short for singleton component. Will this be clearer to users and imply that singleton component can be treated similarly as component?
IsResource > IsSingleton
Couldn't these types implement a UniqueComponent supertrait of Component instead of having an IsResource? I think this is how immutable Components work, right?
Edit: seems I am a bit behind on how immutable components work, but I think the implementation might still be interesting for uniqueness.
Your PR caused a change in the graphical output of an example or rendering test. This might be intentional, but it could also mean that something broke! You can review it at https://pixel-eagle.com/project/B04F67C0-C054-4A6F-92EC-F599FEC2FD1D?filter=PR-20934
If it's expected, please add the M-Deliberate-Rendering-Change label.
If this change seems unrelated to your PR, you can consider updating your PR to target the latest main branch, either by rebasing or merging main into it.
Your PR caused a change in the graphical output of an example or rendering test. This might be intentional, but it could also mean that something broke! You can review it at https://pixel-eagle.com/project/B04F67C0-C054-4A6F-92EC-F599FEC2FD1D?filter=PR-20934
If it's expected, please add the M-Deliberate-Rendering-Change label.
If this change seems unrelated to your PR, you can consider updating your PR to target the latest main branch, either by rebasing or merging main into it.
Your PR caused a change in the graphical output of an example or rendering test. This might be intentional, but it could also mean that something broke! You can review it at https://pixel-eagle.com/project/B04F67C0-C054-4A6F-92EC-F599FEC2FD1D?filter=PR-20934
If it's expected, please add the M-Deliberate-Rendering-Change label.
If this change seems unrelated to your PR, you can consider updating your PR to target the latest main branch, either by rebasing or merging main into it.
Having IsResource as a default query filter
There has been much discussion with regards to IsResource being a default query filter (DFQ) on Discord. And after a request by cart, I decided to look at the impact of unmaking IsResource a DFQ. As a reminder: If B is a default query filter, then Without<B> is automatically added to a query. So Query<&A> quietly becomes Query<&A, Without<B>>.
IsResource is a DFQ
If IsResource is registered as a DFQ, virtually nothing changes compared to the status quo. Query<Entity> returns the same number of entities before and after transitioning to resource-as-components. The benefits of transparency are obvious, since we're not hiding anything. This is why we are mostly focused on the case where IsResource is not a DFQ.
IsResource is not a DFQ
This has the largest effect on broad queries. Broad queries access all entities, so if a system contains a broad query and also a resource, this creates a conflict that wasn't there before resources-as-components.
Examples of broad queries include:
Query<()>Query<Entity>Query<EntityMut>Query<EntityRef>Query<EntityMutExcept>Query<EntityRefExcept>Query<Option<&A>>
The first two are not really big deal. Since both access no components, they don't conflict with a Res<R> or a ResMut<R> in a system. The only thing that changes is the number of entities that they return, which - in tests in particular - can cause problems. See also #20207, #20248, #21685, and the last two commits for my work on this.
The last 5 queries can cause issues because they access all components on the queried entities, meaning that fn(q: Query<EntityMut>, r: Res<R> does conflict. When fixing this PR, the places where this showed up most prevalently were tests, again. After fixing those, there were three other places in Bevy, that demanded attention.
bevy_scene
bevy_scene, by extracting entities and resources separately, runs into problems with resources-as-components. Most of which are helped by manually filtering out resource entities where they may show up:
// dynamic_scene_builder.rs
pub fn extract_entities(mut self, entities: impl Iterator<Item = Entity>) -> Self {
let resource_entities: Vec<Entity> = self.original_world.resource_entities().values().copied().collect();
for entity in entities {
// I know this is inefficient (not the point right now)
if (resource_entities.contains(&entity) { continue; }
// ..
bevy_animation
Since AnimationEntityMut shadows EntityMutExcept, it has the same problems. This is relevant for exactly one system: animate_targets:
// before
pub fn animate_targets(
...
mut targets: Query<(Entity, &AnimationTargetId, &AnimatedBy, AnimationEntityMut)>,
...
) { ... }
// after
pub fn animate_targets(
...
mut targets: Query<(Entity, &AnimationTargetId, &AnimatedBy, AnimationEntityMut), Without<IsResource>>,
...
) { ... }
Do note that AnimationEntityMut is pub so any user code that uses it, also has the same problem.
bevy_input_focus
A broad query is used in tab_navigation.rs:
pub struct TabNavigation<'w, 's> {
// Query for tab groups.
tabgroup_query: Query<'w, 's, (Entity, &'static TabGroup, &'static Children)>,
// Query for tab indices.
tabindex_query: Query<
'w,
's,
(Entity, Option<&'static TabIndex>, Option<&'static Children>),
Without<TabGroup>,
>,
// Query for parents.
parent_query: Query<'w, 's, &'static ChildOf>,
}
Here, tabindex_query pulls in all entities, but there is no conflict and nothing needs to be changed except the test where tabindex_query.iter(&world).count() no longer returns the same number (this was already done in #21685).
Conclusion
Overall, there aren't many places where IsResource not being a DFQ causes problems. It's mostly a nuisance in test suites, and only sparsely happens in other Bevy code. This, of course, does not take into account how much users need to change in order to avoid conflicts, but this at least gives a sample.
Query<Option<&A>> is definitely the most concerning of these, but it's very rare for it to be used on its own without any other qualifying terms.
That said, (iteration over) broad queries should never occur in engine / library code outside of very niche applications like networking. These have a linear performance cost with the total number of entities, regardless of the type of entity, and should be refactored whenever possible.
Evaluation of the current cases:
- Scenes: justified use, scenes are inherently fine.
- Animation: we could trivially extend
AnimationEntityMutto exclude resource entities and avoid breakage for end users as well. - Input Focus: the query in question appears to be used for point queries only: won't have any linear performance consequences.
@alice-i-cecile My initial reaction was also that Query<Option<&T>> was concerning, but then I realized, why wouldn't you just use Query<&T> and then filter out the error cases?
I also don't understand how that conflicts. We're not accessing broad data there, and if the component is missing we don't access anything on the resource entities. Sure we'll iterate through different entities but the shouldn't generate a conflict I think? @Trashtalk217 am I misunderstand here?
This was a very useful exploration though, thank you!
For AnimationEntityMut: If we allow IsResource entities in that query, and then add the three resources used by the animate_targets system to the "except" list, is that enough to support animating resources? Is it useful to animate resources?
If generic code like that winds up being useful for resources without any other changes, that seems like an argument in favor of allowing IsResource entities by default!