flecs
treats virtually all of data used by the ECS in a single homogenous way: as components on entities. In Bevy terms, this means:
Entity
ComponentId
bevy_ecs
code uses TypeId
: it's all ComponentId
(except when generating ComponentId
for Rust types)This path is appealing to many contributors and users of Bevy because:
That's a compelling argument! But there are some drawbacks:
Let's see if we can come up with some rules for these changes that mitigate the drawbacks.
If two things are used in different ways, they need distinct names, and documentation needs to consistently refer to them as distinct.
The fact that "resources are implemented using entities and components" or "each system is stored as an entity" is something we should explain to advanced users: not hide. But that doesn't mean "resources are entities (or components)" or "systems are entities"!
Advanced features are built out of simple ones.
If trait NewEcsDataType: Component
, users should be able to interact with it as a component in every meaningful way, and things should Just Work. This trait bound implies that "all NewEcsDataTypes are also components": we need to keep this promise to users.
While strongly typed APIs and restrictive trait bounds are great for new users and refactoring, they can be quite limiting, both for power users and for dynamic types.
Most users and call sites should use strongly typed APIs that take real Rust traits with unambigious names and bounds added for correctness. These should be what we teach, and preferred wherever we don't explicitly need to be generic.
Under the hood though, these APIs should call (public!) untyped APIs to do the actual operation. These will accept and return ComponentId
and Entity
, which the higher level API will translate to / from the strongly typed forms.
This means that the deepest internals of the ECS should not have trait bounds, use TypeId
or require that any of the data represent actual Rust types.
Whenever possible, high level APIs should provide and use wrappers over both Entity
and ComponentId
. These should be stored, accepted as arguments and returned wherever possible.
However, these should be simple tuple structs with public fields, allowing users to extend the existing functionality with ECS-backed features of their choice.
These newtypes help communicate intent and prevent confusing errors without restricting users or obscuring the implementation.
Interacting with the ECS internals can have surprising, difficult to debug, and destructive results! Messing with component metadata, despawning systems, resetting system state: all extremely confusing for users!
While we could simply make them all inaccessible, power users will rightfully complain. Instead, they should be public, but impossible to accidentally hit.
We may need some additional tools to make these unifications viable.
Many of the constraints that we want to model (schedules, system sets, system ordering constraints) are graph or tag-like. We need relations to make this migration not suck.
Some existing parts of Bevy violate these rules!
Event: Component
Unlike a hypothetical Resource: Component
bound, this doesn't map to users' intuitions properly. Remove it in https://github.com/bevyengine/bevy/pull/17333
These operate in extremely different ways, and using the same name and trait makes teaching and talking about these event-flavored constructs very hard.
Finally, on to the meat of things. There will surely be
Many features like hooks and observers work on resources, but not components. This is very annoying!
This should be a nearly full unification: the concepts and behavior will be distinct, but resource data will transparently be component data to users.
Details:
Entity
T
is stored as a raw T
componentIsResource
and ResourceEntity<T>
ResourceEntity<T>
has an OnInsert
hook that despawns any other existing ResourceEntity<T>
entities, preserving global uniqueness
DynamicResourceEntity
component that uses a more expensive scan (or relies on an index)trait Resource: Component
: everything that you can do with a component you should be able to do with a resource, but the reverse is not truetype Res<T: Resource> = Single<&T, With<ResourceEntity<T>>
, and equivalently for ResMut
This may cause complications with NonSend
types: we should plan how we want to handle that.
We already store one-shot systems and observers as entities: ordinary systems should be entities too.
The final implementation of this work will be relations-heavy, but we can move incrementally still. Schedules can store the metadata still, but point to system entities.
Store your queries as entities too and you can update them using hooks and observers.
Rather than using a dedicated storage for this metadata, we can move it into the ECS proper.
Each component type should have its own ComponentInfo
entity. Resources should refer back to these same entities, rather than storing it on the resource entity itself, to avoid it being despawned when removing the resource.
These are dangerous enough that they should be excluded via default query filters.
This work also enables us to further unify ComponentId
to store an Entity
. As explained in Rule 3, this should be a newtype, not a removal of ComponentId
.