bevyengine / bevy

A refreshingly simple data-driven game engine built in Rust
https://bevyengine.org
Apache License 2.0
36.19k stars 3.57k forks source link

Add `World::entity_scope` to split the borrow on the `World` by extracting a single entity #13128

Open alice-i-cecile opened 6 months ago

alice-i-cecile commented 6 months ago

What problem does this solve or what need does it fill?

When working with avatar-centric games (like platformers or FPS games), one object (the player avatar) is typically wildly more complex and important than others. Writing exclusive systems that fetch "the player" and operate on it in a game-object style can be an effective programming pattern.

In order to make this work, we often want to access that highly important entity, and relate it to "everything else around it". The current most obvious pattern is to fetch that data, clone or remove it out of the world, and then operate on it.

What solution would you like?

Add World::entity_scope, which works like resource_scope, but returns an EntityMut instead of a resource, in addition to a reference to the rest of the world.

This could be done by removing and then re-adding the entity but:

  1. We need to avoid blanket-triggering change detection on all of the components: adding / inserting the object is
  2. The Entity id of the object should remain unchanged.

A strong form of entity disabling (#11090), which makes it fully impossible to access the data of the object would be a clean way to do this, but would have more significant architectural implications. This would also come with less caveats: being able to add children to the requested entity without immediately panicking would be great.

What alternative(s) have you considered?

An EntityWorldMut would be strictly more powerful, and makes more conceptual sense: being able to freely add and remove components would be really useful. However, actually providing that access is fundamentally unsound with the archetypal ECS that Bevy uses: changing this data requires changes to the metadata of the component storage itself, and requires a true hard lock.

Users could manually implement this pattern, by despawning and then spawning the player object, but without a way to clone entities (#1515) or otherwise extract all of their component, actually doing so is tedious.

Furthermore, any existing references to that entity (such as Parent components!) will break, since the Entity ID will be changed. Change detection will also be triggered on every component, effectively rendering it useless for their game object style entity.

Additional context

This is a conceptual sister to https://github.com/bevyengine/bevy/issues/13127: both are in the service of making the "get and work with a single entity in a loosely typed way" workflow easier.

Performance is always nice, but is a secondary concern for this style of API.

An equivalent hierarchy_scope, which extracts a whole tree of entities, is probably useful down the line as well: player objects are often conceptually composed of complete trees in order to support complex animated models.

SkiFire13 commented 6 months ago

Returning an actual &mut World would be unsound even with any form of entity disabling, as it still allows you to do structural changes that might affect the disabled entity. And there's also the extreme case where the World is replaced (i.e. by doing *world = World::new();)

atornity commented 5 months ago

Returning an actual &mut World would be unsound even with any form of entity disabling

how about instead of having access to the whole entity (EntityMut) - you just get access to a set of components?

let _ = world.entity_mut(entity).component_scope::<(Position, Health), _>(|world, (p, h)| {
   // blabla
}).unwrap(); // panics if either Position or Health doesn't exist

this can almost be done already with EntityWorldMut::take + EntityWorldMut::insert but it triggers Added (bad) and it's surprisingly easy to gorget to insert the component back afterwards.

SkiFire13 commented 5 months ago

An approach similar to EntityWorldMut::take + EntityWorldMut::insert could work, but I don't see a way to optimize it to avoid the archetype move, which is pretty costly.

ItsDoot commented 1 month ago

Returning an actual &mut World would be unsound even with any form of entity disabling, as it still allows you to do structural changes that might affect the disabled entity. And there's also the extreme case where the World is replaced (i.e. by doing *world = World::new();)

What if we return a &mut DeferredWorld, since it doesn't allow structural changes?

SkiFire13 commented 1 month ago

What if we return a &mut DeferredWorld, since it doesn't allow structural changes?

I think that might work, but it will likely still be pretty tricky to do correctly, especially if you want to recursively call entity_scope on another entity (since it seems the main ideas for how to implement entity_scope involve doing structural changes)