bevy icon indicating copy to clipboard operation
bevy copied to clipboard

Store Resources as components on singleton entities

Open Trashtalk217 opened this issue 3 months ago • 10 comments

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 Resource and a Component, because Resource expands to also implement Component, this means that this throws a compiler error as it's implemented twice.
  • Because every reflected Resource must also implement ReflectComponent you need to import bevy_ecs::reflect::ReflectComponent every time you use #[reflect(Resource)]. This is kind of unintuitive.

Future Work

  • Simplify Access in the ECS, to only deal with components (and not components and resources).
  • Newtype Res<Resource> to Single<Ref<Resource>> (or something similair).
  • Eliminate ReflectResource.
  • Take stabs at simplifying the public facing API.

Trashtalk217 avatar Sep 08 '25 19:09 Trashtalk217

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);
}

SkiFire13 avatar Sep 23 '25 16:09 SkiFire13

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

Igor-dvr avatar Sep 23 '25 17:09 Igor-dvr

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.

cactusdualcore avatar Sep 30 '25 21:09 cactusdualcore

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.

github-actions[bot] avatar Oct 28 '25 02:10 github-actions[bot]

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.

github-actions[bot] avatar Oct 28 '25 02:10 github-actions[bot]

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.

github-actions[bot] avatar Oct 28 '25 03:10 github-actions[bot]

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.

Trashtalk217 avatar Nov 02 '25 22:11 Trashtalk217

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:

  1. Scenes: justified use, scenes are inherently fine.
  2. Animation: we could trivially extend AnimationEntityMut to exclude resource entities and avoid breakage for end users as well.
  3. Input Focus: the query in question appears to be used for point queries only: won't have any linear performance consequences.

alice-i-cecile avatar Nov 02 '25 22:11 alice-i-cecile

@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!

andriyDev avatar Nov 03 '25 01:11 andriyDev

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!

chescock avatar Nov 03 '25 20:11 chescock