ECS: Access and transactions for multiple components
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 theTin the handle type abstractly constitutes.- Transaction types like
SpaceTransactionneed to be able to modify multiple components of a single entity. This probably means that they stop implementing theTransactiontrait (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 aSpacecomponent” - “The current
Spaceimplementation is broken up into several components cooperating” - “
Handle::<Space>::read()gives you an&Spaceand 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).
One possibility would be to re-define
Spaceas 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:
Spaceis an actual owner of the components that a not-yet-insertedSpacemay 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.
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.
try_modify() will finally need to go away because there is no longer a single thing for it to give mutable access to.
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.
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.
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.
Next step: Prove that multiple components work by splitting something. Some candidates:
Space. Requires defining aSpaceread facade type.Character. Requires breaking up the directCharactermutation 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.
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.