all-is-cubes icon indicating copy to clipboard operation
all-is-cubes copied to clipboard

ECS: Access and transactions for multiple components

Open kpreid opened this issue 6 months ago • 4 comments

The ECS migration took the existing member types (e.g. Space and Character) and turned them into components. Eventually, those components should be broken up into separate parts (like Space's palette and Character’s body, and “behaviors” everywhere) to be able to properly take advantage of ECS. However, in order to do that, some existing assumptions need to be changed:

  • Handle::<T>::read() needs to return some facade that bundles access to all the data which the T in the handle type abstractly constitutes.
  • Transaction types like SpaceTransaction need to be able to modify multiple components of a single entity. This probably means that they stop implementing the Transaction trait (which assumes that the data to be manipulated is a single value which can be immutably or mutably borrowed).
  • Pending handles need to be able to contain multiple components; #633

Note that there is a tension between these desirable properties (using Space as a specific example):

  • Handle<Space> refers to an entity with a Space component”
  • “The current Space implementation is broken up into several components cooperating”
  • Handle::<Space>::read() gives you an &Space and that lets you read everything important about the entity”

One possibility would be to re-define Space as a facade or marker type, which refers to the bundle of components required to having a functioning space in the abstract data-model, and isn't one of those components itself. Another would be “Handle<Space> means access to a Space and all its required components” (but bevy_ecs required components are a poor fit for some situations since they must be defaultable, so we might use a different notion of dependency).

kpreid avatar Jun 22 '25 05:06 kpreid

One possibility would be to re-define Space as a facade or marker type, which refers to the bundle of components required to having a functioning space in the abstract data-model, and isn't one of those components itself.

Suppose that:

  • Space is an actual owner of the components that a not-yet-inserted Space may have, and offers mutation operations.
  • There is a second type which is a wrapper of ECS queries and mirrors the read-only part of Space’s API.
#[derive(Bundle)] // or something of our own similar
struct Space {
    palette: Palette,
    contents: IndexStorage,
    light: LightStorage,
    // ... and other types which are ECS components making up a Space
}
impl Space { /* methods that read/write those components */ }
impl UniverseMember for Space {
    type ReadFacade = ReadSpace;
    // ...
}

/// This is returned by `Handle::<Space>::read()`
struct ReadSpace<'r> { /* queries from &'r World OR references borrowed from &'r Space */ }
impl ReadSpace<'r> { /* &self methods of identical signature to impl Space */ }

This way, we get all of the desired properties, at the price of having two copies of the read-only part of Space's public API (which could maybe be generated by a macro). In particular, in this scheme, each member type can have as much or as little support for being used outside a Universe as suits it.

kpreid avatar Jul 25 '25 14:07 kpreid

Commits 4d5b397a through 65d737f6 introduce the infrastructure for:

  • Handle::read() returning an arbitrarily chosen type.
  • Transactions being able to work on ECS data.

There are no cases of this flexibility actually being used yet.

kpreid avatar Nov 03 '25 18:11 kpreid

try_modify() will finally need to go away because there is no longer a single thing for it to give mutable access to.

kpreid avatar Nov 04 '25 06:11 kpreid

As of 81e1bc89, it’s possible for an individual member type to opt in to having a different Read type than &Self, which means we can start building the facade types even if the multiple underlying components aren’t there yet.

kpreid avatar Nov 04 '25 06:11 kpreid

60cd8896 provides proof-of-concept of actually offering a facade type, struct character::Read distinct from Character.

In bd612adf, space::Mutation gives us a better-defined alternative to try_modify(). That would be good even without any of this ECS nonsense. In 83fdf191 we de-genericize try_modify() so it only works on the last messy case, Characters. That'll need to be fixed, but it at least doesn't require a UniverseMember: Component supertrait bound.

kpreid avatar Nov 05 '25 03:11 kpreid

b346a92f6a17954cd16d12467913c9e257773825 is, hopefully, the last big piece: ReadTicket supports fetching each member type’s read query. We'll need a trait shenanigan for generalizing QueryBlockDataSources past the current downcasting, but it shouldn’t be too bad.

kpreid avatar Nov 05 '25 04:11 kpreid

Next step: Prove that multiple components work by splitting something. Some candidates:

  • Space. Requires defining a Space read facade type.
  • Character. Requires breaking up the direct Character mutation operations. The member type most obviously made of independent parts.
  • All BehaviorSets. Complex, broadly affecting, and possibly should be replaced with plug-in systems instead, but it is the most "this should be separate and isn't" part of the current architecture.

Out of these, I currently think Character is the best option.

kpreid avatar Nov 05 '25 23:11 kpreid

As of bab397871c0c6f132d14263847bdd50bbf639a6e, Character is made of multiple components and its stepping is expressed as systems. Success!

While there is more migration to be done for other types, and cleanup for Character, I think this issue is completed: we have solutions for all the fundamental problems.

kpreid avatar Nov 07 '25 21:11 kpreid