Repository navigation
Description
What problem does this solve or what need does it fill?
Defining relationships / connections between entities by adding an Entity component (or some thin wrapper) to one or both of them is very common, but often error-prone and obscures the nature of what's being done. For example:
#[derive(Bundle)]
pub struct SpringBundle {
pub spring: Spring,
pub transform: Transform,
pub spring_strength: SpringStrength,
pub connected: (Entity, Entity),
}In this case, the Entity in the connected field must always have a Mass component and several other related fields in order for the game to function as intended. This is not at all clear from reading the code though, and is not enforced. We could attempt to connect these springs to UI widgets for example and nothing would happen: it would just silently no-op.
This pattern comes up in much more complex ways when discussing entity groups (#1592), or when using the entities as events pattern.
What solution would you like?
Allow users to define and work with type-like structures for their entities, permitting them to clearly define the required components that the entities that they are pointing to must have.
The idea here is to use archetype invariants (#1481) on a per entity-basis to enforce a) that an entity pointed to in this way has the required components at the time the b) that the entity never loses the required components. This could be extended to ensure that it never gains conflicting components as well, if that feature is either needed or convenient.
Following from the example above, we'd instead write:
#[derive(Bundle)]
pub struct SpringBundle {
pub spring: Spring,
pub transform: Transform,
pub spring_strength: SpringStrength,
pub connected: (KindedEntity<MassBundle>, KindedEntity<MassBundle>),
}This would enforce that those specific entities always have each of the components defined in the MassBundle we already created. This would be done using archetype invariants, tracking which entities are registered as a KindedEntity on any component in any entity.
I expect that the simplest way to do this, under the hood, would be to insert a EntityKind<K> marker component on each entity registered in this way, and remove it upon deregistration. Then, have a blanket archetype invariant:
"If an Entity has an EntityKind<K: Bundle> component, it must always also have every component within that bundle."
This uses the existing always_with rule, and requires no special checking.
(thanks @BoxyUwU for the discussion to help come up with and workshop this idea)
What alternative(s) have you considered?
Pretend we're using Python and write an ever-expanding test suite to ensure type-safety-like behavior.
Use commands to define all of these components-that-point-to-entities, allowing us to check and ensure the behavior of the target at that time. This forces us to wait for commands to process (see #1613), adds more boilerplate, doesn't clarify the type signatures and doesn't stop the invariant from being broken later.
Additional context
It is likely clearer to call these "kinded entities" than "typed entities", in order to be clear that they don't actually use Rust's type system directly.
According to @BoxyUwU, this would be useful for engine-internal code when defining relationships (see #1627, #1527) to clean up the type signatures. This would let us replace:
struct RelationshipInfo {
id: RelationshipId, // this is just used to find this struct in the vec it's stored it
kind: RelationshipKindId, // this is just a newtype wrapper around a u64
target: EntityOrDummyId,
data_layout: ComponentDescriptor,
}with
struct RelationshipInfo {
id: RelationshipId,
kind: Entity,
target: Entity,
data_layout: ComponentDescriptor,
}and then finally with
struct RelationshipInfo {
id: RelationshipId,
kind: KindedEntity<RelationshipKind>,
target: KindedEntity<RelationshipTarget>,
data_layout: ComponentDescriptor,
}A similar design is mentioned in https://users.rust-lang.org/t/design-proposal-dynec-a-new-ecs-framework/71413
Activity
alice-i-cecile commentedon Mar 12, 2021
To expand this feature's usefulness, we really want to be able to convert kinded entities into less restrictive / differently ordered kinded entities. This has some serious difficulties with Rust's type system however; see discussion.
E.g. for a bundle (T1, T2, T3), we should be able to convert it into:
(T1)
(T2)
(T3)
(T1, T2)
(T2, T1)
(T1, T3)
(T3, T1)
(T2, T3)
(T3, T2)
alice-i-cecile commentedon Mar 13, 2021
In effect, this proposal extends the following metaphor:
to include:
tigregalis commentedon Mar 18, 2021
I'm not sure I like that
Kindis used as it is often used in type theory (e.g. "higher-kinded types") and is kind of obscure. So, a naming idea that simplifies things a little and makes it more obvious:SimilarEntityorLikeEntityorAlikeEntity- these are entities with similar components or are "like" each other (but particularly, not necessarily identical).alice-i-cecile commentedon Mar 23, 2021
This is a deliberate choice on my part, FWIW. In effect, these
KindedEntitiesare abstracting over the concept of which types the entity's components have: creating a higher level idea of the "type" of an entity that's distinct from the literalEntitytype :)That said, it may be too academic; I'm not convinced by any of those suggestions, but I'm open to other ideas.
[-]"Typed" / "Kinded" Entities[/-][+]Kinded entities[/+]Weibye commentedon Aug 8, 2022
As an end user I would more easily understand what this feature does if it was named something like
RequiredComponent<T>or evenEntityWithComponent<T>as then I see that I'm referencing an entity that needs to have a certain component.But it might be that "RequiredComponent" doesn't capture the nuance of the feature?
alice-i-cecile commentedon Aug 8, 2022
There are two nuances that the RequiredComponent name doesn't capture.
First, T can be a bundle. I'm even considering extending it to any WorldQuery type, to enable without filters.
Second, KindedEntity is a drop-in replacement for Entity: it stores the same data and is used in the same way, just with stronger guarantees.
Weibye commentedon Aug 8, 2022
Yeah, makes sense!
That would be really powerful and composable in that case, hoping this works out 😄
Zeenobit commentedon Sep 18, 2022
I've come across this a lot in my project, and I've managed to implement a really minimalistic version of this. I don't think it satisfies all the requirements of the original issue, but it might be a step forward.
To make this work, the user would have to define an entity kind, as such:
You can spawn an entity with a specific kind using
commands.spawn_with_kind::<Dummy>(DummyBundle). This returns anEntityKindCommands<'_, '_, '_, Dummy>, which the user can extend usingDummyCommands.You can also query dummy entities using
EntityWithKind<Dummy>in any query.The type
Dummyitself can be used to reference entities of its kind. This implementation has no requirements on Copy/Clone for the entity kind, so whether or not the user can copyDummyaround is in user's control.You can use
EntityWithKindto safely get a newDummy.The big limitation of this system (that I know of, so far), is that each entity may only have one kind.
Edit: Casting from an
EntityRefwould also work like this:Usage:
Zeenobit commentedon Sep 19, 2022
I've been cleaning up the snippet I posted above:
https://gist.github.com/Zeenobit/ac9a2c889f038024bba1414b61092f76
Zeenobit commentedon Oct 5, 2022
I've been working more on my implementation. I've released it as a separate crate:
https://github.com/Zeenobit/bevy_kindly
I've also solved the issue of entities with multiple kinds. I'm basically using a
KindBundleto solve the problem, and entities can have as many kind bundles as they need.I'm still working on the documentation/examples, but there is a working example of a Container/Containable implementation using this system, which should explain how it all works.
joseph-gio commentedon Oct 11, 2022
Could the "kind" be a parameter on the
Entitystruct itself?alice-i-cecile commentedon Oct 11, 2022
Yep, that generic strategy is the one that Cart and I currently prefer :)
Zeenobit commentedon Oct 23, 2022
I've been thinking some more about this issue based on the comments above, and also my experience with Bevy Kindly so far. There are 2 main points I've learned from Bevy Kindly as my project scaled up:
The simplified solution proposed by @JoJoJet ,
would solve the second problem, since there would be no requirement to tag entities with explicit kinds. The big challenge with this implementation is being able to automatically "upcast" from
Entity<(With<A>, With<B>)>toEntity<With<A>>and ultimately toEntity<()>. The other issue I have with it is the verbosity, especially since using anything other thanWithorWithoutfor its terms wouldn't make much sense, and would expose the user to misuse of the feature.So I've been thinking of an entirely different approach, using a concept of "component links":
This would allow users to reference components like so:
And similar to
EntityKindCommandsfrom Bevy Kindly, we can haveComponentCommands:This solves a lot of problems addressed in this issue, without the need of a marker, and less verbose syntax (compared to both Bevy Kindly and
Entity<Filter>). The only big limitation I can think of is being able to link to a single component.I'm still trying to work the details, especially how to resolve a component link from a query, as opposed to from world. But I'm curious what everyone thinks about this as an alternative solution to the same problem domain.
Zeenobit commentedon Oct 24, 2022
I've been playing around with the concept of component links some more, and managed to refactor my project from using Bevy Kindly to using links to test if component links solve the same problems.
The implementation of component links is a bit more complicated:
https://gist.github.com/Zeenobit/c57e238aebcd4ec3c4c9b537e858e0df
But the end result is a much cleaner interface:
Links can also be used as a
WorldQuery:This approach is a lot more scalable than Bevy kindly because there is no need for an explicit
Kind<T>component. Also less verbose than the filtered entities (i.e.Entity<(With<A>, With<B>)>).The biggest limitation with this approach is inability to filter for multiple components. But I haven't come up with a real use case for that yet.
I considered releasing this as a separate crate, but I think it could be made even more ergonomic if it was integrated into Bevy. So I'm curious to hear thoughts on this. For example, it could be used for existing components, like
ParentandChildren:I've also debated renaming
LinktoCom(short forComponent, similar toRes, and analogous toComPtrfrom .NET, or "component pointer", because that's semantically what it is). I'm not sure if that's less or more readable.alice-i-cecile commentedon Oct 24, 2022
How would you envision links fitting into a world with relations? (#3742)
Zeenobit commentedon Oct 24, 2022
I imagine it may be possible to implement entity relations in terms of links. For example:
The syntax isn't as pretty as Flecs, but... it's a baby step towards it! 😁
Edit: Alternatively, links could also be stored in maps, as local resources for example:
Zeenobit commentedon Oct 25, 2022
Thinking about it more, relations, kinded entities, and these component links are really all solving the same problem.
In the gist I posted above, there is a test at the bottom which demonstrates usage of links to describe a relation between Containers and Containables:
In short, some entities are Containers, which can only store entities that are Containable.
In the example, I'm essentially creating a two way "ContainedBy/Contains" relation between the container and the containable using links. One way relations, such as a "ContainedBy" can be defined as a
ContainedBy(Link<Container>), while "Contains" can be implemented asContains(Vec<Link<Containable>>). And so:In my mind, where and how these relations are stored shouldn't be a concern of Bevy (although Bevy could provide tools to deal with links/relations more ergonomically). This would allow the user to store relations either as a map within a local/world resource, or store relations as components on entities, depending on their needs.
Zeenobit commentedon Oct 25, 2022
Additionally, a big motivation for integrating this into Bevy for me is having an
IntoEntitytrait, and refactoring most functions that take anEntityto instead take animpl IntoEntity. ThenIntoEntitycould be implemented forLink<T>, which would turn something like this:Into this:
Another reason is being able to generate a
Link<T>from anyQuery<Q, F>whereQcontainsEntityand:&Tor&mut T), orWith<T>orAdded<T>orChanged<T>But I'm not sure how to solve this problem yet.
I've worked around it in my implementation by implementing
WorldQueryforLink<T>. It's... ok, but a little annoying, especially if you need to access the component and get a link to it:Zeenobit commentedon Oct 31, 2022
Managed to implement a more elegant way to express entity relations directly, using Component Links.
In summary, it'd allow us to describe a one-to-many relationship as such:
In English:
A Container is relatable to many Containables, which become Contained after connection is made.
Container, Containable, and Contained are all components that can be queried and iterated.
I've included the full gist here:
https://gist.github.com/Zeenobit/ce1f24f5230258d0cf5663dc64dc8f2b
There is a test at the bottom which demonstrates how to define and use the relationship.
As is, the macro is very inflexible, and I've only managed to get one-to-many relationships working. But I don't see one-to-one relationships or other variations of the macro to be impossible. I imagine it's just more macro voodoo, which I'm still relatively new to. 😅
Zeenobit commentedon Nov 1, 2023
I've released a new crate to tackle this issue:
https://github.com/Zeenobit/moonshine_kind
This is much more lightweight than the
bevy_kindly, and more aligned with the problems outlined in this issue.CraigBuilds commentedon Nov 14, 2024
Just spitballing ideas:
Now that we have required components we might be able to enforce these "Kinded" invariants using that system.
So
First becomes
My first thought was to add a custom constructor to enforce the invariants that the entities have mass. However this had an issue.
However, isn't this what the
constructabstraction is for?Spawning
Springwill also spawn all of the required components, including the connected entities. They are guaranteed to have Mass components.Because SpringConnections is not Default, and the members are not pub, it needs to be Constructed using Construct, and hence the SpringConnections entities will always have mass.
This idea is only half thought through, however I thought it was worth mentioning.
[-]Kinded entities[/-][+]Kinded entities: typed entities that are guaranteed to have a component[/+]Zeenobit commentedon May 28, 2026
See below for an exploration into Kind-Safe relationships, related to #21481
This relies on the
Expect<T>component, which I've been relying on heavily to enforce kind safety. It also requires anInstanceVec<T>type becauseRelationshipSourceCollectionis not implemented forVec<T: ContainsEntity>.But overall, I thought this idea is interesting for enforcing relationship constraints, so I wanted to share it.
The problem is that I have
Personentities andPotatoentities, and aFriends/FriendsOfrelationship. Any Person can be friends with any other Person, but people can't be friends with potatoes. That's silly!So, the idea is to use
Expect<T>and Component Requirements (my workaround to Archetype Constraints) to ensureFriendsand/orFriendsOfcan only exist onPeople. This means insertingFriendOf(person)onto a Potato would panic (asFriendOfexpectsPerson), and insertingFriendOf(potato)onto a Person would also panic (as Bevy tries to insertFriendsinto a Potato).Of course, it'd be great if this could all be enforced at compile time, but that adds a lot more complexity than I'm willing to introduce.
Full solution:
https://github.com/Zeenobit/moonshine_kind/blob/dc5e5b06168c4e57b7c8799d8fdb3e6de0d04e06/src/instance.rs#L1154