-
-
Notifications
You must be signed in to change notification settings - Fork 3.6k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Kinded entities #1634
Comments
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: |
In effect, this proposal extends the following metaphor:
to include:
|
I'm not sure I like that |
This is a deliberate choice on my part, FWIW. In effect, these That said, it may be too academic; I'm not convinced by any of those suggestions, but I'm open to other ideas. |
As an end user I would more easily understand what this feature does if it was named something like But it might be that "RequiredComponent" doesn't capture the nuance of the feature? |
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. |
Yeah, makes sense!
That would be really powerful and composable in that case, hoping this works out 😄 |
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. use std::marker::PhantomData;
use bevy::ecs::prelude::*;
use bevy::ecs::query::WorldQuery;
use bevy::ecs::system::EntityCommands;
/// Some type which can store an [`Entity`].
pub trait EntityKind {
type Bundle: Bundle;
/// # Safety
/// Assumes `entity` has a [`Kind<Self>`] component.
unsafe fn from_entity_unchecked(entity: Entity) -> Self;
fn entity(&self) -> Entity;
}
/// A [`Component`] which stores the [`EntityKind`] of the [`Entity`] it is attached to.
#[derive(Component)]
struct Kind<T: EntityKind>(PhantomData<T>);
impl<T: EntityKind> Default for Kind<T> {
fn default() -> Self {
Self(PhantomData)
}
}
/// A [`WorldQuery`] which may be used to query an [`Entity`] for the given [`EntityKind`].
#[derive(WorldQuery)]
pub struct EntityWithKind<T: EntityKind>
where
T: 'static + Send + Sync,
{
entity: Entity,
kind: With<Kind<T>>,
}
impl<T: EntityKind> EntityWithKindItem<'_, T>
where
T: 'static + Send + Sync,
{
pub fn entity(&self) -> Entity {
self.entity
}
pub fn instance(&self) -> T {
// SAFE: Query has `With<Kind<T>>`
unsafe { T::from_entity_unchecked(self.entity()) }
}
pub fn commands<'w, 's, 'a>(
&self,
commands: &'a mut Commands<'w, 's>,
) -> EntityKindCommands<'w, 's, 'a, T> {
let entity = commands.entity(self.entity());
// SAFE: Query has `With<Kind<T>>`
unsafe { EntityKindCommands::from_entity_unchecked(entity) }
}
}
/// A wrapper for [`EntityCommands`] which guarantees that the referenced entity has [`EntityKind`] of `T`.
/// This type can be extended to provide commands for specific kinds of entities.
pub struct EntityKindCommands<'w, 's, 'a, T: EntityKind>(
PhantomData<T>,
EntityCommands<'w, 's, 'a>,
);
impl<'w, 's, 'a, T: EntityKind> EntityKindCommands<'w, 's, 'a, T> {
unsafe fn from_entity_unchecked(entity: EntityCommands<'w, 's, 'a>) -> Self {
Self(PhantomData, entity)
}
pub fn commands(&mut self) -> &mut Commands<'w, 's> {
self.entity_mut().commands()
}
pub fn entity(&self) -> &EntityCommands<'w, 's, 'a> {
&self.1
}
pub fn entity_mut(&mut self) -> &mut EntityCommands<'w, 's, 'a> {
&mut self.1
}
}
/// Interface for spawning an [`Entity`] with a [`Kind`].
pub trait SpawnWithKind<'w, 's, 'a> {
fn spawn_with_kind<T: EntityKind>(self, bundle: T::Bundle) -> EntityKindCommands<'w, 's, 'a, T>
where
T: 'static + Send + Sync;
}
/// Implements [`SpawnWithKind`] for [`Commands`].
impl<'w, 's, 'a> SpawnWithKind<'w, 's, 'a> for &'a mut Commands<'w, 's> {
fn spawn_with_kind<T: EntityKind>(self, bundle: T::Bundle) -> EntityKindCommands<'w, 's, 'a, T>
where
T: 'static + Send + Sync,
{
let mut entity = self.spawn();
let kind = Kind::<T>::default();
entity.insert(kind).insert_bundle(bundle);
// SAFE: Entity has just spawned with its kind
unsafe { EntityKindCommands::from_entity_unchecked(entity) }
}
} To make this work, the user would have to define an entity kind, as such: struct Dummy(Entity);
#[derive(Bundle)]
struct DummyBundle {
// Some components all dummies should have
}
impl EntityKind for Dummy {
type Bundle = DummyBundle;
unsafe fn from_entity_unchecked(entity: Entity) -> Self {
Self(entity)
}
fn entity(&self) -> Entity {
self.0
}
}
trait DummyCommands {
// Some command functions only dummies can invoke
}
impl DummyCommands for &mut EntityKindCommands<'_, '_, '_, Dummy> {
// Implement commands using EntityKindCommands
} You can spawn an entity with a specific kind using You can also query dummy entities using The type 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 pub trait EntityCast {
fn cast<T: EntityKind>(self) -> Option<T>;
}
impl EntityCast for EntityRef<'_> {
fn cast<T: EntityKind>(self) -> Option<T> {
self.contains::<Kind<T>>()
// SAFE: `contains::<Kind<T>>` is `true`.
.then(|| unsafe { T::from_entity_unchecked(self.id()) })
}
} Usage: world.entity(entity).cast::<Dummy>().unwrap() |
I've been cleaning up the snippet I posted above: |
I've been working more on my implementation. I've released it as a separate crate: I've also solved the issue of entities with multiple kinds. I'm basically using a 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. |
Could the "kind" be a parameter on the pub struct Entity<Kind: ReadOnlyWorldQuery = ()> {
...
}
#[derive(Component)]
pub struct Parent {
id: Entity<With<Children>>,
} |
Yep, that generic strategy is the one that Cart and I currently prefer :) |
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 , pub struct Entity<Kind: ReadOnlyWorldQuery = ()> {
...
} 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 So I've been thinking of an entirely different approach, using a concept of "component links": /// Acts as a weak reference to a specific component of an entity.
pub struct Link<T: Component>(Entity, PhantomData<T>);
impl<T: Component> Link<T> {
unsafe fn from_entity_unchecked(entity: Entity) -> Self {
Self(entity, PhantomData)
}
fn entity(&self) -> Entity {
self.0
}
fn get<'a>(&self, world: &'a World) -> &'a T {
world.get(self.entity()).unwrap()
}
fn get_mut<'a>(&self, world: &'a mut World) -> Mut<'a, T> {
world.get_mut(self.entity()).unwrap()
}
}
pub trait IntoLink<T: Component> {
fn into_link(self) -> Link<T>;
}
impl<T: Component> IntoLink<T> for Link<T> {
fn into_link(self) -> Link<T> {
self
}
}
#[derive(WorldQuery)]
pub struct ComponentLink<T: Component> {
entity: Entity,
filter: With<T>,
}
impl<T: Component> IntoLink<T> for ComponentLinkItem<'_, T> {
fn into_link(self) -> Link<T> {
// SAFE: Query has `With<T>`
unsafe { Link::from_entity_unchecked(self.entity) }
}
} This would allow users to reference components like so: #[derive(Component, Default)]
pub struct Container(Vec<Link<Containable>>);
#[derive(Component)]
pub enum Containable {
Uncontained,
Contained(Link<Container>),
} And similar to pub trait ContainableCommands {
fn insert_into(self, container: impl IntoLink<Container>);
}
impl ContainableCommands for &mut ComponentCommands<'_, '_, '_, Containable> {
fn insert_into(self, container: impl IntoLink<Container>) {
let containable = self.into_link();
let container = container.into_link();
self.add(move |world: &mut World| {
container.get_mut(world).0.push(containable);
*containable.get_mut(world) = Containable::Contained(container);
});
}
} 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 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. |
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: But the end result is a much cleaner interface: let mut world = World::new();
// Spawn a container and get a link to it
let container: Link<Container> = world.spawn().insert_get_link(Container::default());
// Spawn a containable and get a link to it
let containable: Link<Containable> = world.spawn().insert_get_link(Containable::default());
// Insert containable into container
{
let mut queue = CommandQueue::from_world(&mut world);
let mut commands = Commands::new(&mut queue, &world);
commands.get(&containable).insert_into(container);
queue.apply(&mut world);
}
// Ensure containable is contained by conatainer
assert!(world.component(&container).contains(&containable));
assert!(world.component(&containable).is_contained_by(&container)); Links can also be used as a fn system(query: Query<Link<Container>>) {
let container: Link<Container> = query.single();
} This approach is a lot more scalable than Bevy kindly because there is no need for an explicit 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 struct Parent(Link<Children>);
struct Children(Vec<Link<Parent>>); I've also debated renaming |
How would you envision links fitting into a world with relations? (#3742) |
I imagine it may be possible to implement entity relations in terms of links. For example: #[derive(Component);
struct Person;
#[derive(Component)]
struct Likes(Link<Person>); // <-- This describes the relation
fn system(query: Query<(Link<Person>, &Likes), With<Person>>) {
for (person, Likes(other_person)) in &query {
let person_entity: Entity = person.entity();
// ...
}
} 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: #[derive(Component)]
struct Person;
struct FriendMap(HashMap<Link<Person>, Link<Person>>); |
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: let mut world = World::new();
// Spawn a container and get a link to it
let container: Link<Container> = world.spawn().insert_get_link(Container::default());
// Spawn a containable and get a link to it
let containable: Link<Containable> = world.spawn().insert_get_link(Containable::default());
// Insert containable into container
{
let mut queue = CommandQueue::from_world(&mut world);
let mut commands = Commands::new(&mut queue, &world);
commands.get(&containable).insert_into(container);
queue.apply(&mut world);
}
// Ensure containable is contained by conatainer
assert!(world.component(&container).contains(&containable));
assert!(world.component(&containable).is_contained_by(&container)); 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 #[derive(Component)]
struct Container;
#[derive(Component)]
struct Containable;
struct Contains(Vec<Link<Containable>>);
struct ContainedBy(Link<Container>);
struct ContainsMap(HashMap<Link<Container>, Contains>);
struct ContainedByMap(HashMap<Link<Containable>, ContainedBy>); 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. |
Additionally, a big motivation for integrating this into Bevy for me is having an let link: Link<A> = ...;
world.entity(link.entity()).contains::<A>();
let query: Query<...> = ...;
query.get(link.entity()); Into this: let link: Link<A> = ...;
world.entity(link).contains::<A>()
let query: Query<...> = ...;
query.get(link); Another reason is being able to generate a
But I'm not sure how to solve this problem yet. I've worked around it in my implementation by implementing fn system(query: Query<(&Container, Link<Container>)>) { ... } |
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: relation! { 1 Container => * Containable as Contained } In English: I've included the full gist here: 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. 😅 |
I've released a new crate to tackle this issue: This is much more lightweight than the |
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:In this case, the
Entity
in theconnected
field must always have aMass
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:
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 aKindedEntity
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 anEntityKind<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:
with
and then finally with
A similar design is mentioned in https://users.rust-lang.org/t/design-proposal-dynec-a-new-ecs-framework/71413
The text was updated successfully, but these errors were encountered: