It's All Components and Entities??

flecs treats virtually all of data used by the ECS in a single homogenous way: as components on entities. In Bevy terms, this means:

  • the row-ish identifier is always Entity
  • the column-ish identifier is always ComponentId
    • no bevy_ecs code uses TypeId: it's all ComponentId (except when generating ComponentId for Rust types)
  • each resource is an entity, with its data stored in a component
  • each system is stored as an entity, with metadata and system state on components
    • ordering constraints are stored as relations between systems
    • executors query for entities and then run them
  • metadata for each component, resource and event type is stored in the ECS, with each type getting its own entity

This path is appealing to many contributors and users of Bevy because:

  • new features (hooks, observers, relations, debugging tools) just work on all of these different types of data, without needing reimplementation
    • less complexity, less maintenance burden, lower compile times
  • the existing powerful ECS features can be used to build more ECS features faster and easier
  • because we're using these ECS features to build more ECS features contributors will think of ways to improve them for all users

That's a compelling argument! But there are some drawbacks:

  • new users may be confused about the different concepts and how they relate
  • it may become harder to understand our APIs by reading type signatures
  • semantic errors (e.g. passing a resource into a query) may not be enforced by the compiler, hurting new users and making refactors more painful
  • we may lose performance due to the increased generality
    • e.g. we can't assume at the type level that we only have one resource

Rules

Let's see if we can come up with some rules for these changes that mitigate the drawbacks.

Rule 0: different concepts get different names

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.

Rule 1: trait bounds must be semantically correct

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.

Rule 2: Strongly typed common APIs, weakly typed internals

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.

Rule 3: newtype identifiers, but provide escape hatches

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.

Rule 4: ECS internals should be hard to accidentally interact with

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.

Groundwork

We may need some additional tools to make these unifications viable.

Groundwork 0: relations

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.

Cleanup

Some existing parts of Bevy violate these rules!

Cleanup 0: remove 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

Cleanup 1: split apart observer and queue-based events

These operate in extremely different ways, and using the same name and trait makes teaching and talking about these event-flavored constructs very hard.

Unification

Finally, on to the meat of things. There will surely be

Unification 0: resources as entities

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:

  • each resource gets its own Entity
  • resource data T is stored as a raw T component
  • each resource entity has a marker types IsResource and ResourceEntity<T>
    • the former is useful for inspectors and generic reasoning
    • the latter is useful for enforcing uniqueness
  • ResourceEntity<T> has an OnInsert hook that despawns any other existing ResourceEntity<T> entities, preserving global uniqueness
    • dynamic resources get a 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 true
  • queries operate on resources by default (no default query filters here)
    • in most cases, this is a good default and allows users
  • type Res<T: Resource> = Single<&T, With<ResourceEntity<T>>, and equivalently for ResMut
    • painless migration!
    • still type-safe!

This may cause complications with NonSend types: we should plan how we want to handle that.

Unification 1: systems as entities

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.

Unification 2: queries as entities

Store your queries as entities too and you can update them using hooks and observers.

Unification 3: component and resource types as entities

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.

Select a repo