Skip to content

Conversation

janis-bhm
Copy link
Contributor

@janis-bhm janis-bhm commented Oct 6, 2025

Objective

related: #13128
With Resources-as-Entities, and possibly more *-as-Entities on the horizon, simultaneous mutable access to an entity and the world has and will come up more often: resource_scope, for example, needs mutable access to the resource component on the resource entity, and doesn't want the entity to be available for it's scope.

Solution

I propose the following solution:
Entities can be 'hard' disabled by swapping them into a region at the beginning of their archetype/table and their meta is updated to contain no location.
These regions are skipped by queries and not dropped when the table is removed.
A component_scope ptr::reads a component onto the stack, then disables the entity and calls the closure with a mutable reference to the stack copy of the component.
After the closure finishes, the stack component is ptr::writen back into storage and the entity is enabled: during the scope the component in storage must not be dropped because the scope closure may decide to drop it itself.

A component_scope limited to a single component is the simplest and easiest functionality to build here, because we cannot ensure that the table doesn't move the disabled components around or that the table even remains live for the duration of the scope, so we can't give out references to the ECS storage, and can't use WorldQuery and related traits to easily retrieve multiple components: this will require some more trait magic ontop of WorldQuery, but I think it shouldn't be excessively complicated or unsound.

Remaining Questions

  • Relationships: the relationship hooks unwrap the target entity, which isn't available when disabled, causing a panic.
    The component_scope closure could be passed Commands of some shape onto which to record operations related to the disabled entity.
  • Is this actually worth it/cheaper than an archetype move? This still moves components around in the tables, which is most of what an archetype move is. I haven't benchmarked this (and I would need some help properly benchmarking it). Nested component_scopes should not require a swap when re-enabling the entity, but different functionality build with disabling/enabling might.
  • Things I've forgotten or overlooked (sparse sets).

Testing

there are still lots of TODOs in this PR:

  • Tests
  • Safety comments
  • some polishing, probably
  • actually implementing the logic that forgets the disabled components

Alternatives

  • Removing components (and possibly despawning the entity), re+inserting/spawning them after the scope.
  • I'm spit-balling here but:
    Tables might be made up of chunks in which rows are stable across table growth; an entity could be moved to a dedicated chunk, which can be owned by the entity_scope to be moved back into the table afterwards. This would probably have a significant performance impact, not to mention the architectural impact.

Showcase

use bevy_ecs::prelude::*;
#[derive(Component)]
struct A(u32);
#[derive(Component)]
struct B(u32);
let mut world = World::new();
let scoped_entity = world.spawn(A(1)).id();
let entity = world.spawn(B(1)).id();

world.component_scope(scoped_entity, |world, _, a: &mut A| {
    assert!(world.get_mut::<A>(scoped_entity).is_none());
    let b = world.get_mut::<B>(entity).unwrap();
    a.0 += b.0;
});
assert_eq!(world.get::<A>(scoped_entity).unwrap().0, 2);

@janis-bhm janis-bhm added C-Feature A new feature, making something new possible A-ECS Entities, components, systems, and events X-Contentious There are nontrivial implications that should be thought through D-Unsafe Touches with unsafe code in some way S-Needs-Review Needs reviewer attention (from anyone!) to move forward labels Oct 6, 2025
@alice-i-cecile alice-i-cecile added the M-Needs-Release-Note Work that should be called out in the blog due to impact label Oct 6, 2025
Copy link
Contributor

github-actions bot commented Oct 6, 2025

It looks like your PR has been selected for a highlight in the next release blog post, but you didn't provide a release note.

Please review the instructions for writing release notes, then expand or revise the content in the release notes directory to showcase your changes.

@janis-bhm janis-bhm force-pushed the ecs/entity-scope-v1-component-scope branch from 617f6f4 to 609de31 Compare October 6, 2025 23:35
@alice-i-cecile alice-i-cecile added S-Waiting-on-Author The author needs to make changes or address concerns before this can be merged X-Controversial There is active debate or serious implications around merging this PR and removed X-Contentious There are nontrivial implications that should be thought through labels Oct 6, 2025
@alice-i-cecile
Copy link
Member

Marked as X-Controversial due to the architectural implications, but I quite like the core strategy and really want this functionality.

@ElliottjPierce
Copy link
Contributor

I have not looked very closely at the implementation yet, but here's a few thoughts.

This is probably the best way to implement component scopes. In my own ecs engine, I've implemented something similar to the chunked tables, and it was very much not worth it. Removing, scoping, and re-adding a component is probably also too slow, would create useless or maybe even invalid (with archetype invariants) archetypes, and maybe cause hook issues. Either hooks will run, making the previous sate unrecoverable, or the scope will have an invalid world state–this is general form of the relationship problem you mentioned.

Even in terms of the objective, I'm not convinced this is the right way to go. This is definitely not my decision, but what I've noticed is that these scope functions get really messy and are IMO not good practice. I didn't like resource scopes because they were limiting and promoted lots of nesting. I almost always just did a run_system_once and Commands with a closure or something. With components too, I feel like this could lead to a lot of bugs in user code. Now, every helper function a user makes needs to know if it's running as part of the scope or not, and if so, it may need a special way to handle the scoped entity. (Ex: The scope uses a helper function to sum up all the health components, but the scoped entity has a health component and is missed. Now that function needs to know if its running in a scope or not.) Maybe that's rare; I really don't know, but I don't like how much danger this allows users. I'd much prefer making an easier way to scope safe access to QueryParams.

At a conceptual level, I also think this might be unsound. As an example, what if you scope access to a component value and then clear the world? Now the old copy has been dropped, but you still have a mutable reference to a copy of a now dropped component! That's a big deal if the component has a Vec or something; now its a double free. And you can't just mem::forget the new one since maybe the vec was changed and the allocation moved. To fix this, you'd need some way of knowing just from &mut World if a given component is scoped, and I think tracking and checking that information everywhere is probably more trouble than it's worth. Unless I'm missing some way around this. (Maybe do the add and remove idea, but that has it's own problems. Or maybe add a new hook mode to fix those? Idk.)

I know I said this is probably the best way to do component scopes, and I stand by that (unless you come up with something better 👀 ). All other ways of doing this that I can think of come with massive performance issues and correctness concerns. I think this is the most useful way of doing it; it just needs to be unsafe. After all, you are giving mutable access to a "field" of the world while sill providing &mut World. That is inherently unsafe, even if you add a ton of guardrails. That said, I think we should look into alternatives to the scope pattern here. I think improving the run_system_once + Commands is probably a better way to handle these situations IMO. And in the rare case that the "remove, scope, and re-add" workflow is needed, I'd almost rather it be explicit, so users can see all the risks and side-effects openly. But that's just my opinion, and (for good reason) this is not my decision to make.

I'll try to find time to review the code later if possible. This is a really good idea to explore, but it is very complicated.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
A-ECS Entities, components, systems, and events C-Feature A new feature, making something new possible D-Unsafe Touches with unsafe code in some way M-Needs-Release-Note Work that should be called out in the blog due to impact S-Needs-Review Needs reviewer attention (from anyone!) to move forward S-Waiting-on-Author The author needs to make changes or address concerns before this can be merged X-Controversial There is active debate or serious implications around merging this PR
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants